Introduce new functions to calculate DPI exceptions
This commit is contained in:
+14
-2
@@ -15,6 +15,7 @@ from contextlib import suppress
|
||||
from io import StringIO
|
||||
from math import isclose, isfinite
|
||||
from pathlib import Path
|
||||
from statistics import harmonic_mean
|
||||
from typing import Any, Generic, Sequence, SupportsFloat, SupportsRound, TypeVar
|
||||
|
||||
import img2pdf
|
||||
@@ -81,6 +82,17 @@ class Resolution(Generic[T]):
|
||||
return isfinite(self.x) and isfinite(self.y)
|
||||
return True
|
||||
|
||||
@property
|
||||
def mean(self) -> float:
|
||||
"""Return the harmonic mean of x and y.
|
||||
|
||||
The harmonic mean is used because it is the correct mean to use for
|
||||
averaging rates, such as pixels per inch. If a calculation requires a single
|
||||
value instead of a pair of values, the harmonic mean is the correct value to
|
||||
use.
|
||||
"""
|
||||
return harmonic_mean([self.x, self.y])
|
||||
|
||||
def take_max(
|
||||
self, vals: Iterable[Any], yvals: Iterable[Any] | None = None
|
||||
) -> Resolution:
|
||||
@@ -103,11 +115,11 @@ class Resolution(Generic[T]):
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the resolution."""
|
||||
return f"{self.x:f}x{self.y:f}"
|
||||
return f"{self.x:f}×{self.y:f}"
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
"""Return a repr() of the resolution."""
|
||||
return f"Resolution({self.x}x{self.y} dpi)"
|
||||
return f"Resolution({self.x}×{self.y} dpi)"
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Return True if the resolution is equal to another resolution."""
|
||||
|
||||
@@ -420,12 +420,12 @@ class ImageInfo:
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
def width(self) -> int:
|
||||
"""Width of the image in pixels."""
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
def height(self) -> int:
|
||||
"""Height of the image in pixels."""
|
||||
return self._height
|
||||
|
||||
@@ -458,17 +458,24 @@ class ImageInfo:
|
||||
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
|
||||
|
||||
@property
|
||||
def dpi(self):
|
||||
def dpi(self) -> Resolution:
|
||||
"""Dots per inch of the image.
|
||||
|
||||
Calculated based on where and how the image is drawn in the PDF.
|
||||
"""
|
||||
return _get_dpi(self._shorthand, (self._width, self._height))
|
||||
|
||||
@property
|
||||
def printed_area(self) -> float:
|
||||
"""Physical area of the image in square inches."""
|
||||
if not self.renderable:
|
||||
return 0.0
|
||||
return float(self.width * self.dpi.w * self.height * self.dpi.h)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the image."""
|
||||
return (
|
||||
f"<ImageInfo '{self.name}' {self.type_} {self.width}x{self.height} "
|
||||
f"<ImageInfo '{self.name}' {self.type_} {self.width}×{self.height} "
|
||||
f"{self.color} {self.comp} {self.bpc} {self.enc} {self.dpi}>"
|
||||
)
|
||||
|
||||
@@ -747,12 +754,22 @@ def _pdf_pageinfo_concurrent(
|
||||
return pages
|
||||
|
||||
|
||||
class PageResolutionInfo(NamedTuple):
|
||||
"""Information about the resolution of a page."""
|
||||
|
||||
average_to_max_dpi_ratio: float
|
||||
"""The average DPI of the page divided by the maximum DPI of the page."""
|
||||
|
||||
area_ratio: float
|
||||
"""The maximum DPI area of the page divided by the total drawn area."""
|
||||
|
||||
|
||||
class PageInfo:
|
||||
"""Information about type of contents on each page in a PDF."""
|
||||
|
||||
_has_text: bool | None
|
||||
_has_vector: bool | None
|
||||
_images: list[ImageInfo]
|
||||
_images: list[ImageInfo] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -939,6 +956,44 @@ class PageInfo:
|
||||
else:
|
||||
return '1.5'
|
||||
|
||||
def image_dpi_ratios(self) -> PageResolutionInfo | None:
|
||||
"""Return ratios useful for detecting high DPI images.
|
||||
|
||||
This is useful to detect pages with a small proportion of high-resolution
|
||||
content that is forcing us to use a high DPI for the whole page. The ratio
|
||||
is weighted by the area of each image. If images overlap, the overlapped
|
||||
area counts.
|
||||
|
||||
Vector graphics and text are ignored.
|
||||
|
||||
A ratio of 1.0 means that all images are the same DPI.
|
||||
A large ratio indicates high DPI content.
|
||||
A ratio of less than 1.0 is not possible.
|
||||
|
||||
Returns None if there is no meaningful DPI for the page.
|
||||
"""
|
||||
image_dpis = [image.dpi.hypot for image in self._images if image.renderable]
|
||||
image_areas = [image.printed_area for image in self._images if image.renderable]
|
||||
total_drawn_area = sum(image_areas)
|
||||
if total_drawn_area == 0:
|
||||
return None
|
||||
|
||||
weights = [area / total_drawn_area for area in image_areas]
|
||||
# Calculate harmonic mean of DPIs weighted by area
|
||||
# When the minimum version is Python 3.10, change this to
|
||||
# statistics.harmonic_mean with the weights parameter
|
||||
# rather than doing it manually.
|
||||
weighted_dpi = sum(weights) / sum(
|
||||
weight / dpi for weight, dpi in zip(weights, image_dpis)
|
||||
)
|
||||
max_dpi = max(image_dpis)
|
||||
dpi_average_max_ratio = weighted_dpi / max_dpi
|
||||
|
||||
arg_max_dpi = image_dpis.index(max_dpi)
|
||||
max_area_ratio = image_areas[arg_max_dpi] / total_drawn_area
|
||||
|
||||
return PageResolutionInfo(dpi_average_max_ratio, max_area_ratio)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return string representation."""
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user