2016-02-16 00:14:15 +00:00
|
|
|
--[[--
|
|
|
|
Text rendering module.
|
|
|
|
]]
|
|
|
|
|
2019-03-27 21:50:44 +00:00
|
|
|
local bit = require("bit")
|
2013-10-23 14:37:55 +00:00
|
|
|
local Font = require("ui/font")
|
2013-10-18 20:38:07 +00:00
|
|
|
local Cache = require("cache")
|
|
|
|
local CacheItem = require("cacheitem")
|
2014-10-04 13:53:30 +00:00
|
|
|
local BlitBuffer = require("ffi/blitbuffer")
|
2019-02-18 16:07:27 +00:00
|
|
|
local Device = require("device")
|
2016-12-29 08:10:38 +00:00
|
|
|
local logger = require("logger")
|
2012-05-18 22:35:09 +00:00
|
|
|
|
2019-03-27 21:50:44 +00:00
|
|
|
local band = bit.band
|
|
|
|
local bor = bit.bor
|
|
|
|
local lshift = bit.lshift
|
|
|
|
|
2019-02-18 16:07:27 +00:00
|
|
|
if Device.should_restrict_JIT then
|
2016-04-07 15:28:52 +00:00
|
|
|
require("jit").off(true, true)
|
|
|
|
end
|
|
|
|
|
2012-05-18 22:35:09 +00:00
|
|
|
--[[
|
2016-02-16 01:56:13 +00:00
|
|
|
@TODO: all these functions should probably be methods on Face objects
|
2012-05-18 22:35:09 +00:00
|
|
|
]]--
|
2013-10-18 20:38:07 +00:00
|
|
|
local RenderText = {}
|
2012-05-18 22:35:09 +00:00
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
local GlyphCache = Cache:new{
|
2014-03-13 13:52:43 +00:00
|
|
|
max_memsize = 512*1024,
|
|
|
|
current_memsize = 0,
|
|
|
|
cache = {},
|
|
|
|
-- this will hold the LRU order of the cache
|
|
|
|
cache_order = {}
|
2013-03-24 10:37:33 +00:00
|
|
|
}
|
|
|
|
|
2013-09-03 14:24:49 +00:00
|
|
|
-- iterator over UTF8 encoded characters in a string
|
2016-02-14 21:47:36 +00:00
|
|
|
local function utf8Chars(input_text)
|
2014-03-13 13:52:43 +00:00
|
|
|
local function read_next_glyph(input, pos)
|
|
|
|
if string.len(input) < pos then return nil end
|
|
|
|
local value = string.byte(input, pos)
|
2019-03-27 21:50:44 +00:00
|
|
|
if band(value, 0x80) == 0 then
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @todo check valid ranges
|
2014-03-13 13:52:43 +00:00
|
|
|
return pos+1, value, string.sub(input, pos, pos)
|
2019-03-27 21:50:44 +00:00
|
|
|
elseif band(value, 0xC0) == 0x80 -- invalid, continuation
|
|
|
|
or band(value, 0xF8) == 0xF8 -- 5-or-more byte sequence, illegal due to RFC3629
|
2014-03-13 13:52:43 +00:00
|
|
|
then
|
|
|
|
return pos+1, 0xFFFD, "\xFF\xFD"
|
|
|
|
else
|
|
|
|
local glyph, bytes_left
|
2019-03-27 21:50:44 +00:00
|
|
|
if band(value, 0xE0) == 0xC0 then
|
|
|
|
glyph = band(value, 0x1F)
|
2014-03-13 13:52:43 +00:00
|
|
|
bytes_left = 1
|
2019-03-27 21:50:44 +00:00
|
|
|
elseif band(value, 0xF0) == 0xE0 then
|
|
|
|
glyph = band(value, 0x0F)
|
2014-03-13 13:52:43 +00:00
|
|
|
bytes_left = 2
|
2019-03-27 21:50:44 +00:00
|
|
|
elseif band(value, 0xF8) == 0xF0 then
|
|
|
|
glyph = band(value, 0x07)
|
2014-03-13 13:52:43 +00:00
|
|
|
bytes_left = 3
|
|
|
|
else
|
|
|
|
return pos+1, 0xFFFD, "\xFF\xFD"
|
|
|
|
end
|
2014-11-28 14:48:44 +00:00
|
|
|
if string.len(input) < (pos + bytes_left) then
|
2014-03-13 13:52:43 +00:00
|
|
|
return pos+1, 0xFFFD, "\xFF\xFD"
|
|
|
|
end
|
|
|
|
for i = pos+1, pos + bytes_left do
|
|
|
|
value = string.byte(input, i)
|
2019-03-27 21:50:44 +00:00
|
|
|
if band(value, 0xC0) == 0x80 then
|
|
|
|
glyph = bor(lshift(glyph, 6), band(value, 0x3F))
|
2014-03-13 13:52:43 +00:00
|
|
|
else
|
2014-11-28 14:48:44 +00:00
|
|
|
-- invalid UTF8 continuation - don't be greedy, just skip
|
|
|
|
-- the initial char of the sequence.
|
|
|
|
return pos+1, 0xFFFD, "\xFF\xFD"
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
|
|
|
end
|
2019-08-23 17:53:53 +00:00
|
|
|
--- @todo check for valid ranges here!
|
2014-03-13 13:52:43 +00:00
|
|
|
return pos+bytes_left+1, glyph, string.sub(input, pos, pos+bytes_left)
|
|
|
|
end
|
|
|
|
end
|
2016-02-14 21:47:36 +00:00
|
|
|
return read_next_glyph, input_text, 1
|
2013-09-03 14:24:49 +00:00
|
|
|
end
|
|
|
|
|
2017-08-28 19:25:33 +00:00
|
|
|
--- Returns a rendered glyph
|
|
|
|
--
|
|
|
|
-- @tparam ui.font.FontFaceObj face font face for the text
|
|
|
|
-- @int charcode
|
|
|
|
-- @bool[opt=false] bold whether the text should be measured as bold
|
|
|
|
-- @treturn glyph
|
2014-10-04 13:53:30 +00:00
|
|
|
function RenderText:getGlyph(face, charcode, bold)
|
|
|
|
local hash = "glyph|"..face.hash.."|"..charcode.."|"..(bold and 1 or 0)
|
2014-03-13 13:52:43 +00:00
|
|
|
local glyph = GlyphCache:check(hash)
|
|
|
|
if glyph then
|
|
|
|
-- cache hit
|
|
|
|
return glyph[1]
|
|
|
|
end
|
2014-10-04 13:53:30 +00:00
|
|
|
local rendered_glyph = face.ftface:renderGlyph(charcode, bold)
|
2014-03-13 13:52:43 +00:00
|
|
|
if face.ftface:checkGlyph(charcode) == 0 then
|
|
|
|
for index, font in pairs(Font.fallbacks) do
|
2014-08-20 01:39:50 +00:00
|
|
|
-- use original size before scaling by screen DPI
|
|
|
|
local fb_face = Font:getFace(font, face.orig_size)
|
2015-09-02 15:21:38 +00:00
|
|
|
if fb_face ~= nil then
|
|
|
|
-- for some characters it cannot find in Fallbacks, it will crash here
|
|
|
|
if fb_face.ftface:checkGlyph(charcode) ~= 0 then
|
|
|
|
rendered_glyph = fb_face.ftface:renderGlyph(charcode, bold)
|
|
|
|
break
|
2016-02-14 21:47:36 +00:00
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not rendered_glyph then
|
2016-12-29 08:10:38 +00:00
|
|
|
logger.warn("error rendering glyph (charcode=", charcode, ") for face", face)
|
2014-03-13 13:52:43 +00:00
|
|
|
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
|
2011-12-01 00:35:53 +00:00
|
|
|
end
|
|
|
|
|
2017-08-28 19:25:33 +00:00
|
|
|
--- Returns a substring of a given text that meets the maximum width (in pixels)
|
2016-02-16 01:56:13 +00:00
|
|
|
-- restriction.
|
|
|
|
--
|
|
|
|
-- @string text text to truncate
|
|
|
|
-- @tparam ui.font.FontFaceObj face font face for the text
|
|
|
|
-- @int width maximum width in pixels
|
|
|
|
-- @bool[opt=false] kerning whether the text should be measured with kerning
|
|
|
|
-- @bool[opt=false] bold whether the text should be measured as bold
|
|
|
|
-- @treturn string
|
2017-08-28 19:25:33 +00:00
|
|
|
-- @see truncateTextByWidth
|
2014-01-22 09:18:20 +00:00
|
|
|
function RenderText:getSubTextByWidth(text, face, width, kerning, bold)
|
2014-03-13 13:52:43 +00:00
|
|
|
local pen_x = 0
|
2016-02-16 01:56:13 +00:00
|
|
|
local prevcharcode
|
2014-03-13 13:52:43 +00:00
|
|
|
local char_list = {}
|
|
|
|
for _, charcode, uchar in utf8Chars(text) do
|
|
|
|
if pen_x < width then
|
|
|
|
local glyph = self:getGlyph(face, charcode, bold)
|
|
|
|
if kerning and prevcharcode then
|
|
|
|
local kern = face.ftface:getKerning(prevcharcode, charcode)
|
|
|
|
pen_x = pen_x + kern
|
|
|
|
end
|
|
|
|
pen_x = pen_x + glyph.ax
|
|
|
|
if pen_x <= width then
|
|
|
|
prevcharcode = charcode
|
|
|
|
table.insert(char_list, uchar)
|
|
|
|
else
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return table.concat(char_list)
|
2012-04-29 15:53:48 +00:00
|
|
|
end
|
|
|
|
|
2016-02-16 00:14:15 +00:00
|
|
|
--- Measure rendered size for a given text.
|
|
|
|
--
|
|
|
|
-- Note this function does not render the text into a bitmap. Use it if you
|
2016-03-06 01:30:18 +00:00
|
|
|
-- only need the estimated size information.
|
2016-02-16 00:14:15 +00:00
|
|
|
--
|
2016-02-16 01:56:13 +00:00
|
|
|
-- @int x start position for a given text (within maximum width)
|
|
|
|
-- @int width maximum rendering width in pixels (think of it as size of the bitmap)
|
|
|
|
-- @tparam ui.font.FontFaceObj face font face that will be used for rendering
|
|
|
|
-- @string text text to measure
|
|
|
|
-- @bool[opt=false] kerning whether the text should be measured with kerning
|
|
|
|
-- @bool[opt=false] bold whether the text should be measured as bold
|
|
|
|
-- @treturn RenderTextSize
|
2014-01-22 09:18:20 +00:00
|
|
|
function RenderText:sizeUtf8Text(x, width, face, text, kerning, bold)
|
2014-03-13 13:52:43 +00:00
|
|
|
if not text then
|
2016-12-29 08:10:38 +00:00
|
|
|
logger.warn("sizeUtf8Text called without text");
|
2019-03-27 03:57:19 +00:00
|
|
|
return { x = 0, y_top = 0, y_bottom = 0 }
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2012-10-09 20:08:34 +00:00
|
|
|
|
2014-03-13 13:52:43 +00:00
|
|
|
-- may still need more adaptive pen placement when kerning,
|
|
|
|
-- see: http://freetype.org/freetype2/docs/glyphs/glyphs-4.html
|
|
|
|
local pen_x = 0
|
|
|
|
local pen_y_top = 0
|
|
|
|
local pen_y_bottom = 0
|
|
|
|
local prevcharcode = 0
|
|
|
|
for _, charcode, uchar in utf8Chars(text) do
|
|
|
|
if 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)
|
|
|
|
end
|
|
|
|
pen_x = pen_x + glyph.ax
|
|
|
|
pen_y_top = math.max(pen_y_top, glyph.t)
|
|
|
|
pen_y_bottom = math.max(pen_y_bottom, glyph.bb:getHeight() - glyph.t)
|
|
|
|
prevcharcode = charcode
|
|
|
|
end -- if pen_x < (width - x)
|
|
|
|
end
|
2016-02-16 00:14:15 +00:00
|
|
|
|
|
|
|
--- RenderText size information
|
|
|
|
-- @table RenderTextSize
|
2016-03-06 01:30:18 +00:00
|
|
|
-- @field x length of the text on x coordinate
|
|
|
|
-- @field y_top distance between top-most pixel (scanline) and baseline
|
|
|
|
-- (bearingY)
|
|
|
|
-- @field y_bottom distance between bottom-most pixel (scanline) and
|
|
|
|
-- baseline (height - y_top)
|
2016-02-16 00:14:15 +00:00
|
|
|
return { x = pen_x, y_top = pen_y_top, y_bottom = pen_y_bottom }
|
2012-03-19 18:41:09 +00:00
|
|
|
end
|
|
|
|
|
2016-03-06 01:30:18 +00:00
|
|
|
--- Render a given text into a given BlitBuffer
|
|
|
|
--
|
|
|
|
-- @tparam BlitBuffer dest_bb Buffer to blit into
|
|
|
|
-- @int x starting x coordinate position within dest_bb
|
|
|
|
-- @int baseline y coordinate for baseline, within dest_bb
|
|
|
|
-- @tparam ui.font.FontFaceObj face font face that will be used for rendering
|
|
|
|
-- @string text text to render
|
|
|
|
-- @bool[opt=false] kerning whether the text should be measured with kerning
|
|
|
|
-- @bool[opt=false] bold whether the text should be measured as bold
|
|
|
|
-- @tparam[opt=BlitBuffer.COLOR_BLACK] BlitBuffer.COLOR fgcolor foreground color
|
|
|
|
-- @int[opt=nil] width maximum rendering width
|
2017-01-01 09:24:49 +00:00
|
|
|
-- @tparam[opt] table char_pads array of integers, nb of pixels to add, one for each utf8 char in text
|
2016-03-06 01:30:18 +00:00
|
|
|
-- @return int width of rendered bitmap
|
2017-01-01 09:24:49 +00:00
|
|
|
function RenderText:renderUtf8Text(dest_bb, x, baseline, face, text, kerning, bold, fgcolor, width, char_pads)
|
2014-03-13 13:52:43 +00:00
|
|
|
if not text then
|
2016-12-29 08:10:38 +00:00
|
|
|
logger.warn("renderUtf8Text called without text");
|
2014-03-13 13:52:43 +00:00
|
|
|
return 0
|
|
|
|
end
|
2012-10-09 20:08:34 +00:00
|
|
|
|
2014-10-04 13:53:30 +00:00
|
|
|
if not fgcolor then
|
2014-10-22 13:34:11 +00:00
|
|
|
fgcolor = BlitBuffer.COLOR_BLACK
|
2014-10-04 13:53:30 +00:00
|
|
|
end
|
|
|
|
|
2014-03-13 13:52:43 +00:00
|
|
|
-- may still need more adaptive pen placement when kerning,
|
|
|
|
-- see: http://freetype.org/freetype2/docs/glyphs/glyphs-4.html
|
|
|
|
local pen_x = 0
|
|
|
|
local prevcharcode = 0
|
2016-03-06 01:30:18 +00:00
|
|
|
local text_width = dest_bb:getWidth() - x
|
2014-03-13 13:52:43 +00:00
|
|
|
if width and width < text_width then
|
|
|
|
text_width = width
|
|
|
|
end
|
2017-01-01 09:24:49 +00:00
|
|
|
local char_idx = 0
|
2014-03-13 13:52:43 +00:00
|
|
|
for _, charcode, uchar in utf8Chars(text) do
|
|
|
|
if pen_x < text_width then
|
2014-10-04 13:53:30 +00:00
|
|
|
local glyph = self:getGlyph(face, charcode, bold)
|
2014-03-13 13:52:43 +00:00
|
|
|
if kerning and (prevcharcode ~= 0) then
|
|
|
|
pen_x = pen_x + face.ftface:getKerning(prevcharcode, charcode)
|
|
|
|
end
|
2016-03-06 01:30:18 +00:00
|
|
|
dest_bb:colorblitFrom(
|
2014-03-13 13:52:43 +00:00
|
|
|
glyph.bb,
|
2016-03-06 01:30:18 +00:00
|
|
|
x + pen_x + glyph.l,
|
|
|
|
baseline - glyph.t,
|
2014-03-13 13:52:43 +00:00
|
|
|
0, 0,
|
2014-10-04 13:53:30 +00:00
|
|
|
glyph.bb:getWidth(), glyph.bb:getHeight(),
|
|
|
|
fgcolor)
|
2014-03-13 13:52:43 +00:00
|
|
|
pen_x = pen_x + glyph.ax
|
|
|
|
prevcharcode = charcode
|
|
|
|
end -- if pen_x < text_width
|
2017-01-01 09:24:49 +00:00
|
|
|
if char_pads then
|
|
|
|
char_idx = char_idx + 1
|
2018-08-01 16:33:52 +00:00
|
|
|
pen_x = pen_x + (char_pads[char_idx] or 0)
|
|
|
|
-- We used to use:
|
|
|
|
-- pen_x = pen_x + char_pads[char_idx]
|
|
|
|
-- above will fail if we didnt count the same number of chars, we'll see
|
|
|
|
-- We saw, and it's pretty robust: it never failed before we tried to
|
|
|
|
-- render some binary content, which messes the utf8 sequencing: the
|
|
|
|
-- split to UTF8 is only reversible if text is valid UTF8 (or nearly UTF8).
|
|
|
|
-- TextBoxWidget did this sequencing, counted the number of chars
|
|
|
|
-- and made out 'char_pads', and gave us back the concatenated utf8
|
|
|
|
-- chars as 'text', that we sequenced again above: we may not get the
|
|
|
|
-- same number of chars as we did previously to make char_pads.
|
|
|
|
-- We'd rather not crash (and have binary stuff displayed, even if
|
|
|
|
-- badly). The mess in char_pads is negligeable when that happens.
|
2017-01-01 09:24:49 +00:00
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2012-10-09 20:08:34 +00:00
|
|
|
|
2014-03-13 13:52:43 +00:00
|
|
|
return pen_x
|
2011-12-01 00:35:53 +00:00
|
|
|
end
|
2013-10-18 20:38:07 +00:00
|
|
|
|
2016-11-08 18:05:34 +00:00
|
|
|
local ellipsis, space = "…", " "
|
|
|
|
local ellipsis_width, space_width
|
2017-08-28 19:25:33 +00:00
|
|
|
--- Returns a substring of a given text that meets the maximum width (in pixels)
|
|
|
|
-- restriction with ellipses (…) at the end if required.
|
|
|
|
--
|
|
|
|
-- @string text text to truncate
|
|
|
|
-- @tparam ui.font.FontFaceObj face font face for the text
|
|
|
|
-- @int width maximum width in pixels
|
|
|
|
-- @bool[opt=false] kerning whether the text should be measured with kerning
|
|
|
|
-- @bool[opt=false] bold whether the text should be measured as bold
|
|
|
|
-- @bool[opt=false] prepend_space whether a space should be prepended to the text
|
|
|
|
-- @treturn string
|
|
|
|
-- @see getSubTextByWidth
|
2017-08-28 18:36:15 +00:00
|
|
|
function RenderText:truncateTextByWidth(text, face, max_width, kerning, bold, prepend_space)
|
2016-11-08 18:05:34 +00:00
|
|
|
if not ellipsis_width then
|
|
|
|
ellipsis_width = self:sizeUtf8Text(0, max_width, face, ellipsis).x
|
|
|
|
end
|
|
|
|
if not space_width then
|
|
|
|
space_width = self:sizeUtf8Text(0, max_width, face, space).x
|
|
|
|
end
|
|
|
|
local new_txt_width = max_width - ellipsis_width - space_width
|
2017-08-28 18:36:15 +00:00
|
|
|
local sub_txt = self:getSubTextByWidth(text, face, new_txt_width, kerning, bold)
|
2016-11-08 18:05:34 +00:00
|
|
|
if prepend_space then
|
|
|
|
return space.. sub_txt .. ellipsis
|
|
|
|
else
|
|
|
|
return sub_txt .. ellipsis .. space
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
return RenderText
|