From 5d2286e14088ba0a66f82974b1bb96f8c551911e Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Sun, 5 Oct 2025 18:21:41 +0200 Subject: [PATCH 01/14] `horizontal_layout`: italic correction heuristic Enable a heuristic to create extra spaces whenever italic and upright characters mingle. * new global const `ITALIC_CORRECTION = Ref(true)` to enable or disable correction heuristic. * changes in `TeXChar` constructor to correctly set `is_slanted`. * heuristic algorithm in `horizontal_layout` inspired by [sile](https://github.com/sile-typesetter/sile) (MIT license). --- src/MathTeXEngine.jl | 5 +++- src/engine/fonts.jl | 2 +- src/engine/layout.jl | 47 ++++++++++++++++++++++++++++++++++++ src/engine/layout_context.jl | 7 +++++- src/engine/texelements.jl | 14 ++++++++--- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index ffb03f3..a7d2510 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -17,7 +17,8 @@ import FreeTypeAbstraction: ascender, boundingbox, descender, get_extent, glyph_index, hadvance, inkheight, inkwidth, height_insensitive_boundingbox, leftinkbound, rightinkbound, - topinkbound, bottominkbound + topinkbound, bottominkbound, + hbearing_ori_to_left, hbearing_ori_to_top export TeXToken, tokenize export TeXExpr, texparse, TeXParseError, manual_texexpr @@ -25,6 +26,8 @@ export TeXElement, TeXChar, VLine, HLine, generate_tex_elements export texfont, FontFamily, set_texfont_family!, get_texfont_family export glyph_index +const ITALIC_CORRECTION = Ref(true) + # Reexport from LaTeXStrings export @L_str diff --git a/src/engine/fonts.jl b/src/engine/fonts.jl index 677bfc2..6e674ed 100644 --- a/src/engine/fonts.jl +++ b/src/engine/fonts.jl @@ -257,7 +257,7 @@ get_fontpath(fontstyle::Symbol) = get_fontpath(FontFamily(), fontstyle) function is_slanted(font_family, char_type) font_id = font_family.font_mapping[char_type] - return font_id == :italic + return font_id == :italic || font_id == :bolditalic end slant_angle(font_family) = font_family.slant_angle * π / 180 diff --git a/src/engine/layout.jl b/src/engine/layout.jl index d30c340..2338bba 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -288,6 +288,53 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ function horizontal_layout(elements) + global ITALIC_CORRECTION + if ITALIC_CORRECTION[] + elems = vcat(Space(0), elements) + j = 1 + for (i, elem) in enumerate(elements) + i == 1 && continue + prev = elements[i-1] + if elem isa TeXChar && prev isa TeXChar + if prev.slanted != elem.slanted + height_prev = hbearing_ori_to_top(prev) + depth_prev = inkheight(prev) - hbearing_ori_to_top(prev) + offset = 0 + #= + glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; + `height` (== `y_bearing`) ⇔ `hbearing_ori_to_top` + `tHeight` ⇔ `- inkheight` + `width` (== `x_advance`) ⇔ `hadvance` + `x_bearing` ⇔ `hbearing_ori_to_left` + `glyphWidth` (== `width`) ⇔ `inkwidth` + =# + if prev.slanted && !elem.slanted && height_prev > 0 + # `fromItalicCorrection` in `sile/typesetters/base.lua` + glyph_width_prev = inkwidth(prev) + bearing_x_prev = hbearing_ori_to_left(prev) + width_prev = hadvance(prev) + d = glyph_width_prev + bearing_x_prev + delta = d > width_prev ? d - width_prev : 0 + height_elem = hbearing_ori_to_top(elem) + offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev + elseif !prev.slanted && elem.slanted && depth_prev > 0 + # `toItalicCorrection` in `sile/typesetters/base.lua` + d = hbearing_ori_to_left(elem) + depth_elem = inkheight(elem) - hbearing_ori_to_top(elem) + delta = d < 0 ? -d : 0 + offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem + end + if offset != 0 + @show offset + insert!(elems, i+j, Space(offset)) + j+=1 + end + end + end + end + popfirst!(elems) + elements = elems + end dxs = hadvance.(elements) xs = [0, cumsum(dxs[1:end-1])...] diff --git a/src/engine/layout_context.jl b/src/engine/layout_context.jl index 5c9b8db..ca65af4 100644 --- a/src/engine/layout_context.jl +++ b/src/engine/layout_context.jl @@ -24,6 +24,11 @@ function add_font_modifier(state::LayoutState, modifier) end function get_font(state::LayoutState, char_type) + font_id = get_font_identifier(state, char_type) + return get_font(font_family, font_id) +end + +function get_font_identifier(state::LayoutState, char_type) if state.tex_mode == :text char_type = :text end @@ -40,5 +45,5 @@ function get_font(state::LayoutState, char_type) end end - return get_font(font_family, font_id) + return font_id end \ No newline at end of file diff --git a/src/engine/texelements.jl b/src/engine/texelements.jl index 2d32932..5f975c0 100644 --- a/src/engine/texelements.jl +++ b/src/engine/texelements.jl @@ -157,24 +157,27 @@ function TeXChar(char::Char, state::LayoutState, char_type) return TeXChar(id, font, font_family, false, char) end - font = get_font(state, char_type) + font_id = get_font_identifier(state, char_type) + font = get_font(font_family, font_id) return TeXChar( glyph_index(font, char), font, font_family, - is_slanted(state.font_family, char_type), + font_id == :italic || font_id == :bolditalic, # previously: `is_slanted(state.font_family, char_type)` char) end function TeXChar(name::AbstractString, state::LayoutState, char_type ; represented='?') font_family = state.font_family - font = get_font(state, char_type) + font_id = get_font_identifier(state, char_type) + font = get_font(font_family, font_id) + return TeXChar( glyph_index(font, name), font, font_family, - is_slanted(state.font_family, char_type), + font_id == :italic || font_id == :bolditalic, # previously: `is_slanted(state.font_family, char_type)` represented) end @@ -186,6 +189,9 @@ glyph_index(char::TeXChar) = char.glyph_id hadvance(char::TeXChar) = hadvance(get_extent(char.font, char.glyph_id)) xheight(char::TeXChar) = xheight(char.font_family) +hbearing_ori_to_left(char::TeXChar) = hbearing_ori_to_left(get_extent(char.font, char.glyph_id)) +hbearing_ori_to_top(char::TeXChar) = hbearing_ori_to_top(get_extent(char.font, char.glyph_id)) + function ascender(char::TeXChar) math_font = get_font(char.font_family, :math) return max(ascender(math_font), topinkbound(char)) From f82388f0096be64c38db309b255058cde4ca2ff6 Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Sun, 5 Oct 2025 18:27:45 +0200 Subject: [PATCH 02/14] remove `@show` --- src/engine/layout.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 2338bba..503cbdd 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -325,7 +325,6 @@ function horizontal_layout(elements) offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem end if offset != 0 - @show offset insert!(elems, i+j, Space(offset)) j+=1 end From bf51b1c3909f103926f3db07e07063d73b33f462 Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Sun, 5 Oct 2025 22:34:16 +0200 Subject: [PATCH 03/14] improve heuristic --- src/engine/layout.jl | 37 ++++++++++++++++++++++++++++--------- src/parser/parser.jl | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 503cbdd..5a48855 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -295,10 +295,16 @@ function horizontal_layout(elements) for (i, elem) in enumerate(elements) i == 1 && continue prev = elements[i-1] - if elem isa TeXChar && prev isa TeXChar + #= + # TODO enable? this makes `\mathrm{gg}t` print nice, but could break other stuff + # if preceding element is a group, inspect last element + if prev isa Group && !isempty(prev.elements) + prev = last(prev.elements) + end + =# + !isa(prev, TeXChar) && continue + if elem isa TeXChar if prev.slanted != elem.slanted - height_prev = hbearing_ori_to_top(prev) - depth_prev = inkheight(prev) - hbearing_ori_to_top(prev) offset = 0 #= glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; @@ -308,21 +314,34 @@ function horizontal_layout(elements) `x_bearing` ⇔ `hbearing_ori_to_left` `glyphWidth` (== `width`) ⇔ `inkwidth` =# + height_prev = hbearing_ori_to_top(prev) + bearing_x_prev = hbearing_ori_to_left(prev) + glyph_width_prev = inkwidth(prev) if prev.slanted && !elem.slanted && height_prev > 0 # `fromItalicCorrection` in `sile/typesetters/base.lua` - glyph_width_prev = inkwidth(prev) - bearing_x_prev = hbearing_ori_to_left(prev) width_prev = hadvance(prev) d = glyph_width_prev + bearing_x_prev delta = d > width_prev ? d - width_prev : 0 height_elem = hbearing_ori_to_top(elem) offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev - elseif !prev.slanted && elem.slanted && depth_prev > 0 - # `toItalicCorrection` in `sile/typesetters/base.lua` + elseif !prev.slanted && elem.slanted + # inspired by `toItalicCorrection` in `sile/typesetters/base.lua` d = hbearing_ori_to_left(elem) + depth_prev = inkheight(prev) - hbearing_ori_to_top(prev) depth_elem = inkheight(elem) - hbearing_ori_to_top(elem) - delta = d < 0 ? -d : 0 - offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem + delta = -d + if d < 0 && depth_prev > 0 + # `sile` formula + offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem + else + # but also remove bearing in other cases + # simple: offset = delta + # but we try to detect "padded" glyphs (e.g., parenthesis in many fonts) + # and then, assuming that for other upright glyphs the bearing is somewhat regular, + # use that as a target for the slanted glyph + b = (bearing_x_prev / glyph_width_prev) > 0.25 ? 0 : bearing_x_prev + offset = b + delta + end end if offset != 0 insert!(elems, i+j, Space(offset)) diff --git a/src/parser/parser.jl b/src/parser/parser.jl index ed13f3a..f2194aa 100644 --- a/src/parser/parser.jl +++ b/src/parser/parser.jl @@ -8,7 +8,7 @@ end function Base.showerror(io::IO, e::TeXParseError) println(io, "TeXParseError: ", e.msg) show_state(io, e.stack, e.position, e.tex) - show_tokenization(io, tex) + show_tokenization(io, e.tex) end function show_tokenization(io, tex) From 9d2b3178b0a5025c03b820daa2eede33e2e156b5 Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Sun, 5 Oct 2025 23:01:16 +0200 Subject: [PATCH 04/14] ignore pos bearing if prev not deep --- src/engine/layout.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 5a48855..3d9b3ec 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -333,8 +333,8 @@ function horizontal_layout(elements) if d < 0 && depth_prev > 0 # `sile` formula offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem - else - # but also remove bearing in other cases + elseif d >= 0 + # but also remove/reduce positive bearing # simple: offset = delta # but we try to detect "padded" glyphs (e.g., parenthesis in many fonts) # and then, assuming that for other upright glyphs the bearing is somewhat regular, From 169cd58b15210316a60aca047927474e8789b9d0 Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Mon, 6 Oct 2025 11:09:08 +0200 Subject: [PATCH 05/14] letter spacing up to it --- src/MathTeXEngine.jl | 1 + src/engine/layout.jl | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index a7d2510..370bb15 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -27,6 +27,7 @@ export texfont, FontFamily, set_texfont_family!, get_texfont_family export glyph_index const ITALIC_CORRECTION = Ref(true) +const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(0.75f0) # percentage of x-pixelsize of font (like LetterSpacing in fontspec) # Reexport from LaTeXStrings export @L_str diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 3d9b3ec..dc7d0b9 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -288,7 +288,7 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ function horizontal_layout(elements) - global ITALIC_CORRECTION + global ITALIC_CORRECTION, ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT if ITALIC_CORRECTION[] elems = vcat(Space(0), elements) j = 1 @@ -315,11 +315,11 @@ function horizontal_layout(elements) `glyphWidth` (== `width`) ⇔ `inkwidth` =# height_prev = hbearing_ori_to_top(prev) - bearing_x_prev = hbearing_ori_to_left(prev) - glyph_width_prev = inkwidth(prev) if prev.slanted && !elem.slanted && height_prev > 0 # `fromItalicCorrection` in `sile/typesetters/base.lua` width_prev = hadvance(prev) + glyph_width_prev = inkwidth(prev) + bearing_x_prev = hbearing_ori_to_left(prev) d = glyph_width_prev + bearing_x_prev delta = d > width_prev ? d - width_prev : 0 height_elem = hbearing_ori_to_top(elem) @@ -335,11 +335,8 @@ function horizontal_layout(elements) offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem elseif d >= 0 # but also remove/reduce positive bearing - # simple: offset = delta - # but we try to detect "padded" glyphs (e.g., parenthesis in many fonts) - # and then, assuming that for other upright glyphs the bearing is somewhat regular, - # use that as a target for the slanted glyph - b = (bearing_x_prev / glyph_width_prev) > 0.25 ? 0 : bearing_x_prev + perc = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 + b = perc == 0 ? 0 : perc * _pixelsize(prev) offset = b + delta end end @@ -366,6 +363,19 @@ function layout_text(string, font_family) return horizontal_layout(elements) end +# obtain horizontal size of EM-box of font of `tchar` in pixels +function _pixelsize(tchar::TeXChar) + font = tchar.font + pxsize = @lock font.lock begin + #= + sz = unsafe_load(font.size) + sz.metrics.x_scale // (0x10000 * 64) # this only works if a pixel size has been set + =# + font.max_advance_width / font.units_per_EM + end + return pxsize +end + """ unravel(element::TeXElement, pos, scale) From e2f114fe4f8a06ddc6ed5091e0b1a2878025491b Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Mon, 6 Oct 2025 15:33:08 +0200 Subject: [PATCH 06/14] clean-up + comments --- src/MathTeXEngine.jl | 3 -- src/engine/layout.jl | 66 +++++++++++++++++++++++++++----------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index 370bb15..18283c4 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -26,9 +26,6 @@ export TeXElement, TeXChar, VLine, HLine, generate_tex_elements export texfont, FontFamily, set_texfont_family!, get_texfont_family export glyph_index -const ITALIC_CORRECTION = Ref(true) -const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(0.75f0) # percentage of x-pixelsize of font (like LetterSpacing in fontspec) - # Reexport from LaTeXStrings export @L_str diff --git a/src/engine/layout.jl b/src/engine/layout.jl index dc7d0b9..40d8912 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -1,3 +1,9 @@ +## flag to enable italic correction heuristic +const ITALIC_CORRECTION = Ref(true) +## percentage of x-pixelsize of font (like LetterSpacing in fontspec) +## for bearing before slanted glyph when switching from upright to slanted letters +const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(0.75f0) + """ Return the y value needed for the element to be vertically centered in the middle of the xheight. @@ -26,7 +32,7 @@ function tex_layout(expr, state) head = expr.head args = [expr.args...] shrink = 0.6 - + inside_math = state.tex_mode == :inline_math try if isleaf(expr) # :char, :delimiter, :digit, :punctuation, :symbol char = args[1] @@ -138,7 +144,7 @@ function tex_layout(expr, state) elseif head == :function name = args[1] elements = TeXChar.(collect(name), state, Ref(:function)) - return horizontal_layout(elements) + return horizontal_layout(elements, inside_math) elseif head == :glyph font_id, glyph_id = argument_as_string.(args) font_id = Symbol(font_id) @@ -151,7 +157,7 @@ function tex_layout(expr, state) if isempty(elements) return Space(0.0) end - return horizontal_layout(elements) + return horizontal_layout(elements, mode == :inline_math) elseif head == :integral pad = 0.1 int, sub, super = tex_layout.(args, state) @@ -199,12 +205,12 @@ function tex_layout(expr, state) ) elseif head == :primes primes = [TeXExpr(:char, ''') for _ in 1:only(args)] - return horizontal_layout(tex_layout.(primes, state)) + return horizontal_layout(tex_layout.(primes, state), inside_math) elseif head == :space return Space(args[1]) elseif head == :spaced sym = tex_layout(args[1], state) - return horizontal_layout([Space(0.2), sym, Space(0.2)]) + return horizontal_layout([Space(0.2), sym, Space(0.2)], inside_math) elseif head == :sqrt content = tex_layout(args[1], state) h = inkheight(content) @@ -287,7 +293,25 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ -function horizontal_layout(elements) +function horizontal_layout(elements, inside_math=false) + elements = _italic_correction(Val(inside_math), elements) + dxs = hadvance.(elements) + xs = [0, cumsum(dxs[1:end-1])...] + + return Group(elements, Point2f.(xs, 0)) +end + +function layout_text(string, font_family) + isempty(string) && return Space(0) + + elements = TeXChar.(collect(string), LayoutState(font_family), Ref(:text)) + return horizontal_layout(elements, false) +end + +function _italic_correction(inside_math::Val{false}, elements) + return elements +end +function _italic_correction(inside_math::Val{true}, elements) global ITALIC_CORRECTION, ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT if ITALIC_CORRECTION[] elems = vcat(Space(0), elements) @@ -317,6 +341,8 @@ function horizontal_layout(elements) height_prev = hbearing_ori_to_top(prev) if prev.slanted && !elem.slanted && height_prev > 0 # `fromItalicCorrection` in `sile/typesetters/base.lua` + ## if previous glyph was slanted and printed width `d` is greater + ## than hadvance, then add difference to bearing of upright glyph width_prev = hadvance(prev) glyph_width_prev = inkwidth(prev) bearing_x_prev = hbearing_ori_to_left(prev) @@ -332,11 +358,15 @@ function horizontal_layout(elements) delta = -d if d < 0 && depth_prev > 0 # `sile` formula + ## if previous glyph was upright and goes beyond baseline, + ## if and current glyph has a negative bearing, + ## then increase bearing by flipping sign of bearing distance offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem elseif d >= 0 - # but also remove/reduce positive bearing - perc = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 - b = perc == 0 ? 0 : perc * _pixelsize(prev) + ## but also remove/reduce positive bearing or reduce it to some + ## minimum spacing value + fac = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 + b = fac == 0 ? 0 : fac * _font_pixelsize(prev) offset = b + delta end end @@ -350,27 +380,13 @@ function horizontal_layout(elements) popfirst!(elems) elements = elems end - dxs = hadvance.(elements) - xs = [0, cumsum(dxs[1:end-1])...] - - return Group(elements, Point2f.(xs, 0)) -end - -function layout_text(string, font_family) - isempty(string) && return Space(0) - - elements = TeXChar.(collect(string), LayoutState(font_family), Ref(:text)) - return horizontal_layout(elements) + return elements end # obtain horizontal size of EM-box of font of `tchar` in pixels -function _pixelsize(tchar::TeXChar) +function _font_pixelsize(tchar::TeXChar) font = tchar.font pxsize = @lock font.lock begin - #= - sz = unsafe_load(font.size) - sz.metrics.x_scale // (0x10000 * 64) # this only works if a pixel size has been set - =# font.max_advance_width / font.units_per_EM end return pxsize From 2a1ee904b0188bb035c5be18f7083e5516bb549d Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Mon, 6 Oct 2025 15:50:36 +0200 Subject: [PATCH 07/14] add italic correction tests --- test/layout.jl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/layout.jl b/test/layout.jl index 881a8a0..01a357a 100644 --- a/test/layout.jl +++ b/test/layout.jl @@ -85,6 +85,32 @@ end char, pos, size = first(elems) @test pos[1] == 2 end + + @testset "Italic Correction" begin + MTE.ITALIC_CORRECTION[] = false + els1 = generate_tex_elements(L"(f)x") + + MTE.ITALIC_CORRECTION[] = true + els2 = generate_tex_elements(L"(f)x") + + xpos = ((elem, pos),) -> pos[1] + + ## space between upright `(` and slanted `f` is increased to avoid collisions at bottom left corner + Δ1 = xpos(els1[2]) - xpos(els1[1]) + Δ2 = xpos(els2[2]) - xpos(els2[1]) + @test Δ1 < Δ2 + + ## space between slanted `f` and upright `)` is increased to avoid collisions at top right corner + Δ1 = xpos(els1[3]) - xpos(els1[2]) + Δ2 = xpos(els2[3]) - xpos(els2[2]) + @test Δ1 < Δ2 + + ## with NewComputerModern, some italic glyphs seem to have large positive bearing + ## it should be reduced by correction heuristic + Δ1 = xpos(els1[4]) - xpos(els1[3]) + Δ2 = xpos(els2[4]) - xpos(els2[3]) + @test Δ1 > Δ2 + end end @testset "Generate elements" begin From 865cf06e8bc50cf2dfa2dca1ceb7537bc8551736 Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Mon, 6 Oct 2025 18:00:28 +0200 Subject: [PATCH 08/14] enable italic correction in :delimited --- src/MathTeXEngine.jl | 2 +- src/engine/layout.jl | 143 ++++++++++++++++++++++---------------- src/engine/texelements.jl | 1 - 3 files changed, 85 insertions(+), 61 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index 18283c4..be07bb4 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -18,7 +18,7 @@ import FreeTypeAbstraction: hadvance, inkheight, inkwidth, height_insensitive_boundingbox, leftinkbound, rightinkbound, topinkbound, bottominkbound, - hbearing_ori_to_left, hbearing_ori_to_top + hbearing_ori_to_left export TeXToken, tokenize export TeXExpr, texparse, TeXParseError, manual_texexpr diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 40d8912..c465a8c 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -91,23 +91,41 @@ function tex_layout(expr, state) elseif head == :delimited elements = tex_layout.(args, state) left, content, right = elements - + height = inkheight(content) left_scale = max(1, height / inkheight(left)) right_scale = max(1, height / inkheight(right)) scales = [left_scale, 1, right_scale] - + + ## maybe add spaces for italic correction + _elements = _italic_correction(Val(inside_math), elements, scales; ungroup_once=true) + if length(_elements) > 3 + ## extra spaces have been added, regroup content + left = first(_elements) + right = last(_elements) + content = horizontal_layout(_elements[2:end-1], false) + elements = vcat(left, content, right) + end + dxs = hadvance.(elements) .* scales xs = [0, cumsum(dxs[1:end-1])...] - # TODO Height calculation for the parenthesis looks wrong + ## compute y-positions for delimiters # TODO Check what the algorithm should be there - # Center the delimiters in the middle of the bot and top baselines ? + bot_content = bottominkbound(content) + h_content = inkheight(content) + h_left = inkheight(left) * left_scale + delta_left = max(0, (h_left - h_content)) / 2 + y_left = bot_content - delta_left - (bottominkbound(left) * left_scale) + + h_right = inkheight(right) * right_scale + delta_right = max(0, (h_right - h_content)) / 2 + y_right = bot_content - delta_right - (bottominkbound(right) * right_scale) return Group(elements, Point2f[ - (xs[1], -bottominkbound(left) + bottominkbound(content)), + (xs[1], y_left), (xs[2], 0), - (xs[3], -bottominkbound(right) + bottominkbound(content)) + (xs[3], y_right) ], scales ) @@ -308,73 +326,77 @@ function layout_text(string, font_family) return horizontal_layout(elements, false) end -function _italic_correction(inside_math::Val{false}, elements) +function _italic_correction(inside_math::Val{false}, elements, args...; kwargs...) return elements end -function _italic_correction(inside_math::Val{true}, elements) +function _italic_correction(inside_math::Val{true}, elements, scales=1; ungroup_once=false) global ITALIC_CORRECTION, ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT if ITALIC_CORRECTION[] + @assert isa(scales, Number) || length(elements) == length(scales) elems = vcat(Space(0), elements) j = 1 for (i, elem) in enumerate(elements) i == 1 && continue prev = elements[i-1] - #= - # TODO enable? this makes `\mathrm{gg}t` print nice, but could break other stuff - # if preceding element is a group, inspect last element - if prev isa Group && !isempty(prev.elements) + if isa(prev, Group) && ungroup_once prev = last(prev.elements) end - =# !isa(prev, TeXChar) && continue - if elem isa TeXChar - if prev.slanted != elem.slanted - offset = 0 - #= - glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; - `height` (== `y_bearing`) ⇔ `hbearing_ori_to_top` - `tHeight` ⇔ `- inkheight` - `width` (== `x_advance`) ⇔ `hadvance` - `x_bearing` ⇔ `hbearing_ori_to_left` - `glyphWidth` (== `width`) ⇔ `inkwidth` - =# - height_prev = hbearing_ori_to_top(prev) - if prev.slanted && !elem.slanted && height_prev > 0 - # `fromItalicCorrection` in `sile/typesetters/base.lua` - ## if previous glyph was slanted and printed width `d` is greater - ## than hadvance, then add difference to bearing of upright glyph - width_prev = hadvance(prev) - glyph_width_prev = inkwidth(prev) - bearing_x_prev = hbearing_ori_to_left(prev) - d = glyph_width_prev + bearing_x_prev - delta = d > width_prev ? d - width_prev : 0 - height_elem = hbearing_ori_to_top(elem) - offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev - elseif !prev.slanted && elem.slanted - # inspired by `toItalicCorrection` in `sile/typesetters/base.lua` - d = hbearing_ori_to_left(elem) - depth_prev = inkheight(prev) - hbearing_ori_to_top(prev) - depth_elem = inkheight(elem) - hbearing_ori_to_top(elem) - delta = -d - if d < 0 && depth_prev > 0 - # `sile` formula - ## if previous glyph was upright and goes beyond baseline, - ## if and current glyph has a negative bearing, - ## then increase bearing by flipping sign of bearing distance - offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem - elseif d >= 0 - ## but also remove/reduce positive bearing or reduce it to some - ## minimum spacing value - fac = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 - b = fac == 0 ? 0 : fac * _font_pixelsize(prev) - offset = b + delta - end - end - if offset != 0 - insert!(elems, i+j, Space(offset)) - j+=1 + + if isa(elem, Group) && ungroup_once + elem = first(elem.elements) + end + !isa(elem, TeXChar) && continue + + scale_elem = _get_scale(scales, i) + scale_prev = _get_scale(scales, i-1) + + if prev.slanted != elem.slanted + offset = 0 + #= + glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; + `height` (== `y_bearing`) ⇔ `topinkbound` + `tHeight` ⇔ `- inkheight` + `width` (== `x_advance`) ⇔ `hadvance` + `x_bearing` ⇔ `hbearing_ori_to_left` + `glyphWidth` (== `width`) ⇔ `inkwidth` + =# + height_prev = topinkbound(prev) * scale_prev + if prev.slanted && !elem.slanted && height_prev > 0 + # `fromItalicCorrection` in `sile/typesetters/base.lua` + ## if previous glyph was slanted and printed width `d` is greater + ## than hadvance, then add difference to bearing of upright glyph + width_prev = hadvance(prev) * scale_prev + glyph_width_prev = inkwidth(prev) * scale_prev + bearing_x_prev = hbearing_ori_to_left(prev) * scale_prev + d = glyph_width_prev + bearing_x_prev + delta = d > width_prev ? d - width_prev : 0 + height_elem = topinkbound(elem) * scale_elem + offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev + elseif !prev.slanted && elem.slanted + # inspired by `toItalicCorrection` in `sile/typesetters/base.lua` + d = hbearing_ori_to_left(elem) * scale_elem + depth_prev = (inkheight(prev) - topinkbound(prev)) * scale_prev + depth_elem = (inkheight(elem) - topinkbound(elem)) * scale_elem + delta = -d + if d < 0 && depth_prev > 0 + # `sile` formula + ## if previous glyph was upright and goes beyond baseline, + ## if and current glyph has a negative bearing, + ## then increase bearing by flipping sign of bearing distance + offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem + elseif d >= 0 + ## but also remove/reduce positive bearing or reduce it to some + ## minimum spacing value + fac = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 + b = fac == 0 ? 0 : fac * _font_pixelsize(prev) + offset = b + delta end end + if offset != 0 + insert!(elems, i+j, Space(offset)) + j+=1 + end end end popfirst!(elems) @@ -383,6 +405,9 @@ function _italic_correction(inside_math::Val{true}, elements) return elements end +_get_scale(scales::Number, i)=scales +_get_scale(scales, i)=scales[i] + # obtain horizontal size of EM-box of font of `tchar` in pixels function _font_pixelsize(tchar::TeXChar) font = tchar.font diff --git a/src/engine/texelements.jl b/src/engine/texelements.jl index 5f975c0..d99fead 100644 --- a/src/engine/texelements.jl +++ b/src/engine/texelements.jl @@ -190,7 +190,6 @@ hadvance(char::TeXChar) = hadvance(get_extent(char.font, char.glyph_id)) xheight(char::TeXChar) = xheight(char.font_family) hbearing_ori_to_left(char::TeXChar) = hbearing_ori_to_left(get_extent(char.font, char.glyph_id)) -hbearing_ori_to_top(char::TeXChar) = hbearing_ori_to_top(get_extent(char.font, char.glyph_id)) function ascender(char::TeXChar) math_font = get_font(char.font_family, :math) From e66cce5bd4edc87c5504a8a25d01ffd2caf0d64c Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Mon, 6 Oct 2025 18:51:53 +0200 Subject: [PATCH 09/14] enable delimiter commands after `\left` and `\right` --- src/parser/commands_data.jl | 15 +++++++++++++++ src/parser/commands_registration.jl | 8 ++++++++ src/parser/tokenizer.jl | 7 ++++--- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/parser/commands_data.jl b/src/parser/commands_data.jl index a03364f..5937c93 100644 --- a/src/parser/commands_data.jl +++ b/src/parser/commands_data.jl @@ -110,5 +110,20 @@ punctuation_symbols = split(raw", ; . !") delimiter_symbols = split(raw"| / ( ) [ ] < >") font_names = split(raw"rm cal it tt sf bf default bb frak scr regular") +delimiter_commands = Dict( + raw"\langle" => '⟨', + raw"\rangle" => '⟩', + raw"\vert" => '|', + raw"\Vert" => '‖', + raw"\lbrack" => '[', + raw"\rbrack" => ']', + raw"\lbrace" => '{', + raw"\rbrace" => '}', + raw"\lceil" => '⌈', + raw"\rceil" => '⌉', + raw"\lfloor" => '⌊', + raw"\lfloor" => '⌊', +) + # TODO Add to the parser what come below, if needed wide_accent_commands = split(raw"\widehat \widetilde \widebar") \ No newline at end of file diff --git a/src/parser/commands_registration.jl b/src/parser/commands_registration.jl index dea53cf..da06d8c 100644 --- a/src/parser/commands_registration.jl +++ b/src/parser/commands_registration.jl @@ -155,6 +155,14 @@ for symbol in delimiter_symbols symbol_to_canonical[symbol] = TeXExpr(:delimiter, symbol) end +for (com_str, symbol) in pairs(delimiter_commands) + delim_expr = TeXExpr(:delimiter, symbol) + if !haskey(symbol_to_canonical, symbol) + symbol_to_canonical[symbol] = delim_expr + end + command_definitions[com_str] = (delim_expr, 0) +end + ## ## Default behavior ## diff --git a/src/parser/tokenizer.jl b/src/parser/tokenizer.jl index 9619622..8029e1a 100644 --- a/src/parser/tokenizer.jl +++ b/src/parser/tokenizer.jl @@ -1,3 +1,4 @@ +const token_command = re"\\[a-zA-Z]+" | re"\\." tex_tokens = [ :char => re".", :primes => re"'+", @@ -5,9 +6,9 @@ tex_tokens = [ :underscore => re"_", :rcurly => re"}", :lcurly => re"{", - :command => re"\\[a-zA-Z]+" | re"\\.", - :right => re"\\right.", - :left => re"\\left.", + :command => token_command, + :right => re"\\right." | re"\\right" * token_command, + :left => re"\\left." | re"\\left" * token_command, :newline => (re"\\" * re"\\") | re"\\n", :dollar => re"$" ] From 1a1b9c6b5e0f57ff0f7aa5482abbb5a0cc5aa10e Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Tue, 7 Oct 2025 12:30:36 +0200 Subject: [PATCH 10/14] clean up --- src/MathTeXEngine.jl | 5 +- src/engine/layout.jl | 223 +++++++++++++++++++------------------- src/engine/texelements.jl | 10 +- 3 files changed, 118 insertions(+), 120 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index be07bb4..5a06032 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -17,9 +17,8 @@ import FreeTypeAbstraction: ascender, boundingbox, descender, get_extent, glyph_index, hadvance, inkheight, inkwidth, height_insensitive_boundingbox, leftinkbound, rightinkbound, - topinkbound, bottominkbound, - hbearing_ori_to_left - + topinkbound, bottominkbound + export TeXToken, tokenize export TeXExpr, texparse, TeXParseError, manual_texexpr export TeXElement, TeXChar, VLine, HLine, generate_tex_elements diff --git a/src/engine/layout.jl b/src/engine/layout.jl index c465a8c..4f73418 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -1,8 +1,8 @@ ## flag to enable italic correction heuristic const ITALIC_CORRECTION = Ref(true) -## percentage of x-pixelsize of font (like LetterSpacing in fontspec) +## percentage of x-height of font (like LetterSpacing in fontspec) ## for bearing before slanted glyph when switching from upright to slanted letters -const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(0.75f0) +const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(1.25f0) """ Return the y value needed for the element to be vertically centered in the @@ -32,7 +32,10 @@ function tex_layout(expr, state) head = expr.head args = [expr.args...] shrink = 0.6 + inside_math = state.tex_mode == :inline_math + global_xheight = xheight(font_family) + try if isleaf(expr) # :char, :delimiter, :digit, :punctuation, :symbol char = args[1] @@ -60,7 +63,8 @@ function tex_layout(expr, state) (0, 0), (x + hmid(core) - hmid(accent), y) ], - [1, 1] + [1, 1], + is_slanted(core) ) elseif head == :decorated core, sub, super = tex_layout.(args, state) @@ -86,7 +90,8 @@ function tex_layout(expr, state) -0.2 ), ( super_x, super_y)], - [1, shrink, super_shrink] + [1, shrink, super_shrink], + is_slanted(core) ) elseif head == :delimited elements = tex_layout.(args, state) @@ -96,19 +101,11 @@ function tex_layout(expr, state) left_scale = max(1, height / inkheight(left)) right_scale = max(1, height / inkheight(right)) scales = [left_scale, 1, right_scale] - - ## maybe add spaces for italic correction - _elements = _italic_correction(Val(inside_math), elements, scales; ungroup_once=true) - if length(_elements) > 3 - ## extra spaces have been added, regroup content - left = first(_elements) - right = last(_elements) - content = horizontal_layout(_elements[2:end-1], false) - elements = vcat(left, content, right) - end - + + #= dxs = hadvance.(elements) .* scales xs = [0, cumsum(dxs[1:end-1])...] + =# ## compute y-positions for delimiters # TODO Check what the algorithm should be there @@ -121,14 +118,13 @@ function tex_layout(expr, state) h_right = inkheight(right) * right_scale delta_right = max(0, (h_right - h_content)) / 2 y_right = bot_content - delta_right - (bottominkbound(right) * right_scale) - return Group(elements, - Point2f[ - (xs[1], y_left), - (xs[2], 0), - (xs[3], y_right) - ], - scales - ) + + _elements = [ + Group([left,], Point2f[(0, y_left)], [left_scale], is_slanted(left)) + Group([content,], Point2f[(0, 0)], [1,], is_slanted(content)) + Group([right,], Point2f[(0, y_right)], [right_scale], is_slanted(right)) + ] + return horizontal_layout(_elements; inside_math, global_xheight) elseif head == :font modifier, content = args return tex_layout(content, add_font_modifier(state, modifier)) @@ -157,12 +153,13 @@ function tex_layout(expr, state) return Group( [line, numerator, denominator], - Point2f[(0, y0), (x1, ytop), (x2, ybottom)] + Point2f[(0, y0), (x1, ytop), (x2, ybottom)]; + slanted = is_slanted(numerator) | is_slanted(denominator) ) elseif head == :function name = args[1] elements = TeXChar.(collect(name), state, Ref(:function)) - return horizontal_layout(elements, inside_math) + return horizontal_layout(elements; inside_math, global_xheight) elseif head == :glyph font_id, glyph_id = argument_as_string.(args) font_id = Symbol(font_id) @@ -175,7 +172,7 @@ function tex_layout(expr, state) if isempty(elements) return Space(0.0) end - return horizontal_layout(elements, mode == :inline_math) + return horizontal_layout(elements; inside_math = (mode == :inline_math), global_xheight) elseif head == :integral pad = 0.1 int, sub, super = tex_layout.(args, state) @@ -193,7 +190,8 @@ function tex_layout(expr, state) topinkbound(int) + pad ) ], - [1, shrink, shrink] + [1, shrink, shrink], + is_slanted(int) ) elseif head == :lines length(args) == 1 && return tex_layout(only(args), state) @@ -219,16 +217,17 @@ function tex_layout(expr, state) Point2f[ (0.25, y + lw/2 + 0.2), (0, 0) - ] + ]; + slanted = is_slanted(content) ) elseif head == :primes primes = [TeXExpr(:char, ''') for _ in 1:only(args)] - return horizontal_layout(tex_layout.(primes, state), inside_math) + return horizontal_layout(tex_layout.(primes, state); inside_math, global_xheight) elseif head == :space return Space(args[1]) elseif head == :spaced sym = tex_layout(args[1], state) - return horizontal_layout([Space(0.2), sym, Space(0.2)], inside_math) + return horizontal_layout([Space(0.2), sym, Space(0.2)]; inside_math, global_xheight) elseif head == :sqrt content = tex_layout(args[1], state) h = inkheight(content) @@ -286,7 +285,8 @@ function tex_layout(expr, state) (x0 + dxsub, y0 + under_offset), (x0 + dxsuper, y0 + over_offset) ], - [1, shrink, shrink] + [1, shrink, shrink], + is_slanted(core) ) elseif head == :unicode font_id, glyph_id = argument_as_string.(args) @@ -311,8 +311,8 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ -function horizontal_layout(elements, inside_math=false) - elements = _italic_correction(Val(inside_math), elements) +function horizontal_layout(elements; kwargs...) + elements = _italic_correction(elements; kwargs...) dxs = hadvance.(elements) xs = [0, cumsum(dxs[1:end-1])...] @@ -323,99 +323,96 @@ function layout_text(string, font_family) isempty(string) && return Space(0) elements = TeXChar.(collect(string), LayoutState(font_family), Ref(:text)) - return horizontal_layout(elements, false) + return horizontal_layout(elements) end -function _italic_correction(inside_math::Val{false}, elements, args...; kwargs...) - return elements +function _italic_correction( + elements; + inside_math::Bool=false, + global_xheight::Number=xheight(get_texfont_family()), + scales=1 +) + global ITALIC_CORRECTION + return _italic_correction( + Val(inside_math & ITALIC_CORRECTION[]), elements; global_xheight, scales) end -function _italic_correction(inside_math::Val{true}, elements, scales=1; ungroup_once=false) - global ITALIC_CORRECTION, ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT - if ITALIC_CORRECTION[] - @assert isa(scales, Number) || length(elements) == length(scales) - elems = vcat(Space(0), elements) - j = 1 - for (i, elem) in enumerate(elements) - i == 1 && continue - prev = elements[i-1] - if isa(prev, Group) && ungroup_once - prev = last(prev.elements) - end - !isa(prev, TeXChar) && continue - if isa(elem, Group) && ungroup_once - elem = first(elem.elements) - end - !isa(elem, TeXChar) && continue - - scale_elem = _get_scale(scales, i) - scale_prev = _get_scale(scales, i-1) - - if prev.slanted != elem.slanted - offset = 0 - #= - glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; - `height` (== `y_bearing`) ⇔ `topinkbound` - `tHeight` ⇔ `- inkheight` - `width` (== `x_advance`) ⇔ `hadvance` - `x_bearing` ⇔ `hbearing_ori_to_left` - `glyphWidth` (== `width`) ⇔ `inkwidth` - =# - height_prev = topinkbound(prev) * scale_prev - if prev.slanted && !elem.slanted && height_prev > 0 - # `fromItalicCorrection` in `sile/typesetters/base.lua` - ## if previous glyph was slanted and printed width `d` is greater - ## than hadvance, then add difference to bearing of upright glyph - width_prev = hadvance(prev) * scale_prev - glyph_width_prev = inkwidth(prev) * scale_prev - bearing_x_prev = hbearing_ori_to_left(prev) * scale_prev - d = glyph_width_prev + bearing_x_prev - delta = d > width_prev ? d - width_prev : 0 - height_elem = topinkbound(elem) * scale_elem - offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev - elseif !prev.slanted && elem.slanted - # inspired by `toItalicCorrection` in `sile/typesetters/base.lua` - d = hbearing_ori_to_left(elem) * scale_elem - depth_prev = (inkheight(prev) - topinkbound(prev)) * scale_prev - depth_elem = (inkheight(elem) - topinkbound(elem)) * scale_elem - delta = -d - if d < 0 && depth_prev > 0 - # `sile` formula - ## if previous glyph was upright and goes beyond baseline, - ## if and current glyph has a negative bearing, - ## then increase bearing by flipping sign of bearing distance - offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem - elseif d >= 0 - ## but also remove/reduce positive bearing or reduce it to some - ## minimum spacing value - fac = ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100 - b = fac == 0 ? 0 : fac * _font_pixelsize(prev) - offset = b + delta - end - end - if offset != 0 - insert!(elems, i+j, Space(offset)) - j+=1 +function _italic_correction(::Val{false}, elements; kwargs...) + return elements +end +function _italic_correction( + ::Val{true}, elements; + global_xheight, scales +) + global ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT + + @assert isa(scales, Number) || length(elements) == length(scales) + elems = vcat(Space(0), elements) + j = 1 + + fac_up_to_it = (ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100) + b_up_to_it = fac_up_to_it == 0 ? 0 : fac_up_to_it * global_xheight + + for (i, elem) in enumerate(elements) + i == 1 && continue + prev = elements[i-1] + + scale_elem = _get_scale(scales, i) + scale_prev = _get_scale(scales, i-1) + + if prev.slanted != elem.slanted + offset = 0 + #= + glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; + `height` (== `y_bearing`) ⇔ `topinkbound` + `tHeight` ⇔ `- inkheight` + `width` (== `x_advance`) ⇔ `hadvance` + `x_bearing` ⇔ `leftinkbound` + `glyphWidth` (== `width`) ⇔ `inkwidth` + =# + height_prev = topinkbound(prev) * scale_prev + if is_slanted(prev) && !is_slanted(elem) && height_prev > 0 + # `fromItalicCorrection` in `sile/typesetters/base.lua` + ## if previous glyph was slanted and printed width `d` is greater + ## than hadvance, then add difference to bearing of upright glyph + width_prev = hadvance(prev) * scale_prev + glyph_width_prev = inkwidth(prev) * scale_prev + bearing_x_prev = leftinkbound(prev) * scale_prev + d = glyph_width_prev + bearing_x_prev + delta = d > width_prev ? d - width_prev : 0 + height_elem = topinkbound(elem) * scale_elem + offset = height_prev <= height_elem ? delta : delta * height_elem / height_prev + elseif !is_slanted(prev) && is_slanted(elem) + # inspired by `toItalicCorrection` in `sile/typesetters/base.lua` + d = leftinkbound(elem) * scale_elem + depth_prev = (inkheight(prev) - topinkbound(prev)) * scale_prev + depth_elem = (inkheight(elem) - topinkbound(elem)) * scale_elem + delta = -d + if d < 0 && depth_prev > 0 + # `sile` formula + ## if previous glyph was upright and goes beyond baseline, + ## if and current glyph has a negative bearing, + ## then increase bearing by flipping sign of bearing distance + offset = depth_prev >= depth_elem ? delta : delta * depth_prev / depth_elem + elseif d >= 0 + ## but also remove/reduce positive bearing or reduce it to some + ## minimum spacing value + offset = b_up_to_it * scale_elem + delta end end + if offset != 0 + insert!(elems, i+j, Space(offset)) + j+=1 + end end - popfirst!(elems) - elements = elems end - return elements + popfirst!(elems) + return elems end _get_scale(scales::Number, i)=scales _get_scale(scales, i)=scales[i] -# obtain horizontal size of EM-box of font of `tchar` in pixels -function _font_pixelsize(tchar::TeXChar) - font = tchar.font - pxsize = @lock font.lock begin - font.max_advance_width / font.units_per_EM - end - return pxsize -end """ unravel(element::TeXElement, pos, scale) diff --git a/src/engine/texelements.jl b/src/engine/texelements.jl index d99fead..12339aa 100644 --- a/src/engine/texelements.jl +++ b/src/engine/texelements.jl @@ -189,8 +189,6 @@ glyph_index(char::TeXChar) = char.glyph_id hadvance(char::TeXChar) = hadvance(get_extent(char.font, char.glyph_id)) xheight(char::TeXChar) = xheight(char.font_family) -hbearing_ori_to_left(char::TeXChar) = hbearing_ori_to_left(get_extent(char.font, char.glyph_id)) - function ascender(char::TeXChar) math_font = get_font(char.font_family, :math) return max(ascender(math_font), topinkbound(char)) @@ -294,9 +292,13 @@ struct Group{T} <: TeXElement elements::Vector{<:TeXElement} positions::Vector{Point2f} scales::Vector{T} + slanted::Bool end -Group(elements, positions) = Group(elements, positions, ones(length(elements))) +Group(elements, positions, scales; slanted=false) = Group(elements, positions, scales, slanted) +Group(elements, positions; slanted=false) = Group(elements, positions, ones(length(elements)); slanted) + +is_slanted(g::Group) = g.slanted xpositions(g::Group) = [p[1] for p in g.positions] ypositions(g::Group) = [p[2] for p in g.positions] @@ -339,4 +341,4 @@ end xheight(g::Group) = maximum(xheight.(g.elements) .* g.scales) leftmost_glyph(g::Group) = leftmost_glyph(first(g.elements)) -rightmost_glyph(g::Group) = rightmost_glyph(last(glyph)) \ No newline at end of file +rightmost_glyph(g::Group) = rightmost_glyph(last(g.elements)) \ No newline at end of file From a99f4c50174f6220edf3764f4800529d307cf2cb Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Tue, 7 Oct 2025 13:02:54 +0200 Subject: [PATCH 11/14] fix _italic_correction `.slanted` --- src/engine/layout.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 4f73418..ed83131 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -316,7 +316,7 @@ function horizontal_layout(elements; kwargs...) dxs = hadvance.(elements) xs = [0, cumsum(dxs[1:end-1])...] - return Group(elements, Point2f.(xs, 0)) + return Group(elements, Point2f.(xs, 0); slanted = any(is_slanted, elements)) end function layout_text(string, font_family) @@ -360,7 +360,7 @@ function _italic_correction( scale_elem = _get_scale(scales, i) scale_prev = _get_scale(scales, i-1) - if prev.slanted != elem.slanted + if is_slanted(prev) != is_slanted(elem) offset = 0 #= glyph metrics defined in `sile/justenough/justenoughharfbuzz.c`; From a4270b583bfe609195c8ad1c98b51761ef6edc6b Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Tue, 7 Oct 2025 13:22:54 +0200 Subject: [PATCH 12/14] italic corretion: no spaces; repair tests --- src/engine/layout.jl | 2 ++ test/layout.jl | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/layout.jl b/src/engine/layout.jl index ed83131..34cf113 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -355,7 +355,9 @@ function _italic_correction( for (i, elem) in enumerate(elements) i == 1 && continue + elem isa Space && continue prev = elements[i-1] + prev isa Space && continue scale_elem = _get_scale(scales, i) scale_prev = _get_scale(scales, i-1) diff --git a/test/layout.jl b/test/layout.jl index 01a357a..7cfa48b 100644 --- a/test/layout.jl +++ b/test/layout.jl @@ -87,10 +87,10 @@ end end @testset "Italic Correction" begin - MTE.ITALIC_CORRECTION[] = false + MathTeXEngine.ITALIC_CORRECTION[] = false els1 = generate_tex_elements(L"(f)x") - MTE.ITALIC_CORRECTION[] = true + MathTeXEngine.ITALIC_CORRECTION[] = true els2 = generate_tex_elements(L"(f)x") xpos = ((elem, pos),) -> pos[1] From c628cff450b2788e95f257c7753e8d15bc34256c Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Thu, 16 Oct 2025 13:48:43 +0200 Subject: [PATCH 13/14] revert delimiter changes, see #149 --- src/MathTeXEngine.jl | 1 - src/parser/commands_data.jl | 15 --------------- src/parser/commands_registration.jl | 8 -------- src/parser/tokenizer.jl | 7 +++---- 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index 5a06032..a9c8e16 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -18,7 +18,6 @@ import FreeTypeAbstraction: hadvance, inkheight, inkwidth, height_insensitive_boundingbox, leftinkbound, rightinkbound, topinkbound, bottominkbound - export TeXToken, tokenize export TeXExpr, texparse, TeXParseError, manual_texexpr export TeXElement, TeXChar, VLine, HLine, generate_tex_elements diff --git a/src/parser/commands_data.jl b/src/parser/commands_data.jl index 5937c93..a03364f 100644 --- a/src/parser/commands_data.jl +++ b/src/parser/commands_data.jl @@ -110,20 +110,5 @@ punctuation_symbols = split(raw", ; . !") delimiter_symbols = split(raw"| / ( ) [ ] < >") font_names = split(raw"rm cal it tt sf bf default bb frak scr regular") -delimiter_commands = Dict( - raw"\langle" => '⟨', - raw"\rangle" => '⟩', - raw"\vert" => '|', - raw"\Vert" => '‖', - raw"\lbrack" => '[', - raw"\rbrack" => ']', - raw"\lbrace" => '{', - raw"\rbrace" => '}', - raw"\lceil" => '⌈', - raw"\rceil" => '⌉', - raw"\lfloor" => '⌊', - raw"\lfloor" => '⌊', -) - # TODO Add to the parser what come below, if needed wide_accent_commands = split(raw"\widehat \widetilde \widebar") \ No newline at end of file diff --git a/src/parser/commands_registration.jl b/src/parser/commands_registration.jl index da06d8c..dea53cf 100644 --- a/src/parser/commands_registration.jl +++ b/src/parser/commands_registration.jl @@ -155,14 +155,6 @@ for symbol in delimiter_symbols symbol_to_canonical[symbol] = TeXExpr(:delimiter, symbol) end -for (com_str, symbol) in pairs(delimiter_commands) - delim_expr = TeXExpr(:delimiter, symbol) - if !haskey(symbol_to_canonical, symbol) - symbol_to_canonical[symbol] = delim_expr - end - command_definitions[com_str] = (delim_expr, 0) -end - ## ## Default behavior ## diff --git a/src/parser/tokenizer.jl b/src/parser/tokenizer.jl index 8029e1a..9619622 100644 --- a/src/parser/tokenizer.jl +++ b/src/parser/tokenizer.jl @@ -1,4 +1,3 @@ -const token_command = re"\\[a-zA-Z]+" | re"\\." tex_tokens = [ :char => re".", :primes => re"'+", @@ -6,9 +5,9 @@ tex_tokens = [ :underscore => re"_", :rcurly => re"}", :lcurly => re"{", - :command => token_command, - :right => re"\\right." | re"\\right" * token_command, - :left => re"\\left." | re"\\left" * token_command, + :command => re"\\[a-zA-Z]+" | re"\\.", + :right => re"\\right.", + :left => re"\\left.", :newline => (re"\\" * re"\\") | re"\\n", :dollar => re"$" ] From c34ed554456f7e22322d47981786e4f9daacceea Mon Sep 17 00:00:00 2001 From: manuelbb-upb Date: Tue, 21 Oct 2025 14:40:34 +0200 Subject: [PATCH 14/14] refactor + revert delimited y positioning --- src/MathTeXEngine.jl | 1 + src/engine/fonts.jl | 35 ++++++++++++++++- src/engine/layout.jl | 89 ++++++++++++++++---------------------------- test/layout.jl | 10 +++-- 4 files changed, 73 insertions(+), 62 deletions(-) diff --git a/src/MathTeXEngine.jl b/src/MathTeXEngine.jl index a9c8e16..ffb03f3 100644 --- a/src/MathTeXEngine.jl +++ b/src/MathTeXEngine.jl @@ -18,6 +18,7 @@ import FreeTypeAbstraction: hadvance, inkheight, inkwidth, height_insensitive_boundingbox, leftinkbound, rightinkbound, topinkbound, bottominkbound + export TeXToken, tokenize export TeXExpr, texparse, TeXParseError, manual_texexpr export TeXElement, TeXChar, VLine, HLine, generate_tex_elements diff --git a/src/engine/fonts.jl b/src/engine/fonts.jl index 6e674ed..442fa1f 100644 --- a/src/engine/fonts.jl +++ b/src/engine/fonts.jl @@ -74,6 +74,15 @@ A set of font for LaTeX rendering. (for example necessary to access the big integral glyph). - `slant_angle` the angle by which the italic fonts are slanted, in degree. - `thickness` the thickness of the lines associated to the font. + - `text_italics_correction` is a reference to a Boolean flag to enable or disable + an italics correction heuristic for text.\n + Defaults to `Ref(false)`. + - `math_italics_correction` is a reference to a Boolean flag to enable or disable + an italics correction heuristic in math expressions.\n + Defaults to `Ref(true)`. + - `italics_correction_up_to_it_spacing` is a reference to a space in font units + inserted when switching from upright to italic glyphs.\n + Defaults to `Ref(0f0)`. """ struct FontFamily fonts::Dict{Symbol, String} @@ -82,6 +91,9 @@ struct FontFamily special_chars::Dict{Char, Tuple{String, Int}} slant_angle::Float64 thickness::Float64 + text_italics_correction::Base.RefValue{Bool} + math_italics_correction::Base.RefValue{Bool} + italics_correction_up_to_it_spacing::Base.RefValue{Float32} end function FontFamily(fonts ; @@ -89,7 +101,23 @@ function FontFamily(fonts ; font_modifiers = _default_font_modifiers, special_chars = Dict{Char, Tuple{String, Int}}(), slant_angle = 13, - thickness = 0.0375) + thickness = 0.0375, + text_italics_correction=Ref(false), + math_italics_correction=Ref(true), + italics_correction_up_to_it_spacing=Ref(0f0) +) + + if !(text_italics_correction isa Ref) + text_italics_correction = Ref(text_italics_correction) + end + + if !(math_italics_correction isa Ref) + math_italics_correction = Ref(math_italics_correction) + end + + if !(italics_correction_up_to_it_spacing isa Ref) + italics_correction_up_to_it_spacing = Ref(italics_correction_up_to_it_spacing) + end fonts = merge(_default_fonts, Dict(fonts)) @@ -99,7 +127,10 @@ function FontFamily(fonts ; font_modifiers, special_chars, slant_angle, - thickness + thickness, + text_italics_correction, + math_italics_correction, + italics_correction_up_to_it_spacing ) end diff --git a/src/engine/layout.jl b/src/engine/layout.jl index 34cf113..e782cb3 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -1,9 +1,3 @@ -## flag to enable italic correction heuristic -const ITALIC_CORRECTION = Ref(true) -## percentage of x-height of font (like LetterSpacing in fontspec) -## for bearing before slanted glyph when switching from upright to slanted letters -const ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT = Ref(1.25f0) - """ Return the y value needed for the element to be vertically centered in the middle of the xheight. @@ -33,8 +27,12 @@ function tex_layout(expr, state) args = [expr.args...] shrink = 0.6 - inside_math = state.tex_mode == :inline_math - global_xheight = xheight(font_family) + italics_correction = if state.tex_mode == :inline_math + font_family.math_italics_correction[] + else + font_family.text_italics_correction[] + end + up_to_it_space = font_family.italics_correction_up_to_it_spacing[] try if isleaf(expr) # :char, :delimiter, :digit, :punctuation, :symbol @@ -91,7 +89,7 @@ function tex_layout(expr, state) ), ( super_x, super_y)], [1, shrink, super_shrink], - is_slanted(core) + is_slanted(core) || is_slanted(super) ) elseif head == :delimited elements = tex_layout.(args, state) @@ -101,30 +99,18 @@ function tex_layout(expr, state) left_scale = max(1, height / inkheight(left)) right_scale = max(1, height / inkheight(right)) scales = [left_scale, 1, right_scale] - - #= + dxs = hadvance.(elements) .* scales xs = [0, cumsum(dxs[1:end-1])...] - =# - ## compute y-positions for delimiters + # TODO Height calculation for the parenthesis looks wrong # TODO Check what the algorithm should be there - bot_content = bottominkbound(content) - h_content = inkheight(content) - h_left = inkheight(left) * left_scale - delta_left = max(0, (h_left - h_content)) / 2 - y_left = bot_content - delta_left - (bottominkbound(left) * left_scale) - - h_right = inkheight(right) * right_scale - delta_right = max(0, (h_right - h_content)) / 2 - y_right = bot_content - delta_right - (bottominkbound(right) * right_scale) - _elements = [ - Group([left,], Point2f[(0, y_left)], [left_scale], is_slanted(left)) - Group([content,], Point2f[(0, 0)], [1,], is_slanted(content)) - Group([right,], Point2f[(0, y_right)], [right_scale], is_slanted(right)) + Group([left,], Point2f[(xs[1], -bottominkbound(left) + bottominkbound(content))], [left_scale], is_slanted(left)) + Group([content,], Point2f[(xs[2], 0)], [1,], is_slanted(content)) + Group([right,], Point2f[(xs[3], -bottominkbound(right) + bottominkbound(content))], [right_scale], is_slanted(right)) ] - return horizontal_layout(_elements; inside_math, global_xheight) + return horizontal_layout(_elements; italics_correction, up_to_it_space) elseif head == :font modifier, content = args return tex_layout(content, add_font_modifier(state, modifier)) @@ -154,12 +140,12 @@ function tex_layout(expr, state) return Group( [line, numerator, denominator], Point2f[(0, y0), (x1, ytop), (x2, ybottom)]; - slanted = is_slanted(numerator) | is_slanted(denominator) + slanted = is_slanted(numerator) || is_slanted(denominator) ) elseif head == :function name = args[1] elements = TeXChar.(collect(name), state, Ref(:function)) - return horizontal_layout(elements; inside_math, global_xheight) + return horizontal_layout(elements; italics_correction) elseif head == :glyph font_id, glyph_id = argument_as_string.(args) font_id = Symbol(font_id) @@ -172,7 +158,13 @@ function tex_layout(expr, state) if isempty(elements) return Space(0.0) end - return horizontal_layout(elements; inside_math = (mode == :inline_math), global_xheight) + italics_correction = if mode == :inline_math + font_family.math_italics_correction[] + else + font_family.text_italics_correction[] + end + + return horizontal_layout(elements; italics_correction, up_to_it_space) elseif head == :integral pad = 0.1 int, sub, super = tex_layout.(args, state) @@ -191,7 +183,7 @@ function tex_layout(expr, state) ) ], [1, shrink, shrink], - is_slanted(int) + is_slanted(int) # TODO consider upper limit as well? ) elseif head == :lines length(args) == 1 && return tex_layout(only(args), state) @@ -222,12 +214,12 @@ function tex_layout(expr, state) ) elseif head == :primes primes = [TeXExpr(:char, ''') for _ in 1:only(args)] - return horizontal_layout(tex_layout.(primes, state); inside_math, global_xheight) + return horizontal_layout(tex_layout.(primes, state); italics_correction, up_to_it_space) elseif head == :space return Space(args[1]) elseif head == :spaced sym = tex_layout(args[1], state) - return horizontal_layout([Space(0.2), sym, Space(0.2)]; inside_math, global_xheight) + return horizontal_layout([Space(0.2), sym, Space(0.2)]; italics_correction, up_to_it_space) elseif head == :sqrt content = tex_layout(args[1], state) h = inkheight(content) @@ -311,12 +303,14 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ -function horizontal_layout(elements; kwargs...) - elements = _italic_correction(elements; kwargs...) +function horizontal_layout(elements; italics_correction=false, kwargs...) + if italics_correction + elements = _italics_correction(elements; kwargs...) + end dxs = hadvance.(elements) xs = [0, cumsum(dxs[1:end-1])...] - return Group(elements, Point2f.(xs, 0); slanted = any(is_slanted, elements)) + return Group(elements, Point2f.(xs, 0); slanted = is_slanted(last(elements))) end function layout_text(string, font_family) @@ -326,32 +320,15 @@ function layout_text(string, font_family) return horizontal_layout(elements) end -function _italic_correction( +function _italics_correction( elements; - inside_math::Bool=false, - global_xheight::Number=xheight(get_texfont_family()), + up_to_it_space=0, scales=1 ) - global ITALIC_CORRECTION - return _italic_correction( - Val(inside_math & ITALIC_CORRECTION[]), elements; global_xheight, scales) -end - -function _italic_correction(::Val{false}, elements; kwargs...) - return elements -end -function _italic_correction( - ::Val{true}, elements; - global_xheight, scales -) - global ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT @assert isa(scales, Number) || length(elements) == length(scales) elems = vcat(Space(0), elements) j = 1 - - fac_up_to_it = (ITALIC_CORRECTION_LETTER_SPACING_UP_TO_IT[] / 100) - b_up_to_it = fac_up_to_it == 0 ? 0 : fac_up_to_it * global_xheight for (i, elem) in enumerate(elements) i == 1 && continue @@ -399,7 +376,7 @@ function _italic_correction( elseif d >= 0 ## but also remove/reduce positive bearing or reduce it to some ## minimum spacing value - offset = b_up_to_it * scale_elem + delta + offset = up_to_it_space * scale_elem + delta end end if offset != 0 diff --git a/test/layout.jl b/test/layout.jl index 7cfa48b..6c0117f 100644 --- a/test/layout.jl +++ b/test/layout.jl @@ -87,11 +87,13 @@ end end @testset "Italic Correction" begin - MathTeXEngine.ITALIC_CORRECTION[] = false - els1 = generate_tex_elements(L"(f)x") + ffam = FontFamily() + + ffam.math_italics_correction[] = false + els1 = generate_tex_elements(L"(f)x", ffam) - MathTeXEngine.ITALIC_CORRECTION[] = true - els2 = generate_tex_elements(L"(f)x") + ffam.math_italics_correction[] = true + els2 = generate_tex_elements(L"(f)x", ffam) xpos = ((elem, pos),) -> pos[1]