Skip to content

Commit b7d6d65

Browse files
authored
Glyph indices (#72)
* 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
1 parent d0598f6 commit b7d6d65

File tree

5 files changed

+74
-104
lines changed

5 files changed

+74
-104
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "FreeTypeAbstraction"
22
uuid = "663a7486-cb36-511b-a19d-713bb74d65c9"
3-
version = "0.9.9"
3+
version = "0.10.0"
44

55
[deps]
66
ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4"

src/layout.jl

+7-55
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,18 @@
1-
iter_or_array(x) = repeated(x)
2-
iter_or_array(x::Repeated) = x
3-
iter_or_array(x::AbstractArray) = x
4-
# We treat staticarrays as scalar
5-
iter_or_array(x::Union{Mat, StaticVector}) = repeated(x)
6-
7-
8-
function metrics_bb(char::Char, font::FTFont, pixel_size)
9-
extent = get_extent(font, char) .* Vec2f(pixel_size)
1+
function metrics_bb(glyph, font::FTFont, pixel_size)
2+
extent = get_extent(font, glyph) .* Vec2f(pixel_size)
103
return boundingbox(extent), extent
114
end
125

13-
function boundingbox(char::Char, font::FTFont, pixel_size)
14-
bb, extent = metrics_bb(char, font, pixel_size)
6+
function boundingbox(glyph, font::FTFont, pixel_size)
7+
bb, extent = metrics_bb(glyph, font, pixel_size)
158
return bb
169
end
1710

18-
function glyph_ink_size(char::Char, font::FTFont, pixel_size)
19-
bb, extent = metrics_bb(char, font, pixel_size)
11+
function glyph_ink_size(glyph, font::FTFont, pixel_size)
12+
bb, extent = metrics_bb(glyph, font, pixel_size)
2013
return widths(bb)
2114
end
2215

23-
"""
24-
iterate_extents(f, line::AbstractString, fonts, scales)
25-
Iterates over the extends of the characters (glyphs) in line!
26-
Newlines will be drawn like any other character.
27-
`fonts` can be a vector of fonts, or a single font.
28-
`scales` can be a single float or a Vec2, or a vector of any of those.
29-
30-
`f` will get called with `(char::Char, glyph_box::Rec2D, glyph_advance::Point2f)`.
31-
32-
`char` is the currently iterated char.
33-
34-
`glyph_box` is the boundingbox of the glyph.
35-
widths(box) will be the size of the bitmap, while minimum(box) is where one starts drawing the glyph.
36-
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.
37-
38-
`glyph_advance` The amount one advances after glyph, before drawing next glyph.
39-
"""
40-
function iterate_extents(f, line::AbstractString, fonts, scales)
41-
iterator = zip(line, iter_or_array(scales), iter_or_array(fonts))
42-
lastpos = 0.0
43-
for (char, scale, font) in iterator
44-
glyph_box, extent = metrics_bb(char, font, scale)
45-
mini = minimum(glyph_box) .+ Vec2f(lastpos, 0.0)
46-
glyph_box = Rect2(mini, widths(glyph_box))
47-
glyph_advance = Point2f(extent.advance)
48-
lastpos += glyph_advance[1]
49-
f(char, glyph_box, glyph_advance)
50-
end
51-
end
52-
53-
function glyph_rects(line::AbstractString, fonts, scales)
54-
rects = Rect2[]
55-
iterate_extents(line, fonts, scales) do char, box, advance
56-
push!(rects, box)
57-
end
58-
return rects
59-
end
60-
61-
function boundingbox(line::AbstractString, fonts, scales)
62-
return reduce(union, glyph_rects(line, fonts, scales))
63-
end
64-
6516
function inkboundingbox(ext::FontExtent)
6617
l = leftinkbound(ext)
6718
r = rightinkbound(ext)
@@ -73,6 +24,7 @@ end
7324
function height_insensitive_boundingbox(ext::FontExtent, font::FTFont)
7425
l = leftinkbound(ext)
7526
r = rightinkbound(ext)
27+
# this is wrong because of pixel size
7628
b = descender(font)
7729
t = ascender(font)
7830
return Rect2f((l, b), (r - l, t - b))

src/rendering.jl

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11

2-
function loadchar(face::FTFont, c::Char)
3-
err = FT_Load_Char(face, c, FT_LOAD_RENDER)
4-
check_error(err, "Could not load char to render.")
2+
function load_glyph(face::FTFont, glyph)
3+
gi = glyph_index(face, glyph)
4+
err = FT_Load_Glyph(face, gi, FT_LOAD_RENDER)
5+
check_error(err, "Could not load glyph $(repr(glyph)) from $(face) to render.")
56
end
67

7-
function loadglyph(face::FTFont, c::Char, pixelsize::Integer)
8+
function loadglyph(face::FTFont, glyph, pixelsize::Integer)
89
set_pixelsize(face, pixelsize)
9-
loadchar(face, c)
10-
glyph = unsafe_load(face.glyph)
11-
@assert glyph.format == FreeType.FT_GLYPH_FORMAT_BITMAP
12-
return glyph
10+
load_glyph(face, glyph)
11+
gl = unsafe_load(face.glyph)
12+
@assert gl.format == FreeType.FT_GLYPH_FORMAT_BITMAP
13+
return gl
1314
end
1415

15-
function renderface(face::FTFont, c::Char, pixelsize::Integer)
16-
glyph = loadglyph(face, c, pixelsize)
17-
return glyphbitmap(glyph.bitmap), FontExtent(glyph.metrics)
16+
function renderface(face::FTFont, glyph, pixelsize::Integer)
17+
gl = loadglyph(face, glyph, pixelsize)
18+
return glyphbitmap(gl.bitmap), FontExtent(gl.metrics)
1819
end
1920

20-
function extents(face::FTFont, c::Char, pixelsize::Integer)
21-
return FontExtent(loadglyph(face, c, pixelsize).metrics)
21+
function extents(face::FTFont, glyph, pixelsize::Integer)
22+
return FontExtent(loadglyph(face, glyph, pixelsize).metrics)
2223
end
2324

2425
function glyphbitmap(bitmap::FreeType.FT_Bitmap)

src/types.jl

+18-12
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ end
126126
mutable struct FTFont
127127
ft_ptr::FreeType.FT_Face
128128
use_cache::Bool
129-
extent_cache::Dict{Char, FontExtent{Float32}}
129+
extent_cache::Dict{UInt64, FontExtent{Float32}}
130130
function FTFont(ft_ptr::FreeType.FT_Face, use_cache::Bool=true)
131-
extent_cache = Dict{Tuple{Int, Char}, FontExtent{Float32}}()
131+
extent_cache = Dict{UInt64, FontExtent{Float32}}()
132132
face = new(ft_ptr, use_cache, extent_cache)
133133
finalizer(safe_free, face)
134134
return face
@@ -173,9 +173,9 @@ function set_pixelsize(face::FTFont, size::Integer)
173173
return size
174174
end
175175

176-
function kerning(c1::Char, c2::Char, face::FTFont)
177-
i1 = FT_Get_Char_Index(face, c1)
178-
i2 = FT_Get_Char_Index(face, c2)
176+
function kerning(glyphspec1, glyphspec2, face::FTFont)
177+
i1 = glyph_index(face, glyphspec1)
178+
i2 = glyph_index(face, glyphspec2)
179179
kerning2d = Ref{FreeType.FT_Vector}()
180180
err = FT_Get_Kerning(face, i1, i2, FreeType.FT_KERNING_DEFAULT, kerning2d)
181181
# 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)
185185
return Vec2f(kerning2d[].x / divisor, kerning2d[].y / divisor)
186186
end
187187

188-
function get_extent(face::FTFont, char::Char)
188+
function get_extent(face::FTFont, glyphspec)
189+
gi = glyph_index(face, glyphspec)
189190
if use_cache(face)
190-
get!(get_cache(face), char) do
191-
return internal_get_extent(face, char)
191+
get!(get_cache(face), gi) do
192+
return internal_get_extent(face, gi)
192193
end
193194
else
194-
return internal_get_extent(face, char)
195+
return internal_get_extent(face, gi)
195196
end
196197
end
197198

198-
function internal_get_extent(face::FTFont, char::Char)
199+
glyph_index(face::FTFont, glyphname::String)::UInt64 = FT_Get_Name_Index(face, glyphname)
200+
glyph_index(face::FTFont, char::Char)::UInt64 = FT_Get_Char_Index(face, char)
201+
glyph_index(face::FTFont, int::Integer) = UInt64(int)
202+
203+
function internal_get_extent(face::FTFont, glyphspec)
204+
gi = glyph_index(face, glyphspec)
199205
#=
200206
Load chars without scaling. This leaves all glyph metrics that can be
201207
retrieved in font units, which can be normalized by dividing with the
@@ -204,8 +210,8 @@ function internal_get_extent(face::FTFont, char::Char)
204210
pixelsize can be silently changed by third parties, such as Cairo.
205211
If that happens, all glyph metrics are incorrect. We avoid this by using the normalized space.
206212
=#
207-
err = FT_Load_Char(face, char, FT_LOAD_NO_SCALE)
208-
check_error(err, "Could not load char to get extent.")
213+
err = FT_Load_Glyph(face, gi, FT_LOAD_NO_SCALE)
214+
check_error(err, "Could not load glyph $(repr(glyphspec)) from $(face) to get extent.")
209215
# This gives us the font metrics in normalized units (0, 1), with negative
210216
# numbers interpreted as an offset
211217
return FontExtent(unsafe_load(face.glyph).metrics, Float32(face.units_per_EM))

test/runtests.jl

+34-23
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
ENV["FREETYPE_ABSTRACTION_FONT_PATH"] = @__DIR__ # coverage
22

33
using FreeTypeAbstraction, Colors, ColorVectorSpace, GeometryBasics
4+
using GeometryBasics: Vec2f
45
import FreeTypeAbstraction as FA
56
using FreeType
67
using Test
78

9+
@testset "init and done" begin
10+
@test_throws ErrorException FA.ft_init()
11+
@test FA.ft_done()
12+
@test_throws ErrorException FA.ft_done()
13+
@test FA.ft_init()
14+
end
15+
816
face = FA.findfont("hack")
917

1018
@testset "basics" begin
@@ -15,10 +23,6 @@ face = FA.findfont("hack")
1523
@test FA.ascender(face) isa Real
1624
@test FA.descender(face) isa Real
1725

18-
bb = FA.boundingbox("asdasd", face, 64)
19-
@test round.(Int, minimum(bb)) == Vec(4, -1)
20-
@test round.(Int, widths(bb)) == Vec2(221, 50)
21-
2226
FA.set_pixelsize(face, 64) # should be the default
2327
img, extent = FA.renderface(face, 'C', 64)
2428
@test size(img) == (30, 49)
@@ -33,6 +37,8 @@ face = FA.findfont("hack")
3337
@test FA.rightinkbound(extent) == 34
3438
@test FA.bottominkbound(extent) == -1
3539
@test FA.topinkbound(extent) == 48
40+
@test FA.inkboundingbox(extent) == HyperRectangle{2, Float32}(Float32[4.0, -1.0], Float32[30.0, 49.0])
41+
@test_broken FA.height_insensitive_boundingbox(extent, face) == HyperRectangle{2, Float32}(Float32[4.0, 64 * -0.23583984], Float32[30.0, 64 * 1.2006836])
3642

3743
a = renderstring!(zeros(UInt8, 20, 100), "helgo", face, 10, 10, 10)
3844

@@ -55,6 +61,12 @@ face = FA.findfont("hack")
5561
@test_logs (:warn, "using tuple for pixelsize is deprecated, please use one integer") renderstring!(zeros(UInt8, 20, 100), "helgo", face, (10, 10), 1, 1)
5662
end
5763

64+
@testset "ways to access glyphs" begin
65+
i = FA.glyph_index(face, 'A')
66+
@test FA.glyph_index(face, i) == i
67+
@test FA.glyph_index(face, "A") == i
68+
end
69+
5870
@testset "alignements" begin
5971
a = renderstring!(
6072
zeros(UInt8, 20, 100),
@@ -217,25 +229,6 @@ end
217229
@test true
218230
end
219231

220-
@testset "layout" begin
221-
extent = FA.extents(face, '', 10)
222-
@test extent == FA.extents(face, '', 10)
223-
FA.inkboundingbox(extent)
224-
FA.height_insensitive_boundingbox(extent, face)
225-
226-
FA.boundingbox('a', face, .5)
227-
FA.glyph_ink_size('a', face, .5)
228-
FA.metrics_bb('a', face, .5)
229-
230-
for (ft, sc) in (
231-
(face, .5),
232-
([face, face], [.5, .5]),
233-
(Iterators.repeated(face), Iterators.repeated(.5))
234-
)
235-
FA.boundingbox("ab", ft, sc)
236-
end
237-
end
238-
239232
# Find fonts
240233
# these fonts should be available on all platforms:
241234

@@ -283,3 +276,21 @@ end
283276
end
284277
@test true
285278
end
279+
280+
@testset "Font extent" begin
281+
f1 = FontExtent(Vec2f(1, 2), Vec2f(3, 4), Vec2f(5, 6), Vec2f(7, 8))
282+
f2 = FA.broadcasted(x -> 2 * x, f1)
283+
@test f2 == FontExtent(Vec2f(2, 4), Vec2f(6, 8), Vec2f(10, 12), Vec2f(14, 16))
284+
f3 = FA.broadcasted(*, f1, Vec2f(2, 3))
285+
@test f3 == FontExtent(Vec2f(2, 4), Vec2f(9, 12), Vec2f(10, 18), Vec2f(14, 24))
286+
end
287+
288+
@testset "Boundingbox" begin
289+
for glyph in ('a', FA.glyph_index(face, 'a'), "a")
290+
bb, extent = FA.metrics_bb(glyph, face, 64)
291+
bb2 = FA.boundingbox(glyph, face, 64)
292+
@test bb == bb2
293+
w = GeometryBasics.widths(bb2)
294+
@test w == FA.glyph_ink_size(glyph, face, 64)
295+
end
296+
end

0 commit comments

Comments
 (0)