From 930908052b2a71c36b906ef1ffa568cac819c4d2 Mon Sep 17 00:00:00 2001 From: Joseph Tignor Date: Wed, 27 Nov 2024 13:29:52 -0500 Subject: [PATCH 1/4] Add ability to create multi-page outputs. --- cairosvg/surface.py | 74 +++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/cairosvg/surface.py b/cairosvg/surface.py index d8a92b55..8324ed4d 100644 --- a/cairosvg/surface.py +++ b/cairosvg/surface.py @@ -120,7 +120,8 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96, Specifiy the output with: :param write_to: The filename of file-like object where to write the - output. If None or not provided, return a byte string. + output or MultipageSurface to write to. If None or not + provided, return a byte string. Only ``bytestring`` can be passed as a positional argument, other parameters are keyword-only. @@ -135,20 +136,31 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96, output_width, output_height, background_color, map_rgba=negate_color if negate_colors else None, map_image=invert_image if invert_images else None) - instance.finish() + + # Don't finish surface if surface is the output as it must be + # a multipage surface. + if not isinstance(write_to, Surface): + instance.finish() + if write_to is None: return output.getvalue() 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, + multi_page=False): """Create the surface from a filename or a file-like object. The rendered content is written to ``output`` which can be a filename, a file-like object, ``None`` (render in memory but do not write anything) or the built-in ``bytes`` as a marker. + If ``multi_page`` is True, the provided content is instead only used + as a template for page size and no drawing is done. The created + object can then be used as the ``output`` for subsequent content for + each of which a new page will be added. + Call the ``.finish()`` method to make sure that the output is actually written. @@ -200,29 +212,47 @@ def __init__(self, tree, output, dpi, parent_surface=None, width *= scale height *= scale - # Actual surface dimensions: may be rounded on raster surfaces types - self.cairo, self.width, self.height = self._create_surface( - width * self.device_units_per_user_units, - height * self.device_units_per_user_units) + + self.multi_page = multi_page + self.first_page = True + + if isinstance(output, Surface): + if not output.multi_page: + raise ValueError("Cannot provide a surface as an output unless it is multipage.") + self.cairo = output.cairo + self.output = output.output + self.width = width * self.device_units_per_user_units + self.height = height * self.device_units_per_user_units + # Add new page for this conversion + if not output.first_page: + self.cairo.show_page() + output.first_page = False + else: + # Actual surface dimensions: may be rounded on raster surfaces types + self.cairo, self.width, self.height = self._create_surface( + width * self.device_units_per_user_units, + height * self.device_units_per_user_units) if 0 in (self.width, self.height): raise ValueError('The SVG size is undefined') - self.context = cairo.Context(self.cairo) - # We must scale the context as the surface size is using physical units - self.context.scale( - self.device_units_per_user_units, self.device_units_per_user_units) - # Initial, non-rounded dimensions - self.set_context_size(width, height, viewbox, tree) - self.context.move_to(0, 0) - - if background_color: - self.context.set_source_rgba(*color(background_color)) - self.context.paint() - - self.map_rgba = map_rgba - self.map_image = map_image - self.draw(tree) + # If creating multipage surface don't draw for this instance. + if not self.multi_page: + self.context = cairo.Context(self.cairo) + # We must scale the context as the surface size is using physical units + self.context.scale( + self.device_units_per_user_units, self.device_units_per_user_units) + # Initial, non-rounded dimensions + self.set_context_size(width, height, viewbox, tree) + self.context.move_to(0, 0) + + if background_color: + self.context.set_source_rgba(*color(background_color)) + self.context.paint() + + self.map_rgba = map_rgba + self.map_image = map_image + self.draw(tree) @property def points_per_pixel(self): From 8514d8b554caebb0c80597916dacb544eb7fa8fe Mon Sep 17 00:00:00 2001 From: Joseph Tignor Date: Wed, 27 Nov 2024 14:18:42 -0500 Subject: [PATCH 2/4] Update convert function to support multi-page input/output. --- cairosvg/surface.py | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/cairosvg/surface.py b/cairosvg/surface.py index 8324ed4d..b792db90 100644 --- a/cairosvg/surface.py +++ b/cairosvg/surface.py @@ -5,6 +5,7 @@ import copy import io +from itertools import zip_longest import cairocffi as cairo @@ -101,7 +102,8 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96, output_height=None, **kwargs): """Convert an SVG document to the format for this class. - Specify the input by passing one of these: + Specify the input by passing one of these. They may also be a list + which will result in a multipage output: :param bytestring: The SVG source as a byte-string. :param file_obj: A file-like object. @@ -127,18 +129,37 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96, parameters are keyword-only. """ - tree = Tree( - bytestring=bytestring, file_obj=file_obj, url=url, unsafe=unsafe, - **kwargs) + bytestring = bytestring if isinstance(bytestring, list) else [bytestring] + file_obj = file_obj if isinstance(file_obj, list) else [file_obj] + url = url if isinstance(url, list) else [url] + output = write_to or io.BytesIO() - instance = cls( - tree, output, dpi, None, parent_width, parent_height, scale, - output_width, output_height, background_color, - map_rgba=negate_color if negate_colors else None, - map_image=invert_image if invert_images else None) + + multi_page = max(len(bytestring), len(file_obj), len(url)) > 1 + + if multi_page and not isinstance(write_to, Surface): + template_tree = Tree( + bytestring=bytestring[0], file_obj=file_obj[0], url=url[0], unsafe=unsafe, + **kwargs) + output = cls(template_tree, output, dpi, None, parent_width, parent_height, scale, + output_width, output_height, background_color, + map_rgba=negate_color if negate_colors else None, + map_image=invert_image if invert_images else None, + multi_page=True) + + for bs, fo, u in zip_longest(bytestring, file_obj, url, fillvalue=None): + tree = Tree( + bytestring=bs, file_obj=fo, url=u, unsafe=unsafe, + **kwargs) + instance = cls( + tree, output, dpi, None, parent_width, parent_height, scale, + output_width, output_height, background_color, + map_rgba=negate_color if negate_colors else None, + map_image=invert_image if invert_images else None) # Don't finish surface if surface is the output as it must be - # a multipage surface. + # a multipage surface that is the responsibility of the calling + # function to finish. if not isinstance(write_to, Surface): instance.finish() From df944bfcfad921f40cd1dfa6f0d8352971b8f8b1 Mon Sep 17 00:00:00 2001 From: Joseph Tignor Date: Wed, 27 Nov 2024 14:29:43 -0500 Subject: [PATCH 3/4] Use output dimensions and adjust page size in case of PDF which allows for size change. --- cairosvg/surface.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cairosvg/surface.py b/cairosvg/surface.py index b792db90..8865ed4d 100644 --- a/cairosvg/surface.py +++ b/cairosvg/surface.py @@ -242,11 +242,15 @@ def __init__(self, tree, output, dpi, parent_surface=None, raise ValueError("Cannot provide a surface as an output unless it is multipage.") self.cairo = output.cairo self.output = output.output - self.width = width * self.device_units_per_user_units - self.height = height * self.device_units_per_user_units + self.width = output.width + self.height = output.height # Add new page for this conversion if not output.first_page: self.cairo.show_page() + if isinstance(self.cairo, cairo.surfaces.PDFSurface): + self.width = width * self.device_units_per_user_units + self.height = height * self.device_units_per_user_units + self.cairo.set_size(self.width, self.height) output.first_page = False else: # Actual surface dimensions: may be rounded on raster surfaces types From 509656f4b5ea875a809ad010d30d08288f3aed65 Mon Sep 17 00:00:00 2001 From: Joseph Tignor Date: Wed, 27 Nov 2024 14:33:57 -0500 Subject: [PATCH 4/4] Minor docstring clarification. --- cairosvg/surface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cairosvg/surface.py b/cairosvg/surface.py index 8865ed4d..1ef8f663 100644 --- a/cairosvg/surface.py +++ b/cairosvg/surface.py @@ -122,8 +122,8 @@ def convert(cls, bytestring=None, *, file_obj=None, url=None, dpi=96, Specifiy the output with: :param write_to: The filename of file-like object where to write the - output or MultipageSurface to write to. If None or not - provided, return a byte string. + output or a ```Surface``` created with `multi_page` set to + True. If None or not provided, return a byte string. Only ``bytestring`` can be passed as a positional argument, other parameters are keyword-only.