diff --git a/Project.toml b/Project.toml index de4e312..62c1f86 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "FreeTypeAbstraction" uuid = "663a7486-cb36-511b-a19d-713bb74d65c9" -version = "0.9.9" +version = "0.10.0" [deps] ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" diff --git a/src/layout.jl b/src/layout.jl index a9667e3..4aad489 100644 --- a/src/layout.jl +++ b/src/layout.jl @@ -1,67 +1,18 @@ -iter_or_array(x) = repeated(x) -iter_or_array(x::Repeated) = x -iter_or_array(x::AbstractArray) = x -# We treat staticarrays as scalar -iter_or_array(x::Union{Mat, StaticVector}) = repeated(x) - - -function metrics_bb(char::Char, font::FTFont, pixel_size) - extent = get_extent(font, char) .* Vec2f(pixel_size) +function metrics_bb(glyph, font::FTFont, pixel_size) + extent = get_extent(font, glyph) .* Vec2f(pixel_size) return boundingbox(extent), extent end -function boundingbox(char::Char, font::FTFont, pixel_size) - bb, extent = metrics_bb(char, font, pixel_size) +function boundingbox(glyph, font::FTFont, pixel_size) + bb, extent = metrics_bb(glyph, font, pixel_size) return bb end -function glyph_ink_size(char::Char, font::FTFont, pixel_size) - bb, extent = metrics_bb(char, font, pixel_size) +function glyph_ink_size(glyph, font::FTFont, pixel_size) + bb, extent = metrics_bb(glyph, font, pixel_size) return widths(bb) end -""" - iterate_extents(f, line::AbstractString, fonts, scales) -Iterates over the extends of the characters (glyphs) in line! -Newlines will be drawn like any other character. -`fonts` can be a vector of fonts, or a single font. -`scales` can be a single float or a Vec2, or a vector of any of those. - -`f` will get called with `(char::Char, glyph_box::Rec2D, glyph_advance::Point2f)`. - -`char` is the currently iterated char. - -`glyph_box` is the boundingbox of the glyph. -widths(box) will be the size of the bitmap, while minimum(box) is where one starts drawing the glyph. -For the minimum at y position, 0 is the where e.g. `m` starts, so `g` will start in the negative, while `^` will start positive. - -`glyph_advance` The amount one advances after glyph, before drawing next glyph. -""" -function iterate_extents(f, line::AbstractString, fonts, scales) - iterator = zip(line, iter_or_array(scales), iter_or_array(fonts)) - lastpos = 0.0 - for (char, scale, font) in iterator - glyph_box, extent = metrics_bb(char, font, scale) - mini = minimum(glyph_box) .+ Vec2f(lastpos, 0.0) - glyph_box = Rect2(mini, widths(glyph_box)) - glyph_advance = Point2f(extent.advance) - lastpos += glyph_advance[1] - f(char, glyph_box, glyph_advance) - end -end - -function glyph_rects(line::AbstractString, fonts, scales) - rects = Rect2[] - iterate_extents(line, fonts, scales) do char, box, advance - push!(rects, box) - end - return rects -end - -function boundingbox(line::AbstractString, fonts, scales) - return reduce(union, glyph_rects(line, fonts, scales)) -end - function inkboundingbox(ext::FontExtent) l = leftinkbound(ext) r = rightinkbound(ext) @@ -73,6 +24,7 @@ end function height_insensitive_boundingbox(ext::FontExtent, font::FTFont) l = leftinkbound(ext) r = rightinkbound(ext) + # this is wrong because of pixel size b = descender(font) t = ascender(font) return Rect2f((l, b), (r - l, t - b)) diff --git a/src/rendering.jl b/src/rendering.jl index 0f13756..4d1855c 100644 --- a/src/rendering.jl +++ b/src/rendering.jl @@ -1,24 +1,25 @@ -function loadchar(face::FTFont, c::Char) - err = FT_Load_Char(face, c, FT_LOAD_RENDER) - check_error(err, "Could not load char to render.") +function load_glyph(face::FTFont, glyph) + gi = glyph_index(face, glyph) + err = FT_Load_Glyph(face, gi, FT_LOAD_RENDER) + check_error(err, "Could not load glyph $(repr(glyph)) from $(face) to render.") end -function loadglyph(face::FTFont, c::Char, pixelsize::Integer) +function loadglyph(face::FTFont, glyph, pixelsize::Integer) set_pixelsize(face, pixelsize) - loadchar(face, c) - glyph = unsafe_load(face.glyph) - @assert glyph.format == FreeType.FT_GLYPH_FORMAT_BITMAP - return glyph + load_glyph(face, glyph) + gl = unsafe_load(face.glyph) + @assert gl.format == FreeType.FT_GLYPH_FORMAT_BITMAP + return gl end -function renderface(face::FTFont, c::Char, pixelsize::Integer) - glyph = loadglyph(face, c, pixelsize) - return glyphbitmap(glyph.bitmap), FontExtent(glyph.metrics) +function renderface(face::FTFont, glyph, pixelsize::Integer) + gl = loadglyph(face, glyph, pixelsize) + return glyphbitmap(gl.bitmap), FontExtent(gl.metrics) end -function extents(face::FTFont, c::Char, pixelsize::Integer) - return FontExtent(loadglyph(face, c, pixelsize).metrics) +function extents(face::FTFont, glyph, pixelsize::Integer) + return FontExtent(loadglyph(face, glyph, pixelsize).metrics) end function glyphbitmap(bitmap::FreeType.FT_Bitmap) diff --git a/src/types.jl b/src/types.jl index 27a4947..36d094b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -126,9 +126,9 @@ end mutable struct FTFont ft_ptr::FreeType.FT_Face use_cache::Bool - extent_cache::Dict{Char, FontExtent{Float32}} + extent_cache::Dict{UInt64, FontExtent{Float32}} function FTFont(ft_ptr::FreeType.FT_Face, use_cache::Bool=true) - extent_cache = Dict{Tuple{Int, Char}, FontExtent{Float32}}() + extent_cache = Dict{UInt64, FontExtent{Float32}}() face = new(ft_ptr, use_cache, extent_cache) finalizer(safe_free, face) return face @@ -173,9 +173,9 @@ function set_pixelsize(face::FTFont, size::Integer) return size end -function kerning(c1::Char, c2::Char, face::FTFont) - i1 = FT_Get_Char_Index(face, c1) - i2 = FT_Get_Char_Index(face, c2) +function kerning(glyphspec1, glyphspec2, face::FTFont) + i1 = glyph_index(face, glyphspec1) + i2 = glyph_index(face, glyphspec2) kerning2d = Ref{FreeType.FT_Vector}() err = FT_Get_Kerning(face, i1, i2, FreeType.FT_KERNING_DEFAULT, kerning2d) # Can error if font has no kerning! Since that's somewhat expected, we just return 0 @@ -185,17 +185,23 @@ function kerning(c1::Char, c2::Char, face::FTFont) return Vec2f(kerning2d[].x / divisor, kerning2d[].y / divisor) end -function get_extent(face::FTFont, char::Char) +function get_extent(face::FTFont, glyphspec) + gi = glyph_index(face, glyphspec) if use_cache(face) - get!(get_cache(face), char) do - return internal_get_extent(face, char) + get!(get_cache(face), gi) do + return internal_get_extent(face, gi) end else - return internal_get_extent(face, char) + return internal_get_extent(face, gi) end end -function internal_get_extent(face::FTFont, char::Char) +glyph_index(face::FTFont, glyphname::String)::UInt64 = FT_Get_Name_Index(face, glyphname) +glyph_index(face::FTFont, char::Char)::UInt64 = FT_Get_Char_Index(face, char) +glyph_index(face::FTFont, int::Integer) = UInt64(int) + +function internal_get_extent(face::FTFont, glyphspec) + gi = glyph_index(face, glyphspec) #= Load chars without scaling. This leaves all glyph metrics that can be retrieved in font units, which can be normalized by dividing with the @@ -204,8 +210,8 @@ function internal_get_extent(face::FTFont, char::Char) pixelsize can be silently changed by third parties, such as Cairo. If that happens, all glyph metrics are incorrect. We avoid this by using the normalized space. =# - err = FT_Load_Char(face, char, FT_LOAD_NO_SCALE) - check_error(err, "Could not load char to get extent.") + err = FT_Load_Glyph(face, gi, FT_LOAD_NO_SCALE) + check_error(err, "Could not load glyph $(repr(glyphspec)) from $(face) to get extent.") # This gives us the font metrics in normalized units (0, 1), with negative # numbers interpreted as an offset return FontExtent(unsafe_load(face.glyph).metrics, Float32(face.units_per_EM)) diff --git a/test/runtests.jl b/test/runtests.jl index fa856c5..8ce3bde 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,18 @@ ENV["FREETYPE_ABSTRACTION_FONT_PATH"] = @__DIR__ # coverage using FreeTypeAbstraction, Colors, ColorVectorSpace, GeometryBasics +using GeometryBasics: Vec2f import FreeTypeAbstraction as FA using FreeType using Test +@testset "init and done" begin + @test_throws ErrorException FA.ft_init() + @test FA.ft_done() + @test_throws ErrorException FA.ft_done() + @test FA.ft_init() +end + face = FA.findfont("hack") @testset "basics" begin @@ -15,10 +23,6 @@ face = FA.findfont("hack") @test FA.ascender(face) isa Real @test FA.descender(face) isa Real - bb = FA.boundingbox("asdasd", face, 64) - @test round.(Int, minimum(bb)) == Vec(4, -1) - @test round.(Int, widths(bb)) == Vec2(221, 50) - FA.set_pixelsize(face, 64) # should be the default img, extent = FA.renderface(face, 'C', 64) @test size(img) == (30, 49) @@ -33,6 +37,8 @@ face = FA.findfont("hack") @test FA.rightinkbound(extent) == 34 @test FA.bottominkbound(extent) == -1 @test FA.topinkbound(extent) == 48 + @test FA.inkboundingbox(extent) == HyperRectangle{2, Float32}(Float32[4.0, -1.0], Float32[30.0, 49.0]) + @test_broken FA.height_insensitive_boundingbox(extent, face) == HyperRectangle{2, Float32}(Float32[4.0, 64 * -0.23583984], Float32[30.0, 64 * 1.2006836]) a = renderstring!(zeros(UInt8, 20, 100), "helgo", face, 10, 10, 10) @@ -55,6 +61,12 @@ face = FA.findfont("hack") @test_logs (:warn, "using tuple for pixelsize is deprecated, please use one integer") renderstring!(zeros(UInt8, 20, 100), "helgo", face, (10, 10), 1, 1) end +@testset "ways to access glyphs" begin + i = FA.glyph_index(face, 'A') + @test FA.glyph_index(face, i) == i + @test FA.glyph_index(face, "A") == i +end + @testset "alignements" begin a = renderstring!( zeros(UInt8, 20, 100), @@ -217,25 +229,6 @@ end @test true end -@testset "layout" begin - extent = FA.extents(face, '█', 10) - @test extent == FA.extents(face, '█', 10) - FA.inkboundingbox(extent) - FA.height_insensitive_boundingbox(extent, face) - - FA.boundingbox('a', face, .5) - FA.glyph_ink_size('a', face, .5) - FA.metrics_bb('a', face, .5) - - for (ft, sc) in ( - (face, .5), - ([face, face], [.5, .5]), - (Iterators.repeated(face), Iterators.repeated(.5)) - ) - FA.boundingbox("ab", ft, sc) - end -end - # Find fonts # these fonts should be available on all platforms: @@ -283,3 +276,21 @@ end end @test true end + +@testset "Font extent" begin + f1 = FontExtent(Vec2f(1, 2), Vec2f(3, 4), Vec2f(5, 6), Vec2f(7, 8)) + f2 = FA.broadcasted(x -> 2 * x, f1) + @test f2 == FontExtent(Vec2f(2, 4), Vec2f(6, 8), Vec2f(10, 12), Vec2f(14, 16)) + f3 = FA.broadcasted(*, f1, Vec2f(2, 3)) + @test f3 == FontExtent(Vec2f(2, 4), Vec2f(9, 12), Vec2f(10, 18), Vec2f(14, 24)) +end + +@testset "Boundingbox" begin + for glyph in ('a', FA.glyph_index(face, 'a'), "a") + bb, extent = FA.metrics_bb(glyph, face, 64) + bb2 = FA.boundingbox(glyph, face, 64) + @test bb == bb2 + w = GeometryBasics.widths(bb2) + @test w == FA.glyph_ink_size(glyph, face, 64) + end +end