Skip to content

Commit

Permalink
Glyph indices (#72)
Browse files Browse the repository at this point in the history
* use glyph indices instead of Chars in a bunch of places

* make more Char code glyph generic

* throw more descriptive loading errors

* remove old string boundingbox code

* remove old layout-related tests

* remove iter_or_array and add (broken) tests

* add more tests

* add more tests

* bump minor version
  • Loading branch information
jkrumbiegel authored Jul 18, 2022
1 parent d0598f6 commit b7d6d65
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
62 changes: 7 additions & 55 deletions src/layout.jl
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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))
Expand Down
27 changes: 14 additions & 13 deletions src/rendering.jl
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
30 changes: 18 additions & 12 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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))
Expand Down
57 changes: 34 additions & 23 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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),
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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

2 comments on commit b7d6d65

@jkrumbiegel
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/64481

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.10.0 -m "<description of version>" b7d6d65832e46047225bc02579ba7d124b38ebe2
git push origin v0.10.0

Please sign in to comment.