From 61f37656866998648815d99e8e9ea21781ba03b4 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:53:34 +0100 Subject: [PATCH] WIP --- fpdf/drawing.py | 54 ++++++++++----------- fpdf/fonts.py | 4 +- fpdf/fpdf.py | 3 +- fpdf/svg.py | 62 ++++++++++++++++++------ test/svg/generated_pdf/text-samples.pdf | Bin 1241 -> 1247 bytes test/svg/svg_sources/text-samples.svg | 8 ++- 6 files changed, 84 insertions(+), 47 deletions(-) diff --git a/fpdf/drawing.py b/fpdf/drawing.py index 29ee6cd06..81cfc0e00 100644 --- a/fpdf/drawing.py +++ b/fpdf/drawing.py @@ -3104,13 +3104,13 @@ class DrawingContext: def __init__(self): self._subitems = [] - def add_item(self, item, _copy=True): + def add_item(self, item, clone=True): """ Append an item to this drawing context Args: item (GraphicsContext, PaintedPath): the item to be appended. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. @@ -3119,7 +3119,7 @@ def add_item(self, item, _copy=True): if not isinstance(item, (GraphicsContext, PaintedPath)): raise TypeError(f"{item} doesn't belong in a DrawingContext") - if _copy: + if clone: item = copy.deepcopy(item) self._subitems.append(item) @@ -3358,24 +3358,24 @@ def transform_group(self, transform): ctxt.transform = transform yield self - def add_path_element(self, item, _copy=True): + def add_path_element(self, item, clone=True): """ Add the given element as a path item of this path. Args: item: the item to add to this path. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ if self._starter_move is not None: self._closed = False - self._graphics_context.add_item(self._starter_move, _copy=False) + self._graphics_context.add_item(self._starter_move, clone=False) self._close_context = self._graphics_context self._starter_move = None - self._graphics_context.add_item(item, _copy=_copy) + self._graphics_context.add_item(item, clone=clone) def remove_last_path_element(self): self._graphics_context.remove_last_item() @@ -3405,7 +3405,7 @@ def rectangle(self, x, y, w, h, rx=0, ry=0): self._insert_implicit_close_if_open() self.add_path_element( - RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), _copy=False + RoundedRectangle(Point(x, y), Point(w, h), Point(rx, ry)), clone=False ) self._closed = True self.move_to(x, y) @@ -3440,7 +3440,7 @@ def ellipse(self, cx, cy, rx, ry): The path, to allow chaining method calls. """ self._insert_implicit_close_if_open() - self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), _copy=False) + self.add_path_element(Ellipse(Point(rx, ry), Point(cx, cy)), clone=False) self._closed = True self.move_to(cx, cy) @@ -3484,7 +3484,7 @@ def move_relative(self, x, y): self._insert_implicit_close_if_open() if self._starter_move is not None: self._closed = False - self._graphics_context.add_item(self._starter_move, _copy=False) + self._graphics_context.add_item(self._starter_move, clone=False) self._close_context = self._graphics_context self._starter_move = RelativeMove(Point(x, y)) return self @@ -3500,7 +3500,7 @@ def line_to(self, x, y): Returns: The path, to allow chaining method calls. """ - self.add_path_element(Line(Point(x, y)), _copy=False) + self.add_path_element(Line(Point(x, y)), clone=False) return self def line_relative(self, dx, dy): @@ -3517,7 +3517,7 @@ def line_relative(self, dx, dy): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeLine(Point(dx, dy)), _copy=False) + self.add_path_element(RelativeLine(Point(dx, dy)), clone=False) return self def horizontal_line_to(self, x): @@ -3531,7 +3531,7 @@ def horizontal_line_to(self, x): Returns: The path, to allow chaining method calls. """ - self.add_path_element(HorizontalLine(x), _copy=False) + self.add_path_element(HorizontalLine(x), clone=False) return self def horizontal_line_relative(self, dx): @@ -3547,7 +3547,7 @@ def horizontal_line_relative(self, dx): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeHorizontalLine(dx), _copy=False) + self.add_path_element(RelativeHorizontalLine(dx), clone=False) return self def vertical_line_to(self, y): @@ -3561,7 +3561,7 @@ def vertical_line_to(self, y): Returns: The path, to allow chaining method calls. """ - self.add_path_element(VerticalLine(y), _copy=False) + self.add_path_element(VerticalLine(y), clone=False) return self def vertical_line_relative(self, dy): @@ -3577,7 +3577,7 @@ def vertical_line_relative(self, dy): Returns: The path, to allow chaining method calls. """ - self.add_path_element(RelativeVerticalLine(dy), _copy=False) + self.add_path_element(RelativeVerticalLine(dy), clone=False) return self def curve_to(self, x1, y1, x2, y2, x3, y3): @@ -3599,7 +3599,7 @@ def curve_to(self, x1, y1, x2, y2, x3, y3): ctrl2 = Point(x2, y2) end = Point(x3, y3) - self.add_path_element(BezierCurve(ctrl1, ctrl2, end), _copy=False) + self.add_path_element(BezierCurve(ctrl1, ctrl2, end), clone=False) return self def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): @@ -3633,7 +3633,7 @@ def curve_relative(self, dx1, dy1, dx2, dy2, dx3, dy3): c2d = Point(dx2, dy2) end = Point(dx3, dy3) - self.add_path_element(RelativeBezierCurve(c1d, c2d, end), _copy=False) + self.add_path_element(RelativeBezierCurve(c1d, c2d, end), clone=False) return self def quadratic_curve_to(self, x1, y1, x2, y2): @@ -3651,7 +3651,7 @@ def quadratic_curve_to(self, x1, y1, x2, y2): """ ctrl = Point(x1, y1) end = Point(x2, y2) - self.add_path_element(QuadraticBezierCurve(ctrl, end), _copy=False) + self.add_path_element(QuadraticBezierCurve(ctrl, end), clone=False) return self def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): @@ -3673,7 +3673,7 @@ def quadratic_curve_relative(self, dx1, dy1, dx2, dy2): """ ctrl = Point(dx1, dy1) end = Point(dx2, dy2) - self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), _copy=False) + self.add_path_element(RelativeQuadraticBezierCurve(ctrl, end), clone=False) return self def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): @@ -3720,7 +3720,7 @@ def arc_to(self, rx, ry, rotation, large_arc, positive_sweep, x, y): end = Point(x, y) self.add_path_element( - Arc(radii, rotation, large_arc, positive_sweep, end), _copy=False + Arc(radii, rotation, large_arc, positive_sweep, end), clone=False ) return self @@ -3769,7 +3769,7 @@ def arc_relative(self, rx, ry, rotation, large_arc, positive_sweep, dx, dy): end = Point(dx, dy) self.add_path_element( - RelativeArc(radii, rotation, large_arc, positive_sweep, end), _copy=False + RelativeArc(radii, rotation, large_arc, positive_sweep, end), clone=False ) return self @@ -3777,13 +3777,13 @@ def close(self): """ Explicitly close the current (sub)path. """ - self.add_path_element(Close(), _copy=False) + self.add_path_element(Close(), clone=False) self._closed = True self.move_relative(0, 0) def _insert_implicit_close_if_open(self): if not self._closed: - self._close_context.add_item(ImplicitClose(), _copy=False) + self._close_context.add_item(ImplicitClose(), clone=False) self._close_context = self._graphics_context self._closed = True @@ -3970,19 +3970,19 @@ def clipping_path(self): def clipping_path(self, new_clipath): self._clipping_path = new_clipath - def add_item(self, item, _copy=True): + def add_item(self, item, clone=True): """ Add a path element to this graphics context. Args: item: the path element to add. May be a primitive element or another `GraphicsContext` or a `PaintedPath`. - _copy (bool): if true (the default), the item will be copied before being + clone (bool): if true (the default), the item will be copied before being appended. This prevents modifications to a referenced object from "retroactively" altering its style/shape and should be disabled with caution. """ - if _copy: + if clone: item = copy.deepcopy(item) self.path_items.append(item) diff --git a/fpdf/fonts.py b/fpdf/fonts.py index ecc501f13..6328474f6 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -207,8 +207,8 @@ class CoreFont: "emphasis", ) - def __init__(self, fpdf, fontkey, style): - self.i = len(fpdf.fonts) + 1 + def __init__(self, i, fontkey, style): + self.i = i self.type = "core" self.name = CORE_FONTS[fontkey] self.sp = 250 # strikethrough horizontal position diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index eec4badc6..1994487e8 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -364,7 +364,6 @@ def __init__( self._current_draw_context = None self._drawing_graphics_state_registry = GraphicsStateDictRegistry() - # map page numbers to a set of GraphicsState names: self._record_text_quad_points = False self._resource_catalog = ResourceCatalog() @@ -2146,7 +2145,7 @@ def set_font(self, family=None, style: Union[str, TextEmphasis] = "", size=0): f"Use built-in fonts or FPDF.add_font() beforehand" ) # If it's one of the core fonts, add it to self.fonts - self.fonts[fontkey] = CoreFont(self, fontkey, style) + self.fonts[fontkey] = CoreFont(len(self.fonts) + 1, fontkey, style) # Select it self.font_family = family diff --git a/fpdf/svg.py b/fpdf/svg.py index f4c1e3818..bb7b9f9b6 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -919,17 +919,17 @@ def build_group(self, group, pdf_group=None): if child.tag in xmlns_lookup("svg", "defs"): self.handle_defs(child) elif child.tag in xmlns_lookup("svg", "g"): - pdf_group.add_item(self.build_group(child), False) + pdf_group.add_item(self.build_group(child), clone=False) elif child.tag in xmlns_lookup("svg", "path"): - pdf_group.add_item(self.build_path(child), False) + pdf_group.add_item(self.build_path(child), clone=False) elif child.tag in shape_tags: - pdf_group.add_item(self.build_shape(child), False) + pdf_group.add_item(self.build_shape(child), clone=False) elif child.tag in xmlns_lookup("svg", "use"): - pdf_group.add_item(self.build_xref(child), False) + pdf_group.add_item(self.build_xref(child), clone=False) elif child.tag in xmlns_lookup("svg", "image"): - pdf_group.add_item(self.build_image(child), False) + pdf_group.add_item(self.build_image(child), clone=False) elif child.tag in xmlns_lookup("svg", "text"): - pdf_group.add_item(self.build_text(child), False) + pdf_group.add_item(self.build_text(child), clone=False) else: LOGGER.warning( "Ignoring unsupported SVG tag: <%s> (contributions are welcome to add support for it)", @@ -1014,14 +1014,14 @@ def build_text(self, text): raise NotImplementedError( '"transform" defined on is currently not supported (but contributions are welcome!)' ) - font_family = text.attrib.get("font-family") - font_size = text.attrib.get("font-size") - # TODO: reuse code from line_break & text_region modules. - # We could either: - # 1. handle text regions in this module (svg), with a dedicated SVGText class. - # 2. handle text regions in the drawing module, maybe by defining a PaintedPath.text() method. - # This may be the best approach, as we would benefit from the global transformation performed in SVGObject.transform_to_rect_viewport() - svg_text = None + svg_text = SVGText( + text=text.text, + x=float(text.attrib.get("x", "0")), + y=float(text.attrib.get("y", "0")), + font_family=text.attrib.get("font-family"), + font_size=text.attrib.get("font-size"), + svg_obj=self, + ) self.update_xref(text.attrib.get("id"), svg_text) return svg_text @@ -1061,6 +1061,40 @@ def build_image(self, image): return svg_image +class SVGText(NamedTuple): + text: str + x: Number + y: Number + font_family: str + font_size: Number + svg_obj: SVGObject + + def __deepcopy__(self, _memo): + # Defining this method is required to avoid the .svg_obj reference to be cloned: + return SVGText( + text=self.text, + x=self.x, + y=self.y, + font_family=self.font_family, + font_size=self.font_size, + svg_obj=self.svg_obj, + ) + + @force_nodocument + def render(self, _gsd_registry, _style, last_item, initial_point): + # TODO: + # * handle font_family & font_size + # * invoke current_font.encode_text(self.text) + # * set default font if not font set? + # We need to perform a mirror transform AND invert the Y-axis coordinates, + # so that the text is not horizontally mirrored, + # due to the transformation made by DrawingContext._setup_render_prereqs(): + stream_content = ( + f"q 1 0 0 -1 0 0 cm BT {self.x:.2f} {-self.y:.2f} Td ({self.text}) Tj ET Q" + ) + return stream_content, last_item, initial_point + + class SVGImage(NamedTuple): href: str x: Number diff --git a/test/svg/generated_pdf/text-samples.pdf b/test/svg/generated_pdf/text-samples.pdf index 8124b0b1f2689e7e649bae89e632024875c74d59..02ec1dedeebe012bef16b1c51b8405f9c39c2604 100644 GIT binary patch delta 395 zcmcb~d7pDb1!KL15tp4ES8+*EYGN)|#hli@(_DuPL|nfA?J8W(ojl2`HitV%ASPq8 zu=pjfwE|i4N`I>-hb$3W5_5=Wrsd9uH!P=gFz7F5W4^lC?2z;Hiha+OgKwy4x?g-$ za?P<%YRZd<8=p?_`DV@9T6o4n*g!@&d&)MRK#^OQV$X0yme=c0AJ1p&zu(*u_EJ<&w?S#qnnn1m(?YuQU{1EuHEVEVlik z`B#hluPPBAC#y2`Pu|D0a&j(nQ;>zFf&mC9SXp=H?h;=0>I%ViqO_ zlO0)Xqs<)6oLnp{U5w2Q+$@ZYEes9K4b04p-GGFNg`26JjX5C|v5H~7rUjZE;wwCk66%Q=dspOb+R>6KNpw2i$ZjCtb#%8 z + Text without any attributes + My - cat + cat is - Grumpy! + Grumpy! + + Bottom right