Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
### Changed
* improved performance when rendering paths, SVGs, and opaque raster images with an alpha channel - _cf._ [PR #1675](https://github.com/py-pdf/fpdf2/pull/1675)
* typing annotations added across the codebase as part of the strict typing rollout
* graphics state snapshots now use a `GraphicsState` dataclass for clearer usage and stronger typing
* graphics state snapshots now use a [`GraphicsState` dataclass](https://py-pdf.github.io/fpdf2/fpdf/graphics_state.html) dataclass for clearer usage and stronger typing
* `rotation()`, `skew()`, and `mirror()` now delegate to `transform()` for standardized transform application
* [ImageInfo](https://py-pdf.github.io/fpdf2/fpdf/image_datastructures.html#fpdf.image_datastructures.ImageInfo) now uses typed dictionaries for clearer usage and stronger typing

## [2.8.5] - 2025-10-29
### Added
Expand Down
10 changes: 7 additions & 3 deletions fpdf/font_type_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
GradientUnits,
PathPaintRule,
)
from .image_datastructures import RasterImageInfo
from .pattern import SweepGradient, shape_linear_gradient, shape_radial_gradient

try:
Expand Down Expand Up @@ -1006,14 +1007,15 @@ def load_glyph_image(self, glyph: Type3FontGlyph) -> None:
bio = BytesIO(glyph_bitmap)
bio.seek(0)
_, _, info = self.fpdf.preload_glyph_image(glyph_image_bytes=bio)
info = cast(RasterImageInfo, info)
w = round(self.base_font.ttfont["hmtx"].metrics[glyph.glyph_name][0] + 0.001)
glyph.glyph = (
f"{round(w * self.scale)} 0 d0\n"
"q\n"
f"{(x_max - x_min)* self.scale} 0 0 {(-y_min + y_max)*self.scale} {x_min*self.scale} {y_min*self.scale} cm\n"
f"/I{info['i']} Do\nQ"
)
self.images_used.add(info["i"]) # type: ignore[arg-type]
self.images_used.add(info["i"])
glyph.glyph_width = w


Expand Down Expand Up @@ -1201,6 +1203,7 @@ def load_glyph_image(self, glyph: Type3FontGlyph) -> None:
alpha_image.save(bio, format="PNG")
bio.seek(0)
_, _, info = self.fpdf.preload_glyph_image(glyph_image_bytes=bio)
info = cast(RasterImageInfo, info)

mask_matrix = Transform(
a=(x_max - x_min) * self.scale,
Expand All @@ -1216,7 +1219,7 @@ def load_glyph_image(self, glyph: Type3FontGlyph) -> None:
x_max * self.scale,
y_max * self.scale,
)
soft_mask = ImageSoftMask(cast(int, info["i"]), bbox, mask_matrix)
soft_mask = ImageSoftMask(info["i"], bbox, mask_matrix)

soft_mask.object_id = self.fpdf._resource_catalog.register_soft_mask( # pylint: disable=protected-access
soft_mask
Expand Down Expand Up @@ -1284,6 +1287,7 @@ def load_glyph_image(self, glyph: Type3FontGlyph) -> None:
bio = BytesIO(sbix_glyph.imageData)
bio.seek(0)
_, _, info = self.fpdf.preload_glyph_image(glyph_image_bytes=bio)
info = cast(RasterImageInfo, info)
w = round(self.base_font.ttfont["hmtx"].metrics[glyph.glyph_name][0] + 0.001)
glyf_metrics = self.base_font.ttfont["glyf"].get(glyph.glyph_name)
assert glyf_metrics is not None
Expand All @@ -1298,7 +1302,7 @@ def load_glyph_image(self, glyph: Type3FontGlyph) -> None:
f"{(x_max - x_min) * self.scale} 0 0 {(-y_min + y_max) * self.scale} {x_min * self.scale} {y_min * self.scale} cm\n"
f"/I{info['i']} Do\nQ"
)
self.images_used.add(info["i"]) # type: ignore[arg-type]
self.images_used.add(info["i"])
glyph.glyph_width = w


Expand Down
57 changes: 31 additions & 26 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ class Image: # type: ignore[no-redef]
ImageInfo,
RasterImageInfo,
VectorImageInfo,
is_vector_image_info,
scale_inside_box,
size_in_document_units,
)
from .image_parsing import (
SUPPORTED_IMAGE_FILTERS,
Expand Down Expand Up @@ -5218,7 +5221,7 @@ def image(
)

name, img, info = preload_image(self.image_cache, name, dims)
if isinstance(info, VectorImageInfo):
if is_vector_image_info(info):
return self._vector_image(
name,
cast(SVGObject, img),
Expand All @@ -5237,7 +5240,7 @@ def image(
return self._raster_image(
name,
img,
info,
cast(RasterImageInfo, info),
x,
y,
w,
Expand Down Expand Up @@ -5268,7 +5271,7 @@ def _raster_image(
self._set_min_pdf_version("1.4")

# Automatic width and height calculation if needed
w, h = info.size_in_document_units(w, h, scale=self.k)
w, h = size_in_document_units(info, w, h, scale=self.k)

# Flowing mode
if y is None:
Expand All @@ -5283,7 +5286,7 @@ def _raster_image(
if TYPE_CHECKING:
x = float(x)
if keep_aspect_ratio:
x, y, w, h = info.scale_inside_box(x, y, w, h)
x, y, w, h = scale_inside_box(info, x, y, w, h)
if self.oversized_images and info["usages"] == 1 and not dims:
info = self._downscale_image(name, img, info, w, h, scale=self.k)

Expand All @@ -5299,9 +5302,7 @@ def _raster_image(
if link:
self.link(x, y, w, h, link)

self._resource_catalog.add(
PDFResourceType.X_OBJECT, info["i"], self.page # type: ignore
)
self._resource_catalog.add(PDFResourceType.X_OBJECT, info["i"], self.page)
info["rendered_width"] = w
info["rendered_height"] = h
return info
Expand All @@ -5315,7 +5316,7 @@ def x_by_align(
keep_aspect_ratio: bool,
) -> float:
if keep_aspect_ratio:
_, _, w, h = img_info.scale_inside_box(0, 0, w, h)
_, _, w, h = scale_inside_box(img_info, 0, 0, w, h)
x = Align.coerce(x)
if x == Align.C:
return (self.w - w) / 2
Expand Down Expand Up @@ -5390,7 +5391,7 @@ def _vector_image(
x = self.x_by_align(x, w, h, info, keep_aspect_ratio)
x = float(x)
if keep_aspect_ratio:
x, y, w, h = info.scale_inside_box(x, y, w, h)
x, y, w, h = scale_inside_box(info, x, y, w, h)

_, _, path = svg.transform_to_rect_viewport(
scale=1, width=w, height=h, ignore_svg_top_attrs=True
Expand All @@ -5413,7 +5414,9 @@ def _vector_image(
if link:
self.link(x, y, w, h, link)

return VectorImageInfo(rendered_width=w, rendered_height=h)
info["rendered_width"] = w
info["rendered_height"] = h
return info

def _downscale_image(
self,
Expand All @@ -5427,8 +5430,8 @@ def _downscale_image(
images = self.image_cache.images
width_in_pt, height_in_pt = w * scale, h * scale
lowres_name = f"lowres-{name}"
w = float(info["w"]) # type: ignore[arg-type]
h = float(info["h"]) # type: ignore[arg-type]
w = info["w"]
h = info["h"]
assert self.oversized_images is not None
if (
w > width_in_pt * self.oversized_images_ratio
Expand All @@ -5455,42 +5458,44 @@ def _downscale_image(
round(width_in_pt * self.oversized_images_ratio),
round(height_in_pt * self.oversized_images_ratio),
)
info["usages"] -= 1 # type: ignore[operator] # no need to embed highres version
info["usages"] -= 1 # no need to embed highres version
if info["usages"] == 0:
resources_per_page = self._resource_catalog.resources_per_page
for (_, rtype), resource in resources_per_page.items():
if rtype == PDFResourceType.X_OBJECT and info["i"] in resource:
resource.remove(cast(int, info["i"]))
resource.remove(info["i"])
lowres_info = images.get(lowres_name)
if lowres_info: # Great, we've already done the job!
info = cast(RasterImageInfo, lowres_info)
lowres_w = float(info["w"]) # type: ignore[arg-type]
lowres_h = float(info["h"]) # type: ignore[arg-type]
info = lowres_info
lowres_w = info["w"]
lowres_h = info["h"]
if lowres_w * lowres_h < dims[0] * dims[1]:
# The existing low-res image is too small, we need a bigger low-res image:
cached_i = info["i"]
cached_usages = info["usages"]
info.update(
get_img_info(
name,
img or load_image(name),
self.image_cache.image_filter,
dims,
)
),
)
info["i"] = cached_i
info["usages"] = cached_usages
LOGGER.debug(
"OVERSIZED: Updated low-res image with name=%s id=%d to dims=%s",
lowres_name,
info["i"],
dims,
)
info["usages"] += 1 # type: ignore[operator]
info["usages"] += 1
else:
info = RasterImageInfo(
get_img_info(
name,
img or load_image(name),
self.image_cache.image_filter,
dims,
)
info = get_img_info(
name,
img or load_image(name),
self.image_cache.image_filter,
dims,
)
info["i"] = len(images) + 1
info["usages"] = 1
Expand Down
Loading