Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to allow multipage output. #433

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 86 additions & 31 deletions cairosvg/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import copy
import io
from itertools import zip_longest

import cairocffi as cairo

Expand Down Expand Up @@ -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.
Expand All @@ -120,35 +122,66 @@ 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 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.

"""
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)
instance.finish()

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 that is the responsibility of the calling
# function to finish.
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.

Expand Down Expand Up @@ -200,29 +233,51 @@ 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 = 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
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):
Expand Down