// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Drawing positioned glyphs onto a z2d surface. const std = @import("std"); const Allocator = std.mem.Allocator; const z2d = @import("z2d"); const Face = @import("Face.zig"); const GlyphCache = @import("GlyphCache.zig"); const raster = @import("raster.zig"); const Run = @import("run.zig").Run; pub const Options = struct { /// The text color, pre-multiplied as all of z2d's pixels are. Outline /// glyphs are painted in it, and color glyphs use it where their font /// asks for the foreground. color: z2d.Pixel = .{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 255 } }, /// How glyphs are composited onto the surface. operator: z2d.compositor.Operator = .src_over, anti_aliasing_mode: z2d.options.AntiAliasMode = .default, /// Draw `COLR` and `SVG ` glyphs in color rather than as plain outlines. color_glyphs: bool = true, /// The `CPAL` palette color glyphs use. palette: u16 = 0, /// Where rendered glyphs are kept between draws. Without one, every /// glyph is rendered afresh, which gives the same pixels slower. cache: ?*GlyphCache = null, }; pub const Error = Allocator.Error; /// Draw `run` onto `target` with the run's origin — the start of its /// baseline — at `(x, y)` in pixels. /// /// Each glyph is placed to the nearest quarter pixel, rendered once into an /// image of its own at that offset (or taken from `options.cache`), and /// composited. For drawing under a rotation or any other transformation, /// see `outline.appendRun`. pub fn drawRun( gpa: Allocator, target: *z2d.Surface, run: Run, x: f32, y: f32, options: Options, ) Error!void { const foreground = z2d.pixel.RGBA.fromPixel(options.color); for (run.glyphs) |g| { const gx = quarter(x + g.x) orelse continue; const gy = quarter(y + g.y) orelse continue; const color = options.color_glyphs and run.face.colorKind(g.id) != .none; const key: GlyphCache.Key = .{ .face = @intFromPtr(run.face), .glyph_id = g.id, .size = @bitCast(run.size), .sub_x = gx.sub, .sub_y = gy.sub, .color = color, .anti_aliasing = @backingInt(options.anti_aliasing_mode), .palette = if (color) options.palette else 0, .foreground = if (color) @bitCast(foreground) else 0, }; if (options.cache) |cache| { if (cache.get(key)) |hit| { if (hit) |img| composite(target, img, gx.whole, gy.whole, options); continue; } } var rendered = try raster.render( gpa, run.face, g.id, run.size, @as(f32, @floatFromInt(gx.sub)) / 4, @as(f32, @floatFromInt(gy.sub)) / 4, .{ .anti_aliasing_mode = options.anti_aliasing_mode, .color = color, .palette = options.palette, .foreground = foreground, }, ); if (rendered) |*img| composite(target, img, gx.whole, gy.whole, options); if (options.cache) |cache| { try cache.put(key, rendered); } else if (rendered) |*img| { img.deinit(gpa); } } } const Quarter = struct { whole: i32, sub: u2 }; /// `v` to the nearest quarter, as a whole part and a number of quarters. /// Null for a position too far off any surface to matter. fn quarter(v: f32) ?Quarter { if (!std.math.isFinite(v) or @abs(v) > 1 << 28) return null; const q: i32 = @intFromFloat(@round(v * 4)); return .{ .whole = @divFloor(q, 4), .sub = @intCast(@mod(q, 4)) }; } /// Composite a rendered glyph whose origin is in pixel `(x, y)`. fn composite(target: *z2d.Surface, img: *const raster.Image, x: i32, y: i32, options: Options) void { const dx = x +| img.left; const dy = y +| img.top; if (img.isColor()) { target.composite(&img.surface, options.operator, dx, dy, .{}); return; } // The text color through the mask, then onto the surface. z2d.compositor.SurfaceCompositor.run(target, dx, dy, 2, .{ .{ .operator = .dst_in, .dst = .{ .pixel = options.color }, .src = .{ .surface = &img.surface } }, .{ .operator = options.operator }, }, .{}); } test "positions round to the nearest quarter pixel" { try std.testing.expectEqual(Quarter{ .whole = 3, .sub = 1 }, quarter(3.3).?); try std.testing.expectEqual(Quarter{ .whole = -1, .sub = 3 }, quarter(-0.2).?); try std.testing.expectEqual(Quarter{ .whole = 5, .sub = 0 }, quarter(4.9).?); try std.testing.expectEqual(@as(?Quarter, null), quarter(std.math.inf(f32))); }