From c2e2c00db6d3a054901c620f940238dd701a5ad1 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 15 Nov 2019 15:14:36 +0100 Subject: [PATCH] Font rendering: add some helpers for use by xtext bump base for libkoreader-xtext.so: - ffi/pic.lua: fix memory leak with some unsupported PNG files - FreeType ffi: add methods for use with Harfbuzz shaping - thirdparty: adds libunibreak - Adds libkoreader-xtext.so: enhanced text shaping Add a getFallbackFont(N) to each Lua font object to instantiate (if not yet done) and return the Nth fallback font for the font. Fallback fonts list: add NotoSansArabicUI-Regular.ttf Add RenderText:getGlyphByIndex() to get a glyph bitmap by glyph index, which is what we'll get from Harfbuzz shaping. (RenderText:getGlyph() works with unicode charcode). --- .luacheckrc | 1 + base | 2 +- frontend/ui/font.lua | 74 +++++++++++++++++++++++++++++++++++--- frontend/ui/rendertext.lua | 35 ++++++++++++++++-- 4 files changed, 105 insertions(+), 7 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index d2c16625b..ad5f0ad14 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -133,6 +133,7 @@ read_globals = { "cre", "lfs", "lipc", + "xtext", } exclude_files = { diff --git a/base b/base index c140b752e..6c480198b 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit c140b752e16163ee8836214787caf7343bc4a579 +Subproject commit 6c480198b00ec7ae4e8af25216e8a12fcabda9e6 diff --git a/frontend/ui/font.lua b/frontend/ui/font.lua index 460e9287a..29d5c95b2 100644 --- a/frontend/ui/font.lua +++ b/frontend/ui/font.lua @@ -79,9 +79,10 @@ local Font = { fallbacks = { [1] = "NotoSans-Regular.ttf", [2] = "NotoSansCJKsc-Regular.otf", - [3] = "nerdfonts/symbols.ttf", - [4] = "freefont/FreeSans.ttf", - [5] = "freefont/FreeSerif.ttf", + [3] = "NotoSansArabicUI-Regular.ttf", + [4] = "nerdfonts/symbols.ttf", + [5] = "freefont/FreeSans.ttf", + [6] = "freefont/FreeSerif.ttf", }, -- face table @@ -92,6 +93,49 @@ if is_android then table.insert(Font.fallbacks, 3, "DroidSansFallback.ttf") -- for some ancient pre-4.4 Androids end +-- Synthetized bold strength can be tuned: +-- local bold_strength_factor = 1 -- really too bold +-- local bold_strength_factor = 1/2 -- bold enough +local bold_strength_factor = 3/8 -- as crengine, lighter + +-- Callback to be used by libkoreader-xtext.so to get Freetype +-- instantiated fallback fonts when needed for shaping text +local _getFallbackFont = function(face_obj, num) + if not num or num == 0 then -- return the main font + if not face_obj.embolden_half_strength then + -- cache this value in case we use bold, to avoid recomputation + face_obj.embolden_half_strength = face_obj.ftface:getEmboldenHalfStrength(bold_strength_factor) + end + return face_obj + end + if not face_obj.fallbacks then + face_obj.fallbacks = {} + end + if face_obj.fallbacks[num] ~= nil then + return face_obj.fallbacks[num] + end + local next_num = #face_obj.fallbacks + 1 + local cur_num = 0 + for index, fontname in pairs(Font.fallbacks) do + if fontname ~= face_obj.realname then -- Skip base one if among fallbacks + local fb_face = Font:getFace(fontname, face_obj.orig_size) + if fb_face ~= nil then -- valid font + cur_num = cur_num + 1 + if cur_num == next_num then + face_obj.fallbacks[next_num] = fb_face + if not fb_face.embolden_half_strength then + fb_face.embolden_half_strength = fb_face.ftface:getEmboldenHalfStrength(bold_strength_factor) + end + return fb_face + end + end + end + end + -- no more fallback font + face_obj.fallbacks[next_num] = false + return false +end + --- Gets font face object. -- @string font -- @int size optional size @@ -144,12 +188,34 @@ function Font:getFace(font, size) -- @field hash hash key for this font face face_obj = { orig_font = font, + realname = realname, size = size, orig_size = orig_size, ftface = face, - hash = hash + hash = hash, } self.faces[hash] = face_obj + + -- Callback to be used by libkoreader-xtext.so to get Freetype + -- instantiated fallback fonts when needed for shaping text + face_obj.getFallbackFont = function(num) + return _getFallbackFont(face_obj, num) + end + -- Font features, to be given by libkoreader-xtext.so to HarfBuzz. + -- (Could be tweaked by font if needed. Note that NotoSans does not + -- have common ligatures, like for "fi" or "fl", so we won't see + -- them in the UI.) + -- Use HB defaults, and be sure to enable kerning and ligatures + -- (which might be part of HB defaults, or not, not sure). + face_obj.hb_features = { "+kern", "+liga" } + -- If we'd wanted to disable all features that might be enabled + -- by HarfBuzz (see harfbuzz/src/hb-ot-shape.cc, quite unclear + -- what's enabled or not by default): + -- face_obj.hb_features = { + -- "-kern", "-mark", "-mkmk", "-curs", "-locl", "-liga", + -- "-rlig", "-clig", "-ccmp", "-calt", "-rclt", "-rvrn", + -- "-ltra", "-ltrm", "-rtla", "-rtlm", "-frac", "-numr", + -- "-dnom", "-rand", "-trak", "-vert", } end return face_obj end diff --git a/frontend/ui/rendertext.lua b/frontend/ui/rendertext.lua index a975ef10a..af06547dc 100644 --- a/frontend/ui/rendertext.lua +++ b/frontend/ui/rendertext.lua @@ -172,7 +172,7 @@ function RenderText:sizeUtf8Text(x, width, face, text, kerning, bold) local pen_y_bottom = 0 local prevcharcode = 0 for _, charcode, uchar in utf8Chars(text) do - if pen_x < (width - x) then + if not width or pen_x < (width - x) then local glyph = self:getGlyph(face, charcode, bold) if kerning and (prevcharcode ~= 0) then pen_x = pen_x + (face.ftface):getKerning(prevcharcode, charcode) @@ -264,6 +264,11 @@ function RenderText:renderUtf8Text(dest_bb, x, baseline, face, text, kerning, bo end local ellipsis = "…" + +function RenderText:getEllipsisWidth(face, bold) + return self:sizeUtf8Text(0, false, face, ellipsis, false, bold).x +end + --- Returns a substring of a given text that meets the maximum width (in pixels) -- restriction with ellipses (…) at the end if required. -- @@ -275,10 +280,36 @@ local ellipsis = "…" -- @treturn string -- @see getSubTextByWidth function RenderText:truncateTextByWidth(text, face, max_width, kerning, bold) - local ellipsis_width = self:sizeUtf8Text(0, max_width, face, ellipsis, false, bold).x + local ellipsis_width = self:getEllipsisWidth(face, bold) local new_txt_width = max_width - ellipsis_width local sub_txt = self:getSubTextByWidth(text, face, new_txt_width, kerning, bold) return sub_txt .. ellipsis end +--- Returns a rendered glyph by glyph index +-- xtext/Harfbuzz, after shaping, gives glyph indexes in the font, which +-- is usually different from the unicode codepoint of the original char) +-- +-- @tparam ui.font.FontFaceObj face font face for the text +-- @int glyph index +-- @bool[opt=false] bold whether the glyph should be artificially boldened +-- @treturn glyph +function RenderText:getGlyphByIndex(face, glyphindex, bold) + local hash = "xglyph|"..face.hash.."|"..glyphindex.."|"..(bold and 1 or 0) + local glyph = GlyphCache:check(hash) + if glyph then + -- cache hit + return glyph[1] + end + local rendered_glyph = face.ftface:renderGlyphByIndex(glyphindex, bold and face.embolden_half_strength) + if not rendered_glyph then + logger.warn("error rendering glyph (glyphindex=", glyphindex, ") for face", face) + return + end + glyph = CacheItem:new{rendered_glyph} + glyph.size = glyph[1].bb:getWidth() * glyph[1].bb:getHeight() / 2 + 32 + GlyphCache:insert(hash, glyph) + return rendered_glyph +end + return RenderText