mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-27 20:08:41 +03:00
font/freetype: Enable bitmap glyphs for non-color faces (#3837)
This allows for crisp bitmap font rendering once again. Tested with Terminus (TTF), and at both 1x and 2x DPI the font renders perfectly (click for 1:1 size):  <img alt="Terminus (TTF), size 12, 192 DPI" src="https://github.com/user-attachments/assets/d34804fe-4966-42e8-bf9e-bd10570ad443" width="384" height="50" />
This commit is contained in:
@ -34,3 +34,6 @@ pub const cozette = @embedFile("res/CozetteVector.ttf");
|
||||
/// Monaspace has weird ligature behaviors we want to test in our shapers
|
||||
/// so we embed it here.
|
||||
pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf");
|
||||
|
||||
/// Terminus TTF is a scalable font with bitmap glyphs at various sizes.
|
||||
pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf");
|
||||
|
@ -288,7 +288,6 @@ pub const Face = struct {
|
||||
self.face.loadGlyph(glyph_id, .{
|
||||
.render = true,
|
||||
.color = self.face.hasColor(),
|
||||
.no_bitmap = !self.face.hasColor(),
|
||||
}) catch return false;
|
||||
|
||||
// If the glyph is SVG we assume colorized
|
||||
@ -323,14 +322,6 @@ pub const Face = struct {
|
||||
// glyph properties before render so we don't render here.
|
||||
.render = !self.synthetic.bold,
|
||||
|
||||
// Disable bitmap strikes for now since it causes issues with
|
||||
// our cell metrics and rasterization. In the future, this is
|
||||
// all fixable so we can enable it.
|
||||
//
|
||||
// This must be enabled for color faces though because those are
|
||||
// often colored bitmaps, which we support.
|
||||
.no_bitmap = !self.face.hasColor(),
|
||||
|
||||
// use options from config
|
||||
.no_hinting = !self.load_flags.hinting,
|
||||
.force_autohint = !self.load_flags.@"force-autohint",
|
||||
@ -385,7 +376,7 @@ pub const Face = struct {
|
||||
return error.UnsupportedPixelMode;
|
||||
};
|
||||
|
||||
log.warn("converting from pixel_mode={} to atlas_format={}", .{
|
||||
log.debug("converting from pixel_mode={} to atlas_format={}", .{
|
||||
bitmap_ft.pixel_mode,
|
||||
atlas.format,
|
||||
});
|
||||
@ -1005,3 +996,55 @@ test "svg font table" {
|
||||
|
||||
try testing.expectEqual(430, table.len);
|
||||
}
|
||||
|
||||
const terminus_i =
|
||||
\\........
|
||||
\\........
|
||||
\\...#....
|
||||
\\...#....
|
||||
\\........
|
||||
\\..##....
|
||||
\\...#....
|
||||
\\...#....
|
||||
\\...#....
|
||||
\\...#....
|
||||
\\...#....
|
||||
\\..###...
|
||||
\\........
|
||||
\\........
|
||||
\\........
|
||||
\\........
|
||||
;
|
||||
// Including the newline
|
||||
const terminus_i_pitch = 9;
|
||||
|
||||
test "bitmap glyph" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.terminus_ttf;
|
||||
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
// Any glyph at 12pt @ 96 DPI is a bitmap
|
||||
var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer ft_font.deinit();
|
||||
|
||||
// glyph 77 = 'i'
|
||||
const glyph = try ft_font.renderGlyph(alloc, &atlas, 77, .{});
|
||||
|
||||
// should render crisp
|
||||
try testing.expectEqual(8, glyph.width);
|
||||
try testing.expectEqual(16, glyph.height);
|
||||
for (0..glyph.height) |y| {
|
||||
for (0..glyph.width) |x| {
|
||||
const pixel = terminus_i[y * terminus_i_pitch + x];
|
||||
try testing.expectEqual(
|
||||
@as(u8, if (pixel == '#') 255 else 0),
|
||||
atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,28 +43,16 @@ pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
|
||||
var buf = try alloc.alloc(u8, bm.width * bm.rows);
|
||||
errdefer alloc.free(buf);
|
||||
|
||||
// width divided by 8 because each byte has 8 pixels. This is therefore
|
||||
// the number of bytes in each row.
|
||||
const bytes_per_row = bm.width >> 3;
|
||||
|
||||
var source_i: usize = 0;
|
||||
var target_i: usize = 0;
|
||||
var i: usize = bm.rows;
|
||||
while (i > 0) : (i -= 1) {
|
||||
var j: usize = bytes_per_row;
|
||||
while (j > 0) : (j -= 1) {
|
||||
var bit: u4 = 8;
|
||||
while (bit > 0) : (bit -= 1) {
|
||||
const mask = @as(u8, 1) << @as(u3, @intCast(bit - 1));
|
||||
const bitval: u8 = if (bm.buffer[source_i + (j - 1)] & mask > 0) 0xFF else 0;
|
||||
buf[target_i] = bitval;
|
||||
target_i += 1;
|
||||
for (0..bm.rows) |y| {
|
||||
const row_offset = y * @as(usize, @intCast(bm.pitch));
|
||||
for (0..bm.width) |x| {
|
||||
const byte_offset = row_offset + @divTrunc(x, 8);
|
||||
const mask = @as(u8, 1) << @intCast(7 - (x % 8));
|
||||
const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
|
||||
buf[y * bm.width + x] = bit * 255;
|
||||
}
|
||||
}
|
||||
|
||||
source_i += @intCast(bm.pitch);
|
||||
}
|
||||
|
||||
var copy = bm;
|
||||
copy.buffer = buf.ptr;
|
||||
copy.pixel_mode = freetype.c.FT_PIXEL_MODE_GRAY;
|
||||
|
@ -25,6 +25,9 @@ This project uses several fonts which fall under the SIL Open Font License (OFL-
|
||||
- [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE)
|
||||
- Cozette (MIT)
|
||||
- [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE)
|
||||
- Terminus TTF (OFL-1.1)
|
||||
- [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/)
|
||||
- [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/)
|
||||
|
||||
A full copy of the OFL license can be found at [OFL.txt](./OFL.txt).
|
||||
An accompanying FAQ is also available at <https://openfontlicense.org/>.
|
||||
|
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
Binary file not shown.
Reference in New Issue
Block a user