Skip to content

Commit

Permalink
Merge pull request #516 from Kozea/pdfrw
Browse files Browse the repository at this point in the history
Use pdfrw instead of editing PDF files directly
  • Loading branch information
liZe authored Oct 5, 2017
2 parents 3be18b7 + 0ec7614 commit 0fcf413
Show file tree
Hide file tree
Showing 14 changed files with 651 additions and 742 deletions.
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://cssselect2.readthedocs.io/
.. _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 (WD): 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 (WD/WD): 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 @@ -617,6 +617,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 @@ -1632,6 +1661,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

0 comments on commit 0fcf413

Please sign in to comment.