TextWidget/TextBoxWidget: enhanced text shaping#5598
TextWidget/TextBoxWidget: enhanced text shaping#5598poire-z merged 6 commits intokoreader:masterfrom
Conversation
|
Will need a base bump for koreader/koreader-base#1010 in 3rd commit. |
| -- (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.) | ||
| face_obj.hb_features = { "+kern", "+liga" } |
There was a problem hiding this comment.
So is this technically redundant with HB defaults? I'm slightly confused by the comment following this line.
There was a problem hiding this comment.
Half :) I've indeed been unclear, but that's mostly just cut and pasted from crengine.
And what the HB defaults are are also hard to track. It's in its src/hb-ot-shape.cc.
So, it looks to me +liga is the default, but not +kern (or may be it is...).
I'm just setting here the defaults we use in crengine HB full/best, even if one of them is already the default.
The commented next line is the list of all stuff mentionned in HB "good", that I just cut and pasted with all disabled to see if enabling +liga / +kern made a difference :)
I don't plan on being the reference as I'm not sure what it is :|. I'll update the comment to not state anything about "default" but "If we'd wanted to disable all the features HB may enable" :)
| }) | ||
| end | ||
| table.insert(self.menu_items.developer_options.sub_item_table, { | ||
| text = _("Disable UI enhanced text shaping (xtext)"), |
There was a problem hiding this comment.
The order sounds a bit weird to me.
Disable enhanced UI text shaping
| self._height = math.ceil(face_height) + 2*self.padding | ||
| self._baseline_h = Math.round(face_ascender) + self.padding | ||
| -- With our UI fonts, this usually gives 0.72 to 0.74, so text is aligned | ||
| -- a bit lower than before with the hardcoded 0.7 |
There was a problem hiding this comment.
(Nothing excellent :) that's some previous comment that I just moved - it appears added here, but it's the same that is deleted below :)
There was a problem hiding this comment.
Oh right, I remember now. 🤦♂️
Still excellent though. ;-D
| -- so caller can fetch it again | ||
| self._text_to_draw = self.text | ||
|
|
||
| if self.text and type(self.text) ~= "string" then |
There was a problem hiding this comment.
Should not, but can if we provide a number or else.
We'll see it displayed instead of crashing, and it simplify my next check:
if not self.text or #self.text == 0 then self._is_empty = true self._length = 0
It avoid having nil fixed/handled in lower level code, like in:
koreader/frontend/ui/rendertext.lua
Lines 210 to 214 in e166a77
Lines 311 to 313 in e166a77
There was a problem hiding this comment.
Would something this work in this context? (That'd go underneath the method.) It's a convenient way to ensure conformity without bothering users.
dbg:guard(TextWidget, "updateSize",
function()
assert(type(self.text) == "string",
"Wrong text type (expected string)")
end)There was a problem hiding this comment.
I can, it works as you expect (crash on the emulator). Same in setText(text) then.
I think I've seen some output from logger.warn("renderUtf8Text called without text") in the past, so it may happen we get a nil. It's ok if we crash then when that happens?
(Can't cause any crash while tapping around - but I'm sure we'll get some.)
There was a problem hiding this comment.
Assuming it only crashes when it comes across a problem so that the problem can be addressed, that's indeed the idea. ;-)
But if it's too big an issue atm then it'd probably be best to leave that for a follow-up PR.
There was a problem hiding this comment.
I've already added and tested it, so it will go in :)
If you confirm dbg.guard() does not kick in nor add overhead on our devices, it's fine with me - as long as I get there the safety of my tostring() (othewise, XText Lua check will throw an error).
There was a problem hiding this comment.
Yeah, we possibly have a few nil renderUtf8Text lying around, that does ring a bell ;).
| -- Only when not self.use_xtext: | ||
|
|
||
| -- Note: we use kerning=true in all RenderText calls | ||
| --- @todo Don't use kerning for monospaced fonts. (houqp) |
There was a problem hiding this comment.
Not with the use_xtext code (Haven't checked, but I hope HB does the right thing with monospace fonts.
But for the legacy code, it might (it would still use kerning with monospaced fonts).
There was a problem hiding this comment.
OK, I'll remove the @todo and keep that comment in parenthesis. As we'll probably don't have to fix it if we keep using xtext.
| local face = self.face.getFallbackFont(xglyph.font_num) -- callback (not a method) | ||
| local glyph = RenderText:getGlyphByIndex(face, xglyph.glyph, self.bold) | ||
| local color = self.fgcolor | ||
| if self._alt_color_for_rtl then |
There was a problem hiding this comment.
So this is always disabled unless you change it in the code?
There was a problem hiding this comment.
(Might be of some interest in that developer settings menu.)
There was a problem hiding this comment.
So this is always disabled unless you change it in the code?
Right, always disabled.
Not worth busying developper settings with it I think - was useful when doing this PR, but I guess RTL is alright now, and I shouldn't need it - unless we feel it's needed when we play with mirroring the UI, but I doubt it.
| end | ||
|
|
||
| function TextBoxWidget:onCloseWidget() | ||
| -- free when UIManager:close() was called |
There was a problem hiding this comment.
Free when UIManager:close() was called.
(I mean, it just looks so out of place with the next comment. :-P)
There was a problem hiding this comment.
Yes :) That's verbatim from the other widgets that have it, so, I just kept it for coherency even if it sounds gramatically strange.
And... it's I that introduced that strange wording :)
|
|
||
| function TextBoxWidget:onCloseWidget() | ||
| -- free when UIManager:close() was called | ||
| -- Free the between-renderings (between page scrolls) freeable ressources |
There was a problem hiding this comment.
French ressources need freeing too ! :)
|
Yeah, I'd probably +1 using a real Bold font (which we already ship anyway ^^) ;). |
NiLuJe
left a comment
There was a problem hiding this comment.
Had to missclick, the simple "Single comment" button just plain disappeared on me -_-".
| -- We do the revert of what's done in :init(): | ||
| -- self.line_height_px = Math.round( (1 + self.line_height) * self.face.size ) | ||
| local font_size = height_px / nb_lines / (1 + line_height_em) | ||
| font_size = font_size * 1000000 / Screen:scaleBySize(1000000) -- invert scaleBySize |
There was a problem hiding this comment.
Doing the proper math in a real matching function in ffi/framebuffer.lua might be worth it?
(i.e., having a real px to pt helper function available).
There was a problem hiding this comment.
Probably, we used that same trick in coverbrowser some weeks ago.
(But I suck at proper math :)
There was a problem hiding this comment.
Apparently as simple as basically copying scaleBySize but changing the return to math.floor(px / (size_scale + dpi_scale) * 2) ;).
There was a problem hiding this comment.
So, should it be that (scaleBySize() input being pt and not px)?
-function fb:scaleBySize(px)
+function fb:scaleBySize(pt)
-- larger screen needs larger scale
@@ -299,3 +299,15 @@ function fb:scaleBySize(px)
-- scaled positive px should also be positive
- return math.ceil(px * (size_scale + dpi_scale) / 2)
+ return math.ceil(pt * (size_scale + dpi_scale) / 2)
+end
+
+function fb:unscaleBySize(px)
+ -- Revert of previous function:
+ -- px = scaleBySize(pt)
+ -- pt = unscaleBySize(px)
+ local size_scale = math.min(self:getWidth(), self:getHeight())/600
+ local dpi_scale = size_scale
+ if self.device and self.device.display_dpi ~= self.dpi then
+ dpi_scale = self.dpi / 160
+ end
+ return math.floor(px / (size_scale + dpi_scale) * 2)
endThere was a problem hiding this comment.
Well, it's not technically points per-se, as that's pretty much set in stone as px = round(dpi / 72.0 * pt), which isn't what we're doing.
It's more something like absolute pixels or whatever (I only mentioned point earlier because the theory is similar ;)).
There was a problem hiding this comment.
Not a Python quirk ;).
#!/usr/bin/env luajit
dpi = 167
width = 600
height = 800
function scaleBySize(px)
-- larger screen needs larger scale
size_scale = math.min(width, height)/600
-- if users custom screen dpi, also scale by dpi
dpi_scale = size_scale
dpi_scale = dpi / 160
-- scaled positive px should also be positive
scaled_px = px * (size_scale + dpi_scale) / 2
return {scaled_px, math.ceil(scaled_px)}
end
function unScaleBySize(px)
-- larger screen needs larger scale
size_scale = math.min(width, height)/600
-- if users custom screen dpi, also scale by dpi
dpi_scale = size_scale
dpi_scale = dpi / 160
-- scaled positive px should also be positive
unscaled_px = px / (size_scale + dpi_scale) * 2
return {unscaled_px, math.floor(unscaled_px)}
end
---
function tprint(tbl, indent)
if not indent then indent = 0 end
for k, v in pairs(tbl) do
formatting = string.rep(" ", indent) .. k .. ": "
if type(v) == "table" then
print(formatting)
tprint(v, indent+1)
elseif type(v) == 'boolean' then
print(formatting .. tostring(v))
else
print(formatting .. v)
end
end
end
print("scaleBySize(500)")
tprint(scaleBySize(500))
print("unScaleBySize(511)")
tprint(unScaleBySize(511))
print("scaleBySize(20)")
tprint(scaleBySize(20))
print("unScaleBySize(21)")
tprint(unScaleBySize(21))
dpi = 167 * 3
print("scaleBySize(18)")
tprint(scaleBySize(18))
print("unScaleBySize(38)")
tprint(unScaleBySize(38))
print("18 via * 10**6")
tprint(scaleBySize(18 * 1000000 / scaleBySize(1000000)[2]))scaleBySize(500)
1: 510.9375
2: 511
unScaleBySize(511)
1: 500.06116207951
2: 500
scaleBySize(20)
1: 20.4375
2: 21
unScaleBySize(21)
1: 20.550458715596
2: 20
scaleBySize(18)
1: 37.18125
2: 38
unScaleBySize(38)
1: 18.39636913767
2: 18
18 via * 10**6
1: 18
2: 18
There was a problem hiding this comment.
TL;DR: Smells like yet another case of "I don't know what the hell's happening inside the testsuite, but it doesn't make any sense" ;).
There was a problem hiding this comment.
(GH is still being clunky, I've only just now got your previous answers interleaved in the thread -_-").
There was a problem hiding this comment.
GH has recently been "helpfully" hiding comments behind a "More comments" type thing for me. D:
The presumed default used to be 800x600 @ 167 but that was changed to 160 for easier math. See koreader/koreader-base@c978857
Otherwise the unit test would make no sense! ;-)
fb:setDPI(167)
assert.are.equals(31, fb:scaleBySize(30))
fb:setDPI(167 * 3)
assert.are.equals(62, fb:scaleBySize(30))There was a problem hiding this comment.
Or at least, I supported it for that reason: koreader/koreader-base#843 (comment)
I'm wondering if maybe we should switch the other one to 160 instead since it's basically just a bit of legacy. It's practically the same thing. I doubt you'll even notice a difference in pixels except at really high DPI, except 160 DPI is exactly one inch making for much easier (head) calculations.
| -- so caller can fetch it again | ||
| self._text_to_draw = self.text | ||
|
|
||
| if self.text and type(self.text) ~= "string" then |
There was a problem hiding this comment.
Yeah, we possibly have a few nil renderUtf8Text lying around, that does ring a bell ;).
|
I have not spend much time on the cursor positionning in RTL/bidi contexts. So, this will need some more work. See |
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).
Alternative code to size and draw text with the help of the xtext Lua C module. Enabled by default (can be disabled via an added menu item in "Developer options"). New properties can be specified by calling widgets, only used when xtext is used: - lang: use specified language instead of the UI language - para_direction_rtl: true/false to override the default direction for the UI language - auto_para_direction: detect direction of each paragraph in text
Alternative code to size, split lines and draw text with the help of the xtext Lua C module. Enabled by default (can be disabled via an added menu item in "Developer options"). New properties can be specified by calling widgets, only used when xtext is used: - lang: use specified language instead of the UI language - para_direction_rtl: true/false to override the default direction for the UI language - auto_para_direction: detect direction of each paragraph in text - alignment_strict: prevent the inversion of the specified alignment= that is done when a paragraph direction is set or detected as RTL. Also: fix possible memory leak (present even when not using xtext) by calling :free() in onCloseWidget() like it's done in ImageWidget.
ffb3908 to
8a16056
Compare
- height_adjust: if true, reduce height to a multiple of line_height (for nicer centering) - height_overflow_show_ellipsis: if height overflow, append ellipsis to last shown line (Implemented in both use_xtext and legacy code path.) Use them in Menu.lua to clean up/shorten the code used for multiline menu items by delegating the work to TextBoxWidget, or using TextWidget when we end up needing only a single line.
|
I'm not wrong, I did put that in the script. :-P koreader/platform/appimage/AppRun Lines 10 to 11 in 89c0bd0 |
|
But this would mean I can no longer just run |
|
@Frenzie: Which explains why you couldn't repro w/ the AppImage. Yay! :). |
Hmm, yeah, I can't think of a neat solution to that. I can think of a very terrible one, though: an LD_PRELOAD hack that sets the env var. Kindles do that >_<". (Kids, don't do that at home!) |
You mean that in relation to the Android5 issue? |
|
@NiLuJe This doesn't seem to actually do anything? diff --git a/kodev b/kodev
index 0c1ac28f..a41c842e 100755
--- a/kodev
+++ b/kodev
@@ -101,6 +101,7 @@ function setup_env() {
files=$(find . -maxdepth 1 -name 'koreader-emulator-*' | ${SETUP_ENV_GREP_COMMAND})/koreader
assert_ret_zero $? "Emulator not found. Please build it first."
export EMU_DIR=${files[0]}
+ export LD_LIBRARY_PATH=${EMU_DIR}/libs:$LD_LIBRARY_PATH
}
function kodev-fetch-thirdparty() { |
|
The simple fact of linking against the STL implies ABI compat checks. Here: That won't do on Kindle ;). |
|
@Frenzie: PWD changes between when that's set and when we run ;). Something like diff --git a/kodev b/kodev
index 0c1ac28f..84bca093 100755
--- a/kodev
+++ b/kodev
@@ -100,7 +100,8 @@ function setup_env() {
fi
files=$(find . -maxdepth 1 -name 'koreader-emulator-*' | ${SETUP_ENV_GREP_COMMAND})/koreader
assert_ret_zero $? "Emulator not found. Please build it first."
- export EMU_DIR=${files[0]}
+ export EMU_DIR="${files[0]}"
+ export LD_LIBRARY_PATH="$(realpath ${EMU_DIR})/libs:${LD_LIBRARY_PATH}"
}
function kodev-fetch-thirdparty() {Works, by ensuring that's a canonical absolute path ;). |
|
That, or One, or both of those, will gloriously not work on macOS, because Apple. Unless one install coreutils. |
|
btw with archlinux i also get the system versions of FT and harfbuzz, but the emulator works even with xtext i do have gtk installed, and it works both if run via kodev or via ./reader.lua |
|
I'm running FT snapshots on my system, and @Frenzie is on a Debian thingy that may be running some older versions, while you're exactly matching the vendroed version on Arch, which might explain why nothing blows up. Having two copies of the same library initialized in the same process is still a highly volatile proposition ;). |
|
@Frenzie I found a difference, my my results your results |
I generally run Debian stable or testing on my desktop. That's currently stable, it'll turn into testing whenever I start to feel it's a bit outdated, i.e., when I start to feel the need to backport more than a select handful of programs. Right now my only backport is po4a, because Debian comes with 0.55 (even in unstable ;_;) and 0.56 contains this fix: On my laptop I normally run the latest *buntu. Then I've got another old laptop which varies (currently Mint), doesn't do too much since I got my current sidegrade in '16, and an external drive with a bunch of live images. I've also got an old netbook that runs the latest Xubuntu LTS. And of course Ubuntu 16.04 in Docker. I think that's about all of it. So yeah, mostly Debubuntu in various degrees of "outdated" (compared to Arch/Gentoo). |
|
@yparitcher: I'm guessing that was a simply a post |
|
nope koreader @ 89c0bd0 base was @6c48019 before however my kindle KT4 build, built last night has and it is working just fine without |
|
@yparitcher: I meant @Frenzie's build ;). You've just reminded me that we enforce |
As per @NiLuJe's suggestions from <koreader#5598 (comment)>. I'm not super happy with this but being able to easily run the emulator is the most important thing.
As per @NiLuJe's suggestions from <#5598 (comment)>. I'm not super happy with this but being able to easily run the emulator is the most important thing.
|
Note: I forgot to implement this in the use_xtext variant of TextBoxWidget: koreader/frontend/ui/widget/textboxwidget.lua Lines 356 to 361 in ea67b9b It's only use is in CoverBrowser Mosaic mode, with "fake" text covers, where we can now have words splitted in the middle :| (but it also avoids going to excessive small font size) koreader/plugins/coverbrowser.koplugin/mosaicmenu.lua Lines 215 to 219 in ea67b9b |
|
@Frenzie regarding your gitter comment maybe there's a sneaky mistake in the xtext/fribidi Lua API use: you can disable all use of xtext via |
As per @NiLuJe's suggestions from <koreader#5598 (comment)>. I'm not super happy with this but being able to easily run the emulator is the most important thing.
Have TextWdiget and TextBoxWidget use the new Lua C module (added in koreader/koreader-base#1010) as an alternative text layout method, using Harfbuzz, FriBiDi and libunibreak.
This is needed to correctly shape Arabic scripts (cursive glyphs), propertly draw RTL text (Hebrew,
Arabic) and bidi text (arabic/hebrew filenames in english UI).
Screenshots and discussion in #5359.
With this PR, we globally switch to use Harfbuzz for text shaping. It will feel like a change (text more condensed, I didn't like it initially, but switching back to the previous Freetype-only rendering, that old rendering really feels ugly :)
This should be enough to render Arabic text correctly. It still needs more work on the UI widgets and the language bootstrap code, and gettext/L to properly bidi isolate some bit of strings - to properly align arabic and hebrew to the right.
(We'll also need more unrelated work to mirror the UI elements when the UI language is RTL - but this PR and a next one should have addressed all things related to text.)
For now, for testing, to enable RTL magic, add somewhere in reader.lua (this will have to be properly handled in language.lua)
Also, I had to rework #5496 as it was using TextBoxWidget internals. So, I ended adding stuff to TextBoxWidget to do the work, and Menu.lua ended up quite shorter :) @robert00s : can you check last commit, and see if the code, with my comments, address correctly what you did with #5496.
I also took the liberty of adding a new setting
Items > Reduce font size to show more textto bring back the previous behaviour that I prefer :) See #5496 (comment).Some misc technical notes:
I initially went with touching as little TextWidget and TextBoxWidget, and add my stuff in xtextwidget.lua and xtextboxwidget.lua, and use something like:
to enable it. It resulted in some redundant code (the code that deals with y coordinates), as this updates only the work on placing glyphs on the x asis. So, I ended up merging the new code into our TextWidget and TextBoxWidget, using a simple boolean to toggle one or the other (use_xtext = true/false).
For
TextBoxWidget:_splitToLines(), I had to use a uglygoto idx_continueand::idx_continue:: -- (Label for goto when use_xtext=true)- just because I didn't want to indent the previous code and have it part of the diff/git blame. I'll get rid of it later by indenting the whole thing in a later commit if that's really too ugly :)Haven't benchmarked that code. But on my device, it feels as smooth as before.
I know (from the code and library we use) this needs quite more processor cycles than before, so it feels a bit gratuitous as the previous rendering code was good enough. But I have no idea how much this added work compares to all the other CPU intensive work, like blitting blitbuffers, and if it is negligeable :)
If needed, we could optimize the other widgets to cache more their TextWidgets (our menu create new ones each time we switch menu or menu pages) - or use other cache strategies.
Also, we might need to work on our bold font use and handling. In many cases, we use the normal font, and provide




bold=truewhich does synthetized bold (while we may have a real nicer bold font).With XText/Harfbuzz, we use another Freetype emboldening API that don't change metrics, and we can tune the boldness to our liking. Also, as it doesn't change metrics, it feels different, and I quite like it that the metrics don't change, things get more aligned, eg:
Before (that
Chapter 6sticks out from the other ones):After with
bold_strength_factor = 3/8 -- as crengine, lighter:With
bold_strength_factor = 1/2 -- bold enough:With
bold_strength_factor = 1 -- really too bold: