Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glyph indices #72

Merged
merged 9 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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