Skip to content

Commit

Permalink
Merge pull request #1434 from Kozea/emojis
Browse files Browse the repository at this point in the history
Draw PNG and SVG emojis
  • Loading branch information
liZe committed Sep 13, 2021
2 parents 9a720ac + 32eb7c2 commit a809eff
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 17 deletions.
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 @@ -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)
Expand All @@ -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'
Expand Down
60 changes: 57 additions & 3 deletions weasyprint/draw.py
Expand Up @@ -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
Expand Down Expand Up @@ -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']
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
7 changes: 4 additions & 3 deletions weasyprint/images.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
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

0 comments on commit a809eff

Please sign in to comment.