2011-07-02 14:27:13 +04:00
|
|
|
# coding: utf8
|
2012-03-22 02:19:27 +04:00
|
|
|
"""
|
|
|
|
weasyprint.text
|
|
|
|
---------------
|
2011-08-19 18:52:56 +04:00
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
Interface with Pango to decide where to do line breaks and to draw text.
|
2011-08-19 18:52:56 +04:00
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
|
|
|
:license: BSD, see LICENSE for details.
|
2011-08-19 18:52:56 +04:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2012-02-17 21:49:58 +04:00
|
|
|
from __future__ import division, unicode_literals
|
2011-09-30 13:54:56 +04:00
|
|
|
|
2012-07-23 20:52:19 +04:00
|
|
|
import os
|
|
|
|
from cgi import escape
|
|
|
|
|
2012-06-21 17:10:17 +04:00
|
|
|
import cairo
|
|
|
|
|
2012-07-28 21:23:47 +04:00
|
|
|
from .compat import xrange, basestring
|
2012-07-23 20:52:19 +04:00
|
|
|
from .logger import LOGGER
|
|
|
|
|
|
|
|
|
2012-07-29 18:25:43 +04:00
|
|
|
USING_INTROSPECTION = bool(os.environ.get('WEASYPRINT_USE_INTROSPECTION'))
|
|
|
|
if not USING_INTROSPECTION:
|
2012-07-23 20:52:19 +04:00
|
|
|
try:
|
2012-09-11 20:26:39 +04:00
|
|
|
import pygtk
|
2012-07-23 20:52:19 +04:00
|
|
|
except ImportError:
|
2012-07-29 18:25:43 +04:00
|
|
|
USING_INTROSPECTION = True
|
2011-08-11 19:26:08 +04:00
|
|
|
|
2012-07-29 18:25:43 +04:00
|
|
|
if USING_INTROSPECTION:
|
|
|
|
from gi.repository import Pango, PangoCairo
|
2012-07-23 20:52:19 +04:00
|
|
|
from gi.repository.PangoCairo import create_layout as create_pango_layout
|
|
|
|
|
2012-07-29 18:37:13 +04:00
|
|
|
if Pango.version() < 12903:
|
|
|
|
LOGGER.warn('Using Pango-introspection %s. Versions before '
|
2012-07-29 20:39:16 +04:00
|
|
|
'1.29.3 are known to be buggy.', Pango.version_string())
|
2012-07-29 18:37:13 +04:00
|
|
|
|
2012-07-23 20:52:19 +04:00
|
|
|
PANGO_VARIANT = {
|
|
|
|
'normal': Pango.Variant.NORMAL,
|
|
|
|
'small-caps': Pango.Variant.SMALL_CAPS,
|
|
|
|
}
|
|
|
|
PANGO_STYLE = {
|
|
|
|
'normal': Pango.Style.NORMAL,
|
|
|
|
'italic': Pango.Style.ITALIC,
|
|
|
|
'oblique': Pango.Style.OBLIQUE,
|
|
|
|
}
|
2012-08-17 19:37:33 +04:00
|
|
|
PANGO_STRETCH = {
|
|
|
|
'ultra-condensed': Pango.Stretch.ULTRA_CONDENSED,
|
|
|
|
'extra-condensed': Pango.Stretch.EXTRA_CONDENSED,
|
|
|
|
'condensed': Pango.Stretch.CONDENSED,
|
|
|
|
'semi-condensed': Pango.Stretch.SEMI_CONDENSED,
|
|
|
|
'normal': Pango.Stretch.NORMAL,
|
|
|
|
'semi-expanded': Pango.Stretch.SEMI_EXPANDED,
|
|
|
|
'expanded': Pango.Stretch.EXPANDED,
|
|
|
|
'extra-expanded': Pango.Stretch.EXTRA_EXPANDED,
|
|
|
|
'ultra-expanded': Pango.Stretch.ULTRA_EXPANDED,
|
|
|
|
}
|
2012-07-23 20:52:19 +04:00
|
|
|
PANGO_WRAP_WORD = Pango.WrapMode.WORD
|
|
|
|
|
|
|
|
def set_text(layout, text):
|
|
|
|
layout.set_text(text, -1)
|
|
|
|
|
|
|
|
def parse_markup(markup):
|
|
|
|
_, attributes_list, _, _ = Pango.parse_markup(markup, -1, '\x00')
|
|
|
|
return attributes_list
|
|
|
|
|
|
|
|
def get_size(line):
|
|
|
|
_ink_extents, logical_extents = line.get_extents()
|
|
|
|
return (units_to_double(logical_extents.width),
|
|
|
|
units_to_double(logical_extents.height))
|
|
|
|
|
|
|
|
def show_first_line(cairo_context, pango_layout, hinting):
|
|
|
|
"""Draw the given ``line`` to the Cairo ``context``."""
|
|
|
|
if hinting:
|
|
|
|
PangoCairo.update_layout(cairo_context, pango_layout)
|
|
|
|
lines = pango_layout.get_lines_readonly()
|
|
|
|
PangoCairo.show_layout_line(cairo_context, lines[0])
|
|
|
|
|
|
|
|
else:
|
2012-09-11 20:26:39 +04:00
|
|
|
pygtk.require('2.0')
|
2012-07-23 20:52:19 +04:00
|
|
|
import pango as Pango
|
|
|
|
import pangocairo
|
|
|
|
|
|
|
|
PANGO_VARIANT = {
|
|
|
|
'normal': Pango.VARIANT_NORMAL,
|
|
|
|
'small-caps': Pango.VARIANT_SMALL_CAPS,
|
|
|
|
}
|
|
|
|
PANGO_STYLE = {
|
|
|
|
'normal': Pango.STYLE_NORMAL,
|
|
|
|
'italic': Pango.STYLE_ITALIC,
|
|
|
|
'oblique': Pango.STYLE_OBLIQUE,
|
|
|
|
}
|
2012-08-17 19:37:33 +04:00
|
|
|
PANGO_STRETCH = {
|
|
|
|
'ultra-condensed': Pango.STRETCH_ULTRA_CONDENSED,
|
|
|
|
'extra-condensed': Pango.STRETCH_EXTRA_CONDENSED,
|
|
|
|
'condensed': Pango.STRETCH_CONDENSED,
|
|
|
|
'semi-condensed': Pango.STRETCH_SEMI_CONDENSED,
|
|
|
|
'normal': Pango.STRETCH_NORMAL,
|
|
|
|
'semi-expanded': Pango.STRETCH_SEMI_EXPANDED,
|
|
|
|
'expanded': Pango.STRETCH_EXPANDED,
|
|
|
|
'extra-expanded': Pango.STRETCH_EXTRA_EXPANDED,
|
|
|
|
'ultra-expanded': Pango.STRETCH_ULTRA_EXPANDED,
|
|
|
|
}
|
2012-07-23 20:52:19 +04:00
|
|
|
PANGO_WRAP_WORD = Pango.WRAP_WORD
|
|
|
|
set_text = Pango.Layout.set_text
|
|
|
|
|
|
|
|
def create_pango_layout(context):
|
|
|
|
return pangocairo.CairoContext(context).create_layout()
|
|
|
|
|
|
|
|
def parse_markup(markup):
|
|
|
|
attributes_list, _, _ = Pango.parse_markup(markup, '\x00')
|
|
|
|
return attributes_list
|
|
|
|
|
|
|
|
def get_size(line):
|
|
|
|
_ink_extents, logical_extents = line.get_extents()
|
|
|
|
_x, _y, width, height = logical_extents
|
|
|
|
return units_to_double(width), units_to_double(height)
|
|
|
|
|
|
|
|
def show_first_line(cairo_context, pango_layout, hinting):
|
|
|
|
"""Draw the given ``line`` to the Cairo ``context``."""
|
|
|
|
context = pangocairo.CairoContext(cairo_context)
|
|
|
|
if hinting:
|
|
|
|
context.update_layout(pango_layout)
|
|
|
|
context.show_layout_line(pango_layout.get_line(0))
|
2011-10-13 20:23:49 +04:00
|
|
|
|
|
|
|
|
2012-06-21 17:10:17 +04:00
|
|
|
NON_HINTED_DUMMY_CONTEXT = cairo.Context(cairo.PDFSurface(None, 1, 1))
|
|
|
|
HINTED_DUMMY_CONTEXT = cairo.Context(cairo.ImageSurface(
|
|
|
|
cairo.FORMAT_ARGB32, 1, 1))
|
|
|
|
|
2012-05-31 03:14:15 +04:00
|
|
|
|
2012-07-23 20:52:19 +04:00
|
|
|
def units_from_double(value):
|
|
|
|
return int(value * Pango.SCALE)
|
|
|
|
|
|
|
|
|
|
|
|
def units_to_double(value):
|
|
|
|
# True division, with the __future__ import
|
|
|
|
return value / Pango.SCALE
|
|
|
|
|
|
|
|
|
2012-06-21 17:10:17 +04:00
|
|
|
def create_layout(text, style, hinting, max_width):
|
|
|
|
"""Return an opaque Pango object to be passed to other functions
|
|
|
|
in this module.
|
2011-08-19 18:52:56 +04:00
|
|
|
|
2012-06-21 17:10:17 +04:00
|
|
|
:param text: Unicode
|
|
|
|
:param style: a :class:`StyleDict` of computed values
|
|
|
|
:param hinting: whether to enable text hinting or not
|
|
|
|
:param max_width:
|
|
|
|
The maximum available width in the same unit as ``style.font_size``,
|
|
|
|
or ``None`` for unlimited width.
|
|
|
|
|
|
|
|
"""
|
2012-07-23 20:52:19 +04:00
|
|
|
layout = create_pango_layout(
|
2012-06-21 17:10:17 +04:00
|
|
|
HINTED_DUMMY_CONTEXT if hinting else NON_HINTED_DUMMY_CONTEXT)
|
|
|
|
font = Pango.FontDescription()
|
2012-07-28 21:23:47 +04:00
|
|
|
assert not isinstance(style.font_family, basestring), (
|
|
|
|
'font_family should be a list')
|
2012-07-10 17:17:42 +04:00
|
|
|
font.set_family(','.join(style.font_family))
|
2012-06-21 17:10:17 +04:00
|
|
|
font.set_variant(PANGO_VARIANT[style.font_variant])
|
|
|
|
font.set_style(PANGO_STYLE[style.font_style])
|
2012-08-17 19:37:33 +04:00
|
|
|
font.set_stretch(PANGO_STRETCH[style.font_stretch])
|
2012-07-23 20:52:19 +04:00
|
|
|
font.set_absolute_size(units_from_double(style.font_size))
|
2012-06-21 17:10:17 +04:00
|
|
|
font.set_weight(style.font_weight)
|
|
|
|
layout.set_font_description(font)
|
2012-07-23 20:52:19 +04:00
|
|
|
layout.set_wrap(PANGO_WRAP_WORD)
|
|
|
|
set_text(layout, text)
|
|
|
|
# Make sure that max_width * Pango.SCALE == max_width * 1024 fits in a
|
|
|
|
# signed integer. Treat bigger values same as None: unconstrained width.
|
|
|
|
if max_width is not None and max_width < 2**21:
|
|
|
|
layout.set_width(units_from_double(max_width))
|
2012-06-21 17:10:17 +04:00
|
|
|
word_spacing = style.word_spacing
|
|
|
|
letter_spacing = style.letter_spacing
|
|
|
|
if letter_spacing == 'normal':
|
|
|
|
letter_spacing = 0
|
|
|
|
if text and (word_spacing != 0 or letter_spacing != 0):
|
2012-07-23 20:52:19 +04:00
|
|
|
word_spacing = units_from_double(word_spacing)
|
|
|
|
letter_spacing = units_from_double(letter_spacing)
|
2012-06-21 17:10:17 +04:00
|
|
|
markup = escape(text).replace(
|
|
|
|
' ', '<span letter_spacing="%i"> </span>' % (
|
|
|
|
word_spacing + letter_spacing,))
|
|
|
|
markup = '<span letter_spacing="%i">%s</span>' % (
|
2012-06-22 03:29:26 +04:00
|
|
|
letter_spacing, markup)
|
2012-07-23 20:52:19 +04:00
|
|
|
layout.set_attributes(parse_markup(markup))
|
2012-06-21 17:10:17 +04:00
|
|
|
return layout
|
|
|
|
|
|
|
|
|
|
|
|
def split_first_line(*args, **kwargs):
|
|
|
|
"""Fit as much as possible in the available width for one line of text.
|
|
|
|
|
|
|
|
Return ``(length, width, height, resume_at)``.
|
|
|
|
|
|
|
|
``show_line``: a closure that takes a cairo Context and draws the
|
|
|
|
first line.
|
|
|
|
``length``: length in UTF-8 bytes of the first line
|
|
|
|
``width``: width in pixels of the first line
|
|
|
|
``height``: height in pixels of the first line
|
|
|
|
``baseline``: baseline in pixels of the first line
|
|
|
|
``resume_at``: The number of UTF-8 bytes to skip for the next line.
|
|
|
|
May be ``None`` if the whole text fits in one line.
|
|
|
|
This may be greater than ``length`` in case of preserved
|
|
|
|
newline characters.
|
2011-08-19 18:52:56 +04:00
|
|
|
|
2011-07-29 03:13:07 +04:00
|
|
|
"""
|
2012-06-21 17:10:17 +04:00
|
|
|
layout = create_layout(*args, **kwargs)
|
2012-07-23 20:52:19 +04:00
|
|
|
first_line = layout.get_line(0)
|
2012-06-21 17:10:17 +04:00
|
|
|
length = first_line.length
|
2012-07-23 20:52:19 +04:00
|
|
|
width, height = get_size(first_line)
|
|
|
|
baseline = units_to_double(layout.get_iter().get_baseline())
|
|
|
|
if layout.get_line_count() >= 2:
|
|
|
|
resume_at = layout.get_line(1).start_index
|
|
|
|
else:
|
|
|
|
resume_at = None
|
2012-06-21 17:10:17 +04:00
|
|
|
return layout, length, resume_at, width, height, baseline
|
|
|
|
|
|
|
|
|
2012-07-12 18:10:30 +04:00
|
|
|
def line_widths(box, enable_hinting, width, skip=None):
|
2012-06-21 17:10:17 +04:00
|
|
|
"""Return the width for each line."""
|
2012-06-22 13:40:15 +04:00
|
|
|
# TODO: without the lstrip, we get an extra empty line at the beginning. Is
|
|
|
|
# there a better solution to avoid that?
|
|
|
|
layout = create_layout(
|
2012-07-12 18:10:30 +04:00
|
|
|
box.text[(skip or 0):].lstrip(), box.style, enable_hinting, width)
|
2012-07-23 20:52:19 +04:00
|
|
|
for i in xrange(layout.get_line_count()):
|
|
|
|
width, _height = get_size(layout.get_line(i))
|
|
|
|
yield width
|