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

Use pdfrw instead of editing PDF files directly #516

Merged
merged 13 commits into from
Oct 5, 2017
Merged
5 changes: 2 additions & 3 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,13 @@ paged media "describing how:
- content such as page counters are placed in the headers and footers; and
- orphans and widows can be controlled."

The ``bleed`` and ``marks`` properties from this document are **not**
implemented. All the other features are available, including:
All the features of this draft are available, including:

- the ``@page`` rule and the ``:left``, ``:right``, ``:first`` and ``:blank``
selectors;
- the page margin boxes;
- the page-based counters (with known bugs `#91`_, `#93`_, `#289`_);
- the page ``size`` property;
- the page ``size``, ``bleed`` and ``marks`` properties;
- the named pages.

.. _CSS Paged Media Module Level 3: http://dev.w3.org/csswg/css3-page/
Expand Down
2 changes: 2 additions & 0 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ WeasyPrint |version| depends on:
* cssselect2_ ≥ 0.1
* CairoSVG_ ≥ 1.0.20
* Pyphen_ ≥ 0.8
* pdfrw_ ≥ 0.4
* Optional: GDK-PixBuf_ [#]_

.. _CPython: http://www.python.org/
Expand All @@ -25,6 +26,7 @@ WeasyPrint |version| depends on:
.. _cssselect2: https://pypi.python.org/pypi/cssselect2
.. _CairoSVG: http://cairosvg.org/
.. _Pyphen: http://pyphen.org/
.. _pdfrw: https://github.com/pmaupin/pdfrw/
.. _GDK-PixBuf: https://live.gnome.org/GdkPixbuf


Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
'cssselect2>=0.1',
'cffi>=0.6',
'cairocffi>=0.5',
'Pyphen>=0.8'
'Pyphen>=0.8',
'pdfrw>=0.4',
# C dependencies: Gdk-Pixbuf (optional), Pango, cairo.
]

Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
# A test function that returns True if the given property name has an
# initial value that is not always the same when computed.
RE_INITIAL_NOT_COMPUTED = re.compile(
'^(display|column_gap|'
'^(display|column_gap|(bleed_(left|right|top|bottom))|'
'(border_[a-z]+|outline|column_rule)_(width|color))$').match


Expand Down
16 changes: 15 additions & 1 deletion weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def _computing_order():
"""Some computed values are required by others, so order matters."""
first = [
'font_stretch', 'font_weight', 'font_family', 'font_variant',
'font_style', 'font_size', 'line_height']
'font_style', 'font_size', 'line_height', 'marks']
order = sorted(INITIAL_VALUES)
for name in first:
order.remove(name)
Expand Down Expand Up @@ -329,6 +329,20 @@ def length(computer, name, value, font_size=None, pixels_only=False):
return result if pixels_only else Dimension(result, 'px')


@register_computer('bleed-left')
@register_computer('bleed-right')
@register_computer('bleed-top')
@register_computer('bleed-bottom')
def bleed(computer, name, value):
if value == 'auto':
if 'crop' in computer.computed['marks']:
return Dimension(8, 'px') # 6pt
else:
return Dimension(0, 'px')
else:
return length(computer, name, value)


@register_computer('letter-spacing')
def pixel_length(computer, name, value):
if value == 'normal':
Expand Down
5 changes: 5 additions & 0 deletions weasyprint/css/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
# Paged Media 3: https://www.w3.org/TR/css3-page/
'size': None, # set to A4 in computed_values
'page': 'auto',
'bleed_left': 'auto',
'bleed_right': 'auto',
'bleed_top': 'auto',
'bleed_bottom': 'auto',
'marks': 'none',

# Text 3/4: https://www.w3.org/TR/css-text-4/
'hyphenate_character': '‐', # computed value chosen by the user agent
Expand Down
1 change: 1 addition & 0 deletions weasyprint/css/tests_ua.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
Simplified user-agent stylesheet for HTML5 in tests.
*/
@page { bleed: 0 }
html, body, div, h1, h2, h3, h4, ol, p, ul, hr, pre, section, article
{ display: block; }
li { display: list-item }
Expand Down
30 changes: 30 additions & 0 deletions weasyprint/css/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,35 @@ def page(token):
return 'auto' if token.lower_value == 'auto' else token.value


@validator("bleed-left")
@validator("bleed-right")
@validator("bleed-top")
@validator("bleed-bottom")
@single_token
def bleed(token):
"""``bleed`` property validation."""
keyword = get_keyword(token)
if keyword == 'auto':
return 'auto'
else:
return get_length(token)


@validator()
def marks(tokens):
"""``marks`` property validation."""
if len(tokens) == 2:
keywords = [get_keyword(token) for token in tokens]
if 'crop' in keywords and 'cross' in keywords:
return keywords
elif len(tokens) == 1:
keyword = get_keyword(tokens[0])
if keyword in ('crop', 'cross'):
return [keyword]
elif keyword == 'none':
return 'none'


@validator('outline-style')
@single_keyword
def outline_style(keyword):
Expand Down Expand Up @@ -1630,6 +1659,7 @@ def expander_decorator(function):
@expander('border-width')
@expander('margin')
@expander('padding')
@expander('bleed')
def expand_four_sides(base_url, name, tokens):
"""Expand properties setting a token for the four sides of a box."""
# Make sure we have 4 tokens
Expand Down
18 changes: 14 additions & 4 deletions weasyprint/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ def __init__(self, page_box, enable_hinting=False):
#: The page height, including margins, in CSS pixels.
self.height = page_box.margin_height()

#: The page bleed width, in CSS pixels.
self.bleed = {
side: page_box.style['bleed_%s' % side].value
for side in ('top', 'right', 'bottom', 'left')}

#: A list of ``(bookmark_level, bookmark_label, target)`` tuples.
#: :obj:`bookmark_level` and :obj:`bookmark_label` are respectively
#: an integer and a Unicode string, based on the CSS properties
Expand Down Expand Up @@ -481,10 +486,15 @@ def write_pdf(self, target=None, zoom=1, attachments=None):
LOGGER.info('Step 6 - Drawing')
for page in self.pages:
surface.set_size(
math.floor(page.width * scale),
math.floor(page.height * scale))
page.paint(context, scale=scale)
surface.show_page()
math.floor(scale * (
page.width + page.bleed['left'] + page.bleed['right'])),
math.floor(scale * (
page.height + page.bleed['top'] + page.bleed['bottom'])))
with stacked(context):
context.translate(
page.bleed['left'] * scale, page.bleed['top'] * scale)
page.paint(context, scale=scale)
surface.show_page()
surface.finish()

LOGGER.info('Step 7 - Adding PDF metadata')
Expand Down
112 changes: 110 additions & 2 deletions weasyprint/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,67 @@

from .compat import xrange
from .formatting_structure import boxes
from .images import SVGImage
from .layout.backgrounds import BackgroundLayer
from .stacking import StackingContext
from .text import show_first_line

SIDES = ('top', 'right', 'bottom', 'left')
CROP = '''
<!-- horizontal top left -->
<path d="M0,{bleed_top} h{half_bleed_left}" />
<!-- horizontal top right -->
<path d="M0,{bleed_top} h{half_bleed_right}"
transform="translate({width},0) scale(-1,1)" />
<!-- horizontal bottom right -->
<path d="M0,{bleed_bottom} h{half_bleed_right}"
transform="translate({width},{height}) scale(-1,-1)" />
<!-- horizontal bottom left -->
<path d="M0,{bleed_bottom} h{half_bleed_left}"
transform="translate(0,{height}) scale(1,-1)" />
<!-- vertical top left -->
<path d="M{bleed_left},0 v{half_bleed_top}" />
<!-- vertical bottom right -->
<path d="M{bleed_right},0 v{half_bleed_bottom}"
transform="translate({width},{height}) scale(-1,-1)" />
<!-- vertical bottom left -->
<path d="M{bleed_left},0 v{half_bleed_bottom}"
transform="translate(0,{height}) scale(1,-1)" />
<!-- vertical top right -->
<path d="M{bleed_right},0 v{half_bleed_top}"
transform="translate({width},0) scale(-1,1)" />
'''
CROSS = '''
<!-- top -->
<circle r="{half_bleed_top}"
transform="scale(0.5)
translate({width},{half_bleed_top}) scale(0.5)" />
<path d="M-{half_bleed_top},{half_bleed_top} h{bleed_top}
M0,0 v{bleed_top}"
transform="scale(0.5) translate({width},0)" />
<!-- bottom -->
<circle r="{half_bleed_bottom}"
transform="translate(0,{height}) scale(0.5)
translate({width},-{half_bleed_bottom}) scale(0.5)" />
<path d="M-{half_bleed_bottom},-{half_bleed_bottom} h{bleed_bottom}
M0,0 v-{bleed_bottom}"
transform="translate(0,{height}) scale(0.5) translate({width},0)" />
<!-- left -->
<circle r="{half_bleed_left}"
transform="scale(0.5)
translate({half_bleed_left},{height}) scale(0.5)" />
<path d="M{half_bleed_left},-{half_bleed_left} v{bleed_left}
M0,0 h{bleed_left}"
transform="scale(0.5) translate(0,{height})" />
<!-- right -->
<circle r="{half_bleed_right}"
transform="translate({width},0) scale(0.5)
translate(-{half_bleed_right},{height}) scale(0.5)" />
<path d="M-{half_bleed_right},-{half_bleed_right} v{bleed_right}
M0,0 h-{bleed_right}"
transform="translate({width},0)
scale(0.5) translate(0,{height})" />
'''


@contextlib.contextmanager
Expand Down Expand Up @@ -91,10 +148,14 @@ def lighten(color):

def draw_page(page, context, enable_hinting):
"""Draw the given PageBox."""
bleed = {
side: page.style['bleed_%s' % side].value
for side in ('top', 'right', 'bottom', 'left')}
marks = page.style['marks']
stacking_context = StackingContext.from_page(page)
draw_background(
context, stacking_context.box.background, enable_hinting,
clip_box=False)
clip_box=False, bleed=bleed, marks=marks)
draw_background(
context, page.canvas_background, enable_hinting, clip_box=False)
draw_border(context, page, enable_hinting)
Expand Down Expand Up @@ -261,7 +322,8 @@ def rounded_box_path(context, radii):
context.restore()


def draw_background(context, bg, enable_hinting, clip_box=True):
def draw_background(context, bg, enable_hinting, clip_box=True, bleed=None,
marks=()):
"""Draw the background color and image to a ``cairo.Context``.

If ``clip_box`` is set to ``False``, the background is not clipped to the
Expand All @@ -286,11 +348,57 @@ def draw_background(context, bg, enable_hinting, clip_box=True):
with stacked(context):
painting_area = bg.layers[-1].painting_area
if painting_area:
if bleed:
# Painting area is the PDF BleedBox
x, y, width, height = painting_area
painting_area = (
x - bleed['left'], y - bleed['top'],
width + bleed['left'] + bleed['right'],
height + bleed['top'] + bleed['bottom'])
context.rectangle(*painting_area)
context.clip()
context.set_source_rgba(*bg.color)
context.paint()

if bleed and marks:
x, y, width, height = bg.layers[-1].painting_area
x -= bleed['left']
y -= bleed['top']
width += bleed['left'] + bleed['right']
height += bleed['top'] + bleed['bottom']
svg = '''
<svg height="{height}" width="{width}"
fill="transparent" stroke="black" stroke-width="1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
'''
if 'crop' in marks:
svg += CROP
if 'cross' in marks:
svg += CROSS
svg += '</svg>'
half_bleed = {key: value * 0.5 for key, value in bleed.items()}
image = SVGImage(svg.format(
height=height, width=width,
bleed_left=bleed['left'], bleed_right=bleed['right'],
bleed_top=bleed['top'], bleed_bottom=bleed['bottom'],
half_bleed_left=half_bleed['left'],
half_bleed_right=half_bleed['right'],
half_bleed_top=half_bleed['top'],
half_bleed_bottom=half_bleed['bottom'],
), '', None)
# Painting area is the PDF media box
size = (width, height)
position = (x, y)
repeat = ('no-repeat', 'no-repeat')
unbounded = True
painting_area = position + size
positioning_area = (0, 0, width, height)
clipped_boxes = []
layer = BackgroundLayer(
image, size, position, repeat, unbounded, painting_area,
positioning_area, clipped_boxes)
bg.layers.insert(0, layer)
# Paint in reversed order: first layer is "closest" to the viewer.
for layer in reversed(bg.layers):
draw_background_image(context, layer, bg.image_rendering)
Expand Down
6 changes: 4 additions & 2 deletions weasyprint/layout/backgrounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ def layout_box_backgrounds(page, box, get_image_from_uri):
style = box.style
if style.visibility == 'hidden':
box.background = None
return
if page != box: # Pages need a background for bleed box
return

images = [get_image_from_uri(value) if type_ == 'url' else value
for type_, value in style.background_image]
color = style.get_color('background_color')
if color.alpha == 0 and not any(images):
box.background = None
return
if page != box: # Pages need a background for bleed box
return

box.background = Background(
color=color, image_rendering=style.image_rendering, layers=[
Expand Down
Loading