1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-09-17 15:37:34 +03:00

Move resolution to *.write_png only.

This commit is contained in:
Simon Sapin 2012-10-05 20:12:05 +02:00
parent e1f5863bc8
commit eda4bc604e
5 changed files with 86 additions and 70 deletions

View File

@ -103,7 +103,7 @@ class HTML(object):
from .html import HTML5_UA_STYLESHEET
return [HTML5_UA_STYLESHEET]
def render(self, stylesheets=None, resolution=96, enable_hinting=False):
def render(self, stylesheets=None, enable_hinting=False):
"""Lay out and paginate the document, but do not (yet) export it
to PDF or another format.
@ -119,18 +119,11 @@ class HTML(object):
Whether text, borders and background should be *hinted* to fall
at device pixel boundaries. Should be enabled for pixel-based
output (like PNG) but not vector based output (like PDF).
:type resolution: float
:param resolution:
The output resolution in cairo user units per CSS inch. At 96 dpi
(the default), user units match the CSS ``px`` unit.
For example, :class:`cairo.PDFSurface`s device units are
in PostScript points (72dpi), so ``resolution=72`` will set
the right scale for physical units.
:returns: A :class:`Document` object.
"""
from .document import Document
return Document._render(self, stylesheets, resolution, enable_hinting)
return Document._render(self, stylesheets, enable_hinting)
def write_pdf(self, target=None, stylesheets=None):
@ -147,7 +140,7 @@ class HTML(object):
``None`` (the PDF is written to :obj:`target`.)
"""
return self.render(stylesheets, resolution=72).write_pdf(target)
return self.render(stylesheets).write_pdf(target)
def write_png(self, target=None, stylesheets=None, resolution=96):
"""Paint the pages vertically to a single PNG image.
@ -171,8 +164,8 @@ class HTML(object):
"""
png_bytes, _width, _height = (
self.render(stylesheets, resolution, enable_hinting=True)
.write_png(target))
self.render(stylesheets, enable_hinting=True)
.write_png(target, resolution))
return png_bytes

View File

@ -49,10 +49,14 @@ def _get_metadata(box, bookmarks, links, anchors, matrix):
# In case of duplicate IDs, only the first is an anchor.
has_anchor = anchor_name and anchor_name not in anchors
# TODO: account for CSS transforms
# matrix *= …
if has_bookmark or has_link or has_anchor:
pos_x, pos_y, width, height = box.hit_area()
pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
width, height = matrix.transform_distance(width, height)
if matrix:
pos_x, pos_y = matrix.transform_point(pos_x, pos_y)
width, height = matrix.transform_distance(width, height)
if has_bookmark:
bookmarks.append((bookmark_level, bookmark_label, (pos_x, pos_y)))
if has_link:
@ -75,14 +79,12 @@ class Page(object):
instantiated directly.
"""
def __init__(self, page, enable_hinting=False, resolution=96):
dppx = resolution / 96
def __init__(self, page_box, enable_hinting=False):
#: The page width, including margins, in cairo user units.
self.width = page.margin_width() * dppx
self.width = page_box.margin_width()
#: The page height, including margins, in cairo user units.
self.height = page.margin_height() * dppx
self.height = page_box.margin_height()
#: A list of ``(bookmark_level, bookmark_label, point)`` tuples.
#: A point is ``(x, y)`` in cairo units from the top-left of the page.
@ -103,22 +105,24 @@ class Page(object):
#: form the top-left of the page.)
self.anchors = {}
_get_metadata(page, self.bookmarks, self.links, self.anchors,
cairo.Matrix(xx=dppx, yy=dppx))
self._page_box = page
_get_metadata(
page_box, self.bookmarks, self.links, self.anchors, matrix=None)
self._page_box = page_box
self._enable_hinting = enable_hinting
self._dppx = dppx
def paint(self, cairo_context, left_x=0, top_y=0, clip=False):
def paint(self, cairo_context, left_x=0, top_y=0, scale=1, 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.
X coordinate of the left of the page, in cairo user units.
:type top_y: float
:param top_y:
Y coordinate of the top of the page, in user units.
Y coordinate of the top of the page, in cairo user units.
:type scale: float
:param scale:
Zoom scale in CSS pixels per cairo user unit.
:type clip: bool
:param clip:
Whether to clip/cut content outside the page. If false or
@ -128,26 +132,27 @@ class Page(object):
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:
# Make (0, 0) the top-left corner:
cairo_context.translate(left_x, top_y)
# Make user units CSS pixels:
cairo_context.scale(scale, scale)
if clip:
width = self.width
height = self.height
cairo_context.translate(left_x, top_y)
# The top-left corner is now (0, 0)
if clip:
if self._enable_hinting:
width, height = (
cairo_context.user_to_device_distance(width, height))
# Hint in device space
width = int(math.ceil(width))
height = int(math.ceil(height))
width, height = (
cairo_context.device_to_user_distance(width, height))
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)
@ -160,7 +165,7 @@ class Document(object):
"""
@classmethod
def _render(cls, html, stylesheets, resolution, enable_hinting):
def _render(cls, html, stylesheets, 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)
@ -171,7 +176,7 @@ class Document(object):
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])
return cls([Page(p, enable_hinting) for p in page_boxes])
def __init__(self, pages):
#: A list of :class:`Page` objects.
@ -301,8 +306,10 @@ class Document(object):
# (1, 1) is overridden by .set_size() below.
surface = cairo.PDFSurface(file_obj, 1, 1)
context = cairo.Context(surface)
context.scale(0.75, 0.75)
for page in self.pages:
surface.set_size(page.width, page.height)
surface.set_size(page.width * 0.75, page.height * 0.75)
# 0.75 = 72 PDF point per inch / 96 CSS pixel per inch
page.paint(context)
surface.show_page()
surface.finish()
@ -319,7 +326,7 @@ class Document(object):
with open(target, 'wb') as fd:
shutil.copyfileobj(file_obj, fd)
def write_png(self, target=None):
def write_png(self, target=None, resolution=96):
"""Paint the pages vertically to a single PNG image.
There is no decoration around pages other than those specified in CSS
@ -328,6 +335,10 @@ class Document(object):
:param target:
A filename, file-like object, or ``None``.
:type resolution: float
:param resolution:
The output resolution in PNG pixels per CSS inch. At 96 dpi
(the default), PNG pixels match the CSS ``px`` unit.
:returns:
A ``(png_bytes, png_width, png_height)`` tuple. :obj:`png_bytes`
is a byte string if :obj:`target` is ``None``, otherwise ``None``
@ -336,23 +347,25 @@ class Document(object):
final image, in PNG pixels.
"""
dppx = resolution / 96
# 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]
widths = [int(math.ceil(p.width * dppx)) for p in self.pages]
heights = [int(math.ceil(p.height * dppx)) for p in self.pages]
max_width = max(widths)
sum_heights = sum(heights)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, max_width, 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)
page.paint(context, pos_x, pos_y, scale=dppx, clip=True)
pos_y += height
if target is None:

View File

@ -39,7 +39,7 @@ import cairo
from . import VERSION_STRING
from .logger import LOGGER
from .compat import xrange, iteritems
from .compat import xrange, iteritems, izip
from .urls import iri_to_uri
from .formatting_structure import boxes
@ -310,19 +310,23 @@ def prepare_metadata(document, bookmark_root_id):
"""
# X and width unchanged; Y = page_height - Y; height = -height
# Overall * 0.75 for CSS pixels to PDF points:
matrices = [cairo.Matrix(xx=0.75, yy=-0.75, y0=page.height * 0.75)
for page in document.pages]
page_heights = [page.height for page in document.pages]
links = []
for page_number, page_links in enumerate(document.resolve_links()):
for page_links, matrix in izip(document.resolve_links(), matrices):
new_page_links = []
for link_type, target, rectangle in page_links:
if link_type == 'internal':
target_page, target_x, target_y = target
target = (target_page, target_x,
page_heights[target_page] - target_y)
# x, y, w, h => x0, y0, x1, y1
target = ((target_page,) +
matrices[target_page].transform_point(target_x, target_y))
rect_x, rect_y, width, height = rectangle
pdf_y1 = page_heights[page_number] - rect_y
rectangle = rect_x, pdf_y1, rect_x + width, pdf_y1 - height
rect_x, rect_y = matrix.transform_point(rect_x, rect_y)
width, height = matrix.transform_distance(width, height)
# x, y, w, h => x0, y0, x1, y1
rectangle = rect_x, rect_y, rect_x + width, rect_y + height
new_page_links.append((link_type, target, rectangle))
links.append(new_page_links)
@ -334,7 +338,8 @@ def prepare_metadata(document, bookmark_root_id):
flatten_bookmarks(document.make_bookmark_tree()),
bookmark_root_id + 1):
target_page, target_x, target_y = target
target = target_page, target_x, page_heights[target_page] - target_y
target = (target_page,) + matrices[target_page].transform_point(
target_x, target_y)
bookmark = {
'Count': 0, 'First': None, 'Last': None, 'Prev': None,
'Next': None, 'Parent': last_id_by_depth[depth - 1],
@ -377,6 +382,7 @@ def write_pdf_metadata(document, fileobj):
pdf.extend_dict(pdf.catalog, pdf_format(
'/Outlines {0} 0 R /PageMode /UseOutlines', bookmark_root_id))
for bookmark in bookmarks:
print(bookmark)
content = [pdf_format('<< /Title {0!P}\n', bookmark['label'])]
content.append(pdf_format(
'/A << /Type /Action /S /GoTo /D [{0} /XYZ {1:f} {2:f} 0] >>',

View File

@ -460,8 +460,7 @@ def test_low_level_api():
''')
pdf_bytes = html.write_pdf(stylesheets=[css])
assert pdf_bytes.startswith(b'%PDF')
assert html.render([css], resolution=72).write_pdf() == pdf_bytes
assert html.render([css]).write_pdf() != pdf_bytes # Beware of resolution
assert html.render([css]).write_pdf() == pdf_bytes
png_bytes = html.write_png(stylesheets=[css])
document = html.render([css], enable_hinting=True)
@ -488,13 +487,25 @@ def test_low_level_api():
surface.write_to_png(file_obj)
check_png_pattern(file_obj.getvalue(), rotated=True)
document = html.render([css], enable_hinting=True, resolution=192)
document = html.render([css], enable_hinting=True)
page, = document.pages
assert (page.width, page.height) == (16, 16)
png_bytes, width, height = document.write_png()
assert (page.width, page.height) == (8, 8)
png_bytes, width, height = document.write_png(resolution=192)
assert (width, height) == (16, 16)
check_png_pattern(png_bytes, x2=True)
def png_size(result):
png_bytes, width, height = result
surface = cairo.ImageSurface.create_from_png(io.BytesIO(png_bytes))
assert (surface.get_width(), surface.get_height()) == (width, height)
return width, height
document = html.render([css], enable_hinting=True)
page, = document.pages
assert (page.width, page.height) == (8, 8)
# A resolution that is not multiple of 96:
assert png_size(document.write_png(resolution=145.2)) == (13, 13)
document = TestHTML(string='''
<style>
@page:first { size: 5px 10px } @page { size: 6px 4px }
@ -507,12 +518,6 @@ def test_low_level_api():
assert (page_1.width, page_1.height) == (5, 10)
assert (page_2.width, page_2.height) == (6, 4)
def png_size(result):
png_bytes, width, height = result
surface = cairo.ImageSurface.create_from_png(io.BytesIO(png_bytes))
assert (surface.get_width(), surface.get_height()) == (width, height)
return width, height
result = document.write_png()
# (Max of both widths, Sum of both heights)
assert png_size(result) == (6, 14)

View File

@ -42,9 +42,8 @@ def test_pdf_parser():
def get_metadata(html, base_url=resource_filename('<inline HTML>')):
return pdf.prepare_metadata(
TestHTML(string=html, base_url=base_url).render(
resolution=72, stylesheets=[
CSS(string='@page { size: 500pt 1000pt; margin: 50pt }')]),
TestHTML(string=html, base_url=base_url).render(stylesheets=[
CSS(string='@page { size: 500pt 1000pt; margin: 50pt }')]),
bookmark_root_id=0)