mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
191 lines
7.0 KiB
Python
191 lines
7.0 KiB
Python
# coding: utf8
|
|
"""
|
|
weasyprint.document
|
|
-------------------
|
|
|
|
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
|
|
"""
|
|
|
|
from __future__ import division, unicode_literals
|
|
|
|
import io
|
|
import sys
|
|
import math
|
|
import shutil
|
|
import functools
|
|
|
|
import cairo
|
|
|
|
from . import CSS
|
|
from . import images
|
|
from .css import get_all_computed_styles
|
|
from .formatting_structure.build import build_formatting_structure
|
|
from .layout import layout_document
|
|
from .draw import draw_page, stacked
|
|
from .pdf import write_pdf_metadata
|
|
from .compat import izip
|
|
from .urls import FILESYSTEM_ENCODING
|
|
|
|
|
|
class Page(object):
|
|
"""Represents a single rendered page."""
|
|
def __init__(self, page, enable_hinting=False, resolution=96):
|
|
self._page_box = page
|
|
self._enable_hinting = enable_hinting
|
|
self._dppx = resolution / 96
|
|
#: The page width, including margins, in cairo user units.
|
|
self.width = page.margin_width() * self._dppx
|
|
#: The page height, including margins, in cairo user units.
|
|
self.height = page.margin_height() * self._dppx
|
|
|
|
def paint(self, cairo_context, left_x=0, top_y=0, clip=False):
|
|
"""Paint the surface in cairo, on any type of surface.
|
|
|
|
:param cairo_context: any :class:`cairo.Context` object.
|
|
:type left_x: float
|
|
:param left_x:
|
|
X coordinate of the left of the page, in user units.
|
|
:type top_y: float
|
|
:param top_y:
|
|
Y coordinate of the top of the page, in user units.
|
|
:type clip: bool
|
|
:param clip:
|
|
Whether to clip/cut content outside the page. If false or
|
|
not provided, content can overflow.
|
|
|
|
"""
|
|
with stacked(cairo_context):
|
|
if self._enable_hinting:
|
|
left_x, top_y = cairo_context.user_to_device(left_x, top_y)
|
|
width, height = cairo_context.user_to_device_distance(
|
|
self.width, self.height)
|
|
# Hint in device space
|
|
left_x = int(left_x)
|
|
top_y = int(top_y)
|
|
width = int(math.ceil(width))
|
|
height = int(math.ceil(height))
|
|
left_x, top_y = cairo_context.device_to_user(left_x, top_y)
|
|
width, height = cairo_context.device_to_user_distance(
|
|
width, height)
|
|
else:
|
|
width = self.width
|
|
height = self.height
|
|
cairo_context.translate(left_x, top_y)
|
|
# The top-left corner is now (0, 0)
|
|
if clip:
|
|
cairo_context.rectangle(0, 0, width, height)
|
|
cairo_context.clip()
|
|
cairo_context.scale(self._dppx, self._dppx)
|
|
# User units are now CSS pixels
|
|
draw_page(self._page_box, cairo_context, self._enable_hinting)
|
|
|
|
|
|
class Document(object):
|
|
@classmethod
|
|
def render(cls, html, stylesheets, resolution, enable_hinting):
|
|
style_for = get_all_computed_styles(html, user_stylesheets=[
|
|
css if hasattr(css, 'rules')
|
|
else CSS(guess=css, media_type=html.media_type)
|
|
for css in stylesheets or []])
|
|
get_image_from_uri = functools.partial(
|
|
images.get_image_from_uri, {}, html.url_fetcher)
|
|
page_boxes = layout_document(
|
|
enable_hinting, style_for, get_image_from_uri,
|
|
build_formatting_structure(
|
|
html.root_element, style_for, get_image_from_uri))
|
|
return cls([Page(p, enable_hinting, resolution) for p in page_boxes])
|
|
|
|
def __init__(self, pages):
|
|
#: A list of :class:`Page` objects.
|
|
self.pages = pages
|
|
|
|
def copy(self, pages='all'):
|
|
"""Return a new :class:`Document` with a subset of the pages."""
|
|
if pages == 'all':
|
|
pages = self.pages
|
|
return type(self)(pages)
|
|
|
|
def write_pdf(self, target=None):
|
|
"""Paint pages; write PDF bytes to ``target``, or return them
|
|
if ``target`` is ``None``.
|
|
|
|
This function also adds PDF metadata (bookmarks, hyperlinks, …).
|
|
PDF files coming straight from :class:`cairo.PDFSurface` do not have
|
|
such metadata.
|
|
|
|
:param target: a filename, file object, or ``None``
|
|
:returns: a bytestring if ``target`` is ``None``.
|
|
|
|
"""
|
|
# Use an in-memory buffer. We will need to seek for metadata
|
|
# TODO: avoid this if target can seek? Benchmark first.
|
|
file_obj = io.BytesIO()
|
|
# (1, 1) is overridden by .set_size() below.
|
|
surface = cairo.PDFSurface(file_obj, 1, 1)
|
|
context = cairo.Context(surface)
|
|
for page in self.pages:
|
|
surface.set_size(page.width, page.height)
|
|
page.paint(context)
|
|
surface.show_page()
|
|
surface.finish()
|
|
|
|
write_pdf_metadata(self.pages, file_obj)
|
|
|
|
if target is None:
|
|
return file_obj.getvalue()
|
|
else:
|
|
file_obj.seek(0)
|
|
if hasattr(target, 'write'):
|
|
shutil.copyfileobj(file_obj, target)
|
|
else:
|
|
with open(target, 'wb') as fd:
|
|
shutil.copyfileobj(file_obj, fd)
|
|
|
|
def write_png(self, target=None, with_size=False):
|
|
"""Paint pages vertically; write PNG bytes to ``target``, or return them
|
|
if ``target`` is ``None``.
|
|
|
|
:param target: a filename, file object, or ``None``
|
|
:param with_size: if true, also return the size of the PNG image.
|
|
:returns:
|
|
``output`` or ``(output, png_width, png_height)`` (depending
|
|
on ``with_size``). ``output`` is a byte string (if ``target``
|
|
is ``None``) or ``None``.
|
|
|
|
"""
|
|
# This duplicates the hinting logic in Page.paint. There is a
|
|
# dependency cycle otherwise:
|
|
# this → hinting logic → context → surface → this
|
|
# But since we do no transform here, cairo_context.user_to_device and
|
|
# friends are identity functions.
|
|
widths = [int(math.ceil(p.width)) for p in self.pages]
|
|
heights = [int(math.ceil(p.height)) for p in self.pages]
|
|
max_width = max(widths)
|
|
sum_heights = sum(heights)
|
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, max_width, sum_heights)
|
|
context = cairo.Context(surface)
|
|
|
|
pos_y = 0
|
|
for page, width, height in izip(self.pages, widths, heights):
|
|
pos_x = (max_width - width) / 2
|
|
with stacked(context):
|
|
page.paint(context, pos_x, pos_y, clip=True)
|
|
pos_y += height
|
|
|
|
if target is None:
|
|
target = io.BytesIO()
|
|
surface.write_to_png(target)
|
|
png_bytes = target.getvalue()
|
|
else:
|
|
if sys.version_info[0] < 3 and isinstance(target, unicode):
|
|
# py2cairo 1.8 does not support unicode filenames.
|
|
target = target.encode(FILESYSTEM_ENCODING)
|
|
surface.write_to_png(target)
|
|
png_bytes = None
|
|
if with_size:
|
|
return png_bytes, max_width, sum_heights
|
|
else:
|
|
return png_bytes
|