From a594b84c715d569e5f793fd6ba76c38e4004af15 Mon Sep 17 00:00:00 2001 From: Andy Friedman Date: Mon, 16 Oct 2023 13:01:33 -0400 Subject: [PATCH 1/8] SVG tags now read before any other tags are read, shapes in tags now work, added additional installs to Development.md --- .gitignore | 6 ++++++ docs/Development.md | 2 ++ fpdf/svg.py | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index a52401320..e3cf8866d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ # my files .env +testo.py +test_svgs.py +clipping-tests/* +.DS_Store +.ipynb_checkpoints/* +fpdf_scrap.ipynb # codecov.io coverage.xml diff --git a/docs/Development.md b/docs/Development.md index 15755b1d5..f49bb18e1 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -70,6 +70,8 @@ pre-commit install To run tests, `cd` into `fpdf2` repository, install the dependencies using `pip install -r test/requirements.txt`, and run `pytest`. +You may also need to install [SWIG](https://swig.org/index.html) and [Ghostscript](https://www.ghostscript.com/). + You can run a single test by executing: `pytest -k function_name`. Alternatively, you can use [Tox](https://tox.readthedocs.io/en/latest/). diff --git a/fpdf/svg.py b/fpdf/svg.py index 491ecc4c5..e750ab221 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -859,6 +859,10 @@ def handle_defs(self, defs): self.build_group(child) if child.tag in xmlns_lookup("svg", "path"): self.build_path(child) + elif child.tag in shape_tags: + self.build_shape(child) + + # We could/should also support that are rect, circle, ellipse, line, polyline, polygon... # this assumes xrefs only reference already-defined ids. @@ -869,7 +873,7 @@ def build_xref(self, xref): pdf_group = GraphicsContext() apply_styles(pdf_group, xref) - for candidate in xmlns_lookup("xlink", "href"): + for candidate in xmlns_lookup("xlink", "href", "id"): try: ref = xref.attrib[candidate] break @@ -882,7 +886,7 @@ def build_xref(self, xref): pdf_group.add_item(self.cross_references[ref]) except KeyError: raise ValueError( - f"use {xref} references nonexistent ref id {ref}" + f"use {xref} references nonexistent ref id {ref}, existing refs: {self.cross_references.keys()}" ) from None if "x" in xref.attrib or "y" in xref.attrib: @@ -901,15 +905,18 @@ def build_group(self, group, pdf_group=None): pdf_group = GraphicsContext() apply_styles(pdf_group, group) + # handle defs before anything else + # TODO: add test + for child in [child for child in group if child.tag in xmlns_lookup("svg", "defs")]: + self.handle_defs(child) + for child in group: - if child.tag in xmlns_lookup("svg", "defs"): - self.handle_defs(child) if child.tag in xmlns_lookup("svg", "g"): pdf_group.add_item(self.build_group(child)) if child.tag in xmlns_lookup("svg", "path"): pdf_group.add_item(self.build_path(child)) elif child.tag in shape_tags: - pdf_group.add_item(getattr(ShapeBuilder, shape_tags[child.tag])(child)) + pdf_group.add_item(self.build_shape(child)) if child.tag in xmlns_lookup("svg", "use"): pdf_group.add_item(self.build_xref(child)) @@ -919,6 +926,7 @@ def build_group(self, group, pdf_group=None): pass return pdf_group + @force_nodocument def build_path(self, path): @@ -937,3 +945,21 @@ def build_path(self, path): pass return pdf_path + + @force_nodocument + def build_shape(self, shape): + shape_path = getattr(ShapeBuilder, shape_tags[shape.tag])(shape) + + try: + self.cross_references["#" + shape.attrib["id"]] = shape_path + except KeyError: + pass + + return shape_path + + + def assign_to_xrefs(self, tag, assignable): + try: + self.cross_references["#" + tag.attrib["id"]] = assignable + except KeyError: + pass From 63195ad2f94c023d3db031bed8edc8b33436dd71 Mon Sep 17 00:00:00 2001 From: Andy Friedman Date: Mon, 16 Oct 2023 14:59:18 -0400 Subject: [PATCH 2/8] clip-path and clipPath implemented in SVG conversion --- fpdf/svg.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/fpdf/svg.py b/fpdf/svg.py index e750ab221..c36400b55 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -26,6 +26,7 @@ GraphicsContext, GraphicsStyle, PaintedPath, + ClippingPath, Transform, ) @@ -323,21 +324,22 @@ def apply_styles(stylable, svg_element): if tfstr: stylable.transform = convert_transforms(tfstr) - @force_nodocument class ShapeBuilder: """A namespace within which methods for converting basic shapes can be looked up.""" @staticmethod - def new_path(tag): + def new_path(tag, clipping_path: bool=False): """Create a new path with the appropriate styles.""" path = PaintedPath() + if clipping_path: + path = ClippingPath() apply_styles(path, tag) return path @classmethod - def rect(cls, tag): + def rect(cls, tag, clipping_path: bool=False): """Convert an SVG into a PDF path.""" # svg rect is wound clockwise if "x" in tag.attrib: @@ -387,7 +389,7 @@ def rect(cls, tag): if ry > (height / 2): ry = height / 2 - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) path.rectangle(x, y, width, height, rx, ry) return path @@ -861,6 +863,13 @@ def handle_defs(self, defs): self.build_path(child) elif child.tag in shape_tags: self.build_shape(child) + if child.tag in xmlns_lookup("svg", "clipPath"): + try: + clip_id = child.attrib['id'] + except KeyError: + clip_id = None + for child_ in child: + self.build_clipping_path(child_, clip_id) # We could/should also support that are rect, circle, ellipse, line, polyline, polygon... @@ -933,6 +942,7 @@ def build_path(self, path): """Convert an SVG tag into a PDF path object.""" pdf_path = PaintedPath() apply_styles(pdf_path, path) + self.apply_clipping_path(pdf_path, path) svg_path = path.attrib.get("d", None) @@ -945,10 +955,12 @@ def build_path(self, path): pass return pdf_path - + @force_nodocument def build_shape(self, shape): + """Convert an SVG shape tag into a PDF path object. Necessary to make xref (because ShapeBuilder doesn't have access to this object.)""" shape_path = getattr(ShapeBuilder, shape_tags[shape.tag])(shape) + self.apply_clipping_path(shape_path, shape) try: self.cross_references["#" + shape.attrib["id"]] = shape_path @@ -956,10 +968,20 @@ def build_shape(self, shape): pass return shape_path + + def build_clipping_path(self, shape, clip_id): + clipping_path_shape = getattr(ShapeBuilder, shape_tags[shape.tag])(shape, True) - - def assign_to_xrefs(self, tag, assignable): try: - self.cross_references["#" + tag.attrib["id"]] = assignable + self.cross_references["#" + clip_id] = clipping_path_shape except KeyError: pass + + return clipping_path_shape + + @force_nodocument + def apply_clipping_path(self, stylable, svg_element): + clipping_path = svg_element.attrib.get("clip-path") + if clipping_path: + clipping_path_id = re.search(r"url\((\#\w+)\)", clipping_path) + stylable.clipping_path = self.cross_references[clipping_path_id[1]] From 964965196be07c6bd51484c8167024c72c384e6c Mon Sep 17 00:00:00 2001 From: Andy Friedman Date: Mon, 16 Oct 2023 14:59:45 -0400 Subject: [PATCH 3/8] ran black --- fpdf/svg.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fpdf/svg.py b/fpdf/svg.py index c36400b55..41b7179de 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -324,12 +324,13 @@ def apply_styles(stylable, svg_element): if tfstr: stylable.transform = convert_transforms(tfstr) + @force_nodocument class ShapeBuilder: """A namespace within which methods for converting basic shapes can be looked up.""" @staticmethod - def new_path(tag, clipping_path: bool=False): + def new_path(tag, clipping_path: bool = False): """Create a new path with the appropriate styles.""" path = PaintedPath() if clipping_path: @@ -339,7 +340,7 @@ def new_path(tag, clipping_path: bool=False): return path @classmethod - def rect(cls, tag, clipping_path: bool=False): + def rect(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" # svg rect is wound clockwise if "x" in tag.attrib: @@ -865,12 +866,11 @@ def handle_defs(self, defs): self.build_shape(child) if child.tag in xmlns_lookup("svg", "clipPath"): try: - clip_id = child.attrib['id'] + clip_id = child.attrib["id"] except KeyError: clip_id = None for child_ in child: self.build_clipping_path(child_, clip_id) - # We could/should also support that are rect, circle, ellipse, line, polyline, polygon... @@ -916,7 +916,9 @@ def build_group(self, group, pdf_group=None): # handle defs before anything else # TODO: add test - for child in [child for child in group if child.tag in xmlns_lookup("svg", "defs")]: + for child in [ + child for child in group if child.tag in xmlns_lookup("svg", "defs") + ]: self.handle_defs(child) for child in group: @@ -935,7 +937,6 @@ def build_group(self, group, pdf_group=None): pass return pdf_group - @force_nodocument def build_path(self, path): @@ -968,7 +969,7 @@ def build_shape(self, shape): pass return shape_path - + def build_clipping_path(self, shape, clip_id): clipping_path_shape = getattr(ShapeBuilder, shape_tags[shape.tag])(shape, True) From d465cbdbcf7acc24ed871883aaefbce97d244545 Mon Sep 17 00:00:00 2001 From: Andy Friedman Date: Wed, 18 Oct 2023 12:55:59 -0400 Subject: [PATCH 4/8] todo fixed --- fpdf/svg.py | 46 ++- output_audit.py | 310 ++++++++++++++++++ test/svg/generated_pdf/actual.pdf | Bin 0 -> 1329 bytes test/svg/generated_pdf/actual_qpdf.pdf | Bin 0 -> 2023 bytes test/svg/generated_pdf/circle01_def_test.pdf | Bin 0 -> 1331 bytes .../circle01_def_test_no_def.pdf | Bin 0 -> 1329 bytes test/svg/generated_pdf/clip_path.pdf | Bin 0 -> 1259 bytes test/svg/generated_pdf/expected_qpdf.pdf | Bin 0 -> 2023 bytes test/svg/parameters.py | 6 +- test/svg/svg_sources/circle01_def_test.svg | 16 + test/svg/svg_sources/clip_path.svg | 18 + 11 files changed, 368 insertions(+), 28 deletions(-) create mode 100644 output_audit.py create mode 100644 test/svg/generated_pdf/actual.pdf create mode 100644 test/svg/generated_pdf/actual_qpdf.pdf create mode 100644 test/svg/generated_pdf/circle01_def_test.pdf create mode 100644 test/svg/generated_pdf/circle01_def_test_no_def.pdf create mode 100644 test/svg/generated_pdf/clip_path.pdf create mode 100644 test/svg/generated_pdf/expected_qpdf.pdf create mode 100644 test/svg/svg_sources/circle01_def_test.svg create mode 100644 test/svg/svg_sources/clip_path.svg diff --git a/fpdf/svg.py b/fpdf/svg.py index 41b7179de..8f5f93ab8 100644 --- a/fpdf/svg.py +++ b/fpdf/svg.py @@ -396,19 +396,19 @@ def rect(cls, tag, clipping_path: bool = False): return path @classmethod - def circle(cls, tag): + def circle(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" cx = float(tag.attrib.get("cx", 0)) cy = float(tag.attrib.get("cy", 0)) r = float(tag.attrib["r"]) - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) path.circle(cx, cy, r) return path @classmethod - def ellipse(cls, tag): + def ellipse(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" cx = float(tag.attrib.get("cx", 0)) cy = float(tag.attrib.get("cy", 0)) @@ -416,7 +416,7 @@ def ellipse(cls, tag): rx = tag.attrib.get("rx", "auto") ry = tag.attrib.get("ry", "auto") - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) if (rx == ry == "auto") or (rx == 0) or (ry == 0): return path @@ -460,11 +460,11 @@ def polyline(cls, tag): return path @classmethod - def polygon(cls, tag): + def polygon(cls, tag, clipping_path: bool = False): """Convert an SVG into a PDF path.""" points = tag.attrib["points"] - path = cls.new_path(tag) + path = cls.new_path(tag, clipping_path) points = "M" + points + "Z" svg_path_converter(path, points) @@ -668,6 +668,12 @@ def __init__(self, svg_text): self.extract_shape_info(svg_tree) self.convert_graphics(svg_tree) + @force_nodocument + def update_xref(self, key, referenced): + if key: + key = "#" + key if not key.startswith("#") else key + self.cross_references[key] = referenced + @force_nodocument def extract_shape_info(self, root_tag): """Collect shape info from the given SVG.""" @@ -872,8 +878,6 @@ def handle_defs(self, defs): for child_ in child: self.build_clipping_path(child_, clip_id) - # We could/should also support that are rect, circle, ellipse, line, polyline, polygon... - # this assumes xrefs only reference already-defined ids. # I don't know if this is required by the SVG spec. @force_nodocument @@ -895,7 +899,7 @@ def build_xref(self, xref): pdf_group.add_item(self.cross_references[ref]) except KeyError: raise ValueError( - f"use {xref} references nonexistent ref id {ref}, existing refs: {self.cross_references.keys()}" + f"use {xref} references nonexistent ref id {ref}" ) from None if "x" in xref.attrib or "y" in xref.attrib: @@ -915,7 +919,6 @@ def build_group(self, group, pdf_group=None): apply_styles(pdf_group, group) # handle defs before anything else - # TODO: add test for child in [ child for child in group if child.tag in xmlns_lookup("svg", "defs") ]: @@ -931,10 +934,7 @@ def build_group(self, group, pdf_group=None): if child.tag in xmlns_lookup("svg", "use"): pdf_group.add_item(self.build_xref(child)) - try: - self.cross_references["#" + group.attrib["id"]] = pdf_group - except KeyError: - pass + self.update_xref(group.attrib.get("id"), pdf_group) return pdf_group @@ -945,15 +945,12 @@ def build_path(self, path): apply_styles(pdf_path, path) self.apply_clipping_path(pdf_path, path) - svg_path = path.attrib.get("d", None) + svg_path = path.attrib.get("d") if svg_path is not None: svg_path_converter(pdf_path, svg_path) - try: - self.cross_references["#" + path.attrib["id"]] = pdf_path - except KeyError: - pass + self.update_xref(path.attrib.get("id"), pdf_path) return pdf_path @@ -963,20 +960,15 @@ def build_shape(self, shape): shape_path = getattr(ShapeBuilder, shape_tags[shape.tag])(shape) self.apply_clipping_path(shape_path, shape) - try: - self.cross_references["#" + shape.attrib["id"]] = shape_path - except KeyError: - pass + self.update_xref(shape.attrib.get("id"), shape_path) return shape_path + @force_nodocument def build_clipping_path(self, shape, clip_id): clipping_path_shape = getattr(ShapeBuilder, shape_tags[shape.tag])(shape, True) - try: - self.cross_references["#" + clip_id] = clipping_path_shape - except KeyError: - pass + self.update_xref(clip_id, clipping_path_shape) return clipping_path_shape diff --git a/output_audit.py b/output_audit.py new file mode 100644 index 000000000..377a7b126 --- /dev/null +++ b/output_audit.py @@ -0,0 +1,310 @@ +a = [ + b"%PDF-1.4", + b"%\xbf\xf7\xa2\xfe", + b"%QDF-1.0", + b"", + b"%% Original object ID: 2 0", + b"1 0 obj", + b"<<", + b" /OpenAction [", + b" 3 0 R", + b" /FitH", + b" null", + b" ]", + b" /PageLayout /OneColumn", + b" /Pages 4 0 R", + b" /Type /Catalog", + b">>", + b"endobj", + b"", + b"%% Original object ID: 10 0", + b"2 0 obj", + b"<<", + b" /CreationDate (D:19691231190000Z19'00')", + b">>", + b"endobj", + b"", + b"%% Page 1", + b"%% Original object ID: 3 0", + b"3 0 obj", + b"<<", + b" /Contents 5 0 R", + b" /Group <<", + b" /CS /DeviceRGB", + b" /S /Transparency", + b" /Type /Group", + b" >>", + b" /Parent 4 0 R", + b" /Resources 7 0 R", + b" /Type /Page", + b">>", + b"endobj", + b"", + b"%% Original object ID: 1 0", + b"4 0 obj", + b"<<", + b" /Count 1", + b" /Kids [", + b" 3 0 R", + b" ]", + b" /MediaBox [", + b" 0", + b" 0", + b" 340.16", + b" 113.39", + b" ]", + b" /Type /Pages", + b">>", + b"endobj", + b"", + b"%% Contents for page 1", + b"%% Original object ID: 4 0", + b"5 0 obj", + b"<<", + b" /Length 6 0 R", + b">>", + b"2 J", + b"0.57 w", + b"q 1 0 0 -1 0 113.3858 cm /GS3 gs [] 0 d q 0.2835 0 0 0.2835 -0 0 cm /GS0 gs q /GS1 gs 0 0 1 RG 0 0 m 1 1 1198 398 re S Q q /GS2 gs 1 0 0 rg 0 0 1 RG 0 0 m 700 200 m 700 255.2285 655.2285 300 600 300 c 544.7715 300 500 255.2285 500 200 c 500 144.7715 544.7715 100 600 100 c 655.2285 100 700 144.7715 700 200 c h B Q Q Q", + b"endstream", + b"endobj", + b"", + b"6 0 obj", + b"330", + b"endobj", + b"", + b"%% Original object ID: 9 0", + b"7 0 obj", + b"<<", + b" /ExtGState <<", + b" /GS0 8 0 R", + b" /GS1 9 0 R", + b" /GS2 10 0 R", + b" /GS3 11 0 R", + b" >>", + b" /ProcSet [", + b" /PDF", + b" /Text", + b" /ImageB", + b" /ImageC", + b" /ImageI", + b" ]", + b">>", + b"endobj", + b"", + b"%% Original object ID: 5 0", + b"8 0 obj", + b"<<", + b" /LC 0", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 6 0", + b"9 0 obj", + b"<<", + b" /LW 2", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 7 0", + b"10 0 obj", + b"<<", + b" /LW 10", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 8 0", + b"11 0 obj", + b"<<", + b" /D [", + b" [", + b" ]", + b" 0", + b" ]", + b" /LW 0.567", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"xref", + b"0 12", + b"0000000000 65535 f ", + b"0000000052 00000 n ", + b"0000000208 00000 n ", + b"0000000309 00000 n ", + b"0000000499 00000 n ", + b"0000000673 00000 n ", + b"0000001058 00000 n ", + b"0000001105 00000 n ", + b"0000001309 00000 n ", + b"0000001385 00000 n ", + b"0000001461 00000 n ", + b"0000001539 00000 n ", + b"trailer <<", + b" /Info 2 0 R", + b" /Root 1 0 R", + b" /Size 12", + b" /ID [<4dadfd0c58a5e5ee5c563f500783c5af>]", + b">>", + b"startxref", + b"1622", + b"%%EOF", +] +b = [ + b"%PDF-1.4", + b"%\xbf\xf7\xa2\xfe", + b"%QDF-1.0", + b"", + b"%% Original object ID: 2 0", + b"1 0 obj", + b"<<", + b" /OpenAction [", + b" 3 0 R", + b" /FitH", + b" null", + b" ]", + b" /PageLayout /OneColumn", + b" /Pages 4 0 R", + b" /Type /Catalog", + b">>", + b"endobj", + b"", + b"%% Original object ID: 10 0", + b"2 0 obj", + b"<<", + b" /CreationDate (D:20231017193139Z19'31')", + b">>", + b"endobj", + b"", + b"%% Page 1", + b"%% Original object ID: 3 0", + b"3 0 obj", + b"<<", + b" /Contents 5 0 R", + b" /Group <<", + b" /CS /DeviceRGB", + b" /S /Transparency", + b" /Type /Group", + b" >>", + b" /Parent 4 0 R", + b" /Resources 7 0 R", + b" /Type /Page", + b">>", + b"endobj", + b"", + b"%% Original object ID: 1 0", + b"4 0 obj", + b"<<", + b" /Count 1", + b" /Kids [", + b" 3 0 R", + b" ]", + b" /MediaBox [", + b" 0", + b" 0", + b" 340.16", + b" 113.39", + b" ]", + b" /Type /Pages", + b">>", + b"endobj", + b"", + b"%% Contents for page 1", + b"%% Original object ID: 4 0", + b"5 0 obj", + b"<<", + b" /Length 6 0 R", + b">>", + b"2 J", + b"0.57 w", + b"q 1 0 0 -1 0 113.3858 cm /GS3 gs [] 0 d q 0.2835 0 0 0.2835 -0 0 cm /GS0 gs q /GS1 gs 0 0 1 RG 0 0 m 1 1 1198 398 re S Q q /GS2 gs 1 0 0 rg 0 0 1 RG 0 0 m 700 200 m 700 255.2285 655.2285 300 600 300 c 544.7715 300 500 255.2285 500 200 c 500 144.7715 544.7715 100 600 100 c 655.2285 100 700 144.7715 700 200 c h B Q Q Q", + b"endstream", + b"endobj", + b"", + b"6 0 obj", + b"330", + b"endobj", + b"", + b"%% Original object ID: 9 0", + b"7 0 obj", + b"<<", + b" /ExtGState <<", + b" /GS0 8 0 R", + b" /GS1 9 0 R", + b" /GS2 10 0 R", + b" /GS3 11 0 R", + b" >>", + b" /ProcSet [", + b" /PDF", + b" /Text", + b" /ImageB", + b" /ImageC", + b" /ImageI", + b" ]", + b">>", + b"endobj", + b"", + b"%% Original object ID: 5 0", + b"8 0 obj", + b"<<", + b" /LC 0", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 6 0", + b"9 0 obj", + b"<<", + b" /LW 2", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 7 0", + b"10 0 obj", + b"<<", + b" /LW 10", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"%% Original object ID: 8 0", + b"11 0 obj", + b"<<", + b" /D [", + b" [", + b" ]", + b" 0", + b" ]", + b" /LW 0.567", + b" /Type /ExtGState", + b">>", + b"endobj", + b"", + b"xref", + b"0 12", + b"0000000000 65535 f ", + b"0000000052 00000 n ", + b"0000000208 00000 n ", + b"0000000309 00000 n ", + b"0000000499 00000 n ", + b"0000000673 00000 n ", + b"0000001058 00000 n ", + b"0000001105 00000 n ", + b"0000001309 00000 n ", + b"0000001385 00000 n ", + b"0000001461 00000 n ", + b"0000001539 00000 n ", + b"trailer <<", + b" /Info 2 0 R", + b" /Root 1 0 R", + b" /Size 12", + b" /ID [<9c8b942c79f09e4c428cff498685f1e1>]", + b">>", + b"startxref", + b"1622", + b"%%EOF", +] diff --git a/test/svg/generated_pdf/actual.pdf b/test/svg/generated_pdf/actual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8b27299b3dd468bbc52c2a999aa96436e51cf190 GIT binary patch literal 1329 zcmah}U1-x#6jlUbVTwZp!GTv~Owl#DO_MI8Ox9%Ws?&{D8C$Io+q>D>B_+9a-4OAI zsVE}bgZMB-5mZD4^+Ax~&m0Oq2tN3OBK{yEh=ZZv)4+4nb`2f4`;z3`^YeY@d?$6C zTBregd>lpsGWYg#fdI#=X3n4pbNse+%0_V!uA&K!-%e6#J!s}pTmXyc6FgW(7>gcJ zf!m!!S%UaZy^q*jb2CSb6ierIrR~TPqa{hxrcum^_)wa*AtRT`Kmr!pt`C_xiuewL zsAeWNU=++YV(_A+BC1aHjM?WZ!yuN?8vuOlY8<>a^`)o#>EciQOP_3g-E?wdW_11Ws%y9ZuI>t4v}Pu&_wW8QKDFk} zi|+niO`Ar?_NNmQg^A_pl1lT~1ITQ|(B}tS}FDl!B zB0I<^+Z3QvVZ+LNKM^uj`bmD^UlA*NVM!`&^88po<6Gh9m!XLjwnhc^sAQv-p3V@< z`7y=P!-VnWqozqgF3un#Mz4vSPQp|oFynycll?6rtwqI3t53pS!7GO}zoaUX)S{}I Z+`O=dGbNkq7InI;NHSMf*V+-{{sH0>f5HF& literal 0 HcmV?d00001 diff --git a/test/svg/generated_pdf/actual_qpdf.pdf b/test/svg/generated_pdf/actual_qpdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..69f5321ec6dc428ef6871bd3e386aaef3b6e0285 GIT binary patch literal 2023 zcmbVN!EW0)5WVv&=3->`5Q~&V$tr>XjooEef4%0+VKySgDuL4+O zfVVi2!X5CjKM81gyc_Y9T>_9UcuI;Q}MTn|iF}wCgtFni3EM{HYF1Fke)ceZF4Y+xf~;_jTPcUvZeAO?yHR zzv$KveT02H4v^?}+_pqj>Xor=F?x7@x$^UF%)M=-#TKio?KCG(kMs*yp--PCSQw0O z_OV<|QH5>Nl=dOAL3uycbx0wjAdl912mbG^h5Rzd&<~4=-$aHHGRJSf+N@=^In1e< z!Nu*``sy9_ij3kXu-{iEV_0cEqjR>os1UK;GC1fY9@cb0anSi35e;+!NA3o?F!@;* z#dt3NXeT)K{!oX?pLCDxAp-2pp>G&rF%I^S!4Vc)seEIqrrfQWK$i z=)t2P=t1gD)Qd`Mt;HhJTBW5IK|zQJ)*d{Hpm4CKlJx!4Z9srE@vbMsq~(Owo*?S8^ggl%ZWn&u6ocfF14DibkFyKB^Pd z$mU1&GHfA6FSb!c)u@&=(vC8WVwJs4iB2u#E}sdTMm~ppz6!(gCm%+Z99cV*2ON>}X-b@h-DNZVkw1WpLxxpip$x>p1#;it3fTWB; z!tvciPt#$9JuY_2q9)Nsxx!@f`qbV=H3n1XyRW(kuwgEsoO#lMHbJ+(C{7loygENtDnKs$w9KEu>d5 z1?1FVSlJpULZ;d{$+I<1cEJs-$tTOury9(I;fGL*d7uY1nAgdVnp!4HO#9aq&s-)< zFCQ}u3U;vv5z&VXWOov#5`h`})Sy@OyRj-Mepg6#OM(>gi&%0C*sBEnA*Eyc2zyEv S)l6!4S&_V4Q&TV+;{E{)iGKG0 literal 0 HcmV?d00001 diff --git a/test/svg/generated_pdf/circle01_def_test_no_def.pdf b/test/svg/generated_pdf/circle01_def_test_no_def.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8b27299b3dd468bbc52c2a999aa96436e51cf190 GIT binary patch literal 1329 zcmah}U1-x#6jlUbVTwZp!GTv~Owl#DO_MI8Ox9%Ws?&{D8C$Io+q>D>B_+9a-4OAI zsVE}bgZMB-5mZD4^+Ax~&m0Oq2tN3OBK{yEh=ZZv)4+4nb`2f4`;z3`^YeY@d?$6C zTBregd>lpsGWYg#fdI#=X3n4pbNse+%0_V!uA&K!-%e6#J!s}pTmXyc6FgW(7>gcJ zf!m!!S%UaZy^q*jb2CSb6ierIrR~TPqa{hxrcum^_)wa*AtRT`Kmr!pt`C_xiuewL zsAeWNU=++YV(_A+BC1aHjM?WZ!yuN?8vuOlY8<>a^`)o#>EciQOP_3g-E?wdW_11Ws%y9ZuI>t4v}Pu&_wW8QKDFk} zi|+niO`Ar?_NNmQg^A_pl1lT~1ITQ|(B}tS}FDl!B zB0I<^+Z3QvVZ+LNKM^uj`bmD^UlA*NVM!`&^88po<6Gh9m!XLjwnhc^sAQv-p3V@< z`7y=P!-VnWqozqgF3un#Mz4vSPQp|oFynycll?6rtwqI3t53pS!7GO}zoaUX)S{}I Z+`O=dGbNkq7InI;NHSMf*V+-{{sH0>f5HF& literal 0 HcmV?d00001 diff --git a/test/svg/generated_pdf/clip_path.pdf b/test/svg/generated_pdf/clip_path.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50db007360e6c7d60b5cb22a3ebd38648a88144e GIT binary patch literal 1259 zcmah}PiWIn92VRd3)>DNqDRPQPV1p_?2(&^jM+(r?h*HuE7_d1o8U z5u|1D8308$%`&lplrg4Q9-M(4&$zb0&s(2P&h0(?t*bn@trMR*5NUoDyK{45C3R07 z9zA;E``?S(uf$P%%kTMJO}$4}yq~9McEgApKYjjd@rQk?B{Q;g`|G|pBfSq|3vWj! zZ(#3Z+}h>s=?@l{zfA7Qwnw7uQrE*B_nTjTyc-x_>|VXpR_=R|D^Fb;-#Ytb;rteH zx^?0mTx~C#m*q#xS0{`P+u-E;b00c=FH1|d+3WLvc6J7usJu1sHKASW%*kLYs}LyS zP#hEPgteb|>q{a0H})8CSYpLZVmZ4@5E)hmi9`}`NlPZxX-$iDEOZY-aSl+pWS>HD z2q>D$s{_zxv5NMFo{A$35-hnNgb*r{r3{Y|v3<3tzxE6#E%Hm2mVP2F-F2Bv0PL6h zp(IGq!9yr<Jw%?|jRu?BX~BXGMa?0j@=uAY7ZByf#&jJMg)1`;YZ)ET zp^^EAiG&_E^vXd&kesrUcX3Wpa5+TL3msfgboc@?Kd^a3MGq4z2CfWL;zY(~3q*s= EKZ4_EKL7v# literal 0 HcmV?d00001 diff --git a/test/svg/generated_pdf/expected_qpdf.pdf b/test/svg/generated_pdf/expected_qpdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..69f5321ec6dc428ef6871bd3e386aaef3b6e0285 GIT binary patch literal 2023 zcmbVN!EW0)5WVv&=3->`5Q~&V$tr>XjooEef4%0+VKySgDuL4+O zfVVi2!X5CjKM81gyc_Y9T>_9UcuI;Q}MTn|iF}wCgtFni3EM{HYF1Fke)ceZF4Y+xf~;_jTPcUvZeAO?yHR zzv$KveT02H4v^?}+_pqj>Xor=F?x7@x$^UF%)M=-#TKio?KCG(kMs*yp--PCSQw0O z_OV<|QH5>Nl=dOAL3uycbx0wjAdl912mbG^h5Rzd&<~4=-$aHHGRJSf+N@=^In1e< z!Nu*``sy9_ij3kXu-{iEV_0cEqjR>os1UK;GC1fY9@cb0anSi35e;+!NA3o?F!@;* z#dt3NXeT)K{!oX?pLCDxAp-2pp>G&rF%I^S!4Vc)seEIqrrf