diff --git a/pkg/freetype/freetype-zig.h b/pkg/freetype/freetype-zig.h index 29e546154..dcc65e514 100644 --- a/pkg/freetype/freetype-zig.h +++ b/pkg/freetype/freetype-zig.h @@ -5,3 +5,5 @@ #include #include #include +#include +#include diff --git a/pkg/macos/graphics/context.zig b/pkg/macos/graphics/context.zig index d1c6c943f..77e4344e0 100644 --- a/pkg/macos/graphics/context.zig +++ b/pkg/macos/graphics/context.zig @@ -141,6 +141,22 @@ pub fn Context(comptime T: type) type { @bitCast(rect), ); } + + pub fn scaleCTM(self: *T, sx: c.CGFloat, sy: c.CGFloat) void { + c.CGContextScaleCTM( + @ptrCast(self), + sx, + sy, + ); + } + + pub fn translateCTM(self: *T, tx: c.CGFloat, ty: c.CGFloat) void { + c.CGContextTranslateCTM( + @ptrCast(self), + tx, + ty, + ); + } }; } diff --git a/src/config/Config.zig b/src/config/Config.zig index f36132ea9..68b456d7a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -425,13 +425,16 @@ pub const compatibility = std.StaticStringMap( /// /// Available flags: /// -/// * `hinting` - Enable or disable hinting, enabled by default. -/// * `force-autohint` - Use the freetype auto-hinter rather than the -/// font's native hinter. Enabled by default. -/// * `monochrome` - Instructs renderer to use 1-bit monochrome -/// rendering. This option doesn't impact the hinter. -/// Enabled by default. -/// * `autohint` - Use the freetype auto-hinter. Enabled by default. +/// * `hinting` - Enable or disable hinting. Enabled by default. +/// +/// * `force-autohint` - Always use the freetype auto-hinter instead of +/// the font's native hinter. Enabled by default. +/// +/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering. +/// This will disable anti-aliasing, and probably not look very good unless +/// you're using a pixel font. Disabled by default. +/// +/// * `autohint` - Enable the freetype auto-hinter. Enabled by default. /// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` @"freetype-load-flags": FreetypeLoadFlags = .{}, @@ -6961,7 +6964,7 @@ pub const FreetypeLoadFlags = packed struct { // to these defaults. hinting: bool = true, @"force-autohint": bool = true, - monochrome: bool = true, + monochrome: bool = false, autohint: bool = true, }; diff --git a/src/font/face.zig b/src/font/face.zig index 6355578db..363576ff0 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -94,6 +94,17 @@ pub const RenderOptions = struct { /// optionally by the rasterizer to better layout the glyph. cell_width: ?u2 = null, + /// Constraint and alignment properties for the glyph. The rasterizer + /// should call the `constrain` function on this with the original size + /// and bearings of the glyph to get remapped values that the glyph + /// should be scaled/moved to. + constraint: Constraint = .none, + + /// The number of cells, horizontally that the glyph is free to take up + /// when resized and aligned by `constraint`. This is usually 1, but if + /// there's whitespace to the right of the cell then it can be 2. + constraint_width: u2 = 1, + /// Thicken the glyph. This draws the glyph with a thicker stroke width. /// This is purely an aesthetic setting. /// @@ -108,6 +119,198 @@ pub const RenderOptions = struct { /// /// CoreText only. thicken_strength: u8 = 255, + + /// See the `constraint` field. + pub const Constraint = struct { + /// Don't constrain the glyph in any way. + pub const none: Constraint = .{}; + + /// Vertical sizing rule. + size_vertical: Size = .none, + /// Horizontal sizing rule. + size_horizontal: Size = .none, + + /// Vertical alignment rule. + align_vertical: Align = .none, + /// Horizontal alignment rule. + align_horizontal: Align = .none, + + /// Top padding when resizing. + pad_top: f64 = 0.0, + /// Left padding when resizing. + pad_left: f64 = 0.0, + /// Right padding when resizing. + pad_right: f64 = 0.0, + /// Bottom padding when resizing. + pad_bottom: f64 = 0.0, + + /// Maximum ratio of width to height when resizing. + max_xy_ratio: ?f64 = null, + + pub const Size = enum { + /// Don't change the size of this glyph. + none, + /// Move the glyph and optionally scale it down + /// proportionally to fit within the given axis. + fit, + /// Move and resize the glyph proportionally to + /// cover the given axis. + cover, + /// Same as `cover` but not proportional. + stretch, + }; + + pub const Align = enum { + /// Don't move the glyph on this axis. + none, + /// Move the glyph so that its leading (bottom/left) + /// edge aligns with the leading edge of the axis. + start, + /// Move the glyph so that its trailing (top/right) + /// edge aligns with the trailing edge of the axis. + end, + /// Move the glyph so that it is centered on this axis. + center, + }; + + /// The size and position of a glyph. + pub const GlyphSize = struct { + width: f64, + height: f64, + x: f64, + y: f64, + }; + + /// Apply this constraint to the provided glyph + /// size, given the available width and height. + pub fn constrain( + self: Constraint, + glyph: GlyphSize, + /// Available width + cell_width: f64, + /// Available height + cell_height: f64, + ) GlyphSize { + var g = glyph; + + const w = cell_width - + self.pad_left * cell_width - + self.pad_right * cell_width; + const h = cell_height - + self.pad_top * cell_height - + self.pad_bottom * cell_height; + + // Subtract padding from the bearings so that our + // alignment and sizing code works correctly. We + // re-add before returning. + g.x -= self.pad_left * cell_width; + g.y -= self.pad_bottom * cell_height; + + switch (self.size_horizontal) { + .none => {}, + .fit => if (g.width > w) { + const orig_height = g.height; + // Adjust our height and width to proportionally + // scale them to fit the glyph to the cell width. + g.height *= w / g.width; + g.width = w; + // Set our x to 0 since anything else would mean + // the glyph extends outside of the cell width. + g.x = 0; + // Compensate our y to keep things vertically + // centered as they're scaled down. + g.y += (orig_height - g.height) / 2; + } else if (g.width + g.x > w) { + // If the width of the glyph can fit in the cell but + // is currently outside due to the left bearing, then + // we reduce the left bearing just enough to fit it + // back in the cell. + g.x = w - g.width; + } else if (g.x < 0) { + g.x = 0; + }, + .cover => { + const orig_height = g.height; + + g.height *= w / g.width; + g.width = w; + + g.x = 0; + + g.y += (orig_height - g.height) / 2; + }, + .stretch => { + g.width = w; + g.x = 0; + }, + } + + switch (self.size_vertical) { + .none => {}, + .fit => if (g.height > h) { + const orig_width = g.width; + // Adjust our height and width to proportionally + // scale them to fit the glyph to the cell height. + g.width *= h / g.height; + g.height = h; + // Set our y to 0 since anything else would mean + // the glyph extends outside of the cell height. + g.y = 0; + // Compensate our x to keep things horizontally + // centered as they're scaled down. + g.x += (orig_width - g.width) / 2; + } else if (g.height + g.y > h) { + // If the height of the glyph can fit in the cell but + // is currently outside due to the bottom bearing, then + // we reduce the bottom bearing just enough to fit it + // back in the cell. + g.y = h - g.height; + } else if (g.y < 0) { + g.y = 0; + }, + .cover => { + const orig_width = g.width; + + g.width *= h / g.height; + g.height = h; + + g.y = 0; + + g.x += (orig_width - g.width) / 2; + }, + .stretch => { + g.height = h; + g.y = 0; + }, + } + + if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) { + const orig_width = g.width; + g.width = g.height * ratio; + g.x += (orig_width - g.width) / 2; + }; + + switch (self.align_horizontal) { + .none => {}, + .start => g.x = 0, + .end => g.x = w - g.width, + .center => g.x = (w - g.width) / 2, + } + + switch (self.align_vertical) { + .none => {}, + .start => g.y = 0, + .end => g.y = h - g.height, + .center => g.y = (h - g.height) / 2, + } + + // Re-add our padding before returning. + g.x += self.pad_left * cell_width; + g.y += self.pad_bottom * cell_height; + + return g; + } + }; }; test { diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 06bba661f..35f094848 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -291,22 +291,29 @@ pub const Face = struct { // in the bottom left and +Y pointing up. var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null); + // Determine whether this is a color glyph. + const is_color = self.isColorGlyph(glyph_index); + // And whether it's (probably) a bitmap (sbix). + const sbix = is_color and self.color != null and self.color.?.sbix; + // If we're rendering a synthetic bold then we will gain 50% of // the line width on every edge, which means we should increase // our width and height by the line width and subtract half from // our origin points. - if (self.synthetic_bold) |line_width| { + // + // We don't add extra size if it's a sbix color font though, + // since bitmaps aren't affected by synthetic bold. + if (!sbix) if (self.synthetic_bold) |line_width| { rect.size.width += line_width; rect.size.height += line_width; rect.origin.x -= line_width / 2; rect.origin.y -= line_width / 2; - } + }; // We make an assumption that font smoothing ("thicken") // adds no more than 1 extra pixel to any edge. We don't // add extra size if it's a sbix color font though, since // bitmaps aren't affected by smoothing. - const sbix = self.color != null and self.color.?.sbix; if (opts.thicken and !sbix) { rect.size.width += 2.0; rect.size.height += 2.0; @@ -314,29 +321,43 @@ pub const Face = struct { rect.origin.y -= 1.0; } - // We compute the minimum and maximum x and y values. - // We round our min points down and max points up. - const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{ - @intFromFloat(@floor(rect.origin.x)), - @intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)), - @intFromFloat(@floor(rect.origin.y)), - @intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)), - }; + // If our rect is smaller than a quarter pixel in either axis + // then it has no outlines or they're too small to render. + // + // In this case we just return 0-sized glyph struct. + if (rect.size.width < 0.25 or rect.size.height < 0.25) + return font.Glyph{ + .width = 0, + .height = 0, + .offset_x = 0, + .offset_y = 0, + .atlas_x = 0, + .atlas_y = 0, + .advance_x = 0, + }; - // This bitmap is blank. I've seen it happen in a font, I don't know why. - // If it is empty, we just return a valid glyph struct that does nothing. - if (x1 <= x0 or y1 <= y0) return font.Glyph{ - .width = 0, - .height = 0, - .offset_x = 0, - .offset_y = 0, - .atlas_x = 0, - .atlas_y = 0, - .advance_x = 0, - }; + const metrics = opts.grid_metrics; + const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width); + const cell_height: f64 = @floatFromInt(metrics.cell_height); - const width: u32 = @intCast(x1 - x0); - const height: u32 = @intCast(y1 - y0); + const glyph_size = opts.constraint.constrain( + .{ + .width = rect.size.width, + .height = rect.size.height, + .x = rect.origin.x, + .y = rect.origin.y + @as(f64, @floatFromInt(metrics.cell_baseline)), + }, + cell_width, + cell_height, + ); + + const width = glyph_size.width; + const height = glyph_size.height; + const x = glyph_size.x; + const y = glyph_size.y; + + const px_width: u32 = @intFromFloat(@ceil(width)); + const px_height: u32 = @intFromFloat(@ceil(height)); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -344,7 +365,7 @@ pub const Face = struct { depth: u32, space: *macos.graphics.ColorSpace, context_opts: c_uint, - } = if (!self.isColorGlyph(glyph_index)) .{ + } = if (!is_color) .{ .color = false, .depth = 1, .space = try macos.graphics.ColorSpace.createNamed(.linearGray), @@ -371,17 +392,17 @@ pub const Face = struct { // usually stabilizes pretty quickly and is very infrequent so I think // the allocation overhead is acceptable compared to the cost of // caching it forever or having to deal with a cache lifetime. - const buf = try alloc.alloc(u8, width * height * color.depth); + const buf = try alloc.alloc(u8, px_width * px_height * color.depth); defer alloc.free(buf); @memset(buf, 0); const context = macos.graphics.BitmapContext.context; const ctx = try macos.graphics.BitmapContext.create( buf, - width, - height, + px_width, + px_height, 8, - width * color.depth, + px_width * color.depth, color.space, color.context_opts, ); @@ -390,14 +411,14 @@ pub const Face = struct { // Perform an initial fill. This ensures that we don't have any // uninitialized pixels in the bitmap. if (color.color) - context.setRGBFillColor(ctx, 1, 1, 1, 0) + context.setRGBFillColor(ctx, 0, 0, 0, 0) else - context.setGrayFillColor(ctx, 1, 0); + context.setGrayFillColor(ctx, 0, 0); context.fillRect(ctx, .{ .origin = .{ .x = 0, .y = 0 }, .size = .{ - .width = @floatFromInt(width), - .height = @floatFromInt(height), + .width = @floatFromInt(px_width), + .height = @floatFromInt(px_height), }, }); @@ -427,49 +448,34 @@ pub const Face = struct { context.setLineWidth(ctx, line_width); } + context.scaleCTM( + ctx, + width / rect.size.width, + height / rect.size.height, + ); + // We want to render the glyphs at (0,0), but the glyphs themselves // are offset by bearings, so we have to undo those bearings in order // to get them to 0,0. - self.font.drawGlyphs(&glyphs, &.{ - .{ - .x = @floatFromInt(-x0), - .y = @floatFromInt(-y0), - }, - }, ctx); + self.font.drawGlyphs(&glyphs, &.{.{ + .x = -@floor(rect.origin.x), + .y = -@floor(rect.origin.y), + }}, ctx); - const region = region: { - // We reserve a region that's 1px wider and taller than we need - // in order to create a 1px separation between adjacent glyphs - // to prevent interpolation with adjacent glyphs while sampling - // from the atlas. - var region = try atlas.reserve( - alloc, - width + 1, - height + 1, - ); - - // We adjust the region width and height back down since we - // don't need the extra pixel, we just needed to reserve it - // so that it isn't used for other glyphs in the future. - region.width -= 1; - region.height -= 1; - break :region region; - }; + // Write our rasterized glyph to the atlas. + const region = try atlas.reserve(alloc, px_width, px_height); atlas.set(region, buf); - const metrics = opts.grid_metrics; - // This should be the distance from the bottom of // the cell to the top of the glyph's bounding box. - // - // The calculation is distance from bottom of cell to - // baseline plus distance from baseline to top of glyph. - const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1; + const offset_y: i32 = + @as(i32, @intFromFloat(@floor(y))) + + @as(i32, @intCast(px_height)); // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = x0; + var result: i32 = @intFromFloat(@round(x)); // If our cell was resized then we adjust our glyph's // position relative to the new center. This keeps glyphs @@ -490,8 +496,8 @@ pub const Face = struct { _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); return .{ - .width = width, - .height = height, + .width = px_width, + .height = px_height, .offset_x = offset_x, .offset_y = offset_y, .atlas_x = region.x, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index accb891a4..9e057ceea 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -21,6 +21,8 @@ const fastmem = @import("../../fastmem.zig"); const quirks = @import("../../quirks.zig"); const config = @import("../../config.zig"); +const F26Dot6 = opentype.sfnt.F26Dot6; + const log = std.log.scoped(.font_face); pub const Face = struct { @@ -58,14 +60,6 @@ pub const Face = struct { bold: bool = false, } = .{}, - /// The matrix applied to a regular font to create a synthetic italic. - const italic_matrix: freetype.c.FT_Matrix = .{ - .xx = 0x10000, - .xy = 0x044ED, // approx. tan(15) - .yx = 0, - .yy = 0x10000, - }; - /// Initialize a new font face with the given source in-memory. pub fn initFile( lib: Library, @@ -330,26 +324,32 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); - const metrics = opts.grid_metrics; + // We enable hinting by default, and disable it if either of the + // constraint alignments are not center or none, since this means + // that the glyph needs to be aligned flush to the cell edge, and + // hinting can mess that up. + const do_hinting = self.load_flags.hinting and + switch (opts.constraint.align_horizontal) { + .start, .end => false, + .center, .none => true, + } and + switch (opts.constraint.align_vertical) { + .start, .end => false, + .center, .none => true, + }; - // If we have synthetic italic, then we apply a transformation matrix. - // We have to undo this because synthetic italic works by increasing - // the ref count of the base face. - if (self.synthetic.italic) self.face.setTransform(&italic_matrix, null); - defer if (self.synthetic.italic) self.face.setTransform(null, null); - - // If our glyph has color, we want to render the color + // Load the glyph. try self.face.loadGlyph(glyph_index, .{ + // If our glyph has color, we want to render the color .color = self.face.hasColor(), - // If we have synthetic bold, we have to set some additional - // glyph properties before render so we don't render here. - .render = !self.synthetic.bold, + // We don't render, because we'll invoke the render + // manually after applying constraints further down. + .render = false, // use options from config - .no_hinting = !self.load_flags.hinting, + .no_hinting = !do_hinting, .force_autohint = !self.load_flags.@"force-autohint", - .monochrome = !self.load_flags.monochrome, .no_autohint = !self.load_flags.autohint, // NO_SVG set to true because we don't currently support rendering @@ -359,260 +359,310 @@ pub const Face = struct { }); const glyph = self.face.handle.*.glyph; - // For synthetic bold, we embolden the glyph and render it. + const glyph_width: f64 = f26dot6ToF64(glyph.*.metrics.width); + const glyph_height: f64 = f26dot6ToF64(glyph.*.metrics.height); + + // If our glyph is smaller than a quarter pixel in either axis + // then it has no outlines or they're too small to render. + // + // In this case we just return 0-sized glyph struct. + if (glyph_width < 0.25 or glyph_height < 0.25) + return font.Glyph{ + .width = 0, + .height = 0, + .offset_x = 0, + .offset_y = 0, + .atlas_x = 0, + .atlas_y = 0, + .advance_x = 0, + }; + + // For synthetic bold, we embolden the glyph. if (self.synthetic.bold) { // We need to scale the embolden amount based on the font size. // This is a heuristic I found worked well across a variety of // founts: 1 pixel per 64 units of height. - const height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); + const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); const ratio: f64 = 64.0 / 2048.0; - const amount = @ceil(height * ratio); + const amount = @ceil(font_height * ratio); _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); - try self.face.renderGlyph(.normal); } - // This bitmap is blank. I've seen it happen in a font, I don't know why. - // If it is empty, we just return a valid glyph struct that does nothing. - const bitmap_ft = glyph.*.bitmap; - if (bitmap_ft.rows == 0) return .{ - .width = 0, - .height = 0, - .offset_x = 0, - .offset_y = 0, - .atlas_x = 0, - .atlas_y = 0, - .advance_x = 0, - }; + // Next we need to apply any constraints. + const metrics = opts.grid_metrics; - // Ensure we know how to work with the font format. And assure that - // or color depth is as expected on the texture atlas. If format is null - // it means there is no native color format for our Atlas and we must try - // conversion. - const format: ?font.Atlas.Format = switch (bitmap_ft.pixel_mode) { - freetype.c.FT_PIXEL_MODE_MONO => null, - freetype.c.FT_PIXEL_MODE_GRAY => .grayscale, - freetype.c.FT_PIXEL_MODE_BGRA => .bgra, + const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width); + const cell_height: f64 = @floatFromInt(metrics.cell_height); + + const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX); + const glyph_y: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingY) - glyph_height; + + const glyph_size = opts.constraint.constrain( + .{ + .width = glyph_width, + .height = glyph_height, + .x = glyph_x, + .y = glyph_y + @as(f64, @floatFromInt(metrics.cell_baseline)), + }, + cell_width, + cell_height, + ); + + const width = glyph_size.width; + const height = glyph_size.height; + // This may need to be adjusted later on. + var x = glyph_size.x; + const y = glyph_size.y; + + // Now we can render the glyph. + var bitmap: freetype.c.FT_Bitmap = undefined; + _ = freetype.c.FT_Bitmap_Init(&bitmap); + defer _ = freetype.c.FT_Bitmap_Done(self.lib.lib.handle, &bitmap); + switch (glyph.*.format) { + freetype.c.FT_GLYPH_FORMAT_OUTLINE => { + // Manually adjust the glyph outline with this transform. + // + // This offers better precision than using the freetype transform + // matrix, since that has 16.16 coefficients, and also I was having + // weird issues that I can only assume where due to freetype doing + // some bad caching or something when I did this using the matrix. + const scale_x = width / glyph_width; + const scale_y = height / glyph_height; + const skew: f64 = + if (self.synthetic.italic) + // We skew by 12 degrees to synthesize italics. + @tan(std.math.degreesToRadians(12)) + else + 0.0; + + var bbox_before: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_before); + + const outline = &glyph.*.outline; + for (outline.points[0..@intCast(outline.n_points)]) |*p| { + // Convert to f64 for processing + var px = f26dot6ToF64(p.x); + var py = f26dot6ToF64(p.y); + + // Scale + px *= scale_x; + py *= scale_y; + + // Skew + px += py * skew; + + // Convert back and store + p.x = @as(i32, @bitCast(F26Dot6.from(px))); + p.y = @as(i32, @bitCast(F26Dot6.from(py))); + } + + var bbox_after: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_after); + + // If our bounding box changed, account for the lsb difference. + // + // This can happen when we skew glyphs that have a bit sticking + // out to the left higher up, like the top of the T or the serif + // on the lower case l in many monospace fonts. + x += f26dot6ToF64(bbox_after.xMin) - f26dot6ToF64(bbox_before.xMin); + + try self.face.renderGlyph( + if (self.load_flags.monochrome) + .mono + else + .normal, + ); + + // Copy the glyph's bitmap, making sure + // that it's 8bpp and densely packed. + if (freetype.c.FT_Bitmap_Convert( + self.lib.lib.handle, + &glyph.*.bitmap, + &bitmap, + 1, + ) != 0) { + return error.BitmapHandlingError; + } + }, + + freetype.c.FT_GLYPH_FORMAT_BITMAP => { + // If our glyph has a non-color bitmap, we need + // to convert it to dense 8bpp so that the scale + // operation works correctly. + switch (glyph.*.bitmap.pixel_mode) { + freetype.c.FT_PIXEL_MODE_BGRA, + freetype.c.FT_PIXEL_MODE_GRAY, + => {}, + else => { + var converted: freetype.c.FT_Bitmap = undefined; + freetype.c.FT_Bitmap_Init(&converted); + if (freetype.c.FT_Bitmap_Convert( + self.lib.lib.handle, + &glyph.*.bitmap, + &converted, + 1, + ) != 0) { + return error.BitmapHandlingError; + } + // Free the existing glyph bitmap and + // replace it with the converted one. + _ = freetype.c.FT_Bitmap_Done( + self.lib.lib.handle, + &glyph.*.bitmap, + ); + glyph.*.bitmap = converted; + }, + } + + const glyph_bitmap = glyph.*.bitmap; + + // Round our target width and height + // as the size for our scaled bitmap. + const w: u32 = @intFromFloat(@round(width)); + const h: u32 = @intFromFloat(@round(height)); + const pitch = w * atlas.format.depth(); + + // Allocate a buffer for our scaled bitmap. + // + // We'll copy this to the original bitmap once we're + // done so we can free it at the end of this scope. + const buf = try alloc.alloc(u8, pitch * h); + defer alloc.free(buf); + + // Resize + if (stb.stbir_resize_uint8( + glyph_bitmap.buffer, + @intCast(glyph_bitmap.width), + @intCast(glyph_bitmap.rows), + glyph_bitmap.pitch, + buf.ptr, + @intCast(w), + @intCast(h), + @intCast(pitch), + atlas.format.depth(), + ) == 0) { + // This should never fail because this is a + // fairly straightforward in-memory operation... + return error.GlyphResizeFailed; + } + + const scaled_bitmap: freetype.c.FT_Bitmap = .{ + .buffer = buf.ptr, + .width = @intCast(w), + .rows = @intCast(h), + .pitch = @intCast(pitch), + .pixel_mode = glyph_bitmap.pixel_mode, + .num_grays = glyph_bitmap.num_grays, + }; + + // Replace the bitmap's buffer and size info. + if (freetype.c.FT_Bitmap_Copy( + self.lib.lib.handle, + &scaled_bitmap, + &bitmap, + ) != 0) { + return error.BitmapHandlingError; + } + }, + + else => |f| { + // Glyph formats are tags, so we can + // output a semi-readable error here. + log.err( + "Can't render glyph with unsupported glyph format \"{s}\"", + .{[4]u8{ + @truncate(f >> 24), + @truncate(f >> 16), + @truncate(f >> 8), + @truncate(f >> 0), + }}, + ); + return error.UnsupportedGlyphFormat; + }, + } + + // If this is a color glyph but we're trying to render it to the + // grayscale atlas, or vice versa, then we throw and error. Maybe + // in the future we could convert, but for now it should be fine. + switch (bitmap.pixel_mode) { + freetype.c.FT_PIXEL_MODE_GRAY => if (atlas.format != .grayscale) { + return error.WrongAtlas; + }, + freetype.c.FT_PIXEL_MODE_BGRA => if (atlas.format != .bgra) { + return error.WrongAtlas; + }, else => { - log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode }); + log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap.pixel_mode }); @panic("unsupported pixel mode"); }, - }; - - // If our atlas format doesn't match, look for conversions if possible. - const bitmap_converted = if (format == null or atlas.format != format.?) blk: { - const func = convert.map[bitmap_ft.pixel_mode].get(atlas.format) orelse { - log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode }); - return error.UnsupportedPixelMode; - }; - - log.debug("converting from pixel_mode={} to atlas_format={}", .{ - bitmap_ft.pixel_mode, - atlas.format, - }); - break :blk try func(alloc, bitmap_ft); - } else null; - defer if (bitmap_converted) |bm| { - const len = @as(usize, @intCast(bm.pitch)) * @as(usize, @intCast(bm.rows)); - alloc.free(bm.buffer[0..len]); - }; - - // Now we need to see if we need to resize this bitmap. This can happen - // in scenarios where we have fixed size glyphs. For example, emoji - // can be quite large (i.e. 128x128) when we have a cell width of 24! - // The issue with large bitmaps is they take a huge amount of space in - // the atlas and force resizes quite frequently. We pay some CPU cost - // up front to resize the glyph to avoid significant CPU cost to resize - // and copy the atlas. - const bitmap_original = bitmap_converted orelse bitmap_ft; - const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: { - const original_width = bitmap_original.width; - const original_height = bitmap_original.rows; - var result = bitmap_original; - // TODO: We are limiting this to only color glyphs, so mainly emoji. - // We can rework this after a future improvement (promised by Qwerasd) - // which implements more flexible resizing rules. - if (atlas.format != .grayscale and opts.cell_width != null) { - const cell_width = opts.cell_width orelse unreachable; - // If we have a cell_width, we constrain - // the glyph to fit within the cell(s). - result.width = metrics.cell_width * @as(u32, cell_width); - result.rows = (result.width * original_height) / original_width; - } else { - // If we don't have a cell_width, we scale to fill vertically - result.rows = metrics.cell_height; - result.width = (metrics.cell_height * original_width) / original_height; - } - - // If we already fit, we don't need to resize - if (original_height <= result.rows and original_width <= result.width) { - break :resized null; - } - - result.pitch = @as(c_int, @intCast(result.width)) * atlas.format.depth(); - - const buf = try alloc.alloc( - u8, - @as(usize, @intCast(result.pitch)) * @as(usize, @intCast(result.rows)), - ); - result.buffer = buf.ptr; - errdefer alloc.free(buf); - - if (stb.stbir_resize_uint8( - bitmap_original.buffer, - @intCast(original_width), - @intCast(original_height), - bitmap_original.pitch, - result.buffer, - @intCast(result.width), - @intCast(result.rows), - result.pitch, - atlas.format.depth(), - ) == 0) { - // This should never fail because this is a fairly straightforward - // in-memory operation... - return error.GlyphResizeFailed; - } - - break :resized result; - }; - defer if (bitmap_resized) |bm| { - const len = @as(usize, @intCast(bm.pitch)) * @as(usize, @intCast(bm.rows)); - alloc.free(bm.buffer[0..len]); - }; - - const bitmap = bitmap_resized orelse (bitmap_converted orelse bitmap_ft); - const tgt_w = bitmap.width; - const tgt_h = bitmap.rows; - - // Must have non-empty bitmap because we return earlier - // if zero. We assume the rest of this that it is nont-zero so - // this is important. - assert(tgt_w > 0 and tgt_h > 0); - - // If we resized our bitmap, we need to recalculate some metrics that - // we use such as the top/left offsets. These need to be scaled by the - // same ratio as the resize. - const glyph_metrics = if (bitmap_resized) |bm| metrics: { - // Our ratio for the resize - const ratio = ratio: { - const new: f64 = @floatFromInt(bm.rows); - const old: f64 = @floatFromInt(bitmap_original.rows); - break :ratio new / old; - }; - - var copy = glyph.*; - copy.bitmap_top = @as(c_int, @intFromFloat(@round(@as(f64, @floatFromInt(copy.bitmap_top)) * ratio))); - copy.bitmap_left = @as(c_int, @intFromFloat(@round(@as(f64, @floatFromInt(copy.bitmap_left)) * ratio))); - break :metrics copy; - } else glyph.*; - - // Allocate our texture atlas region - const region = region: { - // We need to add a 1px padding to the font so that we don't - // get fuzzy issues when blending textures. - const padding = 1; - - // Get the full padded region - var region = try atlas.reserve( - alloc, - tgt_w + (padding * 2), // * 2 because left+right - tgt_h + (padding * 2), // * 2 because top+bottom - ); - - // Modify the region so that we remove the padding so that - // we write to the non-zero location. The data in an Altlas - // is always initialized to zero (Atlas.clear) so we don't - // need to worry about zero-ing that. - region.x += padding; - region.y += padding; - region.width -= padding * 2; - region.height -= padding * 2; - break :region region; - }; - - // Copy the image into the region. - assert(region.width > 0 and region.height > 0); - { - const depth = atlas.format.depth(); - - // We can avoid a buffer copy if our atlas width and bitmap - // width match and the bitmap pitch is just the width (meaning - // the data is tightly packed). - const needs_copy = !(tgt_w == bitmap.width and (bitmap.width * depth) == bitmap.pitch); - - // If we need to copy the data, we copy it into a temporary buffer. - const buffer = if (needs_copy) buffer: { - const temp = try alloc.alloc(u8, tgt_w * tgt_h * depth); - var dst_ptr = temp; - var src_ptr = bitmap.buffer; - var i: usize = 0; - while (i < bitmap.rows) : (i += 1) { - fastmem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]); - dst_ptr = dst_ptr[tgt_w * depth ..]; - src_ptr += @as(usize, @intCast(bitmap.pitch)); - } - break :buffer temp; - } else bitmap.buffer[0..(tgt_w * tgt_h * depth)]; - defer if (buffer.ptr != bitmap.buffer) alloc.free(buffer); - - // Write the glyph information into the atlas - assert(region.width == tgt_w); - assert(region.height == tgt_h); - atlas.set(region, buffer); } - const offset_y: c_int = offset_y: { - // For non-scalable colorized fonts, we assume they are pictographic - // and just center the glyph. So far this has only applied to emoji - // fonts. Emoji fonts don't always report a correct ascender/descender - // (mainly Apple Emoji) so we just center them. Also, since emoji font - // aren't scalable, cell_baseline is incorrect anyways. - // - // NOTE(mitchellh): I don't know if this is right, this doesn't - // _feel_ right, but it makes all my limited test cases work. - if (self.face.hasColor() and !self.face.isScalable()) { - break :offset_y @intCast(tgt_h + (metrics.cell_height -| tgt_h) / 2); + const px_width = bitmap.width; + const px_height = bitmap.rows; + const len: usize = @intCast( + @as(c_uint, @intCast(@abs(bitmap.pitch))) * bitmap.rows, + ); + + // If our bitmap is grayscale, make sure to multiply all pixel + // values by the right factor to bring `num_grays` up to 256. + // + // This is necessary because FT_Bitmap_Convert doesn't do this, + // it just sets num_grays to the correct number and uses the + // original smaller pixel values. + if (bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY and + bitmap.num_grays < 256) + { + const factor: u8 = @intCast(255 / (bitmap.num_grays - 1)); + for (bitmap.buffer[0..len]) |*p| { + p.* *= factor; } + bitmap.num_grays = 256; + } - // The Y offset is the offset of the top of our bitmap PLUS our - // baseline calculation. The baseline calculation is so that everything - // is properly centered when we render it out into a monospace grid. - // Note: we add here because our X/Y is actually reversed, adding goes UP. - break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intCast(metrics.cell_baseline)); - }; + // Must have non-empty bitmap because we return earlier if zero. + // We assume the rest of this that it is non-zero so this is important. + assert(px_width > 0 and px_height > 0); + // If this doesn't match then something is wrong. + assert(px_width * atlas.format.depth() == bitmap.pitch); + + // Allocate our texture atlas region and copy our bitmap in to it. + const region = try atlas.reserve(alloc, px_width, px_height); + atlas.set(region, bitmap.buffer[0..len]); + + // This should be the distance from the bottom of + // the cell to the top of the glyph's bounding box. + const offset_y: i32 = + @as(i32, @intFromFloat(@floor(y))) + + @as(i32, @intCast(px_height)); + + // This should be the distance from the left of + // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = glyph_metrics.bitmap_left; + var result: i32 = @intFromFloat(@floor(x)); - // If our cell was resized to be wider then we center our - // glyph in the cell. + // If our cell was resized then we adjust our glyph's + // position relative to the new center. This keeps glyphs + // centered in the cell whether it was made wider or narrower. if (metrics.original_cell_width) |original_width| { - if (original_width < metrics.cell_width) { - const diff = (metrics.cell_width - original_width) / 2; - result += @intCast(diff); - } + const before: i32 = @intCast(original_width); + const after: i32 = @intCast(metrics.cell_width); + // Increase the offset by half of the difference + // between the widths to keep things centered. + result += @divTrunc(after - before, 2); } break :offset_x result; }; - // log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{ - // tgt_w, - // tgt_h, - // glyph_metrics.bitmap_left, - // offset_y, - // glyph_metrics, - // }); - - // Store glyph metadata return Glyph{ - .width = tgt_w, - .height = tgt_h, + .width = px_width, + .height = px_height, .offset_x = offset_x, .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = f26dot6ToFloat(glyph_metrics.advance.x), + .advance_x = f26dot6ToFloat(glyph.*.advance.x), }; } @@ -631,7 +681,7 @@ pub const Face = struct { } fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 { - return @as(opentype.sfnt.F26Dot6, @bitCast(@as(u32, @intCast(v)))).to(f64); + return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64); } pub const GetMetricsError = error{ @@ -950,13 +1000,15 @@ test "color emoji" { } // resize + // TODO: Comprehensive tests for constraints, + // this is just an adapted legacy test. { const glyph = try ft_font.renderGlyph( alloc, &atlas, ft_font.glyphIndex('🥸').?, .{ .grid_metrics = .{ - .cell_width = 10, + .cell_width = 13, .cell_height = 24, .cell_baseline = 0, .underline_position = 0, @@ -967,6 +1019,11 @@ test "color emoji" { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, + }, .constraint_width = 2, .constraint = .{ + .size_horizontal = .cover, + .size_vertical = .cover, + .align_horizontal = .center, + .align_vertical = .center, } }, ); try testing.expectEqual(@as(u32, 24), glyph.height);