diff --git a/src/engine/fonts.jl b/src/engine/fonts.jl index 677bfc2..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 @@ -257,7 +288,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..e782cb3 100644 --- a/src/engine/layout.jl +++ b/src/engine/layout.jl @@ -26,6 +26,13 @@ function tex_layout(expr, state) head = expr.head args = [expr.args...] shrink = 0.6 + + 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 @@ -54,7 +61,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) @@ -80,31 +88,29 @@ function tex_layout(expr, state) -0.2 ), ( super_x, super_y)], - [1, shrink, super_shrink] + [1, shrink, super_shrink], + is_slanted(core) || is_slanted(super) ) 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] - + dxs = hadvance.(elements) .* scales xs = [0, cumsum(dxs[1:end-1])...] # TODO Height calculation for the parenthesis looks wrong # TODO Check what the algorithm should be there - # Center the delimiters in the middle of the bot and top baselines ? - return Group(elements, - Point2f[ - (xs[1], -bottominkbound(left) + bottominkbound(content)), - (xs[2], 0), - (xs[3], -bottominkbound(right) + bottominkbound(content)) - ], - scales - ) + _elements = [ + 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; italics_correction, up_to_it_space) elseif head == :font modifier, content = args return tex_layout(content, add_font_modifier(state, modifier)) @@ -133,12 +139,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) + return horizontal_layout(elements; italics_correction) elseif head == :glyph font_id, glyph_id = argument_as_string.(args) font_id = Symbol(font_id) @@ -151,7 +158,13 @@ function tex_layout(expr, state) if isempty(elements) return Space(0.0) end - return horizontal_layout(elements) + 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) @@ -169,7 +182,8 @@ function tex_layout(expr, state) topinkbound(int) + pad ) ], - [1, shrink, shrink] + [1, shrink, shrink], + is_slanted(int) # TODO consider upper limit as well? ) elseif head == :lines length(args) == 1 && return tex_layout(only(args), state) @@ -195,16 +209,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)) + 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)]) + 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) @@ -262,7 +277,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) @@ -287,11 +303,14 @@ tex_layout(::Nothing, state) = Space(0) Layout the elements horizontally, like normal text. """ -function horizontal_layout(elements) +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)) + return Group(elements, Point2f.(xs, 0); slanted = is_slanted(last(elements))) end function layout_text(string, font_family) @@ -301,6 +320,79 @@ function layout_text(string, font_family) return horizontal_layout(elements) end +function _italics_correction( + elements; + up_to_it_space=0, + scales=1 +) + + @assert isa(scales, Number) || length(elements) == length(scales) + elems = vcat(Space(0), elements) + j = 1 + + 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) + + if is_slanted(prev) != is_slanted(elem) + 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 = up_to_it_space * scale_elem + delta + end + end + if offset != 0 + insert!(elems, i+j, Space(offset)) + j+=1 + end + end + end + popfirst!(elems) + return elems +end + +_get_scale(scales::Number, i)=scales +_get_scale(scales, i)=scales[i] + + """ unravel(element::TeXElement, pos, scale) 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..12339aa 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 @@ -289,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] @@ -334,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 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) diff --git a/test/layout.jl b/test/layout.jl index 881a8a0..6c0117f 100644 --- a/test/layout.jl +++ b/test/layout.jl @@ -85,6 +85,34 @@ end char, pos, size = first(elems) @test pos[1] == 2 end + + @testset "Italic Correction" begin + ffam = FontFamily() + + ffam.math_italics_correction[] = false + els1 = generate_tex_elements(L"(f)x", ffam) + + ffam.math_italics_correction[] = true + els2 = generate_tex_elements(L"(f)x", ffam) + + 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