diff --git a/weasyprint/document.py b/weasyprint/document.py index 282b6559b9..0e7aabc11a 100644 --- a/weasyprint/document.py +++ b/weasyprint/document.py @@ -16,7 +16,7 @@ import pydyf from fontTools import subset -from fontTools.ttLib import TTFont, TTLibError +from fontTools.ttLib import TTFont, TTLibError, ttFont from . import CSS, Attachment, __version__ from .css import get_all_computed_styles @@ -30,7 +30,7 @@ from .layout import LayoutContext, layout_document from .layout.percentages import percentage from .logger import LOGGER, PROGRESS_LOGGER -from .text.ffi import ffi, pango +from .text.ffi import ffi, harfbuzz, pango from .text.fonts import FontConfiguration from .urls import URLFetchingError @@ -68,6 +68,8 @@ def _w3c_date_to_pdf(string, attr_name): class Font: def __init__(self, file_content, pango_font, index): pango_metrics = pango.pango_font_get_metrics(pango_font, ffi.NULL) + hb_font = pango.pango_font_get_hb_font(pango_font) + hb_face = harfbuzz.hb_font_get_face(hb_font) self._font_description = pango.pango_font_describe(pango_font) self.family = ffi.string(pango.pango_font_description_get_family( self._font_description)) @@ -93,6 +95,9 @@ def __init__(self, file_content, pango_font, index): self.descent = -int( pango.pango_font_metrics_get_descent(pango_metrics) / font_size * 1000) + self.upem = harfbuzz.hb_face_get_upem(hb_face) + self.png = harfbuzz.hb_ot_color_has_png(hb_face) + self.svg = harfbuzz.hb_ot_color_has_svg(hb_face) self.stemv = 80 self.stemh = 80 self.bbox = [0, 0, 0, 0] @@ -1290,12 +1295,14 @@ def write_pdf(self, target=None, zoom=1, attachments=None, finisher=None): fonts_by_file_hash[font.file_hash] = [font] font_references_by_file_hash = {} for file_hash, fonts in fonts_by_file_hash.items(): + content = fonts[0].file_content + if 'fonts' in self.optimize_size: # Optimize font cmap = {} for font in fonts: cmap = {**cmap, **font.cmap} - full_font = io.BytesIO(fonts[0].file_content) + full_font = io.BytesIO(content) optimized_font = io.BytesIO() try: ttfont = TTFont(full_font, fontNumber=fonts[0].index) @@ -1309,9 +1316,31 @@ def write_pdf(self, target=None, zoom=1, attachments=None, finisher=None): content = optimized_font.getvalue() except TTLibError: LOGGER.warning('Unable to optimize font') - content = fonts[0].file_content - else: - content = fonts[0].file_content + + if fonts[0].png or fonts[0].svg: + # Add empty glyphs instead of PNG or SVG emojis + full_font = io.BytesIO(content) + try: + ttfont = TTFont(full_font, fontNumber=fonts[0].index) + if 'loca' not in ttfont or 'glyf' not in ttfont: + ttfont['loca'] = ttFont.getTableClass('loca')() + ttfont['glyf'] = ttFont.getTableClass('glyf')() + ttfont['glyf'].glyphOrder = ttfont.getGlyphOrder() + ttfont['glyf'].glyphs = { + name: ttFont.getTableModule('glyf').Glyph() + for name in ttfont['glyf'].glyphOrder} + else: + for glyph in ttfont['glyf'].glyphs: + ttfont['glyf'][glyph] = ( + ttFont.getTableModule('glyf').Glyph()) + for table_name in ('CBDT', 'CBLC', 'SVG '): + if table_name in ttfont: + del ttfont[table_name] + output_font = io.BytesIO() + ttfont.save(output_font) + content = output_font.getvalue() + except TTLibError: + LOGGER.warning('Unable to save emoji font') # Include font font_type = 'otf' if content[:4] == b'OTTO' else 'ttf' diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 5662b60bf8..ae0a30babf 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -9,11 +9,14 @@ import contextlib import operator from colorsys import hsv_to_rgb, rgb_to_hsv +from io import BytesIO from math import ceil, floor, pi, sqrt, tan from xml.etree import ElementTree +from PIL import Image + from .formatting_structure import boxes -from .images import SVGImage +from .images import RasterImage, SVGImage from .layout import replaced from .layout.backgrounds import BackgroundLayer from .stacking import StackingContext @@ -983,9 +986,12 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis): textbox.pango_layout.reactivate(textbox.style) stream.begin_text() - draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y) + emojis = draw_first_line( + stream, textbox, text_overflow, block_ellipsis, x, y) stream.end_text() + draw_emojis(stream, textbox.style['font_size'], x, y, emojis) + # Draw text decoration values = textbox.style['text_decoration_line'] color = textbox.style['text_decoration_color'] @@ -1011,6 +1017,15 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis): textbox.pango_layout.deactivate() +def draw_emojis(stream, font_size, x, y, emojis): + for emoji in emojis: + for image, font, a, d, e, f in emojis: + stream.push_state() + stream.transform(a=a, d=d, e=x + e * font_size, f=y + f) + image.draw(stream, font_size, font_size, None) + stream.pop_state() + + def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y): """Draw the given ``textbox`` line to the document ``stream``.""" pango.pango_layout_set_single_paragraph_mode( @@ -1067,6 +1082,8 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y): last_font = None string = '' fonts = stream.get_fonts() + x_advance = 0 + emojis = [] for run in runs: # Pango objects glyph_item = ffi.cast('PangoGlyphItem *', run.data) @@ -1084,7 +1101,9 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y): if font_hash in fonts: font = fonts[font_hash] else: - hb_blob = harfbuzz.hb_face_reference_blob(hb_face) + hb_blob = ffi.gc( + harfbuzz.hb_face_reference_blob(hb_face), + harfbuzz.hb_blob_destroy) hb_data = harfbuzz.hb_blob_get_data(hb_blob, stream.length) file_content = ffi.unpack(hb_data, int(stream.length[0])) index = harfbuzz.hb_face_get_index(hb_face) @@ -1148,6 +1167,39 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y): font.cmap[glyph] = utf8_text[utf8_slice].decode('utf-8') previous_utf8_position = utf8_position + if font.svg: + hb_blob = ffi.gc( + harfbuzz.hb_ot_color_glyph_reference_svg(hb_face, glyph), + harfbuzz.hb_blob_destroy) + hb_data = harfbuzz.hb_blob_get_data(hb_blob, stream.length) + if hb_data != ffi.NULL: + svg_data = ffi.unpack(hb_data, int(stream.length[0])) + image = SVGImage(svg_data, None, None, stream) + a = d = font.widths[glyph] / 1000 / font.upem * font_size + emojis.append([image, font, a, d, x_advance, 0]) + elif font.png: + hb_blob = ffi.gc( + harfbuzz.hb_ot_color_glyph_reference_png(hb_font, glyph), + harfbuzz.hb_blob_destroy) + hb_data = harfbuzz.hb_blob_get_data(hb_blob, stream.length) + if hb_data != ffi.NULL: + png_data = ffi.unpack(hb_data, int(stream.length[0])) + pillow_image = Image.open(BytesIO(png_data)) + image_id = f'{font.hash}{glyph}' + image = RasterImage( + pillow_image, image_id, optimize_size=()) + d = font.widths[glyph] / 1000 + a = pillow_image.width / pillow_image.height * d + pango.pango_font_get_glyph_extents( + pango_font, glyph, stream.ink_rect, + stream.logical_rect) + f = units_to_double( + (-stream.logical_rect.y - stream.logical_rect.height)) + f = f / font_size - font_size + emojis.append([image, font, a, d, x_advance, f]) + + x_advance += (font.widths[glyph] + offset) / 1000 + # Close the last glyphs list, remove if empty if string[-1] == '<': string = string[:-1] @@ -1157,6 +1209,8 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y): # Draw text stream.show_text(string) + return emojis + def draw_wave(stream, x, y, width, offset_x, radius): up = 1 diff --git a/weasyprint/images.py b/weasyprint/images.py index 964af12363..4d9b78245d 100644 --- a/weasyprint/images.py +++ b/weasyprint/images.py @@ -35,7 +35,8 @@ def from_exception(cls, exception): class RasterImage: - def __init__(self, pillow_image, optimize_size): + def __init__(self, pillow_image, image_id, optimize_size): + pillow_image.id = image_id self._pillow_image = pillow_image self._optimize_size = optimize_size self._intrinsic_width = pillow_image.width @@ -134,8 +135,8 @@ def get_image_from_uri(cache, url_fetcher, optimize_size, url, raster_exception) else: # Store image id to enable cache in Stream.add_image - pillow_image.id = len(cache) - image = RasterImage(pillow_image, optimize_size) + image_id = len(cache) + image = RasterImage(pillow_image, image_id, optimize_size) except (URLFetchingError, ImageLoadingError) as exception: LOGGER.error('Failed to load image at %r: %s', url, exception) diff --git a/weasyprint/svg/images.py b/weasyprint/svg/images.py index 1775b3d9a0..d0307d4a1e 100644 --- a/weasyprint/svg/images.py +++ b/weasyprint/svg/images.py @@ -22,9 +22,10 @@ def svg(svg, node, font_size): node.get('width'), node.get('height'), font_size) scale_x, scale_y, translate_x, translate_y = preserve_ratio( svg, node, font_size, width, height) - svg.stream.rectangle(0, 0, width, height) - svg.stream.clip() - svg.stream.end() + if svg.tree != node: + svg.stream.rectangle(0, 0, width, height) + svg.stream.clip() + svg.stream.end() svg.stream.transform(a=scale_x, d=scale_y, e=translate_x, f=translate_y) diff --git a/weasyprint/svg/text.py b/weasyprint/svg/text.py index b774963f18..115855f235 100644 --- a/weasyprint/svg/text.py +++ b/weasyprint/svg/text.py @@ -22,7 +22,7 @@ def __init__(self, pango_layout, style): def text(svg, node, font_size): """Draw text node.""" from ..css.properties import INITIAL_VALUES - from ..draw import draw_first_line + from ..draw import draw_emojis, draw_first_line from ..text.line_break import split_first_line # TODO: use real computed values @@ -121,6 +121,7 @@ def text(svg, node, font_size): return svg.stream.begin_text() + emoji_lines = [] # Draw letters for i, ((x, y, dx, dy, r), letter) in enumerate(letters_positions): @@ -160,10 +161,14 @@ def text(svg, node, font_size): layout.reactivate(style) svg.fill_stroke(node, font_size, text=True) - draw_first_line( + emojis = draw_first_line( svg.stream, TextBox(layout, style), 'none', 'none', x_position, y_position) + emoji_lines.append((font_size, x, y, emojis)) svg.stream.pop_state() svg.cursor_position = cursor_position svg.stream.end_text() + + for font_size, x, y, emojis in emoji_lines: + draw_emojis(svg.stream, font_size, x, y, emojis) diff --git a/weasyprint/text/ffi.py b/weasyprint/text/ffi.py index b964c4f64f..0734cbeace 100644 --- a/weasyprint/text/ffi.py +++ b/weasyprint/text/ffi.py @@ -15,10 +15,20 @@ typedef ... hb_font_t; typedef ... hb_face_t; typedef ... hb_blob_t; + typedef uint32_t hb_codepoint_t; hb_face_t * hb_font_get_face (hb_font_t *font); hb_blob_t * hb_face_reference_blob (hb_face_t *face); unsigned int hb_face_get_index (const hb_face_t *face); + unsigned int hb_face_get_upem (const hb_face_t *face); const char * hb_blob_get_data (hb_blob_t *blob, unsigned int *length); + bool hb_ot_color_has_png (hb_face_t *face); + hb_blob_t * hb_ot_color_glyph_reference_png ( + hb_font_t *font, hb_codepoint_t glyph); + bool hb_ot_color_has_svg (hb_face_t *face); + hb_blob_t * hb_ot_color_glyph_reference_svg ( + hb_face_t *face, hb_codepoint_t glyph); + void hb_blob_destroy (hb_blob_t *blob); + // Pango