mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Add first-level bookmarks, use floats for XY positions, fix little things
This commit is contained in:
parent
1ea79c9e9e
commit
111932edf5
@ -416,6 +416,16 @@ def line_height(computer, name, value):
|
||||
return ('PIXELS', pixels)
|
||||
|
||||
|
||||
@register_computer('anchor')
|
||||
def anchor(computer, name, values):
|
||||
"""Compute the ``anchor`` property."""
|
||||
if values == 'none':
|
||||
return None
|
||||
else:
|
||||
_, key = values
|
||||
return computer.element.get(key)
|
||||
|
||||
|
||||
@register_computer('link')
|
||||
def link(computer, name, values):
|
||||
"""Compute the ``link`` property."""
|
||||
@ -432,16 +442,6 @@ def link(computer, name, values):
|
||||
return url
|
||||
|
||||
|
||||
@register_computer('label')
|
||||
def label(computer, name, values):
|
||||
"""Compute the ``label`` property."""
|
||||
if values == 'none':
|
||||
return None
|
||||
else:
|
||||
_, key = values
|
||||
return computer.element.get(key)
|
||||
|
||||
|
||||
@register_computer('transform')
|
||||
def transform(computer, name, value):
|
||||
"""Compute the ``transform`` property."""
|
||||
|
@ -8,8 +8,8 @@ web browsers use.
|
||||
|
||||
*/
|
||||
|
||||
*[id] { -weasy-label: attr(id); }
|
||||
*[name] { -weasy-label: attr(name); }
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
*[name] { -weasy-anchor: attr(name); }
|
||||
*[dir] { unicode-bidi: embed; }
|
||||
*[hidden] { display: none; }
|
||||
*[dir=ltr] { direction: ltr; }
|
||||
@ -294,17 +294,17 @@ form { display: block; unicode-bidi: isolate; }
|
||||
frame { display: block; }
|
||||
frameset { display: block; }
|
||||
|
||||
h1 { display: block; font-size: 2em; font-weight: bold; hyphens: manual; margin-bottom: .67em; margin-top: .67em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; }
|
||||
h1 { display: block; font-size: 2em; font-weight: bold; hyphens: manual; margin-bottom: .67em; margin-top: .67em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 1; -weasy-bookmark-label: contents; }
|
||||
section h1 { font-size: 1.50em; margin-bottom: .83em; margin-top: .83em; }
|
||||
section section h1 { font-size: 1.17em; margin-bottom: 1.00em; margin-top: 1.00em; }
|
||||
section section section h1 { font-size: 1.00em; margin-bottom: 1.33em; margin-top: 1.33em; }
|
||||
section section section section h1 { font-size: .83em; margin-bottom: 1.67em; margin-top: 1.67em; }
|
||||
section section section section section h1 { font-size: .67em; margin-bottom: 2.33em; margin-top: 2.33em; }
|
||||
h2 { display: block; font-size: 1.50em; font-weight: bold; hyphens: manual; margin-bottom: .83em; margin-top: .83em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; }
|
||||
h3 { display: block; font-size: 1.17em; font-weight: bold; hyphens: manual; margin-bottom: 1.00em; margin-top: 1.00em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; }
|
||||
h4 { display: block; font-size: 1.00em; font-weight: bold; hyphens: manual; margin-bottom: 1.33em; margin-top: 1.33em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; }
|
||||
h5 { display: block; font-size: .83em; font-weight: bold; hyphens: manual; margin-bottom: 1.67em; margin-top: 1.67em; page-break-after: avoid; unicode-bidi: isolate; }
|
||||
h6 { display: block; font-size: .67em; font-weight: bold; hyphens: manual; margin-bottom: 2.33em; margin-top: 2.33em; page-break-after: avoid; unicode-bidi: isolate; }
|
||||
h2 { display: block; font-size: 1.50em; font-weight: bold; hyphens: manual; margin-bottom: .83em; margin-top: .83em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 2; -weasy-bookmark-label: contents; }
|
||||
h3 { display: block; font-size: 1.17em; font-weight: bold; hyphens: manual; margin-bottom: 1.00em; margin-top: 1.00em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 3; -weasy-bookmark-label: contents; }
|
||||
h4 { display: block; font-size: 1.00em; font-weight: bold; hyphens: manual; margin-bottom: 1.33em; margin-top: 1.33em; page-break-after: avoid; page-break-inside: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 4; -weasy-bookmark-label: contents; }
|
||||
h5 { display: block; font-size: .83em; font-weight: bold; hyphens: manual; margin-bottom: 1.67em; margin-top: 1.67em; page-break-after: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 5; -weasy-bookmark-label: contents; }
|
||||
h6 { display: block; font-size: .67em; font-weight: bold; hyphens: manual; margin-bottom: 2.33em; margin-top: 2.33em; page-break-after: avoid; unicode-bidi: isolate; -weasy-bookmark-level: 6; -weasy-bookmark-label: contents; }
|
||||
|
||||
head { display: none; }
|
||||
header { display: block; unicode-bidi: isolate; }
|
||||
|
@ -129,8 +129,13 @@ INITIAL_VALUES = {
|
||||
'image_rendering': 'auto',
|
||||
|
||||
# Proprietary
|
||||
'anchor': None, # computed value of 'none'
|
||||
'link': None, # computed value of 'none'
|
||||
'label': None, # computed value of 'none'
|
||||
|
||||
# CSS3 Generated Content for Paged Media
|
||||
# http://dev.w3.org/csswg/css3-gcpm/
|
||||
'bookmark_label': ('keyword', 'none'), # computed value of 'none'
|
||||
'bookmark_level': 'none',
|
||||
}
|
||||
|
||||
|
||||
|
@ -645,7 +645,7 @@ def orphans_widows(token):
|
||||
"""Validation for the ``orphans`` or ``widows`` properties."""
|
||||
if token.type == 'INTEGER':
|
||||
value = token.value
|
||||
if int(value) == value and value >= 1:
|
||||
if value >= 1:
|
||||
return value
|
||||
|
||||
|
||||
@ -819,6 +819,21 @@ def size(tokens):
|
||||
return width, height
|
||||
|
||||
|
||||
@validator(prefixed=True) # Proprietary
|
||||
@single_token
|
||||
def anchor(token):
|
||||
"""Validation for ``anchor``."""
|
||||
if get_keyword(token) == 'none':
|
||||
return 'none'
|
||||
function = parse_function(token)
|
||||
if function:
|
||||
name, args = function
|
||||
prototype = (name, [a.type for a in args])
|
||||
args = [a.value for a in args]
|
||||
if prototype == ('attr', ['IDENT']):
|
||||
return (name, args[0])
|
||||
|
||||
|
||||
@validator(prefixed=True, wants_base_url=True) # Proprietary
|
||||
@single_token
|
||||
def link(token, base_url):
|
||||
@ -837,19 +852,28 @@ def link(token, base_url):
|
||||
return token.value
|
||||
|
||||
|
||||
@validator(prefixed=True) # Proprietary
|
||||
@validator(prefixed=True) # CSS3 GCPM
|
||||
@single_token
|
||||
def label(token):
|
||||
"""Validation for ``label``."""
|
||||
if get_keyword(token) == 'none':
|
||||
def bookmark_label(token):
|
||||
"""Validation for ``bookmark-label``."""
|
||||
keyword = get_keyword(token)
|
||||
if keyword in ('none', 'contents', 'content-before',
|
||||
'content-element', 'content-after'):
|
||||
return ('keyword', keyword)
|
||||
elif token.type == 'STRING':
|
||||
return ('string', token.value)
|
||||
|
||||
|
||||
@validator(prefixed=True) # CSS3 GCPM
|
||||
@single_token
|
||||
def bookmark_level(token):
|
||||
"""Validation for ``bookmark-level``."""
|
||||
if token.type == 'INTEGER':
|
||||
value = token.value
|
||||
if value >= 1:
|
||||
return value
|
||||
elif get_keyword(token) == 'none':
|
||||
return 'none'
|
||||
function = parse_function(token)
|
||||
if function:
|
||||
name, args = function
|
||||
prototype = (name, [a.type for a in args])
|
||||
args = [a.value for a in args]
|
||||
if prototype == ('attr', ['IDENT']):
|
||||
return (name, args[0])
|
||||
|
||||
|
||||
@validator(prefixed=True) # Not in CR yet
|
||||
|
@ -182,12 +182,54 @@ class PDFDocument(Document):
|
||||
|
||||
links = [self._get_link_rectangles(page) for page in self.pages]
|
||||
destinations = dict(self._get_link_destinations())
|
||||
bookmarks_tree = []
|
||||
bookmarks_stack = [bookmarks_tree]
|
||||
bookmark_level = 0
|
||||
for level, label, destination in self._get_bookmarks():
|
||||
if not bookmark_level:
|
||||
bookmark_level = level - 1
|
||||
if level < bookmark_level:
|
||||
for i in range(bookmark_level - level + 1):
|
||||
bookmarks_stack.pop()
|
||||
elif level > bookmark_level:
|
||||
missing_levels = level - bookmark_level
|
||||
for i in range(missing_levels):
|
||||
children = []
|
||||
bookmarks_stack[-1].append((label, destination, children))
|
||||
bookmarks_stack.append(children)
|
||||
else:
|
||||
bookmarks_stack.pop()
|
||||
children = []
|
||||
bookmarks_stack[-1].append((label, destination, children))
|
||||
bookmarks_stack.append(children)
|
||||
bookmark_level = level
|
||||
|
||||
if hasattr(target, 'write'):
|
||||
pdf.write(bytesio, target, links, destinations)
|
||||
pdf.write(bytesio, target, links, destinations, bookmarks_tree)
|
||||
else:
|
||||
with open(target, 'wb') as fd:
|
||||
pdf.write(bytesio, fd, links, destinations)
|
||||
pdf.write(bytesio, fd, links, destinations, bookmarks_tree)
|
||||
|
||||
def _get_bookmarks(self, page=None, box=None):
|
||||
if page is None:
|
||||
for page in self.pages:
|
||||
for bookmark in self._get_bookmarks(page, page):
|
||||
yield bookmark
|
||||
else:
|
||||
if box.bookmark_label and box.style.bookmark_level != 'none':
|
||||
position_x = box.position_x
|
||||
position_y = page.outer_height - box.position_y
|
||||
yield (
|
||||
box.style.bookmark_level,
|
||||
box.bookmark_label,
|
||||
(self.pages.index(page),
|
||||
position_x / LENGTHS_TO_PIXELS['pt'],
|
||||
position_y / LENGTHS_TO_PIXELS['pt']))
|
||||
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
for child in box.children:
|
||||
for bookmark in self._get_bookmarks(page, child):
|
||||
yield bookmark
|
||||
|
||||
def _get_link_rectangles(self, page, box=None):
|
||||
if box is None:
|
||||
@ -216,12 +258,12 @@ class PDFDocument(Document):
|
||||
page, page, names):
|
||||
yield destination
|
||||
else:
|
||||
if box.style.label and (box.style.label not in names):
|
||||
names.add(box.style.label)
|
||||
if box.style.anchor and box.style.anchor not in names:
|
||||
names.add(box.style.anchor)
|
||||
position_x = box.position_x
|
||||
position_y = page.outer_height - box.position_y
|
||||
yield (
|
||||
box.style.label,
|
||||
box.style.anchor,
|
||||
(self.pages.index(page),
|
||||
position_x / LENGTHS_TO_PIXELS['pt'],
|
||||
position_y / LENGTHS_TO_PIXELS['pt']))
|
||||
|
@ -76,10 +76,8 @@ class Box(object):
|
||||
|
||||
# Default, may be overriden on instances.
|
||||
is_table_wrapper = False
|
||||
|
||||
# Default, may be overriden on instances.
|
||||
is_for_root_element = False
|
||||
|
||||
bookmark_label = None
|
||||
|
||||
def __init__(self, element_tag, sourceline, style):
|
||||
self.element_tag = element_tag
|
||||
|
@ -134,6 +134,8 @@ def dom_to_box(document, element, state=None):
|
||||
|
||||
box = box.copy_with_children(children)
|
||||
|
||||
resolve_bookmark_labels(box)
|
||||
|
||||
# Specific handling for the element. (eg. replaced element)
|
||||
return html.handle_element(document, element, box)
|
||||
|
||||
@ -892,3 +894,61 @@ def set_viewport_overflow(root_box):
|
||||
root_box.viewport_overflow = chosen_box.style.overflow
|
||||
chosen_box.style = chosen_box.style.updated_copy({'overflow': 'visible'})
|
||||
return root_box
|
||||
|
||||
|
||||
def box_text_contents(box):
|
||||
if isinstance(box, boxes.TextBox):
|
||||
return box.text
|
||||
elif isinstance(box, boxes.ParentBox):
|
||||
return ''.join(box_text_contents(child) for child in box.children)
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def box_text_content_element(box):
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
return ''.join(
|
||||
box_text_contents(child) for child in box.children
|
||||
if not child.element_tag.endswith((':after', ':before')))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def box_text_content_before(box):
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
return ''.join(
|
||||
box_text_contents(child) for child in box.children
|
||||
if child.element_tag.endswith(':before'))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def box_text_content_after(box):
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
return ''.join(
|
||||
box_text_contents(child) for child in box.children
|
||||
if child.element_tag.endswith(':after'))
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def resolve_bookmark_labels(box):
|
||||
"""Set the used value of the bookmark-label.
|
||||
|
||||
See http://dev.w3.org/csswg/css3-gcpm/#bookmarks
|
||||
|
||||
"""
|
||||
key, value = box.style.bookmark_label
|
||||
if key == 'keyword':
|
||||
if value == 'none':
|
||||
box.bookmark_label = None
|
||||
elif value == 'content-element':
|
||||
box.bookmark_label = box_text_content_element(box)
|
||||
elif value == 'contents':
|
||||
box.bookmark_label = box_text_contents(box)
|
||||
elif value == 'content-before':
|
||||
box.bookmark_label = box_text_content_before(box)
|
||||
elif value == 'content-before':
|
||||
box.bookmark_label = box_text_content_after(box)
|
||||
else:
|
||||
box.bookmark_label = value
|
||||
|
@ -24,7 +24,7 @@ def pdf_encode(unicode_string):
|
||||
return ('\ufeff' + unicode_string).encode('utf-16-be')
|
||||
|
||||
|
||||
def write(bytesio, target, links, destinations):
|
||||
def write(bytesio, target, links, destinations, bookmarks):
|
||||
"""Write PDF from ``bytesio`` to ``target`` adding ``links``."""
|
||||
bytesio.seek(0)
|
||||
position = 0
|
||||
@ -47,6 +47,9 @@ def write(bytesio, target, links, destinations):
|
||||
if line.endswith(b'/Type /Page\n'):
|
||||
pages.append(number)
|
||||
|
||||
if line.endswith(b'/Type /Catalog\n'):
|
||||
catalog = number
|
||||
|
||||
while lines[0] != b'trailer\n':
|
||||
lines.pop(0)
|
||||
|
||||
@ -78,8 +81,8 @@ def write(bytesio, target, links, destinations):
|
||||
if link.startswith('#'):
|
||||
if link[1:] in destinations:
|
||||
text.append((
|
||||
'/A << /Type /Action /S /GoTo'
|
||||
'/D [%d /XYZ %d %d 1]\n'
|
||||
'/A << /Type /Action /S /GoTo '
|
||||
'/D [%d /XYZ %f %f 0]\n'
|
||||
% destinations[link[1:]]
|
||||
).encode('ascii'))
|
||||
else:
|
||||
@ -100,6 +103,33 @@ def write(bytesio, target, links, destinations):
|
||||
b']\n'
|
||||
]))
|
||||
|
||||
if bookmarks:
|
||||
parent = number
|
||||
objects[catalog].insert(
|
||||
-2, ('/Outlines %d 0 R\n' % parent).encode('ascii'))
|
||||
objects[parent] = [(
|
||||
'%d 0 obj\n<< /Type /Outlines '
|
||||
'/Count %d /First %d 0 R /Last %d 0 R\n>>\nendobj\n' % (
|
||||
parent, len(bookmarks), parent + 1, parent + len(bookmarks))
|
||||
).encode('ascii')]
|
||||
number += 1
|
||||
|
||||
for bookmark in bookmarks:
|
||||
label, destination, children = bookmark
|
||||
text = ('%d 0 obj\n<< /Title (' % number).encode('ascii')
|
||||
text += pdf_encode(label)
|
||||
text += (') /Parent %d 0 R\n' % parent).encode('ascii')
|
||||
if number > parent + 1:
|
||||
text += ('/Prev %d 0 R\n' % (number - 1)).encode('ascii')
|
||||
elif number < parent + len(bookmarks):
|
||||
text += ('/Next %d 0 R\n' % (number + 1)).encode('ascii')
|
||||
text += (
|
||||
'/A << /Type /Action /S /GoTo '
|
||||
'/D [%d /XYZ %f %f 0]\n>>\n>>\nendobj\n'
|
||||
% destination).encode('ascii')
|
||||
objects[number] = [text]
|
||||
number += 1
|
||||
|
||||
for i, line in enumerate(trailer):
|
||||
if b'/Size' in line:
|
||||
trailer[i] = (
|
||||
|
Loading…
Reference in New Issue
Block a user