Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Jan 22, 2025
1 parent f20c411 commit 6e9328a
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 47 deletions.
54 changes: 27 additions & 27 deletions fpdf/drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -3769,21 +3769,21 @@ 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

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

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions fpdf/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
62 changes: 48 additions & 14 deletions fpdf/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -1014,14 +1014,14 @@ def build_text(self, text):
raise NotImplementedError(
'"transform" defined on <text> 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

Expand Down Expand Up @@ -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
Expand Down
Binary file modified test/svg/generated_pdf/text-samples.pdf
Binary file not shown.
6 changes: 4 additions & 2 deletions test/svg/svg_sources/text-samples.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 6e9328a

Please sign in to comment.