Permalink
Browse files

Add a low-level public API

  • Loading branch information...
1 parent 463a33c commit 6354398139521dcce2592ee72495453335ab639c @SimonSapin SimonSapin committed Sep 12, 2012
Showing with 157 additions and 106 deletions.
  1. +5 −1 CHANGES
  2. +73 −8 weasyprint/__init__.py
  3. +0 −88 weasyprint/document.py
  4. +35 −0 weasyprint/draw.py
  5. +3 −3 weasyprint/navigator.py
  6. +32 −2 weasyprint/pdf.py
  7. +6 −2 weasyprint/tests/test_draw.py
  8. +3 −2 weasyprint/tests/test_pdf.py
View
@@ -6,7 +6,11 @@ Version 0.15
Not released yet.
-Add support for the ``font-stretch`` property.
+* Add support for the ``font-stretch`` property.
+* Add support for ``@page:blank`` to select blank pages.
+* Bug fixes for importing Pango in some PyGTK installations.
+* Add a low level API the enables painting pages individually on any
+ cairo surface.
Version 0.14
View
@@ -23,6 +23,9 @@
VERSION_STRING = 'WeasyPrint %s (http://weasyprint.org/)' % VERSION
+import io
+import math
+
from .urls import default_url_fetcher
# Make sure the logger is configured early:
from .logger import LOGGER
@@ -141,6 +144,15 @@ def _get_document(self, stylesheets, enable_hinting, ua_stylesheets=None):
return Document(self.root_element, enable_hinting, self.url_fetcher,
self.media_type, user_stylesheets, ua_stylesheets)
+ def render(self, enable_hinting, stylesheets=None):
+ """Render the document and return a list of Page objects.
+
+ This is the low-level API.
+
+ """
+ document = self._get_document(stylesheets, enable_hinting)
+ return [Page(p, enable_hinting) for p in document.render_pages()]
+
def write_pdf(self, target=None, stylesheets=None):
"""Render the document to PDF.
@@ -152,8 +164,9 @@ def write_pdf(self, target=None, stylesheets=None):
:returns:
If :obj:`target` is :obj:`None`, a PDF byte string.
"""
- document = self._get_document(stylesheets, enable_hinting=False)
- return document.write_pdf(target)
+ from .pdf import write_pdf
+ pages = self.render(enable_hinting=False, stylesheets=stylesheets)
+ return write_pdf(pages, target)
def write_png(self, target=None, stylesheets=None, resolution=None):
"""Render the document to a single PNG image.
@@ -166,11 +179,12 @@ def write_png(self, target=None, stylesheets=None, resolution=None):
:returns:
If :obj:`target` is :obj:`None`, a PNG byte string.
"""
- document = self._get_document(stylesheets, enable_hinting=True)
- return document.write_png(target, resolution)
+ from .draw import write_png
+ surfaces = [page.get_image_surface(resolution) for page in
+ self.render(enable_hinting=True, stylesheets=stylesheets)]
+ return write_png(surfaces, target)
- def get_png_pages(self, stylesheets=None, resolution=None,
- _with_pages=False):
+ def get_png_pages(self, stylesheets=None, resolution=None):
"""Render the document to multiple PNG images, one per page.
:param stylesheets:
@@ -181,8 +195,59 @@ def get_png_pages(self, stylesheets=None, resolution=None,
each page, in order.
"""
- document = self._get_document(stylesheets, enable_hinting=True)
- return document.get_png_pages(resolution, _with_pages)
+ for page in self.render(enable_hinting=True, stylesheets=stylesheets):
+ yield page.get_png_bytes(resolution)
+
+
+class Page(object):
+ """Represents a single rendered page."""
+ def __init__(self, page, enable_hinting):
+ self._page_box = page
+ self.enable_hinting = enable_hinting
+ #: The page width, including margins, in CSS pixels (float)
+ self.width = page.margin_width()
+ #: The page height, including margins, in CSS pixels (float)
+ self.height = page.margin_height()
+
+ def paint(self, cairo_context):
+ """Paint the surface on any cairo Context object.
+
+ The user units with the current transformation in the context
+ should be in CSS pixels. A CSS inch is always 96 CSS pixels.
+ In other words, a user resolution of 96 dpi will anchor the scale
+ to physical units.
+
+ """
+ from .draw import draw_page
+ draw_page(self._page_box, cairo_context, self.enable_hinting)
+
+ def get_image_surface(self, resolution=None):
+ """Paint the page on an ImageSurface and return the surface.
+
+ The default resolution is 96: image pixels match CSS pixels.
+
+ """
+ import cairo
+ px_resolution = (resolution or 96) / 96
+ width = int(math.ceil(self.width * px_resolution))
+ height = int(math.ceil(self.height * px_resolution))
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ context = cairo.Context(surface)
+ context.scale(px_resolution, px_resolution)
+ self.paint(context)
+ return surface
+
+ def get_png_bytes(self, resolution=None):
+ """Paint the page and return a PNG image
+
+ The default resolution is 96: PNG pixels match CSS pixels.
+ Returns the image and its size as ``(width, height, png_bytes)``.
+
+ """
+ file_obj = io.BytesIO()
+ surface = self.get_image_surface(resolution)
+ surface.write_to_png(file_obj)
+ return surface.get_width(), surface.get_height(), file_obj.getvalue()
class CSS(Resource):
View
@@ -15,18 +15,14 @@
import io
import sys
import math
-import shutil
import functools
import cairo
from .css import get_all_computed_styles
from .formatting_structure.build import build_formatting_structure
-from .urls import FILESYSTEM_ENCODING
from . import layout
-from . import draw
from . import images
-from . import pdf
class Document(object):
@@ -47,87 +43,3 @@ def render_pages(self):
self.enable_hinting, self.style_for, self.get_image_from_uri,
build_formatting_structure(
self.element_tree, self.style_for, self.get_image_from_uri)))
-
- def draw_page(self, page, context):
- """Draw page on context at scale cairo device units per CSS pixel."""
- return draw.draw_page(page, context, self.enable_hinting)
-
- def get_png_surfaces(self, resolution=None):
- """Yield (width, height, image_surface) tuples, one for each page."""
- px_resolution = (resolution or 96) / 96
- for page in self.render_pages():
- width = int(math.ceil(page.margin_width() * px_resolution))
- height = int(math.ceil(page.margin_height() * px_resolution))
- surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
- context = cairo.Context(surface)
- context.scale(px_resolution, px_resolution)
- self.draw_page(page, context)
- yield width, height, surface, page
-
- def get_png_pages(self, resolution=None, _with_pages=False):
- """Yield (width, height, png_bytes) tuples, one for each page."""
- for width, height, surface, page in self.get_png_surfaces(resolution):
- file_obj = io.BytesIO()
- surface.write_to_png(file_obj)
- if _with_pages:
- yield width, height, file_obj.getvalue(), page
- else:
- yield width, height, file_obj.getvalue()
-
- def write_png(self, target=None, resolution=None):
- """Write a single PNG image."""
- surfaces = list(self.get_png_surfaces(resolution))
- if len(surfaces) == 1:
- _, _, surface, _ = surfaces[0]
- else:
- total_height = sum(height for _, height, _, _ in surfaces)
- max_width = max(width for width, _, _, _ in surfaces)
- surface = cairo.ImageSurface(
- cairo.FORMAT_ARGB32, max_width, total_height)
- context = cairo.Context(surface)
- pos_y = 0
- for width, height, page_surface, _ in surfaces:
- pos_x = (max_width - width) // 2
- context.set_source_surface(page_surface, pos_x, pos_y)
- context.paint()
- pos_y += height
-
- if target is None:
- target = io.BytesIO()
- surface.write_to_png(target)
- return 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)
-
- def write_pdf(self, target=None):
- """Write a single PNG image."""
- # 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()
- # We’ll change the surface size for each page
- surface = cairo.PDFSurface(file_obj, 1, 1)
- context = cairo.Context(surface)
- px_to_pt = pdf.PX_TO_PT
- context.scale(px_to_pt, px_to_pt)
- pages = self.render_pages()
- for page in pages:
- surface.set_size(page.margin_width() * px_to_pt,
- page.margin_height() * px_to_pt)
- self.draw_page(page, context)
- surface.show_page()
- surface.finish()
-
- pdf.write_pdf_metadata(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)
View
@@ -12,12 +12,15 @@
from __future__ import division, unicode_literals
+import io
+import sys
import contextlib
import math
import operator
import cairo
+from .urls import FILESYSTEM_ENCODING
from .formatting_structure import boxes
from .stacking import StackingContext
from .text import show_first_line
@@ -783,3 +786,35 @@ def apply_2d_transforms(context, box):
assert name == 'matrix'
context.transform(cairo.Matrix(*args))
context.translate(-origin_x, -origin_y)
+
+
+def write_png(image_surfaces, target=None):
+ """Concatenate images vertically and write the result as PNG
+
+ :param image_surfaces: a list a cairo ImageSurface objects
+
+ """
+ if len(image_surfaces) == 1:
+ surface = image_surfaces[0]
+ else:
+ total_height = sum(s.get_height() for s in image_surfaces)
+ max_width = max(s.get_width() for s in image_surfaces)
+ surface = cairo.ImageSurface(
+ cairo.FORMAT_ARGB32, max_width, total_height)
+ context = cairo.Context(surface)
+ pos_y = 0
+ for page_surface in image_surfaces:
+ pos_x = (max_width - page_surface.get_width()) // 2
+ context.set_source_surface(page_surface, pos_x, pos_y)
+ context.paint()
+ pos_y += page_surface.get_height()
+
+ if target is None:
+ target = io.BytesIO()
+ surface.write_to_png(target)
+ return 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)
View
@@ -52,11 +52,11 @@ def find_links(box, links, anchors):
def get_pages(html):
- for width, height, png_bytes, page in html.get_png_pages(
- [STYLESHEET], _with_pages=True):
+ for page in html.render(enable_hinting=True, stylesheets=[STYLESHEET]):
links = []
anchors = []
- find_links(page, links, anchors)
+ find_links(page._page_box, links, anchors)
+ width, height, png_bytes = page.get_png_bytes()
data_url = 'data:image/png;base64,' + (
base64_encode(png_bytes).decode('ascii').replace('\n', ''))
yield width, height, data_url, links, anchors
View
@@ -32,7 +32,9 @@
from __future__ import division, unicode_literals
import os
+import io
import re
+import shutil
import string
import cairo
@@ -393,11 +395,11 @@ def walk(box):
# cairo coordinates are pixels right and down from the top-left corner
# PDF coordinates are points right and up from the bottom-left corner
matrix = cairo.Matrix(
- PX_TO_PT, 0, 0, -PX_TO_PT, 0, page.margin_height() * PX_TO_PT)
+ PX_TO_PT, 0, 0, -PX_TO_PT, 0, page.height * PX_TO_PT)
point_to_pdf = matrix.transform_point
distance_to_pdf = matrix.transform_distance
page_links = []
- walk(page)
+ walk(page._page_box)
links_by_page.append(page_links)
# A list (by page) of lists of either:
@@ -481,3 +483,31 @@ def write_pdf_metadata(pages, fileobj):
'{0} 0 R'.format(n) for n in annotations)))
pdf.finish()
+
+
+def write_pdf(pages, target=None):
+ """Write a PDF file."""
+ # 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()
+ # We’ll change the surface size for each page
+ surface = cairo.PDFSurface(file_obj, 1, 1)
+ context = cairo.Context(surface)
+ context.scale(PX_TO_PT, PX_TO_PT)
+ for page in pages:
+ surface.set_size(page.width * PX_TO_PT, page.height * PX_TO_PT)
+ page.paint(context)
+ surface.show_page()
+ surface.finish()
+
+ write_pdf_metadata(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)
@@ -21,10 +21,11 @@
import cairo
import pytest
+from .. import draw
from ..compat import xrange, izip, ints_from_bytes
from ..urls import ensure_url
from ..images import get_pixbuf, save_pixels_to_png
-from .. import HTML
+from .. import HTML, Page
from .testing_utils import (
resource_filename, TestPNGDocument, FONTS, assert_no_logs, capture_logs)
@@ -129,7 +130,10 @@ def document_to_pixels(document, name, expected_width, expected_height):
"""
Render an HTML document to PNG, checks its size and return pixel data.
"""
- return png_to_pixels(document.write_png(), expected_width, expected_height)
+ png_bytes = draw.write_png(
+ [Page(s, enable_hinting=True).get_image_surface()
+ for s in document.render_pages()])
+ return png_to_pixels(png_bytes, expected_width, expected_height)
def png_to_pixels(png_bytes, width, height):
Oops, something went wrong.

0 comments on commit 6354398

Please sign in to comment.