Introduce new functions to calculate DPI exceptions

This commit is contained in:
James R. Barlow
2023-09-20 16:58:59 -07:00
parent c77ae4b34c
commit 78981641f0
2 changed files with 74 additions and 7 deletions
+14 -2
View File
@@ -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."""
+60 -5
View File
@@ -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 (