From 612fc320fcf8f9031dfa62ae8cc332f8d0f3cbe2 Mon Sep 17 00:00:00 2001
From: "L.G.C" <lgcat76@gmail.com>
Date: Thu, 9 Dec 2021 09:44:41 +0100
Subject: [PATCH] Configurable exported viewbox

The area of the SVG that will be exported can be specified by giving
the id of an SVG element. Its bounding box will be used to configure
the area to export.
---
 cairosvg/__init__.py | 28 ++++++++++++++++++----------
 cairosvg/__main__.py |  6 +++++-
 cairosvg/helpers.py  | 13 +++++++++++++
 cairosvg/surface.py  | 21 ++++++++++++++++++---
 4 files changed, 54 insertions(+), 14 deletions(-)

diff --git a/cairosvg/__init__.py b/cairosvg/__init__.py
index 1ce5b344..de144021 100644
--- a/cairosvg/__init__.py
+++ b/cairosvg/__init__.py
@@ -38,62 +38,70 @@
 def svg2svg(bytestring=None, *, file_obj=None, url=None, dpi=96,
             parent_width=None, parent_height=None, scale=1, unsafe=False,
             background_color=None, negate_colors=False, invert_images=False,
-            write_to=None, output_width=None, output_height=None):
+            write_to=None, output_width=None, output_height=None, viewbox_id=None):
     return surface.SVGSurface.convert(
         bytestring=bytestring, file_obj=file_obj, url=url, dpi=dpi,
         parent_width=parent_width, parent_height=parent_height, scale=scale,
         background_color=background_color,
         negate_colors=negate_colors, invert_images=invert_images,
         unsafe=unsafe, write_to=write_to, output_width=output_width,
-        output_height=output_height)
+        output_height=output_height, viewbox_id=viewbox_id)
 
 
 def svg2png(bytestring=None, *, file_obj=None, url=None, dpi=96,
             parent_width=None, parent_height=None, scale=1, unsafe=False,
             background_color=None, negate_colors=False, invert_images=False,
-            write_to=None, output_width=None, output_height=None):
+            write_to=None, output_width=None, output_height=None,
+            viewbox_id=None):
     return surface.PNGSurface.convert(
         bytestring=bytestring, file_obj=file_obj, url=url, dpi=dpi,
         parent_width=parent_width, parent_height=parent_height, scale=scale,
         background_color=background_color, negate_colors=negate_colors,
         invert_images=invert_images, unsafe=unsafe, write_to=write_to,
-        output_width=output_width, output_height=output_height)
+        output_width=output_width, output_height=output_height,
+        viewbox_id=viewbox_id)
 
 
 def svg2pdf(bytestring=None, *, file_obj=None, url=None, dpi=96,
             parent_width=None, parent_height=None, scale=1, unsafe=False,
             background_color=None, negate_colors=False, invert_images=False,
-            write_to=None, output_width=None, output_height=None):
+            write_to=None, output_width=None, output_height=None,
+            viewbox_id=None):
     return surface.PDFSurface.convert(
         bytestring=bytestring, file_obj=file_obj, url=url, dpi=dpi,
         parent_width=parent_width, parent_height=parent_height, scale=scale,
         background_color=background_color, negate_colors=negate_colors,
         invert_images=invert_images, unsafe=unsafe, write_to=write_to,
-        output_width=output_width, output_height=output_height)
+        output_width=output_width, output_height=output_height,
+        viewbox_id=viewbox_id)
 
 
 def svg2ps(bytestring=None, *, file_obj=None, url=None, dpi=96,
            parent_width=None, parent_height=None, scale=1, unsafe=False,
            background_color=None, negate_colors=False, invert_images=False,
-           write_to=None, output_width=None, output_height=None):
+           write_to=None, output_width=None, output_height=None,
+           viewbox_id=None):
     return surface.PSSurface.convert(
         bytestring=bytestring, file_obj=file_obj, url=url, dpi=dpi,
         parent_width=parent_width, parent_height=parent_height, scale=scale,
         background_color=background_color, negate_colors=negate_colors,
         invert_images=invert_images, unsafe=unsafe, write_to=write_to,
-        output_width=output_width, output_height=output_height)
+        output_width=output_width, output_height=output_height,
+        viewbox_id=viewbox_id)
 
 
 def svg2eps(bytestring=None, *, file_obj=None, url=None, dpi=96,
             parent_width=None, parent_height=None, scale=1, unsafe=False,
             background_color=None, negate_colors=False, invert_images=False,
-            write_to=None, output_width=None, output_height=None):
+            write_to=None, output_width=None, output_height=None,
+            viewbox_id=None):
     return surface.EPSSurface.convert(
         bytestring=bytestring, file_obj=file_obj, url=url, dpi=dpi,
         parent_width=parent_width, parent_height=parent_height, scale=scale,
         background_color=background_color, negate_colors=negate_colors,
         invert_images=invert_images, unsafe=unsafe, write_to=write_to,
-        output_width=output_width, output_height=output_height)
+        output_width=output_width, output_height=output_height,
+        viewbox_id=viewbox_id)
 
 
 if __debug__:
diff --git a/cairosvg/__main__.py b/cairosvg/__main__.py
index 3ff6b5d1..9d776abd 100644
--- a/cairosvg/__main__.py
+++ b/cairosvg/__main__.py
@@ -50,6 +50,9 @@ def main(argv=None, stdout=None, stdin=None):
     parser.add_argument(
         '--output-height', default=None, type=float,
         help='desired output height in pixels')
+    parser.add_argument(
+        '--viewbox-id', default=None,
+        help='export viewbox defined by the element with the given id')
 
     parser.add_argument('-o', '--output', default='-', help='output filename')
 
@@ -61,7 +64,8 @@ def main(argv=None, stdout=None, stdin=None):
         'negate_colors': options.negate_colors,
         'invert_images': options.invert_images,
         'output_width': options.output_width,
-        'output_height': options.output_height}
+        'output_height': options.output_height,
+        'viewbox_id': options.viewbox_id}
     stdin = stdin or sys.stdin
     stdout = stdout or sys.stdout
     kwargs['write_to'] = (
diff --git a/cairosvg/helpers.py b/cairosvg/helpers.py
index c3fea7f8..e29c16ed 100644
--- a/cairosvg/helpers.py
+++ b/cairosvg/helpers.py
@@ -386,3 +386,16 @@ def size(surface, string, reference='xy'):
 
     # Unknown size
     return 0
+
+
+def find_child(node, element_id, default=None):
+    if element_id is not None:
+        if node.element.id == element_id:
+            return node
+
+        for child in node.children:
+            found = find_child(child, element_id)
+            if found is not None:
+                return found
+
+    return default
diff --git a/cairosvg/surface.py b/cairosvg/surface.py
index c5569e76..da7fcf03 100644
--- a/cairosvg/surface.py
+++ b/cairosvg/surface.py
@@ -8,6 +8,7 @@
 
 import cairocffi as cairo
 
+from .bounding_box import calculate_bounding_box
 from .colors import color, negate_color
 from .defs import (
     apply_filter_after_painting, apply_filter_before_painting, clip_path,
@@ -15,7 +16,7 @@
     parse_all_defs, pattern, prepare_filter, radial_gradient, use)
 from .helpers import (
     UNITS, PointError, clip_rect, node_format, normalize, paint,
-    preserve_ratio, size, transform)
+    preserve_ratio, size, transform, find_child)
 from .image import image, invert_image
 from .parser import Tree
 from .path import draw_markers, path
@@ -96,6 +97,7 @@ class Surface(object):
     @classmethod
     def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96,
                 parent_width=None, parent_height=None, scale=1, unsafe=False,
+                viewbox_id=None,
                 background_color=None, negate_colors=False,
                 invert_images=False, write_to=None, output_width=None,
                 output_height=None, **kwargs):
@@ -115,6 +117,7 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96,
         :param scale: The ouptut scaling factor.
         :param unsafe: A boolean allowing XML entities and very large files
                        (WARNING: vulnerable to XXE attacks and various DoS).
+        :param viewbox_id: SVG element id defining the area to export.
 
         Specifiy the output with:
 
@@ -132,6 +135,7 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96,
         instance = cls(
             tree, output, dpi, None, parent_width, parent_height, scale,
             output_width, output_height, background_color,
+            viewbox_id=viewbox_id if viewbox_id else None,
             map_rgba=negate_color if negate_colors else None,
             map_image=invert_image if invert_images else None)
         instance.finish()
@@ -141,7 +145,8 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96,
     def __init__(self, tree, output, dpi, parent_surface=None,
                  parent_width=None, parent_height=None,
                  scale=1, output_width=None, output_height=None,
-                 background_color=None, map_rgba=None, map_image=None):
+                 background_color=None, map_rgba=None, map_image=None,
+                 viewbox_id=None):
         """Create the surface from a filename or a file-like object.
 
         The rendered content is written to ``output`` which can be a filename,
@@ -179,7 +184,17 @@ def __init__(self, tree, output, dpi, parent_surface=None,
         self.dpi = dpi
         self.font_size = size(self, '12pt')
         self.stroke_and_fill = True
-        width, height, viewbox = node_format(self, tree)
+
+        width, height, viewbox = (0, 0, None)
+        viewbox_node = find_child(tree, viewbox_id)
+        if viewbox_node:
+            viewbox = calculate_bounding_box(self, viewbox_node)
+            if viewbox:
+                width, height = viewbox[2:]
+
+        if viewbox is None:
+            width, height, viewbox = node_format(self, tree)
+
         if viewbox is None:
             viewbox = (0, 0, width, height)