diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 577c8cf..e0eba12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: version: - - '1.6' + - '1.9' os: - ubuntu-latest - macOS-latest diff --git a/.gitignore b/.gitignore index c8b2554..f39b77b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ deps/deps.jl deps/usr* build.log + +Manifest.toml + +test/output/* \ No newline at end of file diff --git a/Project.toml b/Project.toml index ec9036b..b1b0246 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "FreeType" uuid = "b38be410-82b0-50bf-ab77-7b57e271db43" -version = "4.1.1" +version = "4.1.2" [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" @@ -8,12 +8,18 @@ FreeType2_jll = "d7e528f0-a631-5988-bf34-fe36492bcfd7" [compat] CEnum = "0.2,0.3,0.4,0.5" -FreeType2_jll = "2.10" +FreeType2_jll = "2.13.4" julia = "1.3" +Cairo = "1.1" +Images = "0.25, 0.26" +Downloads = "1" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" [targets] -test = ["Test", "Pkg"] +test = ["Test", "Pkg", "Images", "Cairo", "Downloads"] diff --git a/src/FreeType.jl b/src/FreeType.jl index 46b48c0..2821542 100644 --- a/src/FreeType.jl +++ b/src/FreeType.jl @@ -54,7 +54,7 @@ end const FT_Memory = Ptr{FT_MemoryRec_} struct FT_StreamDesc_ - data::NTuple{8, UInt8} + data::NTuple{8,UInt8} end function Base.getproperty(x::Ptr{FT_StreamDesc_}, f::Symbol) @@ -907,6 +907,258 @@ function FT_Get_Color_Glyph_Layer(face, base_glyph, aglyph_index, acolor_index, ccall((:FT_Get_Color_Glyph_Layer, libfreetype), FT_Bool, (FT_Face, FT_UInt, Ptr{FT_UInt}, Ptr{FT_UInt}, Ptr{FT_LayerIterator}), face, base_glyph, aglyph_index, acolor_index, iterator) end +struct FT_Color_ + blue::FT_Byte + green::FT_Byte + red::FT_Byte + alpha::FT_Byte +end + +const FT_Color = FT_Color_ + +const FT_PALETTE_FOR_LIGHT_BACKGROUND = 0x01 +const FT_PALETTE_FOR_DARK_BACKGROUND = 0x02 + +struct FT_Palette_Data_ + num_palettes::FT_UShort + palette_name_ids::Ptr{FT_UShort} + palette_flags::Ptr{FT_UShort} + num_palette_entries::FT_UShort + palette_entry_name_ids::Ptr{FT_UShort} +end + +const FT_Palette_Data = FT_Palette_Data_ + +function FT_Palette_Data_Get(face, apalette) + ccall((:FT_Palette_Data_Get, libfreetype), FT_Error, (FT_Face, Ptr{FT_Palette_Data}), face, apalette) +end + +function FT_Palette_Select(face, palette_index, apalette) + ccall((:FT_Palette_Select, libfreetype), FT_Error, (FT_Face, FT_UShort, Ptr{Ptr{FT_Color}}), face, palette_index, apalette) +end + +# COLRv1 support (FreeType 2.11+) +struct FT_OpaquePaint_ + p::Ptr{FT_Byte} + insert_root_transform::FT_Bool +end + +const FT_OpaquePaint = FT_OpaquePaint_ + +@cenum FT_PaintFormat_::UInt32 begin + FT_COLR_PAINTFORMAT_COLR_LAYERS = 1 + FT_COLR_PAINTFORMAT_SOLID = 2 + FT_COLR_PAINTFORMAT_LINEAR_GRADIENT = 4 + FT_COLR_PAINTFORMAT_RADIAL_GRADIENT = 6 + FT_COLR_PAINTFORMAT_SWEEP_GRADIENT = 8 + FT_COLR_PAINTFORMAT_GLYPH = 10 + FT_COLR_PAINTFORMAT_COLR_GLYPH = 11 + FT_COLR_PAINTFORMAT_TRANSFORM = 12 + FT_COLR_PAINTFORMAT_TRANSLATE = 14 + FT_COLR_PAINTFORMAT_SCALE = 16 + FT_COLR_PAINTFORMAT_ROTATE = 24 + FT_COLR_PAINTFORMAT_SKEW = 28 + FT_COLR_PAINTFORMAT_COMPOSITE = 32 + FT_COLR_PAINT_FORMAT_MAX = 33 + FT_COLR_PAINTFORMAT_UNSUPPORTED = 255 +end + +const FT_PaintFormat = FT_PaintFormat_ + +struct FT_ColorStop_ + stop_offset::FT_Fixed + color::FT_Color +end + +const FT_ColorStop = FT_ColorStop_ + +struct FT_ColorStopIterator_ + num_color_stops::FT_UInt + current_color_stop::FT_UInt + p::Ptr{FT_Byte} + read_variable::FT_Bool +end + +const FT_ColorStopIterator = FT_ColorStopIterator_ + +struct FT_ColorIndex_ + palette_index::FT_UInt16 + alpha::FT_F2Dot14 +end + +const FT_ColorIndex = FT_ColorIndex_ + +@cenum FT_PaintExtend_::UInt32 begin + FT_COLR_PAINT_EXTEND_PAD = 0 + FT_COLR_PAINT_EXTEND_REPEAT = 1 + FT_COLR_PAINT_EXTEND_REFLECT = 2 +end + +const FT_PaintExtend = FT_PaintExtend_ + +struct FT_ColorLine_ + extend::FT_PaintExtend + color_stop_iterator::FT_ColorStopIterator +end + +const FT_ColorLine = FT_ColorLine_ + +struct FT_Affine23_ + xx::FT_Fixed + xy::FT_Fixed + dx::FT_Fixed + yx::FT_Fixed + yy::FT_Fixed + dy::FT_Fixed +end + +const FT_Affine23 = FT_Affine23_ + +struct FT_PaintColrLayers_ + layer_iterator::FT_LayerIterator +end + +struct FT_PaintSolid_ + color::FT_ColorIndex +end + +struct FT_PaintLinearGradient_ + colorline::FT_ColorLine + p0::FT_Vector + p1::FT_Vector + p2::FT_Vector +end + +struct FT_PaintRadialGradient_ + colorline::FT_ColorLine + c0::FT_Vector + r0::FT_Pos + c1::FT_Vector + r1::FT_Pos +end + +struct FT_PaintSweepGradient_ + colorline::FT_ColorLine + center::FT_Vector + start_angle::FT_Fixed + end_angle::FT_Fixed +end + +struct FT_PaintGlyph_ + paint::FT_OpaquePaint + glyphID::FT_UInt +end + +struct FT_PaintColrGlyph_ + glyphID::FT_UInt +end + +struct FT_PaintTransform_ + paint::FT_OpaquePaint + affine::FT_Affine23 +end + +struct FT_PaintTranslate_ + paint::FT_OpaquePaint + dx::FT_Fixed + dy::FT_Fixed +end + +struct FT_PaintScale_ + paint::FT_OpaquePaint + scale_x::FT_Fixed + scale_y::FT_Fixed + center_x::FT_Fixed + center_y::FT_Fixed +end + +struct FT_PaintRotate_ + paint::FT_OpaquePaint + angle::FT_Fixed + center_x::FT_Fixed + center_y::FT_Fixed +end + +struct FT_PaintSkew_ + paint::FT_OpaquePaint + x_skew_angle::FT_Fixed + y_skew_angle::FT_Fixed + center_x::FT_Fixed + center_y::FT_Fixed +end + +@cenum FT_Composite_Mode_::UInt32 begin + FT_COLR_COMPOSITE_CLEAR = 0 + FT_COLR_COMPOSITE_SRC = 1 + FT_COLR_COMPOSITE_DEST = 2 + FT_COLR_COMPOSITE_SRC_OVER = 3 + FT_COLR_COMPOSITE_DEST_OVER = 4 + FT_COLR_COMPOSITE_SRC_IN = 5 + FT_COLR_COMPOSITE_DEST_IN = 6 + FT_COLR_COMPOSITE_SRC_OUT = 7 + FT_COLR_COMPOSITE_DEST_OUT = 8 + FT_COLR_COMPOSITE_SRC_ATOP = 9 + FT_COLR_COMPOSITE_DEST_ATOP = 10 + FT_COLR_COMPOSITE_XOR = 11 + FT_COLR_COMPOSITE_PLUS = 12 + FT_COLR_COMPOSITE_SCREEN = 13 + FT_COLR_COMPOSITE_OVERLAY = 14 + FT_COLR_COMPOSITE_DARKEN = 15 + FT_COLR_COMPOSITE_LIGHTEN = 16 + FT_COLR_COMPOSITE_COLOR_DODGE = 17 + FT_COLR_COMPOSITE_COLOR_BURN = 18 + FT_COLR_COMPOSITE_HARD_LIGHT = 19 + FT_COLR_COMPOSITE_SOFT_LIGHT = 20 + FT_COLR_COMPOSITE_DIFFERENCE = 21 + FT_COLR_COMPOSITE_EXCLUSION = 22 + FT_COLR_COMPOSITE_MULTIPLY = 23 + FT_COLR_COMPOSITE_HSL_HUE = 24 + FT_COLR_COMPOSITE_HSL_SATURATION = 25 + FT_COLR_COMPOSITE_HSL_COLOR = 26 + FT_COLR_COMPOSITE_HSL_LUMINOSITY = 27 + FT_COLR_COMPOSITE_MAX = 28 +end + +const FT_Composite_Mode = FT_Composite_Mode_ + +struct FT_PaintComposite_ + source_paint::FT_OpaquePaint + composite_mode::FT_Composite_Mode + backdrop_paint::FT_OpaquePaint +end + +# Union type for paint data - we allocate space for the largest variant +struct FT_COLR_Paint_ + format::FT_PaintFormat + u::NTuple{96,UInt8} # Space for union (largest is FT_PaintComposite_ with 3 fields) +end + +const FT_COLR_Paint = FT_COLR_Paint_ + +@cenum FT_Color_Root_Transform_::UInt32 begin + FT_COLOR_INCLUDE_ROOT_TRANSFORM = 0 + FT_COLOR_NO_ROOT_TRANSFORM = 1 + FT_COLOR_ROOT_TRANSFORM_MAX = 2 +end + +const FT_Color_Root_Transform = FT_Color_Root_Transform_ + +function FT_Get_Color_Glyph_Paint(face, base_glyph, root_transform, paint) + ccall((:FT_Get_Color_Glyph_Paint, libfreetype), FT_Bool, (FT_Face, FT_UInt, FT_Color_Root_Transform, Ptr{FT_OpaquePaint}), face, base_glyph, root_transform, paint) +end + +function FT_Get_Paint_Layers(face, iterator, paint) + ccall((:FT_Get_Paint_Layers, libfreetype), FT_Bool, (FT_Face, Ptr{FT_LayerIterator}, Ptr{FT_OpaquePaint}), face, iterator, paint) +end + +function FT_Get_Colorline_Stops(face, color_stop, iterator) + ccall((:FT_Get_Colorline_Stops, libfreetype), FT_Bool, (FT_Face, Ptr{FT_ColorStop}, Ptr{FT_ColorStopIterator}), face, color_stop, iterator) +end + +function FT_Get_Paint(face, opaque_paint, paint) + ccall((:FT_Get_Paint, libfreetype), FT_Bool, (FT_Face, FT_OpaquePaint, Ptr{FT_COLR_Paint}), face, opaque_paint, paint) +end + function FT_Get_FSType_Flags(face) ccall((:FT_Get_FSType_Flags, libfreetype), FT_UShort, (FT_Face,), face) end @@ -991,6 +1243,10 @@ function FT_Outline_Get_CBox(outline, acbox) ccall((:FT_Outline_Get_CBox, libfreetype), Cvoid, (Ptr{FT_Outline}, Ptr{FT_BBox}), outline, acbox) end +function FT_Outline_Get_BBox(outline, abbox) + ccall((:FT_Outline_Get_BBox, libfreetype), FT_Error, (Ptr{FT_Outline}, Ptr{FT_BBox}), outline, abbox) +end + function FT_Outline_Translate(outline, xOffset, yOffset) ccall((:FT_Outline_Translate, libfreetype), Cvoid, (Ptr{FT_Outline}, FT_Pos, FT_Pos), outline, xOffset, yOffset) end diff --git a/test/render_colrv1_emoji.jl b/test/render_colrv1_emoji.jl new file mode 100644 index 0000000..e298b19 --- /dev/null +++ b/test/render_colrv1_emoji.jl @@ -0,0 +1,613 @@ +using Test + +@testset "Render COLRv1 Emoji" begin + using Downloads + using FreeType + using Cairo + + function get_transform(paint::FT_COLR_Paint) + # Get pointer to the beginning of the paint struct, then offset to the union field + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintTransform_}(base_ptr + 8) # Skip 4-byte format enum + 4-byte padding + unsafe_load(union_ptr) + end + + function get_translate(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintTranslate_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_scale(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintScale_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_rotate(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintRotate_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_skew(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintSkew_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_solid(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintSolid_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_linear_gradient(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintLinearGradient_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_radial_gradient(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintRadialGradient_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_sweep_gradient(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintSweepGradient_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_glyph(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintGlyph_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_colr_glyph(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintColrGlyph_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_colr_layers(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintColrLayers_}(base_ptr + 8) + unsafe_load(union_ptr) + end + + function get_composite(paint::FT_COLR_Paint) + base_ptr = pointer_from_objref(Ref(paint)) + union_ptr = Ptr{FT_PaintComposite_}(base_ptr + 8) + unsafe_load(union_ptr)s + end + + """ + Render context for COLRv1 emoji rendering. + """ + mutable struct RenderContext + cr::CairoContext + face::FT_Face + palette::Vector{Tuple{Float64,Float64,Float64,Float64}} # RGBA colors + end + + """ + Load the color palette from the font's CPAL table. + """ + function get_color_palette(face::FT_Face, palette_index::Int=0) + # Try to get palette from CPAL table + palette_ptr = Ref{Ptr{FT_Color}}(C_NULL) + err = FT_Palette_Select(face, UInt16(palette_index), palette_ptr) + + if err == 0 && palette_ptr[] != C_NULL + # Get palette data to know how many entries + palette_data = Ref{FT_Palette_Data}() + err2 = FT_Palette_Data_Get(face, palette_data) + + if err2 == 0 + num_entries = palette_data[].num_palette_entries + println(" Loaded CPAL palette $palette_index with $num_entries entries") + + # Convert FT_Color array to Julia tuples (RGBA as floats) + colors = Tuple{Float64,Float64,Float64,Float64}[] + palette_array = unsafe_wrap(Array, palette_ptr[], num_entries) + + for color in palette_array + r = color.red / 255.0 + g = color.green / 255.0 + b = color.blue / 255.0 + a = color.alpha / 255.0 + push!(colors, (r, g, b, a)) + end + + return colors + end + end + end + + """ + Convert FreeType outline to Cairo path. + """ + function outline_to_cairo_path(cr::CairoContext, outline::FT_Outline) + n_contours = outline.n_contours + if n_contours == 0 + return + end + + points = unsafe_wrap(Array, outline.points, outline.n_points) + tags = unsafe_wrap(Array, outline.tags, outline.n_points) + contours = unsafe_wrap(Array, outline.contours, n_contours) + + point_idx = 1 + for contour_idx in 1:n_contours + contour_end = contours[contour_idx] + 1 # Convert to 1-indexed + + # Start new sub-path + if point_idx <= length(points) + start_point = points[point_idx] + move_to(cr, start_point.x, start_point.y) + first_idx = point_idx + point_idx += 1 + + while point_idx <= contour_end && point_idx <= length(points) + pt = points[point_idx] + tag = tags[point_idx] + + if (tag & 0x01) != 0 # On-curve point + line_to(cr, pt.x, pt.y) + point_idx += 1 + else # Off-curve (control point) + if point_idx + 1 <= contour_end && point_idx + 1 <= length(points) + next_pt = points[point_idx+1] + prev_pt = point_idx > first_idx ? points[point_idx-1] : start_point + + # Convert quadratic to cubic Bezier + c1_x = prev_pt.x + 2.0 / 3.0 * (pt.x - prev_pt.x) + c1_y = prev_pt.y + 2.0 / 3.0 * (pt.y - prev_pt.y) + c2_x = next_pt.x + 2.0 / 3.0 * (pt.x - next_pt.x) + c2_y = next_pt.y + 2.0 / 3.0 * (pt.y - next_pt.y) + + curve_to(cr, c1_x, c1_y, c2_x, c2_y, next_pt.x, next_pt.y) + point_idx += 2 + else + point_idx += 1 + end + end + end + + close_path(cr) + end + end + end + + """ + Render functions for different paint types. + """ + function render_solid(ctx::RenderContext, solid::FT_PaintSolid_) + palette_idx = solid.color.palette_index + 1 # Julia is 1-indexed + alpha = solid.color.alpha / 16384.0 # F2Dot14 format + + if palette_idx <= length(ctx.palette) + r, g, b, a = ctx.palette[palette_idx] + set_source_rgba(ctx.cr, r, g, b, a * alpha) + else + set_source_rgba(ctx.cr, 0.0, 0.0, 0.0, alpha) + end + + Cairo.fill(ctx.cr) + end + + function render_glyph(ctx::RenderContext, glyph_paint::FT_PaintGlyph_, depth::Int) + # Load the glyph outline + err = FT_Load_Glyph(ctx.face, glyph_paint.glyphID, FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP) + if err != 0 + return + end + + # Get the glyph slot + glyph = unsafe_load(ctx.face).glyph + outline = unsafe_load(glyph).outline + + # Convert outline to Cairo path + outline_to_cairo_path(ctx.cr, outline) + + # Render the fill paint + render_paint_tree(ctx, glyph_paint.paint, depth + 1) + end + + function render_radial_gradient(ctx::RenderContext, gradient::FT_PaintRadialGradient_) + c0_x = gradient.c0.x / 65536.0 + c0_y = gradient.c0.y / 65536.0 + r0 = gradient.r0 / 65536.0 + c1_x = gradient.c1.x / 65536.0 + c1_y = gradient.c1.y / 65536.0 + r1 = gradient.r1 / 65536.0 + + pattern = pattern_create_radial(c0_x, c0_y, r0, c1_x, c1_y, r1) + + # Add color stops + iter = Ref(gradient.colorline.color_stop_iterator) + color_stop = Ref{FT_ColorStop}() + + while FT_Get_Colorline_Stops(ctx.face, color_stop, iter) != 0 + stop = color_stop[] + offset = stop.stop_offset / 65536.0 + # FT_ColorStop has FT_Color (BGRA bytes) + r = stop.color.red / 255.0 + g = stop.color.green / 255.0 + b = stop.color.blue / 255.0 + a = stop.color.alpha / 255.0 + Cairo.pattern_add_color_stop_rgba(pattern, offset, r, g, b, a) + end + + set_source(ctx.cr, pattern) + Cairo.fill(ctx.cr) + destroy(pattern) + end + + function render_linear_gradient(ctx::RenderContext, gradient::FT_PaintLinearGradient_) + p0_x = gradient.p0.x / 65536.0 + p0_y = gradient.p0.y / 65536.0 + p1_x = gradient.p1.x / 65536.0 + p1_y = gradient.p1.y / 65536.0 + + pattern = pattern_create_linear(p0_x, p0_y, p1_x, p1_y) + + iter = Ref(gradient.colorline.color_stop_iterator) + color_stop = Ref{FT_ColorStop}() + + while FT_Get_Colorline_Stops(ctx.face, color_stop, iter) != 0 + stop = color_stop[] + offset = stop.stop_offset / 65536.0 + # FT_ColorStop has FT_Color (BGRA bytes) + r = stop.color.red / 255.0 + g = stop.color.green / 255.0 + b = stop.color.blue / 255.0 + a = stop.color.alpha / 255.0 + Cairo.pattern_add_color_stop_rgba(pattern, offset, r, g, b, a) + end + + set_source(ctx.cr, pattern) + Cairo.fill(ctx.cr) + destroy(pattern) + end + + """ + Render the paint tree (with actual graphics output). + """ + function render_paint_tree(ctx::RenderContext, opaque_paint::FT_OpaquePaint, depth::Int=0) + if depth > 20 + return + end + + paint_ref = Ref{FT_COLR_Paint}() + result = FT_Get_Paint(ctx.face, opaque_paint, paint_ref) + + if result == 0 + return + end + + paint = paint_ref[] + fmt = paint.format + + Cairo.save(ctx.cr) + + try + if fmt == FT_COLR_PAINTFORMAT_TRANSFORM + transform = get_transform(paint) + # Apply affine transform + xx = transform.affine.xx / 65536.0 + xy = transform.affine.xy / 65536.0 + dx = transform.affine.dx / 65536.0 + yx = transform.affine.yx / 65536.0 + yy = transform.affine.yy / 65536.0 + dy = transform.affine.dy / 65536.0 + # Apply matrix transformation + current = get_matrix(ctx.cr) + new_matrix = CairoMatrix( + xx * current.xx + yx * current.xy, + xx * current.yx + yx * current.yy, + xy * current.xx + yy * current.xy, + xy * current.yx + yy * current.yy, + dx * current.xx + dy * current.xy + current.x0, + dx * current.yx + dy * current.yy + current.y0 + ) + set_matrix(ctx.cr, new_matrix) + render_paint_tree(ctx, transform.paint, depth + 1) + + elseif fmt == FT_COLR_PAINTFORMAT_TRANSLATE + translate = get_translate(paint) + Cairo.translate(ctx.cr, translate.dx / 65536.0, translate.dy / 65536.0) + render_paint_tree(ctx, translate.paint, depth + 1) + + elseif fmt == FT_COLR_PAINTFORMAT_SCALE + scale_paint = get_scale(paint) + sx = scale_paint.scale_x / 65536.0 + sy = scale_paint.scale_y / 65536.0 + cx = scale_paint.center_x / 65536.0 + cy = scale_paint.center_y / 65536.0 + Cairo.translate(ctx.cr, cx, cy) + Cairo.scale(ctx.cr, sx, sy) + Cairo.translate(ctx.cr, -cx, -cy) + render_paint_tree(ctx, scale_paint.paint, depth + 1) + + elseif fmt == FT_COLR_PAINTFORMAT_ROTATE + rotate = get_rotate(paint) + angle = rotate.angle / 65536.0 * π + cx = rotate.center_x / 65536.0 + cy = rotate.center_y / 65536.0 + Cairo.translate(ctx.cr, cx, cy) + Cairo.rotate(ctx.cr, angle) + Cairo.translate(ctx.cr, -cx, -cy) + render_paint_tree(ctx, rotate.paint, depth + 1) + + elseif fmt == FT_COLR_PAINTFORMAT_SOLID + solid = get_solid(paint) + render_solid(ctx, solid) + + elseif fmt == FT_COLR_PAINTFORMAT_GLYPH + glyph = get_glyph(paint) + render_glyph(ctx, glyph, depth) + + elseif fmt == FT_COLR_PAINTFORMAT_LINEAR_GRADIENT + gradient = get_linear_gradient(paint) + render_linear_gradient(ctx, gradient) + + elseif fmt == FT_COLR_PAINTFORMAT_RADIAL_GRADIENT + gradient = get_radial_gradient(paint) + render_radial_gradient(ctx, gradient) + + elseif fmt == FT_COLR_PAINTFORMAT_COLR_LAYERS + layers = get_colr_layers(paint) + layer_paint = Ref{FT_OpaquePaint}() + iter = Ref(layers.layer_iterator) + + for i in 1:layers.layer_iterator.num_layers + if FT_Get_Paint_Layers(ctx.face, iter, layer_paint) != 0 + render_paint_tree(ctx, layer_paint[], depth + 1) + end + end + + elseif fmt == FT_COLR_PAINTFORMAT_COLR_GLYPH + colr_glyph = get_colr_glyph(paint) + sub_opaque = Ref{FT_OpaquePaint}(FT_OpaquePaint_(C_NULL, 0)) + if FT_Get_Color_Glyph_Paint(ctx.face, colr_glyph.glyphID, + FT_COLOR_NO_ROOT_TRANSFORM, sub_opaque) != 0 + render_paint_tree(ctx, sub_opaque[], depth + 1) + end + + elseif fmt == FT_COLR_PAINTFORMAT_COMPOSITE + composite = get_composite(paint) + # Simple compositing - render backdrop then source + render_paint_tree(ctx, composite.backdrop_paint, depth + 1) + render_paint_tree(ctx, composite.source_paint, depth + 1) + end + finally + Cairo.restore(ctx.cr) + end + end + + """ + Recursively traverse and print the paint tree structure with full union extraction! + """ + function walk_paint_tree(face::FT_Face, opaque_paint::FT_OpaquePaint, depth::Int=0) + # Safety: prevent infinite recursion + if depth > 128 + @warn "Reached maximum paint tree depth, stopping recursion to prevent infinite loop" + return + end + + # Get the paint data + paint_ref = Ref{FT_COLR_Paint}() + result = FT_Get_Paint(face, opaque_paint, paint_ref) + + if result == 0 + error("Failed to get paint data at depth $depth") + return + end + + paint = paint_ref[] + fmt = paint.format + + # Extract union data and recurse into children + if fmt == FT_COLR_PAINTFORMAT_TRANSFORM + transform = get_transform(paint) + + # Convert fixed-point to float for display + xx = transform.affine.xx / 65536.0 + xy = transform.affine.xy / 65536.0 + dx = transform.affine.dx / 65536.0 + yx = transform.affine.yx / 65536.0 + yy = transform.affine.yy / 65536.0 + dy = transform.affine.dy / 65536.0 + + + walk_paint_tree(face, transform.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_TRANSLATE + translate = get_translate(paint) + dx = translate.dx / 65536.0 + dy = translate.dy / 65536.0 + + walk_paint_tree(face, translate.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_SCALE + scale = get_scale(paint) + sx = scale.scale_x / 65536.0 + sy = scale.scale_y / 65536.0 + cx = scale.center_x / 65536.0 + cy = scale.center_y / 65536.0 + + walk_paint_tree(face, scale.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_ROTATE + rotate = get_rotate(paint) + angle = rotate.angle / 65536.0 * 180.0 # Convert to degrees + cx = rotate.center_x / 65536.0 + cy = rotate.center_y / 65536.0 + + walk_paint_tree(face, rotate.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_SKEW + skew = get_skew(paint) + x_angle = skew.x_skew_angle / 65536.0 * 180.0 + y_angle = skew.y_skew_angle / 65536.0 * 180.0 + cx = skew.center_x / 65536.0 + cy = skew.center_y / 65536.0 + + walk_paint_tree(face, skew.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_SOLID + solid = get_solid(paint) + + elseif fmt == FT_COLR_PAINTFORMAT_GLYPH + glyph = get_glyph(paint) + walk_paint_tree(face, glyph.paint, depth + 2) + + elseif fmt == FT_COLR_PAINTFORMAT_COLR_GLYPH + colr_glyph = get_colr_glyph(paint) + + elseif fmt == FT_COLR_PAINTFORMAT_LINEAR_GRADIENT + gradient = get_linear_gradient(paint) + + elseif fmt == FT_COLR_PAINTFORMAT_RADIAL_GRADIENT + gradient = get_radial_gradient(paint) + + elseif fmt == FT_COLR_PAINTFORMAT_SWEEP_GRADIENT + gradient = get_sweep_gradient(paint) + start_angle = gradient.start_angle / 65536.0 * 180.0 + end_angle = gradient.end_angle / 65536.0 * 180.0 + + + elseif fmt == FT_COLR_PAINTFORMAT_COLR_LAYERS + layers = get_colr_layers(paint) + # Iterate through layers + layer_paint = Ref{FT_OpaquePaint}() + iter = Ref(layers.layer_iterator) + for i in 1:layers.layer_iterator.num_layers + if FT_Get_Paint_Layers(face, iter, layer_paint) != 0 + walk_paint_tree(face, layer_paint[], depth + 2) + end + end + + elseif fmt == FT_COLR_PAINTFORMAT_COMPOSITE + composite = get_composite(paint) + walk_paint_tree(face, composite.source_paint, depth + 2) + walk_paint_tree(face, composite.backdrop_paint, depth + 2) + end + end + + # Main rendering function + function render_color_emoji(emoji::Char, output_path::String; size::Int=512, debug::Bool=true) + # Initialize FreeType + ft_library = Ref{FT_Library}() + err = FT_Init_FreeType(ft_library) + if err != 0 + error("Failed to initialize FreeType: $err") + end + + font_path = joinpath(@__DIR__, "output/Noto-COLRv1.ttf") + if !isfile(font_path) + @warn "Font not found: $font_path. Downloading Noto-COLRv1.ttf..." + Downloads.download("https://github.com/googlefonts/noto-emoji/raw/refs/heads/main/fonts/Noto-COLRv1.ttf", "output/Noto-COLRv1.ttf") + end + + face_ref = Ref{FT_Face}() + err = FT_New_Face(ft_library[], font_path, 0, face_ref) + if err != 0 + error("Failed to load font: $err") + end + face = face_ref[] + + # Set size + err = FT_Set_Pixel_Sizes(face, size, size) + if err != 0 + error("Failed to set pixel size: $err") + end + + # Get glyph index + glyph_index = FT_Get_Char_Index(face, UInt(emoji)) + if glyph_index == 0 + error("Glyph not found for character: $emoji") + end + + # Load glyph to get metrics + scale_factor = 1.0 + horiAdvance_px = 0.0 + baseline_offset = 0.0 + + err = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT) + if err == 0 + glyph_slot = unsafe_load(unsafe_load(face).glyph) + metrics = glyph_slot.metrics + + # Convert from 1/64th pixel units to pixels + horiAdvance_px = metrics.horiAdvance / 64.0 + + # Calculate scale factor based on horizontal advance + if horiAdvance_px > 0 + scale_factor = size / horiAdvance_px + end + + # Hacky centering of emoji. Simple 20% offset + canvas_height_font_units = size / scale_factor + baseline_offset = canvas_height_font_units * 0.20 + end + + opaque_paint = Ref{FT_OpaquePaint}(FT_OpaquePaint_(C_NULL, 0)) + result = FT_Get_Color_Glyph_Paint( + face, + glyph_index, + FT_COLOR_INCLUDE_ROOT_TRANSFORM, + opaque_paint + ) + if result == 0 + error("No COLRv1 paint found for this glyph") + end + + # Create Cairo surface + surface = CairoARGBSurface(size, size) + cr = CairoContext(surface) + + # White background + set_source_rgb(cr, 1.0, 1.0, 1.0) + rectangle(cr, 0, 0, size, size) + fill(cr) + + # Flip Y axis (FreeType uses bottom-left origin, Cairo uses top-left) + translate(cr, 0, size) + scale(cr, 1, -1) + + # Apply scale factor to fit emoji based on horizontal advance + scale(cr, scale_factor, scale_factor) + + # Center the glyph vertically by positioning the baseline + if baseline_offset != 0.0 + translate(cr, 0, baseline_offset) + end + + # Load color palette + palette = get_color_palette(face, 0) + + # Create render context + ctx = RenderContext(cr, face, palette) + + # Render the paint tree + render_paint_tree(ctx, opaque_paint[], 0) + + # Write to file + write_to_png(surface, output_path) + + # Cleanup + destroy(cr) + destroy(surface) + FT_Done_Face(face) + FT_Done_FreeType(ft_library[]) + end + + render_color_emoji('🙅', "output/emoji_crossed_arms.png", size=512, debug=false) + render_color_emoji('🎨', "output/emoji_artist_palette.png", size=512, debug=false) + render_color_emoji('🚀', "output/emoji_rocket.png", size=512, debug=false) +end diff --git a/test/render_outline_emoji.jl b/test/render_outline_emoji.jl new file mode 100644 index 0000000..d91be3f --- /dev/null +++ b/test/render_outline_emoji.jl @@ -0,0 +1,153 @@ +using FreeType +using Images +using Test + +# Initialize FreeType library +ft_library = Ref{FT_Library}() + +@testset "Render Outline Emoji" begin + + function find_font(font_name::String) + # Common font directories on different platforms + font_dirs = String[] + + if Sys.islinux() + append!(font_dirs, [ + "/usr/share/fonts", + "/usr/local/share/fonts", + "$(homedir())/.fonts", + "$(homedir())/.local/share/fonts" + ]) + elseif Sys.isapple() + append!(font_dirs, [ + "/Library/Fonts", + "/System/Library/Fonts", + "$(homedir())/Library/Fonts" + ]) + elseif Sys.iswindows() + append!(font_dirs, [ + "C:\\Windows\\Fonts" + ]) + end + + # Search for font file + search_parts = split(lowercase(font_name), r"\W+", keepempty=false) + + best_match = nothing + best_score = (0, 0) + + for dir in font_dirs + if !isdir(dir) + continue + end + + # Recursively search in font directory + for (root, dirs, files) in walkdir(dir) + for file in files + # Only check font files + if !endswith(lowercase(file), r"\.(ttf|otf|ttc)$") + continue + end + + file_lower = lowercase(file) + + # Score based on matching search parts + family_score = sum(length(part) for part in search_parts if occursin(part, file_lower); init=0) + + if family_score > 0 + score = (family_score, -length(file)) # Prefer shorter names + if score > best_score + best_score = score + best_match = joinpath(root, file) + end + end + end + end + end + + if best_match !== nothing + println("Found font: $best_match") + else + @warn "Could not find font matching '$font_name'" + end + + return best_match + end + + # Initialize FreeType + err = FT_Init_FreeType(ft_library) + if err != 0 + error("Failed to initialize FreeType library: error code $err") + end + + # Try to find Noto Emoji font + font_name = "Symbola" + font_path = find_font(font_name) + if font_path === nothing + @error "Could not find $font_name font. Please install it or specify another emoji font." + return + end + + face_ref = Ref{FT_Face}() + FT_New_Face(ft_library[], font_path, 0, face_ref) + face = face_ref[] + + # Test with various emoji + test_emoji = ['😀', '🎨', '🚀', '❤', 'Ω'] + + for (i, emoji) in enumerate(test_emoji) + + err = FT_Set_Pixel_Sizes(face, Cuint(64), Cuint(64)) + #size = FT_Select_Size(face, 0) # Select first size + + # Get the glyph index for this character + glyph_index = FT_Get_Char_Index(face, UInt(emoji)) + + + load_flags = FT_LOAD_RENDER + err = FT_Load_Glyph(face, glyph_index, load_flags) + if err != 0 + error("Failed to load glyph for '$emoji': error code $err") + end + + # Get the glyph slot (contains bitmap and metrics) + face_rec = unsafe_load(face) + glyph_slot = unsafe_load(face_rec.glyph) + bitmap = glyph_slot.bitmap + metrics = glyph_slot.metrics + + img = nothing + + if bitmap.pixel_mode == FT_PIXEL_MODE_GRAY + # Grayscale bitmap (8-bit per pixel) + width = Int(bitmap.width) + height = Int(bitmap.rows) + pitch = Int(bitmap.pitch) + + # Create grayscale image + img = Matrix{UInt8}(undef, height, width) + + row_ptr = bitmap.buffer + for r in 1:height + row_data = unsafe_wrap(Array, row_ptr, width) + for c in 1:width + img[r, c] = row_data[c] + end + row_ptr += pitch + end + + # Convert to Images format + img = Images.Gray.(img ./ 255) + else + @warn "Unsupported pixel mode: $(bitmap.pixel_mode)" + continue + end + + if img !== nothing + output_file = "output/emoji_$(i)_U+$(string(UInt32(emoji), base=16, pad=4)).png" + Images.save(output_file, img) + end + end + + FT_Done_FreeType(ft_library[]) +end \ No newline at end of file diff --git a/test/render_word.jl b/test/render_word.jl new file mode 100644 index 0000000..71a81e4 --- /dev/null +++ b/test/render_word.jl @@ -0,0 +1,169 @@ +""" +Minimal example: Render a word using FreeType.jl +""" + +using FreeType +using Images + +# Initialize FreeType library +ft_library = Ref{FT_Library}() + +@testset "Render Word" begin + # Load and render a single character + function render_char(face::FT_Face, char::Char) + # Get the glyph index for this character + glyph_index = FT_Get_Char_Index(face, UInt(char)) + + if glyph_index == 0 + @warn "Character '$char' not found in font" + return nothing, nothing + end + + # Load the glyph with rendering + err = FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER) + if err != 0 + error("Failed to load glyph for '$char': error code $err") + end + + # Get the glyph slot (contains bitmap and metrics) + face_rec = unsafe_load(face) + glyph_slot = unsafe_load(face_rec.glyph) + bitmap = glyph_slot.bitmap + metrics = glyph_slot.metrics + + # Copy bitmap data to Julia array + if bitmap.width == 0 || bitmap.rows == 0 + return zeros(UInt8, 0, 0), metrics + end + + bmp = Matrix{UInt8}(undef, bitmap.rows, bitmap.width) + row_ptr = bitmap.buffer + + for r in 1:bitmap.rows + src = unsafe_wrap(Array, row_ptr, bitmap.width) + bmp[r, :] = src + row_ptr += bitmap.pitch + end + + return bmp, metrics + end + + # Render a word into a matrix + function render_word(face::FT_Face, word::String, pixel_size::Int=64) + + err = FT_Set_Pixel_Sizes(face, pixel_size, pixel_size) + if err != 0 + error("Failed to set pixel size: error code $err") + end + + # First pass: calculate dimensions + bitmaps = [] + metrics_list = [] + total_width = 0 + max_bearing_y = 0 + min_bearing_y = 0 + + for char in word + bitmap, metrics = render_char(face, char) + if bitmap === nothing + continue + end + + push!(bitmaps, bitmap) + push!(metrics_list, metrics) + + # Advance width (in 1/64th pixels) + advance_x = metrics.horiAdvance ÷ 64 + total_width += advance_x + + # Calculate vertical bounds + bearing_y = metrics.horiBearingY ÷ 64 + height = metrics.height ÷ 64 + + max_bearing_y = max(max_bearing_y, bearing_y) + min_bearing_y = min(min_bearing_y, bearing_y - height) + end + + # Calculate canvas height + canvas_height = max_bearing_y - min_bearing_y + if canvas_height == 0 + canvas_height = pixel_size + end + + # Create output canvas (height × width) + canvas = zeros(UInt8, canvas_height, total_width) + + # Second pass: place glyphs on canvas + pen_x = 0 + + for (i, char) in enumerate(word) + if i > length(bitmaps) + break + end + + bitmap = bitmaps[i] + metrics = metrics_list[i] + + if isempty(bitmap) + # Space or empty glyph + pen_x += metrics.horiAdvance ÷ 64 + continue + end + + # Calculate position + bearing_x = metrics.horiBearingX ÷ 64 + bearing_y = metrics.horiBearingY ÷ 64 + + # Position on canvas (origin at top-left) + x_offset = pen_x + bearing_x + y_offset = max_bearing_y - bearing_y + + # Copy bitmap to canvas + bmp_height, bmp_width = size(bitmap) + for r in 1:bmp_height + for c in 1:bmp_width + y = y_offset + r + x = x_offset + c + + # Check bounds + if 1 <= y <= canvas_height && 1 <= x <= total_width + canvas[y, x] = max(canvas[y, x], bitmap[r, c]) + end + end + end + + # Advance pen position + pen_x += metrics.horiAdvance ÷ 64 + end + + return canvas + end + + err = FT_Init_FreeType(ft_library) + + font_path = joinpath(@__DIR__, "hack_regular.ttf") + + face_ref = Ref{FT_Face}() + err = FT_New_Face(ft_library[], font_path, 0, face_ref) + if err != 0 + error("Failed to load font '$font_path': error code $err") + end + face = face_ref[] + + # Render a word + word = "FreeType" + pixel_size = 64 + + bitmap = render_word(face, word, pixel_size) + + path = "output/rendered_word.png" + # Convert bitmap to grayscale image + img = Gray.(bitmap ./ 255) + Images.save(path, img) + + # Clean up + FT_Done_Face(face) + FT_Done_FreeType(ft_library[]) + +end + diff --git a/test/runtests.jl b/test/runtests.jl index c0812af..889d05a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,55 +8,58 @@ error = FT_Init_FreeType(library) @testset "test FT_Outline_Decompose" begin -refface = Ref{FT_Face}() -@test FT_New_Face(library[], joinpath(@__DIR__, "hack_regular.ttf"), 0, refface) == 0 + refface = Ref{FT_Face}() + @test FT_New_Face(library[], joinpath(@__DIR__, "hack_regular.ttf"), 0, refface) == 0 -glyph_index = FT_Get_Char_Index(refface[], 'J') -@test glyph_index == 0x00000031 -@test FT_Set_Char_Size(refface[], 0, 16*64, 3, 3) == 0 -@test FT_Load_Glyph(refface[], glyph_index, FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP) == 0 + glyph_index = FT_Get_Char_Index(refface[], 'J') + @test glyph_index == 0x00000031 + @test FT_Set_Char_Size(refface[], 0, 16 * 64, 3, 3) == 0 + @test FT_Load_Glyph(refface[], glyph_index, FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP) == 0 -function pos(p::Ptr{FT_Vector}) - v = unsafe_load(p) - (v.x, v.y) -end + function pos(p::Ptr{FT_Vector}) + v = unsafe_load(p) + (v.x, v.y) + end -paths = [] -function move_to_func(to, user) - push!(paths, (:move, pos(to))) - Cint(0) -end -function line_to_func(to, user) - push!(paths, (:line, pos(to))) - Cint(0) -end -function conic_to_func(control, to, user) - push!(paths, (:conic, pos.((control, to))...)) - Cint(0) -end -function cubic_to_func(control1, control2, to, user) - push!(paths, (:cubic, pos.((control1, control2, to))...)) - Cint(0) -end + paths = [] + function move_to_func(to, user) + push!(paths, (:move, pos(to))) + Cint(0) + end + function line_to_func(to, user) + push!(paths, (:line, pos(to))) + Cint(0) + end + function conic_to_func(control, to, user) + push!(paths, (:conic, pos.((control, to))...)) + Cint(0) + end + function cubic_to_func(control1, control2, to, user) + push!(paths, (:cubic, pos.((control1, control2, to))...)) + Cint(0) + end -move_f = @cfunction $move_to_func Cint (Ptr{FT_Vector}, Ptr{Cvoid}) -line_f = @cfunction $line_to_func Cint (Ptr{FT_Vector}, Ptr{Cvoid}) -conic_f = @cfunction $conic_to_func Cint (Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{Cvoid}) -cubic_f = @cfunction $cubic_to_func Cint (Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{Cvoid}) + move_f = @cfunction $move_to_func Cint (Ptr{FT_Vector}, Ptr{Cvoid}) + line_f = @cfunction $line_to_func Cint (Ptr{FT_Vector}, Ptr{Cvoid}) + conic_f = @cfunction $conic_to_func Cint (Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{Cvoid}) + cubic_f = @cfunction $cubic_to_func Cint (Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{FT_Vector}, Ptr{Cvoid}) -GC.@preserve move_f line_f conic_f cubic_f begin -face = unsafe_load(refface[]) -glyph = unsafe_load(face.glyph) -outline_funcs = FreeType.FT_Outline_Funcs(Base.unsafe_convert.(Ptr{Cvoid}, (move_f, line_f, conic_f, cubic_f))..., 0, 0) -FT_Outline_Decompose(pointer_from_objref.((Ref(glyph.outline), Ref(outline_funcs)))..., C_NULL) -end + GC.@preserve move_f line_f conic_f cubic_f begin + face = unsafe_load(refface[]) + glyph = unsafe_load(face.glyph) + outline_funcs = FreeType.FT_Outline_Funcs(Base.unsafe_convert.(Ptr{Cvoid}, (move_f, line_f, conic_f, cubic_f))..., 0, 0) + FT_Outline_Decompose(pointer_from_objref.((Ref(glyph.outline), Ref(outline_funcs)))..., C_NULL) + end -@test paths == [(:move, (502, -29)), (:conic, (399, -29), (307, -7)), (:conic, (210, 16), (109, 61)), (:line, (109, 297)), (:conic, (200, 216), (297, 176)), (:conic, (396, 135), (499, 135)), (:conic, (566, 135), (617, 153)), (:conic, (669, 171), (698, 210)), (:conic, (754, 284), (754, 487)), (:line, (754, 1323)), (:line, (373, 1323)), (:line, (373, 1493)), (:line, (956, 1493)), (:line, (956, 487)), (:conic, (956, 343), (930, 246)), (:conic, (905, 150), (850, 89)), (:conic, (795, 28), (709, 0)), (:conic, (624, -29), (502, -29))] + @test paths == [(:move, (502, -29)), (:conic, (399, -29), (307, -7)), (:conic, (210, 16), (109, 61)), (:line, (109, 297)), (:conic, (200, 216), (297, 176)), (:conic, (396, 135), (499, 135)), (:conic, (566, 135), (617, 153)), (:conic, (669, 171), (698, 210)), (:conic, (754, 284), (754, 487)), (:line, (754, 1323)), (:line, (373, 1323)), (:line, (373, 1493)), (:line, (956, 1493)), (:line, (956, 487)), (:conic, (956, 343), (930, 246)), (:conic, (905, 150), (850, 89)), (:conic, (795, 28), (709, 0)), (:conic, (624, -29), (502, -29))] -@test FT_Done_FreeType(library[]) == 0 + @test FT_Done_FreeType(library[]) == 0 end +include("render_word.jl") +include("render_outline_emoji.jl") +include("render_colrv1_emoji.jl") # since there are no meaningful tests, please manually do a test for FreeTypeAbstraction # using FreeTypeAbstraction