Skip to content

Commit

Permalink
Supporting <text> tags from SVG files
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Nov 20, 2023
1 parent e23e00d commit 3f147c7
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 24 deletions.
68 changes: 45 additions & 23 deletions fpdf/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,21 +342,14 @@ def new_path(tag, clipping_path: bool = False):
if clipping_path:
path = ClippingPath()
apply_styles(path, tag)

return path

@classmethod
def rect(cls, tag, clipping_path: bool = False):
"""Convert an SVG <rect> into a PDF path."""
# svg rect is wound clockwise
if "x" in tag.attrib:
x = resolve_length(tag.attrib["x"])
else:
x = 0
if "y" in tag.attrib:
y = resolve_length(tag.attrib["y"])
else:
y = 0
x = resolve_length(tag.attrib.get("x", 0))
y = resolve_length(tag.attrib.get("y", 0))
width = tag.attrib.get("width", "0")
if width.endswith("%"):
width = Percent(width[:-1])
Expand Down Expand Up @@ -397,7 +390,6 @@ def rect(cls, tag, clipping_path: bool = False):
ry = height / 2

path = cls.new_path(tag, clipping_path)

path.rectangle(x, y, width, height, rx, ry)
return path

Expand All @@ -409,7 +401,6 @@ def circle(cls, tag, clipping_path: bool = False):
r = float(tag.attrib["r"])

path = cls.new_path(tag, clipping_path)

path.circle(cx, cy, r)
return path

Expand Down Expand Up @@ -447,34 +438,24 @@ def line(cls, tag):
y2 = float(tag.attrib["y2"])

path = cls.new_path(tag)

path.move_to(x1, y1)
path.line_to(x2, y2)

return path

@classmethod
def polyline(cls, tag):
"""Convert an SVG <polyline> into a PDF path."""
points = tag.attrib["points"]

path = cls.new_path(tag)

points = "M" + points
points = "M" + tag.attrib["points"]
svg_path_converter(path, points)

return path

@classmethod
def polygon(cls, tag, clipping_path: bool = False):
"""Convert an SVG <polygon> into a PDF path."""
points = tag.attrib["points"]

path = cls.new_path(tag, clipping_path)

points = "M" + points + "Z"
points = "M" + tag.attrib["points"] + "Z"
svg_path_converter(path, points)

return path


Expand Down Expand Up @@ -878,6 +859,8 @@ def handle_defs(self, defs):
self.build_path(child)
elif child.tag in xmlns_lookup("svg", "image"):
self.build_image(child)
elif child.tag in xmlns_lookup("svg", "text"):
self.build_text(child)
elif child.tag in shape_tags:
self.build_shape(child)
elif child.tag in xmlns_lookup("svg", "clipPath"):
Expand Down Expand Up @@ -947,6 +930,8 @@ def build_group(self, group, pdf_group=None):
pdf_group.add_item(self.build_xref(child))
elif child.tag in xmlns_lookup("svg", "image"):
pdf_group.add_item(self.build_image(child))
elif child.tag in xmlns_lookup("svg", "text"):
pdf_group.add_item(self.build_text(child))
else:
LOGGER.debug("Unsupported SVG tag: <%s>", child.tag)

Expand Down Expand Up @@ -987,6 +972,43 @@ def apply_clipping_path(self, stylable, svg_element):
clipping_path_id = re.search(r"url\((\#\w+)\)", clipping_path)
stylable.clipping_path = self.cross_references[clipping_path_id[1]]

@force_nodocument
def build_text(self, text):
if "dx" in text.attrib or "dy" in text.attrib:
raise NotImplementedError(
'"dx" / "dy" defined on <text> is currently not supported (but contributions are welcome!)'
)
if "lengthAdjust" in text.attrib:
raise NotImplementedError(
'"lengthAdjust" defined on <text> is currently not supported (but contributions are welcome!)'
)
if "rotate" in text.attrib:
raise NotImplementedError(
'"rotate" defined on <text> is currently not supported (but contributions are welcome!)'
)
if "style" in text.attrib:
raise NotImplementedError(
'"style" defined on <text> is currently not supported (but contributions are welcome!)'
)
if "textLength" in text.attrib:
raise NotImplementedError(
'"textLength" defined on <text> is currently not supported (but contributions are welcome!)'
)
if "transform" in text.attrib:
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
self.update_xref(text.attrib.get("id"), svg_text)
return svg_text

@force_nodocument
def build_image(self, image):
href = None
Expand Down
Binary file added test/svg/generated_pdf/text-samples.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions test/svg/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ def Gs(**kwargs):
svgfile("use-image-def.svg"),
id="Use xlink:href to insert an <image> from <defs>",
),
pytest.param(svgfile("text-samples.svg"), id="<text> tests"),
)

svg_path_edge_cases = (
Expand Down
2 changes: 1 addition & 1 deletion test/svg/svg_sources/embedded-raster-images.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions test/svg/svg_sources/text-samples.svg
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 3f147c7

Please sign in to comment.