Skip to content

Commit

Permalink
Add a low-level public API
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonSapin committed Sep 12, 2012
1 parent 463a33c commit 6354398
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 106 deletions.
6 changes: 5 additions & 1 deletion CHANGES
Expand Up @@ -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
Expand Down
81 changes: 73 additions & 8 deletions weasyprint/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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):
Expand Down
88 changes: 0 additions & 88 deletions weasyprint/document.py
Expand Up @@ -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):
Expand All @@ -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)
35 changes: 35 additions & 0 deletions weasyprint/draw.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions weasyprint/navigator.py
Expand Up @@ -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
Expand Down
34 changes: 32 additions & 2 deletions weasyprint/pdf.py
Expand Up @@ -32,7 +32,9 @@
from __future__ import division, unicode_literals

import os
import io
import re
import shutil
import string

import cairo
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
8 changes: 6 additions & 2 deletions weasyprint/tests/test_draw.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 6354398

Please sign in to comment.