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

Draw PNG and SVG emojis #1434

Merged
merged 3 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 35 additions & 6 deletions weasyprint/document.py
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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))
Expand All @@ -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]
Expand Down Expand Up @@ -1286,12 +1291,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)
Expand All @@ -1305,9 +1312,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'
Expand Down
60 changes: 57 additions & 3 deletions weasyprint/draw.py
Expand Up @@ -9,10 +9,13 @@
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 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
Expand Down Expand Up @@ -982,9 +985,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']
Expand All @@ -1010,6 +1016,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(
Expand Down Expand Up @@ -1066,6 +1081,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)
Expand All @@ -1083,7 +1100,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)
Expand Down Expand Up @@ -1147,6 +1166,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]
Expand All @@ -1156,6 +1208,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
Expand Down
7 changes: 4 additions & 3 deletions weasyprint/images.py
Expand Up @@ -34,7 +34,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
Expand Down Expand Up @@ -131,8 +132,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)
Expand Down
7 changes: 4 additions & 3 deletions weasyprint/svg/images.py
Expand Up @@ -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)


Expand Down
9 changes: 7 additions & 2 deletions weasyprint/svg/text.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
10 changes: 10 additions & 0 deletions weasyprint/text/ffi.py
Expand Up @@ -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

Expand Down