diff --git a/build.zig.zon b/build.zig.zon index fc1bf08fd..e98d878f9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -99,6 +99,16 @@ .lazy = true, }, + // Fonts + .jetbrains_mono = .{ + .url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz", + .hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x", + }, + .nerd_fonts_symbols_only = .{ + .url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz", + .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s", + }, + // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 814b20b90..0b904de83 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,6 +54,11 @@ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz", "hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4=" }, + "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { + "name": "jetbrains_mono", + "url": "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz", + "hash": "sha256-xXppHouCrQmLWWPzlZAy5AOPORCHr3cViFulkEYQXMQ=" + }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", @@ -69,6 +74,11 @@ "url": "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz", "hash": "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU=" }, + "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s": { + "name": "nerd_fonts_symbols_only", + "url": "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz", + "hash": "sha256-EWTRuVbUveJI17LwmYxDzJT1ICQxoVZKeTiVsec7DQQ=" + }, "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c": { "name": "oniguruma", "url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 9d50a6fc6..52260181e 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -169,6 +169,14 @@ in hash = "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="; }; } + { + name = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x"; + path = fetchZigArtifact { + name = "jetbrains_mono"; + url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz"; + hash = "sha256-xXppHouCrQmLWWPzlZAy5AOPORCHr3cViFulkEYQXMQ="; + }; + } { name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD"; path = fetchZigArtifact { @@ -193,6 +201,14 @@ in hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="; }; } + { + name = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s"; + path = fetchZigArtifact { + name = "nerd_fonts_symbols_only"; + url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz"; + hash = "sha256-EWTRuVbUveJI17LwmYxDzJT1ICQxoVZKeTiVsec7DQQ="; + }; + } { name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 16180c39c..4427763d8 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -2,6 +2,8 @@ git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23 https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz +https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz +https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 8a911bb55..0848d72d4 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -65,6 +65,12 @@ "dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS", "sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e" }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz", + "dest": "vendor/p/N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x", + "sha256": "c57a691e8b82ad098b5963f3959032e4038f391087af7715885ba59046105cc4" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", @@ -83,6 +89,12 @@ "dest": "vendor/p/N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK", "sha256": "6c28059e2e3eeb42b5b4b16489e3916a6346c1095a74fee3bc65cdc5d89a6215" }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz", + "dest": "vendor/p/N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s", + "sha256": "1164d1b956d4bde248d7b2f0998c43cc94f5202431a1564a793895b1e73b0d04" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz", 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/build/SharedDeps.zig b/src/build/SharedDeps.zig index 5d620ac75..0aab5ecf8 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -500,6 +500,43 @@ pub fn add( try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin()); } + // Fonts + { + // JetBrains Mono + const jb_mono = b.dependency("jetbrains_mono", .{}); + step.root_module.addAnonymousImport( + "jetbrains_mono_regular", + .{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Regular.ttf") }, + ); + step.root_module.addAnonymousImport( + "jetbrains_mono_bold", + .{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Bold.ttf") }, + ); + step.root_module.addAnonymousImport( + "jetbrains_mono_italic", + .{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Italic.ttf") }, + ); + step.root_module.addAnonymousImport( + "jetbrains_mono_bold_italic", + .{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-BoldItalic.ttf") }, + ); + step.root_module.addAnonymousImport( + "jetbrains_mono_variable", + .{ .root_source_file = jb_mono.path("fonts/variable/JetBrainsMono[wght].ttf") }, + ); + step.root_module.addAnonymousImport( + "jetbrains_mono_variable_italic", + .{ .root_source_file = jb_mono.path("fonts/variable/JetBrainsMono-Italic[wght].ttf") }, + ); + + // Symbols-only nerd font + const nf_symbols = b.dependency("nerd_fonts_symbols_only", .{}); + step.root_module.addAnonymousImport( + "nerd_fonts_symbols_only", + .{ .root_source_file = nf_symbols.path("SymbolsNerdFontMono-Regular.ttf") }, + ); + } + // If we're building an exe then we have additional dependencies. if (step.kind != .lib) { // We always statically compile glad diff --git a/src/config/Config.zig b/src/config/Config.zig index 7a08b5583..653ce4178 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -432,13 +432,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 = .{}, @@ -7082,7 +7085,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/SharedGrid.zig b/src/font/SharedGrid.zig index dcfa0a551..3ccac7fa1 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -265,13 +265,35 @@ pub fn renderGlyph( .emoji => &self.atlas_color, }; + var render_opts = opts; + + // Always use these constraints for emoji. + if (p == .emoji) { + render_opts.constraint = .{ + // Make the emoji as wide as possible, scaling proportionally, + // but then scale it down as necessary if its new size exceeds + // the cell height. + .size_horizontal = .cover, + .size_vertical = .fit, + + // Center the emoji in its cells. + .align_horizontal = .center, + .align_vertical = .center, + + // Add a small bit of padding so the emoji + // doesn't quite touch the edges of the cells. + .pad_left = 0.025, + .pad_right = 0.025, + }; + } + // Render into the atlas const glyph = self.resolver.renderGlyph( alloc, atlas, index, glyph_index, - opts, + render_opts, ) catch |err| switch (err) { // If the atlas is full, we resize it error.AtlasFull => blk: { @@ -281,7 +303,7 @@ pub fn renderGlyph( atlas, index, glyph_index, - opts, + render_opts, ); }, @@ -325,7 +347,8 @@ const GlyphKey = struct { cell_width: u2, thicken: bool, thicken_strength: u8, - _padding: u5 = 0, + constraint_width: u2, + _padding: u3 = 0, }, inline fn from(key: GlyphKey) Packed { @@ -336,6 +359,7 @@ const GlyphKey = struct { .cell_width = key.opts.cell_width orelse 0, .thicken = key.opts.thicken, .thicken_strength = key.opts.thicken_strength, + .constraint_width = key.opts.constraint_width, }, }; } diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index e3e61907b..b77b44f23 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -260,34 +260,51 @@ fn collection( .regular, .{ .fallback_loaded = try .init( self.font_lib, - font.embedded.regular, + font.embedded.variable, load_options.faceOptions(), ) }, ); - _ = try c.add( + try (try c.getFace(try c.add( self.alloc, .bold, .{ .fallback_loaded = try .init( self.font_lib, - font.embedded.bold, + font.embedded.variable, load_options.faceOptions(), ) }, + ))).setVariations( + &.{.{ .id = .init("wght"), .value = 700 }}, + load_options.faceOptions(), ); _ = try c.add( self.alloc, .italic, .{ .fallback_loaded = try .init( self.font_lib, - font.embedded.italic, + font.embedded.variable_italic, load_options.faceOptions(), ) }, ); - _ = try c.add( + try (try c.getFace(try c.add( self.alloc, .bold_italic, .{ .fallback_loaded = try .init( self.font_lib, - font.embedded.bold_italic, + font.embedded.variable_italic, + load_options.faceOptions(), + ) }, + ))).setVariations( + &.{.{ .id = .init("wght"), .value = 700 }}, + load_options.faceOptions(), + ); + + // Nerd-font symbols fallback. + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + font.embedded.symbols_nerd_font, load_options.faceOptions(), ) }, ); diff --git a/src/font/embedded.zig b/src/font/embedded.zig index 31b07ff31..1e496075d 100644 --- a/src/font/embedded.zig +++ b/src/font/embedded.zig @@ -6,19 +6,29 @@ //! redistribution and include their license as necessary. /// Default fonts that we prefer for Ghostty. -pub const regular = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf"); -pub const bold = @embedFile("res/JetBrainsMonoNerdFont-Bold.ttf"); -pub const italic = @embedFile("res/JetBrainsMonoNerdFont-Italic.ttf"); -pub const bold_italic = @embedFile("res/JetBrainsMonoNerdFont-BoldItalic.ttf"); +pub const variable = @embedFile("jetbrains_mono_variable"); +pub const variable_italic = @embedFile("jetbrains_mono_variable_italic"); + +/// Symbols-only nerd font. +pub const symbols_nerd_font = @embedFile("nerd_fonts_symbols_only"); + +/// Static jetbrains mono faces, currently unused. +pub const regular = @embedFile("jetbrains_mono_regular"); +pub const bold = @embedFile("jetbrains_mono_bold"); +pub const italic = @embedFile("jetbrains_mono_italic"); +pub const bold_italic = @embedFile("jetbrains_mono_bold_italic"); + +/// Emoji fonts pub const emoji = @embedFile("res/NotoColorEmoji.ttf"); pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf"); +// Fonts below are ONLY used for testing. + /// Fonts with general properties pub const arabic = @embedFile("res/KawkabMono-Regular.ttf"); -pub const variable = @embedFile("res/Lilex-VF.ttf"); -/// Font with nerd fonts embedded. -pub const nerd_font = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf"); +/// A font for testing which is patched with nerd font symbols. +pub const test_nerd_font = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf"); /// Specific font families below: pub const code_new_roman = @embedFile("res/CodeNewRoman-Regular.otf"); 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..c23ede04a 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -15,12 +15,13 @@ const Allocator = std.mem.Allocator; const font = @import("../main.zig"); const Glyph = font.Glyph; const Library = font.Library; -const convert = @import("freetype_convert.zig"); const opentype = @import("../opentype.zig"); 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 +59,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 +323,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 +358,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 +680,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 +999,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 +1018,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); diff --git a/src/font/face/freetype_convert.zig b/src/font/face/freetype_convert.zig deleted file mode 100644 index 3a7cf8c98..000000000 --- a/src/font/face/freetype_convert.zig +++ /dev/null @@ -1,88 +0,0 @@ -//! Various conversions from Freetype formats to Atlas formats. These are -//! currently implemented naively. There are definitely MUCH faster ways -//! to do this (likely using SIMD), but I started simple. -const std = @import("std"); -const freetype = @import("freetype"); -const font = @import("../main.zig"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -/// The mapping from freetype format to atlas format. -pub const map = genMap(); - -/// The map type. -pub const Map = [freetype.c.FT_PIXEL_MODE_MAX]AtlasArray; - -/// Conversion function type. The returning bitmap buffer is guaranteed -/// to be exactly `width * rows * depth` long for freeing it. The caller must -/// free the bitmap buffer. The depth is the depth of the atlas format in the -/// map. -pub const Func = *const fn (Allocator, Bitmap) Allocator.Error!Bitmap; - -/// Alias for the freetype FT_Bitmap type to make it easier to type. -pub const Bitmap = freetype.c.struct_FT_Bitmap_; - -const AtlasArray = std.EnumArray(font.Atlas.Format, ?Func); - -fn genMap() Map { - var result: Map = undefined; - - // Initialize to no converter - var i: usize = 0; - while (i < freetype.c.FT_PIXEL_MODE_MAX) : (i += 1) { - result[i] = .initFill(null); - } - - // Map our converters - result[freetype.c.FT_PIXEL_MODE_MONO].set(.grayscale, monoToGrayscale); - - return result; -} - -pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap { - var buf = try alloc.alloc(u8, bm.width * bm.rows); - errdefer alloc.free(buf); - - 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; - } - } - - var copy = bm; - copy.buffer = buf.ptr; - copy.pixel_mode = freetype.c.FT_PIXEL_MODE_GRAY; - copy.pitch = @as(c_int, @intCast(bm.width)); - return copy; -} - -test { - // Force comptime to run - _ = map; -} - -test "mono to grayscale" { - const testing = std.testing; - const alloc = testing.allocator; - - var mono_data = [_]u8{0b1010_0101}; - const source: Bitmap = .{ - .rows = 1, - .width = 8, - .pitch = 1, - .buffer = @ptrCast(&mono_data), - .num_grays = 0, - .pixel_mode = freetype.c.FT_PIXEL_MODE_MONO, - .palette_mode = 0, - .palette = null, - }; - - const result = try monoToGrayscale(alloc, source); - defer alloc.free(result.buffer[0..(result.width * result.rows)]); - try testing.expect(result.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY); - try testing.expectEqual(@as(u8, 255), result.buffer[0]); -} diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig new file mode 100644 index 000000000..1465a8466 --- /dev/null +++ b/src/font/nerd_font_attributes.zig @@ -0,0 +1,349 @@ +//! This is a generate file, produced by nerd_font_codegen.py +//! DO NOT EDIT BY HAND! +//! +//! This file provides info extracted from the nerd fonts patcher script, +//! specifying the scaling/positioning attributes of various glyphs. + +const Constraint = @import("face.zig").RenderOptions.Constraint; + +/// Get the a constraints for the provided codepoint. +pub fn getConstraint(cp: u21) Constraint { + return switch (cp) { + 0x2500...0x259f, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .center, + .align_vertical = .center, + .pad_left = -0.02, + .pad_right = -0.02, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0x2630, + => .{ + .size_horizontal = .cover, + .size_vertical = .fit, + .align_horizontal = .center, + .align_vertical = .center, + .pad_left = 0.1, + .pad_right = 0.1, + .pad_top = 0.01, + .pad_bottom = 0.01, + }, + 0x276c...0x2771, + => .{ + .size_horizontal = .cover, + .size_vertical = .fit, + .align_horizontal = .center, + .align_vertical = .center, + }, + 0xe0b0, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.06, + .pad_right = -0.06, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0xe0b1, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .max_xy_ratio = 0.7, + }, + 0xe0b2, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.06, + .pad_right = -0.06, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0xe0b3, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .max_xy_ratio = 0.7, + }, + 0xe0b4, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.06, + .pad_right = -0.06, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.59, + }, + 0xe0b5, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .max_xy_ratio = 0.5, + }, + 0xe0b6, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.06, + .pad_right = -0.06, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.59, + }, + 0xe0b7, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .max_xy_ratio = 0.5, + }, + 0xe0b8, + 0xe0bc, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xe0b9, + 0xe0bd, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + }, + 0xe0ba, + 0xe0be, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xe0bb, + 0xe0bf, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + }, + 0xe0c0, + 0xe0c8, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xe0c1, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + }, + 0xe0c2, + 0xe0ca, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + }, + 0xe0c3, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + }, + 0xe0c4, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.01, + .pad_bottom = 0.01, + .max_xy_ratio = 0.86, + }, + 0xe0c5, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.01, + .pad_bottom = 0.01, + .max_xy_ratio = 0.86, + }, + 0xe0c6, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.01, + .pad_bottom = 0.01, + .max_xy_ratio = 0.78, + }, + 0xe0c7, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = 0.03, + .pad_right = 0.03, + .pad_top = 0.01, + .pad_bottom = 0.01, + .max_xy_ratio = 0.78, + }, + 0xe0cc, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.02, + .pad_right = -0.02, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.85, + }, + 0xe0cd, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .max_xy_ratio = 0.865, + }, + 0xe0ce, + 0xe0d0...0xe0d1, + => .{ + .size_horizontal = .cover, + .size_vertical = .cover, + .align_horizontal = .start, + .align_vertical = .center, + }, + 0xe0cf, + 0xe0d3, + 0xe0d5, + => .{ + .size_horizontal = .cover, + .size_vertical = .cover, + .align_horizontal = .center, + .align_vertical = .center, + }, + 0xe0d2, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.02, + .pad_right = -0.02, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0xe0d4, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.02, + .pad_right = -0.02, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0xe0d6, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .start, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0xe0d7, + => .{ + .size_horizontal = .stretch, + .size_vertical = .stretch, + .align_horizontal = .end, + .align_vertical = .center, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.01, + .pad_bottom = -0.01, + .max_xy_ratio = 0.7, + }, + 0x23fb...0x23fe, + 0x2665, + 0x26a1, + 0x2b58, + 0xe000...0xe0a9, + 0xe4fa...0xe7ef, + 0xea60...0xec1e, + 0xed00...0xf847, + 0xf0001...0xf1af0, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .align_horizontal = .center, + .align_vertical = .center, + }, + else => .none, + }; +} diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py new file mode 100644 index 000000000..c2dd7314f --- /dev/null +++ b/src/font/nerd_font_codegen.py @@ -0,0 +1,259 @@ +""" +This file is mostly vibe coded because I don't like Python. It extracts the +patch sets from the nerd fonts font patcher file in order to extract scaling +rules and attributes for different codepoint ranges which it then codegens +in to a Zig file with a function that switches over codepoints and returns +the attributes and scaling rules. + +This does include an `eval` call! This is spooky, but we trust +the nerd fonts code to be safe and not malicious or anything. +""" + +import ast +import math +from pathlib import Path +from collections import defaultdict + + +class PatchSetExtractor(ast.NodeVisitor): + def __init__(self): + self.symbol_table = {} + self.patch_set_values = [] + + def visit_ClassDef(self, node): + if node.name == "font_patcher": + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set": + self.visit_setup_patch_set(item) + + def visit_setup_patch_set(self, node): + # First pass: gather variable assignments + for stmt in node.body: + if isinstance(stmt, ast.Assign): + # Store simple variable assignments in the symbol table + if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name): + var_name = stmt.targets[0].id + self.symbol_table[var_name] = stmt.value + + # Second pass: process self.patch_set + for stmt in node.body: + if isinstance(stmt, ast.Assign): + for target in stmt.targets: + if isinstance(target, ast.Attribute) and target.attr == "patch_set": + if isinstance(stmt.value, ast.List): + for elt in stmt.value.elts: + if isinstance(elt, ast.Dict): + self.process_patch_entry(elt) + + def resolve_symbol(self, node): + """Resolve named variables to their actual values from the symbol table.""" + if isinstance(node, ast.Name) and node.id in self.symbol_table: + return self.safe_literal_eval(self.symbol_table[node.id]) + return self.safe_literal_eval(node) + + def safe_literal_eval(self, node): + """Try to evaluate or stringify an AST node.""" + try: + return ast.literal_eval(node) + except Exception: + # Spooky eval! But we trust nerd fonts to be safe... + if hasattr(ast, "unparse"): + return eval( + ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()} + ) + else: + return f"" + + def process_patch_entry(self, dict_node): + entry = {} + for key_node, value_node in zip(dict_node.keys, dict_node.values): + if isinstance(key_node, ast.Constant) and key_node.value in ( + "Enabled", + "Name", + "Filename", + "Exact", + ): + continue + key = ast.literal_eval(key_node) + value = self.resolve_symbol(value_node) + entry[key] = value + self.patch_set_values.append(entry) + + +def extract_patch_set_values(source_code): + tree = ast.parse(source_code) + extractor = PatchSetExtractor() + extractor.visit(tree) + return extractor.patch_set_values + + +# We have to spoof `self` and `self.args` for the eval. +class SpoofArgs: + careful = True + + +class SpoofSelf: + args = SpoofArgs() + + +def parse_alignment(val): + return { + "l": ".start", + "r": ".end", + "c": ".center", + "": None, + }.get(val, ".none") + + +def get_param(d, key, default): + return float(d.get(key, default)) + + +def attr_key(attr): + """Convert attributes to a hashable key for grouping.""" + stretch = attr.get("stretch", "") + return ( + parse_alignment(attr.get("align", "")), + parse_alignment(attr.get("valign", "")), + stretch, + float(attr.get("params", {}).get("overlap", 0.0)), + float(attr.get("params", {}).get("xy-ratio", -1.0)), + float(attr.get("params", {}).get("ypadding", 0.0)), + ) + + +def coalesce_codepoints_to_ranges(codepoints): + """Convert a sorted list of integers to a list of single values and ranges.""" + ranges = [] + cp_iter = iter(sorted(codepoints)) + try: + start = prev = next(cp_iter) + for cp in cp_iter: + if cp == prev + 1: + prev = cp + else: + ranges.append((start, prev)) + start = prev = cp + ranges.append((start, prev)) + except StopIteration: + pass + return ranges + + +def emit_zig_entry_multikey(codepoints, attr): + align = parse_alignment(attr.get("align", "")) + valign = parse_alignment(attr.get("valign", "")) + stretch = attr.get("stretch", "") + params = attr.get("params", {}) + + overlap = get_param(params, "overlap", 0.0) + xy_ratio = get_param(params, "xy-ratio", -1.0) + y_padding = get_param(params, "ypadding", 0.0) + + ranges = coalesce_codepoints_to_ranges(codepoints) + keys = "\n".join( + f" 0x{start:x}...0x{end:x}," if start != end else f" 0x{start:x}," + for start, end in ranges + ) + + s = f"""{keys} + => .{{\n""" + + # These translations don't quite capture the way + # the actual patcher does scaling, but they're a + # good enough compromise. + if ("xy" in stretch): + s += " .size_horizontal = .stretch,\n" + s += " .size_vertical = .stretch,\n" + elif ("!" in stretch): + s += " .size_horizontal = .cover,\n" + s += " .size_vertical = .fit,\n" + elif ("^" in stretch): + s += " .size_horizontal = .cover,\n" + s += " .size_vertical = .cover,\n" + else: + s += " .size_horizontal = .fit,\n" + s += " .size_vertical = .fit,\n" + + if (align is not None): + s += f" .align_horizontal = {align},\n" + if (valign is not None): + s += f" .align_vertical = {valign},\n" + + if (overlap != 0.0): + pad = -overlap + s += f" .pad_left = {pad},\n" + s += f" .pad_right = {pad},\n" + v_pad = y_padding - math.copysign(min(0.01, abs(overlap)), overlap) + s += f" .pad_top = {v_pad},\n" + s += f" .pad_bottom = {v_pad},\n" + + if (xy_ratio > 0): + s += f" .max_xy_ratio = {xy_ratio},\n" + + s += " }," + + return s + +def generate_zig_switch_arms(patch_set): + entries = {} + for entry in patch_set: + attributes = entry["Attributes"] + + for cp in range(entry["SymStart"], entry["SymEnd"] + 1): + entries[cp] = attributes["default"] + + for k, v in attributes.items(): + if isinstance(k, int): + entries[k] = v + + del entries[0] + + # Group codepoints by attribute key + grouped = defaultdict(list) + for cp, attr in entries.items(): + grouped[attr_key(attr)].append(cp) + + # Emit zig switch arms + result = [] + for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]): + # Use one of the attrs in the group to emit the value + attr = entries[codepoints[0]] + result.append(emit_zig_entry_multikey(codepoints, attr)) + + return "\n".join(result) + + +if __name__ == "__main__": + path = ( + Path(__file__).resolve().parent + / ".." + / ".." + / "vendor" + / "nerd-fonts" + / "font-patcher.py" + ) + with open(path, "r", encoding="utf-8") as f: + source = f.read() + + patch_set = extract_patch_set_values(source) + + out_path = Path(__file__).resolve().parent / "nerd_font_attributes.zig" + + with open(out_path, "w", encoding="utf-8") as f: + f.write("""//! This is a generate file, produced by nerd_font_codegen.py +//! DO NOT EDIT BY HAND! +//! +//! This file provides info extracted from the nerd fonts patcher script, +//! specifying the scaling/positioning attributes of various glyphs. + +const Constraint = @import("face.zig").RenderOptions.Constraint; + +/// Get the a constraints for the provided codepoint. +pub fn getConstraint(cp: u21) Constraint { + return switch (cp) { +""") + f.write(generate_zig_switch_arms(patch_set)) + f.write("\n") + + f.write(" else => .none,\n };\n}\n") diff --git a/src/font/opentype/sfnt.zig b/src/font/opentype/sfnt.zig index 14a3b795a..82c118bce 100644 --- a/src/font/opentype/sfnt.zig +++ b/src/font/opentype/sfnt.zig @@ -76,24 +76,22 @@ fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type { )); const half = @as(T, 1) << @intCast(frac_bits - 1); - frac: std.meta.Int(.unsigned, frac_bits), - int: std.meta.Int(type_info.signedness, int_bits), + const Frac = std.meta.Int(.unsigned, frac_bits); + const Int = std.meta.Int(type_info.signedness, int_bits); + + frac: Frac, + int: Int, pub fn to(self: Self, comptime FloatType: type) FloatType { - const i: FloatType = @floatFromInt(self.int); - const f: FloatType = @floatFromInt(self.frac); - - return i + f / frac_factor; + return @as(FloatType, @floatFromInt( + @as(T, @bitCast(self)), + )) / frac_factor; } pub fn from(float: anytype) Self { - const int = @floor(float); - const frac = @abs(float - int); - - return .{ - .int = @intFromFloat(int), - .frac = @intFromFloat(@round(frac * frac_factor)), - }; + return @bitCast( + @as(T, @intFromFloat(@round(float * frac_factor))), + ); } /// Round to the nearest integer, .5 rounds away from 0. diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 1aaa029dc..f4f01d105 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1769,7 +1769,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { .geist_mono => font.embedded.geist_mono, .jetbrains_mono => font.embedded.jetbrains_mono, .monaspace_neon => font.embedded.monaspace_neon, - .nerd_font => font.embedded.nerd_font, + .nerd_font => font.embedded.test_nerd_font, }; var lib = try Library.init(alloc); diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 97b24aa90..43d744176 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -218,103 +218,64 @@ pub fn isCovering(cp: u21) bool { }; } -pub const FgMode = enum { - /// Normal non-colored text rendering. The text can leave the cell - /// size if it is larger than the cell to allow for ligatures. - normal, +/// Returns the appropriate `constraint_width` for +/// the provided cell when rendering its glyph(s). +pub fn constraintWidth(cell_pin: terminal.Pin) u2 { + const cell = cell_pin.rowAndCell().cell; + const cp = cell.codepoint(); - /// Colored text rendering, specifically Emoji. - color, + if (!ziglyph.general_category.isPrivateUse(cp) and + !ziglyph.blocks.isDingbats(cp)) + { + return cell.gridWidth(); + } - /// Similar to normal but the text must be constrained to the cell - /// size. If a glyph is larger than the cell then it must be resized - /// to fit. - constrained, + // If we are at the end of the screen it must be constrained to one cell. + if (cell_pin.x == cell_pin.node.data.size.cols - 1) return 1; - /// Similar to normal, but the text consists of Powerline glyphs and is - /// optionally exempt from padding color extension and minimum contrast requirements. - powerline, -}; + // If we have a previous cell and it was PUA then we need to + // also constrain. This is so that multiple PUA glyphs align. + // As an exception, we ignore powerline glyphs since they are + // used for box drawing and we consider them whitespace. + if (cell_pin.x > 0) prev: { + const prev_cp = prev_cp: { + var copy = cell_pin; + copy.x -= 1; + const prev_cell = copy.rowAndCell().cell; + break :prev_cp prev_cell.codepoint(); + }; -/// Returns the appropriate foreground mode for the given cell. This is -/// meant to be called from the typical updateCell function within a -/// renderer. -pub fn fgMode( - presentation: font.Presentation, - cell_pin: terminal.Pin, -) FgMode { - return switch (presentation) { - // Emoji is always full size and color. - .emoji => .color, + // We consider powerline glyphs whitespace. + if (isPowerline(prev_cp)) break :prev; - // If it is text it is slightly more complex. If we are a codepoint - // in the private use area and we are at the end or the next cell - // is not empty, we need to constrain rendering. - // - // We do this specifically so that Nerd Fonts can render their - // icons without overlapping with subsequent characters. But if - // the subsequent character is empty, then we allow it to use - // the full glyph size. See #1071. - .text => text: { - const cell = cell_pin.rowAndCell().cell; - const cp = cell.codepoint(); + if (ziglyph.general_category.isPrivateUse(prev_cp)) { + return 1; + } + } - if (!ziglyph.general_category.isPrivateUse(cp) and - !ziglyph.blocks.isDingbats(cp)) - { - break :text .normal; - } - - // Special-case Powerline glyphs. They exhibit box drawing behavior - // and should not be constrained. They have their own special category - // though because they're used for other logic (i.e. disabling - // min contrast). - if (isPowerline(cp)) { - break :text .powerline; - } - - // If we are at the end of the screen its definitely constrained - if (cell_pin.x == cell_pin.node.data.size.cols - 1) break :text .constrained; - - // If we have a previous cell and it was PUA then we need to - // also constrain. This is so that multiple PUA glyphs align. - // As an exception, we ignore powerline glyphs since they are - // used for box drawing and we consider them whitespace. - if (cell_pin.x > 0) prev: { - const prev_cp = prev_cp: { - var copy = cell_pin; - copy.x -= 1; - const prev_cell = copy.rowAndCell().cell; - break :prev_cp prev_cell.codepoint(); - }; - - // Powerline is whitespace - if (isPowerline(prev_cp)) break :prev; - - if (ziglyph.general_category.isPrivateUse(prev_cp)) { - break :text .constrained; - } - } - - // If the next cell is empty, then we allow it to use the - // full glyph size. - const next_cp = next_cp: { - var copy = cell_pin; - copy.x += 1; - const next_cell = copy.rowAndCell().cell; - break :next_cp next_cell.codepoint(); - }; - if (next_cp == 0 or - isSpace(next_cp) or - isPowerline(next_cp)) - { - break :text .normal; - } - - // Must be constrained - break :text .constrained; - }, + // If the next cell is whitespace, then + // we allow it to be up to two cells wide. + const next_cp = next_cp: { + var copy = cell_pin; + copy.x += 1; + const next_cell = copy.rowAndCell().cell; + break :next_cp next_cell.codepoint(); }; + if (next_cp == 0 or + isSpace(next_cp) or + isPowerline(next_cp)) + { + return 2; + } + + // Must be constrained + return 1; +} + +/// Whether min contrast should be disabled for a given glyph. +pub fn noMinContrast(cp: u21) bool { + // TODO: We should disable for all box drawing type characters. + return isPowerline(cp); } // Some general spaces, others intentionally kept @@ -361,7 +322,7 @@ test Contents { // Add some contents. const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; const fg_cell: shaderpkg.CellText = .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -382,7 +343,8 @@ test Contents { // Add a block cursor. const cursor_cell: shaderpkg.CellText = .{ - .mode = .cursor, + .atlas = .grayscale, + .bools = .{ .is_cursor_glyph = true }, .grid_pos = .{ 2, 3 }, .color = .{ 0, 0, 0, 1 }, }; @@ -413,7 +375,7 @@ test "Contents clear retains other content" { // bg and fg cells in row 1 const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_1: shaderpkg.CellText = .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -422,7 +384,7 @@ test "Contents clear retains other content" { // bg and fg cells in row 2 const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_2: shaderpkg.CellText = .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ 4, 2 }, .color = .{ 0, 0, 0, 1 }, }; @@ -453,7 +415,7 @@ test "Contents clear last added content" { // bg and fg cells in row 1 const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_1: shaderpkg.CellText = .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -462,7 +424,7 @@ test "Contents clear last added content" { // bg and fg cells in row 2 const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_2: shaderpkg.CellText = .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ 4, 2 }, .color = .{ 0, 0, 0, 1 }, }; diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index c83897b4c..829563075 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -12,7 +12,8 @@ const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const link = @import("link.zig"); const cellpkg = @import("cell.zig"); -const fgMode = cellpkg.fgMode; +const noMinContrast = cellpkg.noMinContrast; +const constraintWidth = cellpkg.constraintWidth; const isCovering = cellpkg.isCovering; const imagepkg = @import("image.zig"); const Image = imagepkg.Image; @@ -25,6 +26,8 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Terminal = terminal.Terminal; const Health = renderer.Health; +const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint; + const FileType = @import("../file_type.zig").FileType; const macos = switch (builtin.os.tag) { @@ -2924,9 +2927,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); try self.cells.add(self.alloc, .underline, .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, @@ -2956,9 +2958,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); try self.cells.add(self.alloc, .overline, .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, @@ -2988,9 +2989,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, @@ -3015,6 +3015,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const rac = cell_pin.rowAndCell(); const cell = rac.cell; + const cp = cell.codepoint(); + // Render const render = try self.font_grid.renderGlyph( self.alloc, @@ -3024,6 +3026,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, .thicken_strength = self.config.font_thicken_strength, + .cell_width = cell.gridWidth(), + .constraint = getConstraint(cp), + .constraint_width = constraintWidth(cell_pin), }, ); @@ -3033,27 +3038,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } - // We always use fg mode for sprite glyphs, since we know we never - // need to constrain them, and we don't have any color sprites. - // - // Otherwise we defer to `fgMode`. - const mode: shaderpkg.CellText.Mode = - if (render.glyph.sprite) - .fg - else switch (fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - try self.cells.add(self.alloc, .text, .{ - .mode = mode, + .atlas = switch (render.presentation) { + .emoji => .color, + .text => .grayscale, + }, + .bools = .{ .no_min_contrast = noMinContrast(cp) }, .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, @@ -3138,7 +3129,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; self.cells.setCursor(.{ - .mode = .cursor, + .atlas = .grayscale, + .bools = .{ .is_cursor_glyph = true }, .grid_pos = .{ x, screen.cursor.y }, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, @@ -3187,7 +3179,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Add our text try self.cells.add(self.alloc, .text, .{ - .mode = .fg, + .atlas = .grayscale, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .color = .{ fg.r, fg.g, fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 9fe0862ed..bf3bcc6e4 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -269,15 +269,16 @@ pub const CellText = extern struct { bearings: [2]i16 align(4) = .{ 0, 0 }, grid_pos: [2]u16 align(4), color: [4]u8 align(4), - mode: Mode align(1), - constraint_width: u8 align(1) = 0, + atlas: Atlas align(1), + bools: packed struct(u8) { + no_min_contrast: bool = false, + is_cursor_glyph: bool = false, + _padding: u6 = 0, + } align(1) = .{}, - pub const Mode = enum(u8) { - fg = 1, - fg_constrained = 2, - fg_color = 3, - cursor = 4, - fg_powerline = 5, + pub const Atlas = enum(u8) { + grayscale = 0, + color = 1, }; test { diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index 0b67eaff0..80980bac7 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -237,15 +237,16 @@ pub const CellText = extern struct { bearings: [2]i16 align(4) = .{ 0, 0 }, grid_pos: [2]u16 align(4), color: [4]u8 align(4), - mode: Mode align(4), - constraint_width: u32 align(4) = 0, + atlas: Atlas align(1), + bools: packed struct(u8) { + no_min_contrast: bool = false, + is_cursor_glyph: bool = false, + _padding: u6 = 0, + } align(1) = .{}, - pub const Mode = enum(u32) { - fg = 1, - fg_constrained = 2, - fg_color = 3, - cursor = 4, - fg_powerline = 5, + pub const Atlas = enum(u8) { + grayscale = 0, + color = 1, }; // test { diff --git a/src/renderer/shaders/glsl/cell_text.f.glsl b/src/renderer/shaders/glsl/cell_text.f.glsl index fda6d8134..176efcbde 100644 --- a/src/renderer/shaders/glsl/cell_text.f.glsl +++ b/src/renderer/shaders/glsl/cell_text.f.glsl @@ -4,21 +4,15 @@ layout(binding = 0) uniform sampler2DRect atlas_grayscale; layout(binding = 1) uniform sampler2DRect atlas_color; in CellTextVertexOut { - flat uint mode; + flat uint atlas; flat vec4 color; flat vec4 bg_color; vec2 tex_coord; } in_data; -// These are the possible modes that "mode" can be set to. This is -// used to multiplex multiple render modes into a single shader. -// -// NOTE: this must be kept in sync with the fragment shader -const uint MODE_TEXT = 1u; -const uint MODE_TEXT_CONSTRAINED = 2u; -const uint MODE_TEXT_COLOR = 3u; -const uint MODE_TEXT_CURSOR = 4u; -const uint MODE_TEXT_POWERLINE = 5u; +// Values `atlas` can take. +const uint ATLAS_GRAYSCALE = 0u; +const uint ATLAS_COLOR = 1u; // Must declare this output for some versions of OpenGL. layout(location = 0) out vec4 out_FragColor; @@ -27,12 +21,9 @@ void main() { bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0; bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0; - switch (in_data.mode) { + switch (in_data.atlas) { default: - case MODE_TEXT_CURSOR: - case MODE_TEXT_CONSTRAINED: - case MODE_TEXT_POWERLINE: - case MODE_TEXT: + case ATLAS_GRAYSCALE: { // Our input color is always linear. vec4 color = in_data.color; @@ -84,7 +75,7 @@ void main() { return; } - case MODE_TEXT_COLOR: + case ATLAS_COLOR: { // For now, we assume that color glyphs // are already premultiplied linear colors. diff --git a/src/renderer/shaders/glsl/cell_text.v.glsl b/src/renderer/shaders/glsl/cell_text.v.glsl index 10965ddd2..7e38e2f0c 100644 --- a/src/renderer/shaders/glsl/cell_text.v.glsl +++ b/src/renderer/shaders/glsl/cell_text.v.glsl @@ -15,22 +15,22 @@ layout(location = 3) in uvec2 grid_pos; // The color of the rendered text glyph. layout(location = 4) in uvec4 color; -// The mode for this cell. -layout(location = 5) in uint mode; +// Which atlas this glyph is in. +layout(location = 5) in uint atlas; -// The width to constrain the glyph to, in cells, or 0 for no constraint. -layout(location = 6) in uint constraint_width; +// Misc glyph properties. +layout(location = 6) in uint glyph_bools; -// These are the possible modes that "mode" can be set to. This is -// used to multiplex multiple render modes into a single shader. -const uint MODE_TEXT = 1u; -const uint MODE_TEXT_CONSTRAINED = 2u; -const uint MODE_TEXT_COLOR = 3u; -const uint MODE_TEXT_CURSOR = 4u; -const uint MODE_TEXT_POWERLINE = 5u; +// Values `atlas` can take. +const uint ATLAS_GRAYSCALE = 0u; +const uint ATLAS_COLOR = 1u; + +// Masks for the `glyph_bools` attribute +const uint NO_MIN_CONTRAST = 1u; +const uint IS_CURSOR_GLYPH = 2u; out CellTextVertexOut { - flat uint mode; + flat uint atlas; flat vec4 color; flat vec4 bg_color; vec2 tex_coord; @@ -69,7 +69,7 @@ void main() { corner.x = float(vid == 1 || vid == 3); corner.y = float(vid == 2 || vid == 3); - out_data.mode = mode; + out_data.atlas = atlas; // === Grid Cell === // +X @@ -102,25 +102,6 @@ void main() { offset.y = cell_size.y - offset.y; - // If we're constrained then we need to scale the glyph. - if (mode == MODE_TEXT_CONSTRAINED) { - float max_width = cell_size.x * constraint_width; - // If this glyph is wider than the constraint width, - // fit it to the width and remove its horizontal offset. - if (size.x > max_width) { - float new_y = size.y * (max_width / size.x); - offset.y += (size.y - new_y) / 2.0; - offset.x = 0.0; - size.y = new_y; - size.x = max_width; - } else if (max_width - size.x > offset.x) { - // However, if it does fit in the constraint width, make - // sure the offset is small enough to not push it over the - // right edge of the constraint width. - offset.x = max_width - size.x; - } - } - // Calculate the final position of the cell which uses our glyph size // and glyph offset to create the correct bounding box for the glyph. cell_pos = cell_pos + size * corner + offset; @@ -149,11 +130,7 @@ void main() { // If we have a minimum contrast, we need to check if we need to // change the color of the text to ensure it has enough contrast // with the background. - // We only apply this adjustment to "normal" text with MODE_TEXT, - // since we want color glyphs to appear in their original color - // and Powerline glyphs to be unaffected (else parts of the line would - // have different colors as some parts are displayed via background colors). - if (min_contrast > 1.0f && mode == MODE_TEXT) { + if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) { // Ensure our minimum contrast out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color); } @@ -161,8 +138,9 @@ void main() { // Check if current position is under cursor (including wide cursor) bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y); - // If this cell is the cursor cell, then we need to change the color. - if (mode != MODE_TEXT_CURSOR && is_cursor_pos) { + // If this cell is the cursor cell, but we're not processing + // the cursor glyph itself, then we need to change the color. + if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) { out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending); } } diff --git a/src/renderer/shaders/shaders.metal b/src/renderer/shaders/shaders.metal index b62e0c3cf..4797f89e4 100644 --- a/src/renderer/shaders/shaders.metal +++ b/src/renderer/shaders/shaders.metal @@ -509,13 +509,17 @@ fragment float4 cell_bg_fragment( //------------------------------------------------------------------- #pragma mark - Cell Text Shader -// The possible modes that a cell fg entry can take. -enum CellTextMode : uint8_t { - MODE_TEXT = 1u, - MODE_TEXT_CONSTRAINED = 2u, - MODE_TEXT_COLOR = 3u, - MODE_TEXT_CURSOR = 4u, - MODE_TEXT_POWERLINE = 5u, +enum CellTextAtlas : uint8_t { + ATLAS_GRAYSCALE = 0u, + ATLAS_COLOR = 1u, +}; + +// We use a packed struct of bools for misc properties of the glyph. +enum CellTextBools : uint8_t { + // Don't apply min contrast to this glyph. + NO_MIN_CONTRAST = 1u, + // This is the cursor glyph. + IS_CURSOR_GLYPH = 2u, }; struct CellTextVertexIn { @@ -534,16 +538,16 @@ struct CellTextVertexIn { // The color of the rendered text glyph. uchar4 color [[attribute(4)]]; - // The mode for this cell. - uint8_t mode [[attribute(5)]]; + // Which atlas to sample for our glyph. + uint8_t atlas [[attribute(5)]]; - // The width to constrain the glyph to, in cells, or 0 for no constraint. - uint8_t constraint_width [[attribute(6)]]; + // Misc properties of the glyph. + uint8_t bools [[attribute(6)]]; }; struct CellTextVertexOut { float4 position [[position]]; - uint8_t mode [[flat]]; + uint8_t atlas [[flat]]; float4 color [[flat]]; float4 bg_color [[flat]]; float2 tex_coord; @@ -577,7 +581,7 @@ vertex CellTextVertexOut cell_text_vertex( corner.y = float(vid == 2 || vid == 3); CellTextVertexOut out; - out.mode = in.mode; + out.atlas = in.atlas; // === Grid Cell === // +X @@ -610,25 +614,6 @@ vertex CellTextVertexOut cell_text_vertex( offset.y = uniforms.cell_size.y - offset.y; - // If we're constrained then we need to scale the glyph. - if (in.mode == MODE_TEXT_CONSTRAINED) { - float max_width = uniforms.cell_size.x * in.constraint_width; - // If this glyph is wider than the constraint width, - // fit it to the width and remove its horizontal offset. - if (size.x > max_width) { - float new_y = size.y * (max_width / size.x); - offset.y += (size.y - new_y) / 2; - offset.x = 0; - size.y = new_y; - size.x = max_width; - } else if (max_width - size.x > offset.x) { - // However, if it does fit in the constraint width, make - // sure the offset is small enough to not push it over the - // right edge of the constraint width. - offset.x = max_width - size.x; - } - } - // Calculate the final position of the cell which uses our glyph size // and glyph offset to create the correct bounding box for the glyph. cell_pos = cell_pos + size * corner + offset; @@ -665,11 +650,7 @@ vertex CellTextVertexOut cell_text_vertex( // If we have a minimum contrast, we need to check if we need to // change the color of the text to ensure it has enough contrast // with the background. - // We only apply this adjustment to "normal" text with MODE_TEXT, - // since we want color glyphs to appear in their original color - // and Powerline glyphs to be unaffected (else parts of the line would - // have different colors as some parts are displayed via background colors). - if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) { + if (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) { // Ensure our minimum contrast out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color); } @@ -681,8 +662,9 @@ vertex CellTextVertexOut cell_text_vertex( in.grid_pos.x == uniforms.cursor_pos.x + 1 ) && in.grid_pos.y == uniforms.cursor_pos.y; - // If this cell is the cursor cell, then we need to change the color. - if (in.mode != MODE_TEXT_CURSOR && is_cursor_pos) { + // If this cell is the cursor cell, but we're not processing + // the cursor glyph itself, then we need to change the color. + if ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) { out.color = load_color( uniforms.cursor_color, uniforms.use_display_p3, @@ -702,19 +684,12 @@ fragment float4 cell_text_fragment( constexpr sampler textureSampler( coord::pixel, address::clamp_to_edge, - // TODO(qwerasd): This can be changed back to filter::nearest when - // we move the constraint logic out of the GPU code - // which should once again guarantee pixel perfect - // sizing. - filter::linear + filter::nearest ); - switch (in.mode) { + switch (in.atlas) { default: - case MODE_TEXT_CURSOR: - case MODE_TEXT_CONSTRAINED: - case MODE_TEXT_POWERLINE: - case MODE_TEXT: { + case ATLAS_GRAYSCALE: { // Our input color is always linear. float4 color = in.color; @@ -764,7 +739,7 @@ fragment float4 cell_text_fragment( return color; } - case MODE_TEXT_COLOR: { + case ATLAS_COLOR: { // For now, we assume that color glyphs // are already premultiplied linear colors. float4 color = textureColor.sample(textureSampler, in.tex_coord); diff --git a/vendor/nerd-fonts/LICENSE b/vendor/nerd-fonts/LICENSE new file mode 100644 index 000000000..d163912b3 --- /dev/null +++ b/vendor/nerd-fonts/LICENSE @@ -0,0 +1,126 @@ +# Nerd Fonts Licensing + +There are various sources used under various licenses: + +* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below). +* Nerd Fonts original source code files (such as `.sh`, `.py`, `font-patcher` and others) are licensed under the MIT License (MIT) (see below). +* Many other licenses are present in this project for even more detailed breakdown see: [License Audit](https://github.com/ryanoasis/nerd-fonts/blob/-/license-audit.md). + +## Source files not in folders containing an explicit license are using the MIT License (MIT) + +The MIT License (MIT) + +Copyright (c) 2014 Ryan L McIntyre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Various Fonts, Patched Fonts, SVGs, Glyph Fonts, and any files in a folder with explicit SIL OFL 1.1 License + +Copyright (c) 2014, Ryan L McIntyre (https://ryanlmcintyre.com). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/vendor/nerd-fonts/README.md b/vendor/nerd-fonts/README.md new file mode 100644 index 000000000..66dec54cb --- /dev/null +++ b/vendor/nerd-fonts/README.md @@ -0,0 +1,10 @@ +We have a copy of the `font-patcher` file from `nerd-fonts` here, fetched from +https://github.com/ryanoasis/nerd-fonts/blob/master/font-patcher. + +This is MIT licensed, see `LICENSE` in this directory. + +We use parse a section of this file to codegen a lookup table of the nerd font +scaling rules. See `src/font/nerd_font_codegen.py` in the main Ghostty source +tree for more info. + +Last fetched commit: ebc376cbd43f609d8084f47dd348646595ce066e diff --git a/vendor/nerd-fonts/font-patcher.py b/vendor/nerd-fonts/font-patcher.py new file mode 100644 index 000000000..6c7ebfe37 --- /dev/null +++ b/vendor/nerd-fonts/font-patcher.py @@ -0,0 +1,2296 @@ +#!/usr/bin/env python +# coding=utf8 +# Nerd Fonts Version: 3.4.0 +# Script version is further down + +from __future__ import absolute_import, print_function, unicode_literals + +# Change the script version when you edit this script: +script_version = "4.20.5" + +version = "3.4.0" +projectName = "Nerd Fonts" +projectNameAbbreviation = "NF" +projectNameSingular = projectName[:-1] + +import sys +import re +import os +import argparse +from argparse import RawTextHelpFormatter +import errno +import subprocess +import json +from enum import Enum +import logging +try: + import configparser +except ImportError: + sys.exit(projectName + ": configparser module is probably not installed. Try `pip install configparser` or equivalent") +try: + import psMat + import fontforge +except ImportError: + sys.exit( + projectName + ( + ": FontForge module could not be loaded. Try installing fontforge python bindings " + "[e.g. on Linux Debian or Ubuntu: `sudo apt install fontforge python3-fontforge`]" + ) + ) + +sys.path.insert(0, os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'bin', 'scripts', 'name_parser')) +try: + from FontnameParser import FontnameParser + from FontnameTools import FontnameTools + FontnameParserOK = True +except ImportError: + FontnameParserOK = False + +class TableHEADWriter: + """ Access to the HEAD table without external dependencies """ + def getlong(self, pos = None): + """ Get four bytes from the font file as integer number """ + if pos: + self.goto(pos) + return (ord(self.f.read(1)) << 24) + (ord(self.f.read(1)) << 16) + (ord(self.f.read(1)) << 8) + ord(self.f.read(1)) + + def getshort(self, pos = None): + """ Get two bytes from the font file as integer number """ + if pos: + self.goto(pos) + return (ord(self.f.read(1)) << 8) + ord(self.f.read(1)) + + def putlong(self, num, pos = None): + """ Put number as four bytes into font file """ + if pos: + self.goto(pos) + self.f.write(bytearray([(num >> 24) & 0xFF, (num >> 16) & 0xFF ,(num >> 8) & 0xFF, num & 0xFF])) + self.modified = True + + def putshort(self, num, pos = None): + """ Put number as two bytes into font file """ + if pos: + self.goto(pos) + self.f.write(bytearray([(num >> 8) & 0xFF, num & 0xFF])) + self.modified = True + + def calc_checksum(self, start, end, checksum = 0): + """ Calculate a font table checksum, optionally ignoring another embedded checksum value (for table 'head') """ + self.f.seek(start) + for i in range(start, end - 4, 4): + checksum += self.getlong() + checksum &= 0xFFFFFFFF + i += 4 + extra = 0 + for j in range(4): + extra = extra << 8 + if i + j <= end: + extra += ord(self.f.read(1)) + checksum = (checksum + extra) & 0xFFFFFFFF + return checksum + + def find_table(self, tablenames, idx): + """ Search all tables for one of the tables in tablenames and store its metadata """ + # Use font with index idx if this is a font collection file + self.f.seek(0, 0) + tag = self.f.read(4) + if tag == b'ttcf': + self.f.seek(2*2, 1) + self.num_fonts = self.getlong() + if (idx >= self.num_fonts): + raise Exception('Trying to access subfont index {} but have only {} fonts'.format(idx, num_fonts)) + for _ in range(idx + 1): + offset = self.getlong() + self.f.seek(offset, 0) + elif idx != 0: + raise Exception('Trying to access subfont but file is no collection') + else: + self.f.seek(0, 0) + self.num_fonts = 1 + + self.f.seek(4, 1) + numtables = self.getshort() + self.f.seek(3*2, 1) + + for i in range(numtables): + tab_name = self.f.read(4) + self.tab_check_offset = self.f.tell() + self.tab_check = self.getlong() + self.tab_offset = self.getlong() + self.tab_length = self.getlong() + if tab_name in tablenames: + return True + return False + + def find_head_table(self, idx): + """ Search all tables for the HEAD table and store its metadata """ + # Use font with index idx if this is a font collection file + found = self.find_table([ b'head' ], idx) + if not found: + raise Exception('No HEAD table found in font idx {}'.format(idx)) + + + def goto(self, where): + """ Go to a named location in the file or to the specified index """ + if isinstance(where, str): + positions = {'checksumAdjustment': 2+2+4, + 'flags': 2+2+4+4+4, + 'lowestRecPPEM': 2+2+4+4+4+2+2+8+8+2+2+2+2+2, + 'avgWidth': 2, + } + where = self.tab_offset + positions[where] + self.f.seek(where) + + + def calc_full_checksum(self, check = False): + """ Calculate the whole file's checksum """ + self.f.seek(0, 2) + self.end = self.f.tell() + full_check = self.calc_checksum(0, self.end, (-self.checksum_adj) & 0xFFFFFFFF) + if check and (0xB1B0AFBA - full_check) & 0xFFFFFFFF != self.checksum_adj: + sys.exit("Checksum of whole font is bad") + return full_check + + def calc_table_checksum(self, check = False): + tab_check_new = self.calc_checksum(self.tab_offset, self.tab_offset + self.tab_length - 1, (-self.checksum_adj) & 0xFFFFFFFF) + if check and tab_check_new != self.tab_check: + sys.exit("Checksum of 'head' in font is bad") + return tab_check_new + + def reset_table_checksum(self): + new_check = self.calc_table_checksum() + self.putlong(new_check, self.tab_check_offset) + + def reset_full_checksum(self): + new_adj = (0xB1B0AFBA - self.calc_full_checksum()) & 0xFFFFFFFF + self.putlong(new_adj, 'checksumAdjustment') + + def close(self): + self.f.close() + + + def __init__(self, filename): + self.modified = False + self.f = open(filename, 'r+b') + + self.find_head_table(0) + + self.flags = self.getshort('flags') + self.lowppem = self.getshort('lowestRecPPEM') + self.checksum_adj = self.getlong('checksumAdjustment') + +def check_panose_monospaced(font): + """ Check if the font's Panose flags say it is monospaced """ + # https://forum.high-logic.com/postedfiles/Panose.pdf + panose = list(font.os2_panose) + if panose[0] < 2 or panose[0] > 5: + return -1 # invalid Panose info + panose_mono = ((panose[0] == 2 and panose[3] == 9) or + (panose[0] == 3 and panose[3] == 3)) + return 1 if panose_mono else 0 + +def panose_check_to_text(value, panose = False): + """ Convert value from check_panose_monospaced() to human readable string """ + if value == 0: + return "Panose says \"not monospaced\"" + if value == 1: + return "Panose says \"monospaced\"" + return "Panose is invalid" + (" ({})".format(list(panose)) if panose else "") + +def panose_proportion_to_text(value): + """ Interpret a Panose proportion value (4th value) for family 2 (latin text) """ + proportion = { + 0: "Any", 1: "No Fit", 2: "Old Style", 3: "Modern", 4: "Even Width", + 5: "Extended", 6: "Condensed", 7: "Very Extended", 8: "Very Condensed", + 9: "Monospaced" } + return proportion.get(value, "??? {}".format(value)) + +def is_monospaced(font): + """ Check if a font is probably monospaced """ + # Some fonts lie (or have not any Panose flag set), spot check monospaced: + width = -1 + width_mono = True + for glyph in [ 0x49, 0x4D, 0x57, 0x61, 0x69, 0x6d, 0x2E ]: # wide and slim glyphs 'I', 'M', 'W', 'a', 'i', 'm', '.' + if not glyph in font: + # A 'strange' font, believe Panose + return (check_panose_monospaced(font) == 1, None) + # print(" -> {} {}".format(glyph, font[glyph].width)) + if width < 0: + width = font[glyph].width + continue + if font[glyph].width != width: + # Exception for fonts like Code New Roman Regular or Hermit Light/Bold: + # Allow small 'i' and dot to be smaller than normal + # I believe the source fonts are buggy + if glyph in [ 0x69, 0x2E ]: + if width > font[glyph].width: + continue + (xmin, _, xmax, _) = font[glyph].boundingBox() + if width > xmax - xmin: + continue + width_mono = False + break + # We believe our own check more then Panose ;-D + return (width_mono, None if width_mono else glyph) + +def force_panose_monospaced(font): + """ Forces the Panose flag to monospaced if they are unset or halfway ok already """ + # For some Windows applications (e.g. 'cmd'), they seem to honour the Panose table + # https://forum.high-logic.com/postedfiles/Panose.pdf + panose = list(font.os2_panose) + if panose[0] == 0: # 0 (1st value) = family kind; 0 = any (default) + panose[0] = 2 # make kind latin text and display + logger.info("Setting Panose 'Family Kind' to 'Latin Text and Display' (was 'Any')") + font.os2_panose = tuple(panose) + if panose[0] == 2 and panose[3] != 9: + logger.info("Setting Panose 'Proportion' to 'Monospaced' (was '%s')", panose_proportion_to_text(panose[3])) + panose[3] = 9 # 3 (4th value) = proportion; 9 = monospaced + font.os2_panose = tuple(panose) + +def get_advance_width(font, extended, minimum): + """ Get the maximum/minimum advance width in the extended(?) range """ + width = 0 + if not extended: + r = range(0x021, 0x07e) + else: + r = range(0x07f, 0x17f) + for glyph in r: + if not glyph in font: + continue + if glyph in range(0x7F, 0xBF): + continue # ignore special characters like '1/4' etc + if width == 0: + width = font[glyph].width + continue + if not minimum and width < font[glyph].width: + width = font[glyph].width + elif minimum and width > font[glyph].width: + width = font[glyph].width + return width + +def report_advance_widths(font): + return "Advance widths (base/extended): {} - {} / {} - {}".format( + get_advance_width(font, False, True), get_advance_width(font, False, False), + get_advance_width(font, True, True), get_advance_width(font, True, False)) + +def get_btb_metrics(font): + """ Get the baseline to baseline distance for all three metrics """ + hhea_height = font.hhea_ascent - font.hhea_descent + typo_height = font.os2_typoascent - font.os2_typodescent + win_height = font.os2_winascent + font.os2_windescent + win_gap = max(0, font.hhea_linegap - win_height + hhea_height) + hhea_btb = hhea_height + font.hhea_linegap + typo_btb = typo_height + font.os2_typolinegap + win_btb = win_height + win_gap + return (hhea_btb, typo_btb, win_btb, win_gap) + +def get_metrics_names(): + """ Helper to get the line metrics names consistent """ + return ['HHEA','TYPO','WIN'] + +def get_old_average_x_width(font): + """ Determine xAvgCharWidth of the OS/2 table """ + # Fontforge can not create fonts with old (i.e. prior to OS/2 version 3) + # table values, but some very old applications do need them sometimes + # https://learn.microsoft.com/en-us/typography/opentype/spec/os2#xavgcharwidth + s = 0 + weights = { + 'a': 64, 'b': 14, 'c': 27, 'd': 35, 'e': 100, 'f': 20, 'g': 14, 'h': 42, 'i': 63, + 'j': 3, 'k': 6, 'l': 35, 'm': 20, 'n': 56, 'o': 56, 'p': 17, 'q': 4, 'r': 49, + 's': 56, 't': 71, 'u': 31, 'v': 10, 'w': 18, 'x': 3, 'y': 18, 'z': 2, 32: 166, + } + for g in weights: + if g not in font: + logger.critical("Can not determine ancient style xAvgCharWidth") + sys.exit(1) + s += font[g].width * weights[g] + return int(s / 1000) + +def create_filename(fonts): + """ Determine filename from font object(s) """ + # Only consider the standard (i.e. English-US) names + sfnt = { k: v for l, k, v in fonts[0].sfnt_names if l == 'English (US)' } + sfnt_pfam = sfnt.get('Preferred Family', sfnt['Family']) + sfnt_psubfam = sfnt.get('Preferred Styles', sfnt['SubFamily']) + if len(fonts) > 1: + return sfnt_pfam + if len(sfnt_psubfam) > 0: + sfnt_psubfam = '-' + sfnt_psubfam + return (sfnt_pfam + sfnt_psubfam).replace(' ', '') + +def fetch_glyphnames(): + """ Read the glyphname database and put it into a dictionary """ + try: + glyphnamefile = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'glyphnames.json')) + with open(glyphnamefile, 'rb') as f: + namelist = json.load(f) + return { int(v['code'], 16): k for k, v in namelist.items() if 'code' in v } + except Exception as error: + logger.warning("Can not read glyphnames file (%s)", repr(error)) + return {} + +class font_patcher: + def __init__(self, args, conf): + self.args = args # class 'argparse.Namespace' + self.sym_font_args = [] + self.config = conf # class 'configparser.ConfigParser' + self.sourceFont = None # class 'fontforge.font' + self.patch_set = None # class 'list' + self.font_dim = None # class 'dict' + self.font_extrawide = False + self.source_monospaced = None # Later True or False + self.symbolsonly = False # Are we generating the SymbolsOnly font? + self.onlybitmaps = 0 + self.essential = set() + self.xavgwidth = [] # list of ints + self.glyphnames = fetch_glyphnames() + + def patch(self, font): + self.sourceFont = font + self.setup_version() + self.assert_monospace() + self.remove_ligatures() + self.manipulate_hints() + self.get_essential_references() + self.get_sourcefont_dimensions() + self.setup_patch_set() + self.improve_line_dimensions() + self.sourceFont.encoding = 'UnicodeFull' # Update the font encoding to ensure that the Unicode glyphs are available + self.onlybitmaps = self.sourceFont.onlybitmaps # Fetch this property before adding outlines. NOTE self.onlybitmaps initialized and never used + + if self.args.forcemono: + # Force width to be equal on all glyphs to ensure the font is considered monospaced on Windows. + # This needs to be done on all characters, as some information seems to be lost from the original font file. + self.set_sourcefont_glyph_widths() + + # For very wide (almost square or wider) fonts we do not want to generate 2 cell wide Powerline glyphs + if self.font_dim['height'] * 1.8 < self.font_dim['width'] * 2: + logger.warning("Very wide and short font, disabling 2 cell Powerline glyphs") + self.font_extrawide = True + + # Prevent opening and closing the fontforge font. Makes things faster when patching + # multiple ranges using the same symbol font. + PreviousSymbolFilename = "" + symfont = None + + if not os.path.isdir(self.args.glyphdir): + logger.critical("Can not find symbol glyph directory %s " + "(probably you need to download the src/glyphs/ directory?)", self.args.glyphdir) + sys.exit(1) + + if self.args.dry_run: + return + + for patch in self.patch_set: + if patch['Enabled']: + if PreviousSymbolFilename != patch['Filename']: + # We have a new symbol font, so close the previous one if it exists + if symfont: + symfont.close() + symfont = None + symfont_file = os.path.join(self.args.glyphdir, patch['Filename']) + if not os.path.isfile(symfont_file): + logger.critical("Can not find symbol source for '%s' (i.e. %s)", + patch['Name'], symfont_file) + sys.exit(1) + if not os.access(symfont_file, os.R_OK): + logger.critical("Can not open symbol source for '%s' (i.e. %s)", + patch['Name'], symfont_file) + sys.exit(1) + symfont = fontforge.open(symfont_file) + symfont.encoding = 'UnicodeFull' + + # Match the symbol font size to the source font size + symfont.em = self.sourceFont.em + PreviousSymbolFilename = patch['Filename'] + + # If patch table doesn't include a source start, re-use the symbol font values + SrcStart = patch['SrcStart'] + if not SrcStart: + SrcStart = patch['SymStart'] + self.copy_glyphs(SrcStart, symfont, patch['SymStart'], patch['SymEnd'], patch['Exact'], patch['ScaleRules'], patch['Name'], patch['Attributes']) + + if symfont: + symfont.close() + + # The grave accent and fontforge: + # If the type is 'auto' fontforge changes it to 'mark' on export. + # We can not prevent this. So set it to 'baseglyph' instead, as + # that resembles the most common expectations. + # This is not needed with fontforge March 2022 Release anymore. + if "grave" in self.sourceFont: + self.sourceFont["grave"].glyphclass="baseglyph" + + + def generate(self, sourceFonts): + sourceFont = sourceFonts[0] + # the `PfEd-comments` flag is required for Fontforge to save '.comment' and '.fontlog'. + if int(fontforge.version()) >= 20201107: + gen_flags = (str('opentype'), str('PfEd-comments'), str('no-FFTM-table')) + else: + gen_flags = (str('opentype'), str('PfEd-comments')) + if len(sourceFonts) > 1: + layer = None + # use first non-background layer + for l in sourceFont.layers: + if not sourceFont.layers[l].is_background: + layer = l + break + outfile = os.path.normpath(os.path.join( + sanitize_filename(self.args.outputdir, True), + sanitize_filename(create_filename(sourceFonts)) + ".ttc")) + sourceFonts[0].generateTtc(outfile, sourceFonts[1:], flags=gen_flags, layer=layer) + message = " Generated {} fonts\n \\===> '{}'".format(len(sourceFonts), outfile) + else: + fontname = create_filename(sourceFonts) + if not fontname: + fontname = sourceFont.cidfontname + outfile = os.path.normpath(os.path.join( + sanitize_filename(self.args.outputdir, True), + sanitize_filename(fontname) + self.args.extension)) + bitmaps = str() + if len(sourceFont.bitmapSizes): + logger.debug("Preserving bitmaps %s", repr(sourceFont.bitmapSizes)) + bitmaps = str('otf') # otf/ttf, both is bf_ttf + if self.args.dry_run: + logger.debug("=====> Filename '%s'", outfile) + return + sourceFont.generate(outfile, bitmap_type=bitmaps, flags=gen_flags) + message = " {}\n \\===> '{}'".format(sourceFont.fullname, outfile) + + # Adjust flags that can not be changed via fontforge + if re.search(r'\.[ot]tf$', self.args.font, re.IGNORECASE) and re.search(r'\.[ot]tf$', outfile, re.IGNORECASE): + if not os.path.isfile(outfile) or os.path.getsize(outfile) < 1: + logger.critical("Something went wrong and Fontforge did not generate the new font - look for messages above") + sys.exit(1) + try: + source_font = TableHEADWriter(self.args.font) + dest_font = TableHEADWriter(outfile) + for idx in range(source_font.num_fonts): + logger.debug("Tweaking %d/%d", idx + 1, source_font.num_fonts) + xwidth_s = '' + xwidth = self.xavgwidth[idx] if len(self.xavgwidth) > idx else None + if isinstance(xwidth, int): + if isinstance(xwidth, bool) and xwidth: + source_font.find_table([b'OS/2'], idx) + xwidth = source_font.getshort('avgWidth') + xwidth_s = ' (copied from source)' + dest_font.find_table([b'OS/2'], idx) + d_xwidth = dest_font.getshort('avgWidth') + if d_xwidth != xwidth: + logger.debug("Changing xAvgCharWidth from %d to %d%s", d_xwidth, xwidth, xwidth_s) + dest_font.putshort(xwidth, 'avgWidth') + dest_font.reset_table_checksum() + source_font.find_head_table(idx) + dest_font.find_head_table(idx) + if source_font.flags & 0x08 == 0 and dest_font.flags & 0x08 != 0: + logger.debug("Changing flags from 0x%X to 0x%X", dest_font.flags, dest_font.flags & ~0x08) + dest_font.putshort(dest_font.flags & ~0x08, 'flags') # clear 'ppem_to_int' + if source_font.lowppem != dest_font.lowppem: + logger.debug("Changing lowestRecPPEM from %d to %d", dest_font.lowppem, source_font.lowppem) + dest_font.putshort(source_font.lowppem, 'lowestRecPPEM') + if dest_font.modified: + dest_font.reset_table_checksum() + if dest_font.modified: + dest_font.reset_full_checksum() + except Exception as error: + logger.error("Can not handle font flags (%s)", repr(error)) + finally: + try: + source_font.close() + dest_font.close() + except: + pass + if self.args.is_variable: + logger.critical("Source font is a variable open type font (VF) and the patch results will most likely not be what you want") + print(message) + + if self.args.postprocess: + subprocess.call([self.args.postprocess, outfile]) + print("\n") + logger.info("Post Processed: %s", outfile) + + + def setup_name_backup(self, font): + """ Store the original font names to be able to rename the font multiple times """ + font.persistent = { + "fontname": font.fontname, + "fullname": font.fullname, + "familyname": font.familyname, + } + + + def setup_font_names(self, font): + font.fontname = font.persistent["fontname"] + if isinstance(font.persistent["fullname"], str): + font.fullname = font.persistent["fullname"] + if isinstance(font.persistent["familyname"], str): + font.familyname = font.persistent["familyname"] + verboseAdditionalFontNameSuffix = "" + additionalFontNameSuffix = "" + if not self.args.complete: + # NOTE not all symbol fonts have appended their suffix here + if self.args.fontawesome: + additionalFontNameSuffix += " A" + verboseAdditionalFontNameSuffix += " Plus Font Awesome" + if self.args.fontawesomeextension: + additionalFontNameSuffix += " AE" + verboseAdditionalFontNameSuffix += " Plus Font Awesome Extension" + if self.args.octicons: + additionalFontNameSuffix += " O" + verboseAdditionalFontNameSuffix += " Plus Octicons" + if self.args.powersymbols: + additionalFontNameSuffix += " PS" + verboseAdditionalFontNameSuffix += " Plus Power Symbols" + if self.args.codicons: + additionalFontNameSuffix += " C" + verboseAdditionalFontNameSuffix += " Plus Codicons" + if self.args.pomicons: + additionalFontNameSuffix += " P" + verboseAdditionalFontNameSuffix += " Plus Pomicons" + if self.args.fontlogos: + additionalFontNameSuffix += " L" + verboseAdditionalFontNameSuffix += " Plus Font Logos" + if self.args.material: + additionalFontNameSuffix += " MDI" + verboseAdditionalFontNameSuffix += " Plus Material Design Icons" + if self.args.weather: + additionalFontNameSuffix += " WEA" + verboseAdditionalFontNameSuffix += " Plus Weather Icons" + + # add mono signifier to beginning of name suffix + if self.args.single: + variant_abbrev = "M" + variant_full = " Mono" + elif self.args.nonmono and not self.symbolsonly: + variant_abbrev = "P" + variant_full = " Propo" + else: + variant_abbrev = "" + variant_full = "" + + ps_suffix = projectNameAbbreviation + variant_abbrev + additionalFontNameSuffix + + # add 'Nerd Font' to beginning of name suffix + verboseAdditionalFontNameSuffix = " " + projectNameSingular + variant_full + verboseAdditionalFontNameSuffix + additionalFontNameSuffix = " " + projectNameSingular + variant_full + additionalFontNameSuffix + + if FontnameParserOK and self.args.makegroups > 0: + user_supplied_name = False # User supplied names are kept unchanged + if not isinstance(self.args.force_name, str): + use_fullname = isinstance(font.fullname, str) # Usually the fullname is better to parse + # Use fullname if it is 'equal' to the fontname + if font.fullname: + use_fullname |= font.fontname.lower() == FontnameTools.postscript_char_filter(font.fullname).lower() + # Use fullname for any of these source fonts (that are impossible to disentangle from the fontname, we need the blanks) + for hit in [ 'Meslo' ]: + use_fullname |= font.fontname.lower().startswith(hit.lower()) + parser_name = font.fullname if use_fullname else font.fontname + # Gohu fontnames hide the weight, but the file names are ok... + if parser_name.startswith('Gohu'): + parser_name = os.path.splitext(os.path.basename(self.args.font))[0] + else: + if self.args.force_name == 'full': + parser_name = font.fullname + elif self.args.force_name == 'postscript': + parser_name = font.fontname + elif self.args.force_name == 'filename': + parser_name = os.path.basename(font.path).split('.')[0] + else: + parser_name = self.args.force_name + user_supplied_name = True + if not isinstance(parser_name, str) or len(parser_name) < 1: + logger.critical("Specified --name not usable because the name will be empty") + sys.exit(2) + n = FontnameParser(parser_name, logger) + if not n.parse_ok: + logger.warning("Have only minimal naming information, check resulting name. Maybe specify --makegroups 0") + n.drop_for_powerline() + n.enable_short_families(not user_supplied_name, self.args.makegroups in [ 2, 3, 5, 6, ], self.args.makegroups in [ 3, 6, ]) + if not n.set_expect_no_italic(self.args.noitalic): + logger.critical("Detected 'Italic' slant but --has-no-italic specified") + sys.exit(1) + + # All the following stuff is ignored in makegroups-mode + + # basically split the font name around the dash "-" to get the fontname and the style (e.g. Bold) + # this does not seem very reliable so only use the style here as a fallback if the font does not + # have an internal style defined (in sfnt_names) + # using '([^-]*?)' to get the item before the first dash "-" + # using '([^-]*(?!.*-))' to get the item after the last dash "-" + fontname, fallbackStyle = re.match("^([^-]*).*?([^-]*(?!.*-))$", font.fontname).groups() + + # dont trust 'font.familyname' + familyname = fontname + + # fullname (filename) can always use long/verbose font name, even in windows + if font.fullname != None: + fullname = font.fullname + verboseAdditionalFontNameSuffix + else: + fullname = font.cidfontname + verboseAdditionalFontNameSuffix + + fontname = fontname + additionalFontNameSuffix.replace(" ", "") + + # let us try to get the 'style' from the font info in sfnt_names and fallback to the + # parse fontname if it fails: + try: + # search tuple: + subFamilyTupleIndex = [x[1] for x in font.sfnt_names].index("SubFamily") + + # String ID is at the second index in the Tuple lists + sfntNamesStringIDIndex = 2 + + # now we have the correct item: + subFamily = font.sfnt_names[subFamilyTupleIndex][sfntNamesStringIDIndex] + except IndexError: + sys.stderr.write("{}: Could not find 'SubFamily' for given font, falling back to parsed fontname\n".format(projectName)) + subFamily = fallbackStyle + + # some fonts have inaccurate 'SubFamily', if it is Regular let us trust the filename more: + if subFamily == "Regular" and len(fallbackStyle): + subFamily = fallbackStyle + + # This is meant to cover the case where the SubFamily is "Italic" and the filename is *-BoldItalic. + if len(subFamily) < len(fallbackStyle): + subFamily = fallbackStyle + + if len(subFamily) == 0: + subFamily = "Regular" + + familyname += " " + projectNameSingular + variant_full + + # Don't truncate the subfamily to keep fontname unique. MacOS treats fonts with + # the same name as the same font, even if subFamily is different. Make sure to + # keep the resulting fontname (PostScript name) valid by removing spaces. + fontname += '-' + subFamily.replace(' ', '') + + # rename font + # + # comply with SIL Open Font License (OFL) + reservedFontNameReplacements = { + 'source' : 'sauce', + 'Source' : 'Sauce', + 'Bitstream Vera Sans Mono' : 'Bitstrom Wera', + 'BitstreamVeraSansMono' : 'BitstromWera', + 'bitstream vera sans mono' : 'bitstrom wera', + 'bitstreamverasansmono' : 'bitstromwera', + 'hermit' : 'hurmit', + 'Hermit' : 'Hurmit', + 'hasklig' : 'hasklug', + 'Hasklig' : 'Hasklug', + 'Share' : 'Shure', + 'share' : 'shure', + 'IBMPlex' : 'Blex', + 'ibmplex' : 'blex', + 'IBM-Plex' : 'Blex', + 'IBM Plex' : 'Blex', + 'terminus' : 'terminess', + 'Terminus' : 'Terminess', + 'liberation' : 'literation', + 'Liberation' : 'Literation', + 'iAWriter' : 'iMWriting', + 'iA Writer' : 'iM Writing', + 'iA-Writer' : 'iM-Writing', + 'Anka/Coder' : 'AnaConder', + 'anka/coder' : 'anaconder', + 'Cascadia Code' : 'Caskaydia Cove', + 'cascadia code' : 'caskaydia cove', + 'CascadiaCode' : 'CaskaydiaCove', + 'cascadiacode' : 'caskaydiacove', + 'Cascadia Mono' : 'Caskaydia Mono', + 'cascadia mono' : 'caskaydia mono', + 'CascadiaMono' : 'CaskaydiaMono', + 'cascadiamono' : 'caskaydiamono', + 'Fira Mono' : 'Fura Mono', + 'Fira Sans' : 'Fura Sans', + 'FiraMono' : 'FuraMono', + 'FiraSans' : 'FuraSans', + 'fira mono' : 'fura mono', + 'fira sans' : 'fura sans', + 'firamono' : 'furamono', + 'firasans' : 'furasans', + 'IntelOneMono' : 'IntoneMono', + 'IntelOne Mono' : 'Intone Mono', + 'Intel One Mono' : 'Intone Mono', + } + + # remove overly verbose font names + # particularly regarding Powerline sourced Fonts (https://github.com/powerline/fonts) + additionalFontNameReplacements = { + 'for Powerline': '', + 'ForPowerline': '' + } + + additionalFontNameReplacements2 = { + 'Powerline': '' + } + + projectInfo = ( + "Patched with '" + projectName + " Patcher' (https://github.com/ryanoasis/nerd-fonts)\n\n" + "* Website: https://www.nerdfonts.com\n" + "* Version: " + version + "\n" + "* Development Website: https://github.com/ryanoasis/nerd-fonts\n" + "* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/-/changelog.md" + ) + + familyname = replace_font_name(familyname, reservedFontNameReplacements) + fullname = replace_font_name(fullname, reservedFontNameReplacements) + fontname = replace_font_name(fontname, reservedFontNameReplacements) + familyname = replace_font_name(familyname, additionalFontNameReplacements) + fullname = replace_font_name(fullname, additionalFontNameReplacements) + fontname = replace_font_name(fontname, additionalFontNameReplacements) + familyname = replace_font_name(familyname, additionalFontNameReplacements2) + fullname = replace_font_name(fullname, additionalFontNameReplacements2) + fontname = replace_font_name(fontname, additionalFontNameReplacements2) + + if self.args.makegroups < 0: + logger.warning("Renaming disabled! Make sure to comply with font license, esp RFN clause!") + elif not (FontnameParserOK and self.args.makegroups > 0): + # replace any extra whitespace characters: + font.familyname = " ".join(familyname.split()) + font.fullname = " ".join(fullname.split()) + font.fontname = " ".join(fontname.split()) + + font.appendSFNTName(str('English (US)'), str('Preferred Family'), font.familyname) + font.appendSFNTName(str('English (US)'), str('Family'), font.familyname) + font.appendSFNTName(str('English (US)'), str('Compatible Full'), font.fullname) + font.appendSFNTName(str('English (US)'), str('SubFamily'), subFamily) + else: + # Add Nerd Font suffix unless user specifically asked for some excplicit name via --name + if not user_supplied_name: + short_family = projectNameAbbreviation + variant_abbrev if self.args.makegroups >= 4 else projectNameSingular + variant_full + # inject_suffix(family, ps_fontname, short_family) + n.inject_suffix(verboseAdditionalFontNameSuffix, ps_suffix, short_family) + n.rename_font(font) + + font.comment = projectInfo + font.fontlog = projectInfo + + + def setup_version(self): + """ Add the Nerd Font version to the original version """ + # print("Version was {}".format(sourceFont.version)) + if self.sourceFont.version != None: + self.sourceFont.version += ";" + projectName + " " + version + else: + self.sourceFont.version = str(self.sourceFont.cidversion) + ";" + projectName + " " + version + self.sourceFont.sfntRevision = None # Auto-set (refreshed) by fontforge + self.sourceFont.appendSFNTName(str('English (US)'), str('Version'), "Version " + self.sourceFont.version) + # The Version SFNT name is later reused by the NameParser for UniqueID + # print("Version now is {}".format(sourceFont.version)) + + + def remove_ligatures(self): + # let's deal with ligatures (mostly for monospaced fonts) + # Usually removes 'fi' ligs that end up being only one cell wide, and 'ldot' + if self.args.removeligatures: + logger.info("Removing ligatures from configfile `Subtables` section") + if 'Subtables' not in self.config: + logger.warning("No ligature data (config file missing?)") + return + ligature_subtables = json.loads(self.config.get('Subtables', 'ligatures', fallback='[]')) + for subtable in ligature_subtables: + logger.debug("Removing subtable: %s", subtable) + try: + self.sourceFont.removeLookupSubtable(subtable) + logger.debug("Successfully removed subtable: %s", subtable) + except Exception: + logger.error("Failed to remove subtable: %s", subtable) + + + def manipulate_hints(self): + """ Redo the hinting on some problematic glyphs """ + if 'Hinting' not in self.config: + return + redo = json.loads(self.config.get('Hinting', 're_hint', fallback='[]')) + if not len(redo): + return + logger.debug("Working on {} rehinting rules (this may create a lot of fontforge warnings)".format(len(redo))) + count = 0 + for gname in self.sourceFont: + for regex in redo: + if re.fullmatch(regex, gname): + glyph = self.sourceFont[gname] + glyph.autoHint() + glyph.autoInstr() + count += 1 + break + logger.info("Rehinted {} glyphs".format(count)) + + def assert_monospace(self): + # Check if the sourcefont is monospaced + width_mono, offending_char = is_monospaced(self.sourceFont) + self.source_monospaced = width_mono + if self.args.nonmono: + return + panose_mono = check_panose_monospaced(self.sourceFont) + logger.debug("Monospace check: %s; glyph-width-mono %s", + panose_check_to_text(panose_mono, self.sourceFont.os2_panose), repr(width_mono)) + # The following is in fact "width_mono != panose_mono", but only if panose_mono is not 'unknown' + if (width_mono and panose_mono == 0) or (not width_mono and panose_mono == 1): + logger.warning("Monospaced check: Panose assumed to be wrong") + logger.warning("Monospaced check: %s and %s", + report_advance_widths(self.sourceFont), + panose_check_to_text(panose_mono, self.sourceFont.os2_panose)) + if self.args.forcemono and not width_mono: + logger.warning("Sourcefont is not monospaced - forcing to monospace not advisable, " + "results might be useless%s", + " - offending char: {:X}".format(offending_char) if offending_char is not None else "") + if self.args.forcemono <= 1: + logger.critical("Font will not be patched! Give --mono (or -s) twice to force patching") + sys.exit(1) + if width_mono: + force_panose_monospaced(self.sourceFont) + + + def setup_patch_set(self): + """ Creates list of dicts to with instructions on copying glyphs from each symbol font into self.sourceFont """ + + box_enabled = self.source_monospaced and not self.symbolsonly # Box glyph only for monospaced and not for Symbols Only + box_keep = False + if box_enabled or self.args.forcebox: + self.sourceFont.selection.select(("ranges",), 0x2500, 0x259f) + box_glyphs_target = len(list(self.sourceFont.selection)) + box_glyphs_current = len(list(self.sourceFont.selection.byGlyphs)) + if box_glyphs_target > box_glyphs_current or self.args.forcebox: + # Sourcefont does not have all of these glyphs, do not mix sets (overwrite existing) + if box_glyphs_current > 0: + logger.debug("%d/%d box drawing glyphs will be replaced", + box_glyphs_current, box_glyphs_target) + box_enabled = True + else: + # Sourcefont does have all of these glyphs + # box_keep = True # just scale do not copy (need to scale to fit new cell size) + box_enabled = False # Cowardly not scaling existing glyphs, although the code would allow this + + # Stretch 'xz' or 'pa' (preserve aspect ratio) + # Supported params: overlap | careful | xy-ratio | dont_copy | ypadding + # Overlap value is used horizontally but vertically limited to 0.01 + # Careful does not overwrite/modify existing glyphs + # The xy-ratio limits the x-scale for a given y-scale to make the ratio <= this value (to prevent over-wide glyphs) + # '1' means occupu 1 cell (default for 'xy') + # '2' means occupy 2 cells (default for 'pa') + # '!' means do the 'pa' scaling even with non mono fonts (else it just scales down, never up) + # '^' means that scaling shall fill the whole cell and not only the icon-cap-height (for mono fonts, other always use the whole cell) + # Dont_copy does not overwrite existing glyphs but rescales the preexisting ones + # + # Be careful, stretch may not change within a ScaleRule! + + SYM_ATTR_DEFAULT = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}} + } + SYM_ATTR_POWERLINE = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': '^pa', 'params': {}}, + + # Arrow tips + 0xe0b0: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.06, 'xy-ratio': 0.7}}, + 0xe0b1: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'xy-ratio': 0.7}}, + 0xe0b2: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.06, 'xy-ratio': 0.7}}, + 0xe0b3: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'xy-ratio': 0.7}}, + + # Inverse arrow tips + 0xe0d6: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'xy-ratio': 0.7}}, + 0xe0d7: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'xy-ratio': 0.7}}, + + # Rounded arcs + 0xe0b4: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.06, 'xy-ratio': 0.59}}, + 0xe0b5: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'xy-ratio': 0.5}}, + 0xe0b6: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.06, 'xy-ratio': 0.59}}, + 0xe0b7: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'xy-ratio': 0.5}}, + + # Bottom Triangles + 0xe0b8: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05}}, + 0xe0b9: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {}}, + 0xe0ba: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05}}, + 0xe0bb: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {}}, + + # Top Triangles + 0xe0bc: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05}}, + 0xe0bd: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {}}, + 0xe0be: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05}}, + 0xe0bf: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {}}, + + # Flames + 0xe0c0: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': 0.05}}, + 0xe0c1: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {}}, + 0xe0c2: {'align': 'r', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': 0.05}}, + 0xe0c3: {'align': 'r', 'valign': 'c', 'stretch': '^xy2', 'params': {}}, + + # Small squares + 0xe0c4: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}}, + 0xe0c5: {'align': 'r', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.86}}, + + # Bigger squares + 0xe0c6: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}}, + 0xe0c7: {'align': 'r', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': -0.03, 'xy-ratio': 0.78}}, + + # Waveform + 0xe0c8: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': 0.05}}, + 0xe0ca: {'align': 'r', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': 0.05}}, + + # Hexagons + 0xe0cc: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'overlap': 0.02, 'xy-ratio': 0.85}}, + 0xe0cd: {'align': 'l', 'valign': 'c', 'stretch': '^xy2', 'params': {'xy-ratio': 0.865}}, + + # Legos + 0xe0ce: {'align': 'l', 'valign': 'c', 'stretch': '^pa', 'params': {}}, + 0xe0cf: {'align': 'c', 'valign': 'c', 'stretch': '^pa', 'params': {}}, + 0xe0d0: {'align': 'l', 'valign': 'c', 'stretch': '^pa', 'params': {}}, + 0xe0d1: {'align': 'l', 'valign': 'c', 'stretch': '^pa', 'params': {}}, + + # Top and bottom trapezoid + 0xe0d2: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}}, + 0xe0d4: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.02, 'xy-ratio': 0.7}} + } + SYM_ATTR_TRIGRAPH = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa1!', 'params': {'overlap': -0.10, 'careful': True}} + } + SYM_ATTR_FONTA = { + # 'pa' == preserve aspect ratio + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {}}, + + # Don't center these arrows vertically + 0xf0dc: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}, + 0xf0dd: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}}, + 0xf0de: {'align': 'c', 'valign': '', 'stretch': 'pa', 'params': {}} + } + SYM_ATTR_HEAVYBRACKETS = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': '^pa1!', 'params': {'ypadding': 0.3, 'careful': True}} + } + SYM_ATTR_BOX = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.02, 'dont_copy': box_keep}}, + # No overlap with checkered greys (commented out because that raises problems on rescaling clients) + # 0x2591: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + # 0x2592: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + # 0x2593: {'align': 'c', 'valign': 'c', 'stretch': 'xy', 'params': {'dont_copy': box_keep}}, + } + SYM_ATTR_PROGRESS = { + 'default': {'align': 'c', 'valign': 'c', 'stretch': '^pa1!', 'params': {'overlap': -0.03, 'careful': True}}, # Cirles + # All the squares: + 0xee00: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'careful': True}}, + 0xee01: {'align': 'c', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.10, 'careful': True}}, + 0xee02: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'careful': True}}, + 0xee03: {'align': 'r', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'careful': True}}, + 0xee04: {'align': 'c', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.10, 'careful': True}}, + 0xee05: {'align': 'l', 'valign': 'c', 'stretch': '^xy', 'params': {'overlap': 0.05, 'careful': True}}, + } + CUSTOM_ATTR = { + # previous custom scaling => do not touch the icons + # 'default': {'align': 'c', 'valign': '', 'stretch': '', 'params': {}} + 'default': {'align': 'c', 'valign': 'c', 'stretch': 'pa', 'params': {'careful': self.args.careful}} + } + + # Most glyphs we want to maximize (individually) during the scale + # However, there are some that need to be small or stay relative in + # size to each other. + # The glyph-specific behavior can be given as ScaleRules in the patch-set. + # + # ScaleRules can contain two different kind of rules (possibly in parallel): + # - ScaleGlyph: + # Here one specific glyph is used as 'scale blueprint'. Other glyphs are + # scaled by the same factor as this glyph. This is useful if you have one + # 'biggest' glyph and all others should stay relatively in size. + # Shifting in addition to scaling can be selected too (see below). + # - ScaleGroups: + # Here you specify a group of glyphs that should be handled together + # with the same scaling and shifting (see bottom). The basis for it is + # a 'combined bounding box' of all glyphs in that group. All glyphs are + # handled as if they fill that combined bounding box. + # (- ScaleGroupsVert: Removed with this commit) + # + # The ScaleGlyph method: You set 'ScaleGlyph' to the unicode of the reference glyph. + # Note that there can be only one per patch-set. + # Additionally you set 'GlyphsToScale' that contains all the glyphs that shall be + # handled (scaled) like the reference glyph. + # It is a List of: ((glyph code) or (tuple of two glyph codes that form a closed range)) + # 'GlyphsToScale': [ + # 0x0100, 0x0300, 0x0400, # The single glyphs 0x0100, 0x0300, and 0x0400 + # (0x0200, 0x0210), # All glyphs 0x0200 to 0x0210 including both 0x0200 and 0x0210 + # ]} + # If you want to not only scale but also shift as the reference glyph you give the + # data as 'GlyphsToScale+'. Note that only one set is used and the plus version is preferred. + # + # For the ScaleGroup method you define any number groups of glyphs and each group is + # handled separately. The combined bounding box of all glyphs in the group is determined + # and based on that the scale and shift (see bottom) for all the glyphs in the group. + # You define the groups as value of 'ScaleGroups'. + # It is a List of: ((lists of glyph codes) or (ranges of glyph codes)) + # 'ScaleGroups': [ + # [0x0100, 0x0300, 0x0400], # One group consists of glyphs 0x0100, 0x0300, and 0x0400 + # range(0x0200, 0x0210 + 1), # Another group contains glyphs 0x0200 to 0x0210 incl. + # + # Note the subtle differences: tuple vs. range; closed vs open range; etc + # See prepareScaleRules() for some more details. + # For historic reasons ScaleGroups is sometimes called 'new method' and ScaleGlyph 'old'. + # The codepoints mentioned here are symbol-font-codepoints. + # + # Shifting: + # If we have a combined bounding box stored in a range, that + # box is used to align all symbols in the range identically. + # - If the symbol font is proportinal only the v alignment is synced. + # - If the symbol font is monospaced v and h alignemnts are synced. + # To make sure the behavior is as expected you are required to set a ShiftMode property + # accordingly. It just checks, you can not (!) select what is done with that property. + + BOX_SCALE_LIST = {'ShiftMode': 'xy', 'ScaleGroups': [ + [*range(0x2500, 0x2570 + 1), *range(0x2574, 0x257f + 1)], # box drawing + range(0x2571, 0x2573 + 1), # diagonals + range(0x2580, 0x259f + 1), # blocks and greys (greys are less tall originally, so overlap will be less) + ]} + CODI_SCALE_LIST = {'ShiftMode': 'xy', 'ScaleGroups': [ + [0xea61, 0xeb13], # lightbulb + range(0xeab4, 0xeab7 + 1), # chevrons + [0xea7d, *range(0xea99, 0xeaa1 + 1), 0xebcb], # arrows + [0xeaa2, 0xeb9a, 0xec08, 0xec09], # bells + range(0xead4, 0xead6 + 1), # dot and arrow + [0xeb43, 0xec0b, 0xec0c], # (pull) request changes + range(0xeb6e, 0xeb71 + 1), # triangles + [*range(0xeb89, 0xeb8b + 1), 0xec07], # smallish dots + range(0xebd5, 0xebd7 + 1), # compasses + ]} + DEVI_SCALE_LIST = None + FONTA_SCALE_LIST = {'ShiftMode': '', 'ScaleGroups': [ + [0xf005, 0xf006, 0xf089], # star, star empty, half star + range(0xf026, 0xf028 + 1), # volume off, down, up + range(0xf02b, 0xf02c + 1), # tag, tags + range(0xf031, 0xf035 + 1), # font et al + range(0xf044, 0xf046 + 1), # edit, share, check (boxes) + range(0xf048, 0xf052 + 1), # multimedia buttons + range(0xf060, 0xf063 + 1), # arrows + [0xf053, 0xf054, 0xf077, 0xf078], # chevron all directions + range(0xf07d, 0xf07e + 1), # resize + range(0xf0a4, 0xf0a7 + 1), # pointing hands + [0xf0d7, 0xf0d8, 0xf0d9, 0xf0da, 0xf0dc, 0xf0dd, 0xf0de], # caret all directions and same looking sort + range(0xf100, 0xf107 + 1), # angle + range(0xf130, 0xf131 + 1), # mic + range(0xf141, 0xf142 + 1), # ellipsis + range(0xf153, 0xf15a + 1), # currencies + range(0xf175, 0xf178 + 1), # long arrows + range(0xf182, 0xf183 + 1), # male and female + range(0xf221, 0xf22d + 1), # gender or so + range(0xf255, 0xf25b + 1), # hand symbols + ]} + HEAVY_SCALE_LIST = {'ShiftMode': 'xy', 'ScaleGroups': [ + range(0x276c, 0x2771+1) + ]} + OCTI_SCALE_LIST = {'ShiftMode': '', 'ScaleGroups': [ + [*range(0xf03d, 0xf040 + 1), 0xf019, 0xf030, 0xf04a, 0xf051, 0xf071, 0xf08c ], # arrows + [0xF0E7, # Smily and ... + 0xf044, 0xf05a, 0xf05b, 0xf0aa, # triangles + 0xf052, 0xf053, 0xf296, 0xf2f0, # small stuff + 0xf078, 0xf0a2, 0xf0a3, 0xf0a4, # chevrons + 0xf0ca, 0xf081, 0xf092, # dash, X, github-text + ], + [0xf09c, 0xf09f, 0xf0de], # bells + range(0xf2c2, 0xf2c5 + 1), # move to + [0xf07b, 0xf0a1, 0xf0d6, 0xf306], # bookmarks + ]} + PROGR_SCALE_LIST = {'ShiftMode': 'xy', 'ScaleGroups': [ + range(0xedff, 0xee05 + 1), # boxes... with helper glyph EDFF for Y padding + range(0xee06, 0xee0b + 1), # circles + ]} + WEATH_SCALE_LIST = {'ShiftMode': '', 'ScaleGroups': [ + [0xf03c, 0xf042, 0xf045 ], # degree signs + [0xf043, 0xf044, 0xf048, 0xf04b, 0xf04c, 0xf04d, 0xf057, 0xf058, 0xf087, 0xf088], # arrows + range(0xf053, 0xf055 + 1), # thermometers + [*range(0xf059, 0xf061 + 1), 0xf0b1], # wind directions + range(0xf089, 0xf094 + 1), # clocks + range(0xf095, 0xf0b0 + 1), # moon phases + range(0xf0b7, 0xf0c3 + 1), # wind strengths + [0xf06e, 0xf070 ], # solar/lunar eclipse + [0xf051, 0xf052, 0xf0c9, 0xf0ca, 0xf072 ], # sun/moon up/down + [0xf049, 0xf056, 0xf071, *range(0xf073, 0xf07c + 1), 0xf08a], # other things + # Note: Codepoints listed before that are also in the following range + # will take the scaling of the previous group (the ScaleGroups are + # searched through in definition order). + # But be careful, the combined bounding box for the following group + # _will_ include all glyphs in its definition: Make sure the exempt + # glyphs from above are smaller (do not extend) the combined bounding + # box of this range: + [ *range(0xf000, 0xf041 + 1), + *range(0xf064, 0xf06d + 1), + *range(0xf07d, 0xf083 + 1), + *range(0xf085, 0xf086 + 1), + *range(0xf0b2, 0xf0b6 + 1) + ], # lots of clouds (weather states) (Please read note above!) + ]} + MDI_SCALE_LIST = None # Maybe later add some selected ScaleGroups + + + # Define the character ranges + # Symbol font ranges + self.patch_set = [ + {'Enabled': True, 'Name': "Seti-UI + Custom", 'Filename': "original-source.otf", 'Exact': False, 'SymStart': 0xE4FA, 'SymEnd': 0xE5FF, 'SrcStart': 0xE5FA, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': True, 'Name': "Heavy Angle Brackets", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x276C, 'SymEnd': 0x2771, 'SrcStart': None, 'ScaleRules': HEAVY_SCALE_LIST, 'Attributes': SYM_ATTR_HEAVYBRACKETS}, + {'Enabled': box_enabled, 'Name': "Box Drawing", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0x2500, 'SymEnd': 0x259F, 'SrcStart': None, 'ScaleRules': BOX_SCALE_LIST, 'Attributes': SYM_ATTR_BOX}, + {'Enabled': True, 'Name': "Progress Indicators", 'Filename': "extraglyphs.sfd", 'Exact': True, 'SymStart': 0xEE00, 'SymEnd': 0xEE0B, 'SrcStart': None, 'ScaleRules': PROGR_SCALE_LIST, 'Attributes': SYM_ATTR_PROGRESS}, + {'Enabled': True, 'Name': "Devicons", 'Filename': "devicons/devicons.otf", 'Exact': False, 'SymStart': 0xE600, 'SymEnd': 0xE7EF, 'SrcStart': 0xE700, 'ScaleRules': DEVI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0A0, 'SymEnd': 0xE0A2, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerline, 'Name': "Powerline Symbols", 'Filename': "powerline-symbols/PowerlineSymbols.otf", 'Exact': True, 'SymStart': 0xE0B0, 'SymEnd': 0xE0B3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "powerline-extra/PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0A3, 'SymEnd': 0xE0A3, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "powerline-extra/PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0B4, 'SymEnd': 0xE0C8, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "powerline-extra/PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CA, 'SymEnd': 0xE0CA, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "powerline-extra/PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0xE0CC, 'SymEnd': 0xE0D7, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_POWERLINE}, + {'Enabled': self.args.powerlineextra, 'Name': "Powerline Extra Symbols", 'Filename': "powerline-extra/PowerlineExtraSymbols.otf", 'Exact': True, 'SymStart': 0x2630, 'SymEnd': 0x2630, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_TRIGRAPH}, + {'Enabled': self.args.pomicons, 'Name': "Pomicons", 'Filename': "pomicons/Pomicons.otf", 'Exact': True, 'SymStart': 0xE000, 'SymEnd': 0xE00A, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.fontawesome, 'Name': "Font Awesome", 'Filename': "font-awesome/FontAwesome.otf", 'Exact': True, 'SymStart': 0xED00, 'SymEnd': 0xF2FF, 'SrcStart': None, 'ScaleRules': FONTA_SCALE_LIST, 'Attributes': SYM_ATTR_FONTA}, + {'Enabled': self.args.fontawesomeextension, 'Name': "Font Awesome Extension", 'Filename': "font-awesome-extension.ttf", 'Exact': False, 'SymStart': 0xE000, 'SymEnd': 0xE0A9, 'SrcStart': 0xE200, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Maximize + {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x23FB, 'SymEnd': 0x23FE, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Power, Power On/Off, Power On, Sleep + {'Enabled': self.args.powersymbols, 'Name': "Power Symbols", 'Filename': "Unicode_IEC_symbol_font.otf", 'Exact': True, 'SymStart': 0x2B58, 'SymEnd': 0x2B58, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, # Heavy Circle (aka Power Off) + {'Enabled': False , 'Name': "Material legacy", 'Filename': "materialdesign/materialdesignicons-webfont.ttf", 'Exact': False, 'SymStart': 0xF001, 'SymEnd': 0xF847, 'SrcStart': 0xF500, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.material, 'Name': "Material", 'Filename': "materialdesign/MaterialDesignIconsDesktop.ttf", 'Exact': True, 'SymStart': 0xF0001,'SymEnd': 0xF1AF0,'SrcStart': None, 'ScaleRules': MDI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.weather, 'Name': "Weather Icons", 'Filename': "weather-icons/weathericons-regular-webfont.ttf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF0EB, 'SrcStart': 0xE300, 'ScaleRules': WEATH_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.fontlogos, 'Name': "Font Logos", 'Filename': "font-logos.ttf", 'Exact': True, 'SymStart': 0xF300, 'SymEnd': 0xF381, 'SrcStart': None, 'ScaleRules': None, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.otf", 'Exact': False, 'SymStart': 0xF000, 'SymEnd': 0xF105, 'SrcStart': 0xF400, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Magnifying glass + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.otf", 'Exact': True, 'SymStart': 0x2665, 'SymEnd': 0x2665, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Heart + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.otf", 'Exact': True, 'SymStart': 0X26A1, 'SymEnd': 0X26A1, 'SrcStart': None, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, # Zap + {'Enabled': self.args.octicons, 'Name': "Octicons", 'Filename': "octicons/octicons.otf", 'Exact': False, 'SymStart': 0xF27C, 'SymEnd': 0xF306, 'SrcStart': 0xF4A9, 'ScaleRules': OCTI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.codicons, 'Name': "Codicons", 'Filename': "codicons/codicon.ttf", 'Exact': True, 'SymStart': 0xEA60, 'SymEnd': 0xEC1E, 'SrcStart': None, 'ScaleRules': CODI_SCALE_LIST, 'Attributes': SYM_ATTR_DEFAULT}, + {'Enabled': self.args.custom, 'Name': "Custom", 'Filename': self.args.custom, 'Exact': True, 'SymStart': 0x0000, 'SymEnd': 0x0000, 'SrcStart': None, 'ScaleRules': None, 'Attributes': CUSTOM_ATTR} + ] + + def improve_line_dimensions(self): + # Make the total line size even. This seems to make the powerline separators + # center more evenly. + if self.args.adjustLineHeight: + if (self.sourceFont.os2_winascent + self.sourceFont.os2_windescent) % 2 != 0: + # All three are equal before due to get_sourcefont_dimensions() + self.sourceFont.hhea_ascent += 1 + self.sourceFont.os2_typoascent += 1 + self.sourceFont.os2_winascent += 1 + + def add_glyphrefs_to_essential(self, unicode): + self.essential.add(unicode) + # According to fontforge spec, altuni is either None or a tuple of tuples + # Those tuples contained in altuni are of the following "format": + # (unicode-value, variation-selector, reserved-field) + altuni = self.sourceFont[unicode].altuni + if altuni is not None: + for altcode in [ v for v, s, r in altuni if v >= 0 ]: + # If alternate unicode already exists in self.essential, + # that means it has gone through this function before. + # Therefore we skip it to avoid infinite loop. + # A unicode value of -1 basically means unused and is also worth skipping. + if altcode not in self.essential: + self.add_glyphrefs_to_essential(altcode) + # From fontforge documentation: + # glyph.references return a tuple of tuples containing, for each reference in foreground, + # a glyph name, a transformation matrix, and (depending on ff version) whether the + # reference is currently selected. + references = self.sourceFont[unicode].references + for refcode in [ self.sourceFont[n].unicode for n, *_ in references ]: # tuple of 2 or 3 depending on ff version + if refcode not in self.essential and refcode >= 0: + self.add_glyphrefs_to_essential(refcode) + + def get_essential_references(self): + """Find glyphs that are needed for the basic glyphs""" + # Sometimes basic glyphs are constructed from multiple other glyphs. + # Find out which other glyphs are also needed to keep the basic + # glyphs intact. + # 0x0000-0x017f is the Latin Extended-A range + # 0xfb00-0xfb06 are 'fi' and other ligatures + basic_glyphs = { c for c in range(0x21, 0x17f + 1) if c in self.sourceFont } + # Collect substitution destinations + for glyph in list(basic_glyphs) + [*range(0xfb00, 0xfb06 + 1)]: + if not glyph in self.sourceFont: + continue + for possub in self.sourceFont[glyph].getPosSub('*'): + if possub[1] == 'Substitution' or possub[1] == 'Ligature': + basic_glyphs.add(glyph) + basic_glyphs.add(self.sourceFont[possub[2]].unicode) + basic_glyphs.discard(-1) # the .notdef glyph + for glyph in basic_glyphs: + self.add_glyphrefs_to_essential(glyph) + + def get_sourcefont_dimensions(self): + """ This gets the font dimensions (cell width and height), and makes them equal on all platforms """ + # Step 1 + # There are three ways to describe the baseline to baseline distance + # (a.k.a. line spacing) of a font. That is all a kuddelmuddel + # and we try to sort this out here + # See also https://glyphsapp.com/learn/vertical-metrics + # See also https://github.com/source-foundry/font-line + (hhea_btb, typo_btb, win_btb, win_gap) = get_btb_metrics(self.sourceFont) + use_typo = self.sourceFont.os2_use_typo_metrics != 0 + + Metric = Enum('Metric', get_metrics_names()) + + if not self.args.metrics: + # We use either TYPO (1) or WIN (2) and compare with HHEA + # and use HHEA (0) if the fonts seems broken - no WIN, see #1056 + our_btb = typo_btb if use_typo else win_btb + if our_btb == hhea_btb: + metrics = Metric.TYPO if use_typo else Metric.WIN # conforming font + elif abs(our_btb - hhea_btb) / our_btb < 0.03: + logger.info("Font vertical metrics slightly off (%.1f%%)", (our_btb - hhea_btb) / our_btb * 100.0) + metrics = Metric.TYPO if use_typo else Metric.WIN + else: + # Try the other metric + our_btb = typo_btb if not use_typo else win_btb + if our_btb == hhea_btb: + use_typo = not use_typo + logger.warning("Font vertical metrics probably wrong USE TYPO METRICS, assume opposite (i.e. %s)", repr(use_typo)) + self.sourceFont.os2_use_typo_metrics = 1 if use_typo else 0 + metrics = Metric.TYPO if use_typo else Metric.WIN + else: + # We trust the WIN metric more, see experiments in #1056 + logger.warning("Font vertical metrics inconsistent (HHEA %d / TYPO %d / WIN %d), using WIN", hhea_btb, typo_btb, win_btb) + our_btb = win_btb + metrics = Metric.WIN + else: + metrics = Metric[self.args.metrics] + logger.debug("Metrics in the font: HHEA %d / TYPO %d / WIN %d", hhea_btb, typo_btb, win_btb) + if metrics == Metric.HHEA: + our_btb = hhea_btb + elif metrics == Metric.TYPO: + our_btb = typo_btb + else: + our_btb = win_btb + logger.info("Manually selected metrics: %s (%d)", self.args.metrics, our_btb) + + # print("FINI hhea {} typo {} win {} use {} {} {}".format(hhea_btb, typo_btb, win_btb, use_typo, our_btb != hhea_btb, self.sourceFont.fontname)) + + self.font_dim = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0, 'width' : 0, 'height': 0, 'iconheight': 0, 'ypadding': 0} + + if metrics == Metric.HHEA: + self.font_dim['ymin'] = self.sourceFont.hhea_descent - half_gap(self.sourceFont.hhea_linegap, False) + self.font_dim['ymax'] = self.sourceFont.hhea_ascent + half_gap(self.sourceFont.hhea_linegap, True) + elif metrics == Metric.TYPO: + self.font_dim['ymin'] = self.sourceFont.os2_typodescent - half_gap(self.sourceFont.os2_typolinegap, False) + self.font_dim['ymax'] = self.sourceFont.os2_typoascent + half_gap(self.sourceFont.os2_typolinegap, True) + elif metrics == Metric.WIN: + self.font_dim['ymin'] = -self.sourceFont.os2_windescent - half_gap(win_gap, False) + self.font_dim['ymax'] = self.sourceFont.os2_winascent + half_gap(win_gap, True) + else: + logger.debug("Metrics is strange") + pass # Will fail the metrics check some line later + + if isinstance(self.args.cellopt, list): + logger.debug("Overriding cell Y{%d:%d} with Y{%d:%d}", + self.font_dim['ymin'], self.font_dim['ymax'], + self.args.cellopt[2], self.args.cellopt[3]) + self.font_dim['ymin'] = self.args.cellopt[2] + self.font_dim['ymax'] = self.args.cellopt[3] + our_btb = self.args.cellopt[3] - self.args.cellopt[2] + + # Calculate font height + self.font_dim['height'] = -self.font_dim['ymin'] + self.font_dim['ymax'] + if self.font_dim['height'] == 0: + # This can only happen if the input font is empty + # Assume we are using our prepared templates + self.symbolsonly = True + self.font_dim = { + 'xmin' : 0, + 'ymin' : -self.sourceFont.descent, + 'xmax' : self.sourceFont.em, + 'ymax' : self.sourceFont.ascent, + 'width' : self.sourceFont.em, + 'height' : self.sourceFont.descent + self.sourceFont.ascent, + 'iconheight': self.sourceFont.descent + self.sourceFont.ascent, + 'ypadding' : 0, + } + our_btb = self.sourceFont.descent + self.sourceFont.ascent + if self.font_dim['height'] <= 0: + logger.critical("Can not detect sane font height") + sys.exit(1) + + self.font_dim['iconheight'] = self.font_dim['height'] + if self.args.single and self.sourceFont.capHeight > 0 and not isinstance(self.args.cellopt, list): + # Limit the icon height on monospaced fonts because very slender and tall icons render + # excessively tall otherwise. We ignore that effect for the other variants because it + # does not look so much out of place there. + # Icons can be bigger than the letter capitals, but not the whole cell: + self.font_dim['iconheight'] = (self.sourceFont.capHeight * 2 + self.font_dim['height']) / 3 + + # Make all metrics equal + self.sourceFont.os2_typolinegap = 0 + self.sourceFont.os2_typoascent = self.font_dim['ymax'] + self.sourceFont.os2_typodescent = self.font_dim['ymin'] + self.sourceFont.os2_winascent = self.sourceFont.os2_typoascent + self.sourceFont.os2_windescent = -self.sourceFont.os2_typodescent + self.sourceFont.hhea_ascent = self.sourceFont.os2_typoascent + self.sourceFont.hhea_descent = self.sourceFont.os2_typodescent + self.sourceFont.hhea_linegap = self.sourceFont.os2_typolinegap + self.sourceFont.os2_use_typo_metrics = 1 + (check_hhea_btb, check_typo_btb, check_win_btb, _) = get_btb_metrics(self.sourceFont) + if check_hhea_btb != check_typo_btb or check_typo_btb != check_win_btb or check_win_btb != our_btb: + logger.critical("Error in baseline to baseline code detected") + sys.exit(1) + + # Step 2 + # Find the biggest char width and advance width + # 0x00-0x17f is the Latin Extended-A range + warned1 = self.args.nonmono # Do not warn if proportional target + warned2 = warned1 + for glyph in range(0x21, 0x17f): + if glyph in range(0x7F, 0xBF) or glyph in [ + 0x132, 0x133, # IJ, ij (in Overpass Mono) + 0x022, 0x027, 0x060, # Single and double quotes in Inconsolata LGC + 0x0D0, 0x10F, 0x110, 0x111, 0x127, 0x13E, 0x140, 0x165, # Eth and others with stroke or caron in RobotoMono + 0x149, # napostrophe in DaddyTimeMono + 0x02D, # hyphen for Monofur + ]: + continue # ignore special characters like '1/4' etc and some specifics + try: + (_, _, xmax, _) = self.sourceFont[glyph].boundingBox() + except TypeError: + continue + # print("WIDTH {:X} {} ({} {})".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) + if self.font_dim['width'] < self.sourceFont[glyph].width: + self.font_dim['width'] = self.sourceFont[glyph].width + if not warned1 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z + logger.debug("Extended glyphs wider than basic glyphs, results might be useless") + logger.debug("%s", report_advance_widths(self.sourceFont)) + warned1 = True + # print("New MAXWIDTH-A {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) + if xmax > self.font_dim['xmax']: + self.font_dim['xmax'] = xmax + if not warned2 and glyph > 0x7a: # NOT 'basic' glyph, which includes a-zA-Z + logger.debug("Extended glyphs wider bounding box than basic glyphs") + warned2 = True + # print("New MAXWIDTH-B {:X} {} -> {} {}".format(glyph, self.sourceFont[glyph].width, self.font_dim['width'], xmax)) + if self.font_dim['width'] < self.font_dim['xmax']: + logger.debug("Font has negative right side bearing in extended glyphs") + self.font_dim['xmax'] = self.font_dim['width'] # In fact 'xmax' is never used + if self.font_dim['width'] <= 0: + logger.critical("Can not detect sane font width") + sys.exit(1) + if isinstance(self.args.cellopt, list): + logger.debug("Overriding cell X{%d:%d} with X{%d:%d}", + self.font_dim['xmin'], self.font_dim['xmin'] + self.font_dim['width'], + self.args.cellopt[0], self.args.cellopt[1]) + self.font_dim['xmin'] = self.args.cellopt[0] + self.font_dim['xmax'] = self.args.cellopt[1] + self.font_dim['width'] = self.args.cellopt[1] + if self.args.cellopt: + logger.info("Cell coordinates (Xmin:Xmax:Ymin:Ymax) %s%d:%d:%d:%d", + '' if not isinstance(self.args.cellopt, list) else 'overridden with ', + self.font_dim['xmin'], self.font_dim['width'], + self.font_dim['ymax'] - self.font_dim['height'], self.font_dim['ymax']) + logger.debug("Final font cell dimensions %d w x %d h%s", + self.font_dim['width'], self.font_dim['height'], + ' (with icon cell {} h)'.format(int(self.font_dim['iconheight'])) if self.font_dim['iconheight'] != self.font_dim['height'] else '') + try: + middle = lambda x, y: abs(x - y) / 2 + min(x, y) + x_bb = self.sourceFont['x'].boundingBox(); + X_bb = self.sourceFont['X'].boundingBox(); + logger.debug("Center x-height/cell/capitals %d/%d/%d", + middle(x_bb[1], x_bb[3]), + middle(self.font_dim['ymin'], self.font_dim['ymax']), + middle(X_bb[1], X_bb[3])) + except: + pass + + self.xavgwidth.append(self.args.xavgwidth) + if isinstance(self.xavgwidth[-1], int) and self.xavgwidth[-1] == 0: + self.xavgwidth[-1] = get_old_average_x_width(self.sourceFont) + + + def get_target_width(self, stretch): + """ Get the target width (1 or 2 'cell') for a given stretch parameter """ + # For monospaced fonts all chars need to be maximum 'one' space wide + # other fonts allows double width glyphs for 'pa' or if requested with '2' + if self.args.single or ('pa' not in stretch and '2' not in stretch) or '1' in stretch: + return 1 + return 2 + + def get_scale_factors(self, sym_dim, stretch, overlap=None): + """ Get scale in x and y as tuple """ + # It is possible to have empty glyphs, so we need to skip those. + if not sym_dim['width'] or not sym_dim['height']: + return (1.0, 1.0) + + target_width = self.font_dim['width'] * self.get_target_width(stretch) + if overlap: + target_width += self.font_dim['width'] * overlap + scale_ratio_x = target_width / sym_dim['width'] + + # font_dim['height'] represents total line height, keep our symbols sized based upon font's em + # Use the font_dim['height'] only for explicit 'y' scaling (not 'pa') + target_height = self.font_dim['height'] if '^' in stretch else self.font_dim['iconheight'] + target_height *= 1.0 - self.font_dim['ypadding'] + if overlap: + target_height *= 1.0 + min(0.01, overlap) # never aggressive vertical overlap + scale_ratio_y = target_height / sym_dim['height'] + + if 'pa' in stretch: + # We want to preserve x/y aspect ratio, so find biggest scale factor that allows symbol to fit + scale_ratio_x = min(scale_ratio_x, scale_ratio_y) + if not self.args.single and not '!' in stretch and not overlap: + # non monospaced fonts just scale down on 'pa', not up + scale_ratio_x = min(scale_ratio_x, 1.0) + scale_ratio_y = scale_ratio_x + else: + # Keep the not-stretched direction + if not 'x' in stretch: + scale_ratio_x = 1.0 + if not 'y' in stretch: + scale_ratio_y = 1.0 + + return (scale_ratio_x, scale_ratio_y) + + + def copy_glyphs(self, sourceFontStart, symbolFont, symbolFontStart, symbolFontEnd, exactEncoding, scaleRules, setName, attributes): + """ Copies symbol glyphs into self.sourceFont """ + progressText = '' + careful = False + sourceFontCounter = 0 + + if self.args.careful: + careful = True + + # Create glyphs from symbol font + # + # If we are going to copy all Glyphs, then assume we want to be careful + # and only copy those that are not already contained in the source font + if symbolFontStart == 0: + symbolFont.selection.all() + careful = True + else: + symbolFont.selection.select((str("ranges"), str("unicode")), symbolFontStart, symbolFontEnd) + + # Get number of selected non-empty glyphs with codes >=0 (i.e. not -1 == notdef) + symbolFontSelection = [ x for x in symbolFont.selection.byGlyphs if x.unicode >= 0 ] + glyphSetLength = len(symbolFontSelection) + + if not self.args.quiet: + modify = attributes['default']['params'].get('dont_copy') + sys.stdout.write("{} {} Glyphs from {} Set\n".format( + "Adding" if not modify else "Rescaling", glyphSetLength, setName)) + + currentSourceFontGlyph = -1 # initialize for the exactEncoding case + width_warning = False + + for index, sym_glyph in enumerate(symbolFontSelection): + sym_attr = attributes.get(sym_glyph.unicode) + if sym_attr is None: + sym_attr = attributes['default'] + + if self.font_extrawide: + # Do not allow 'xy2' scaling + sym_attr['stretch'] = sym_attr['stretch'].replace('2', '') + + if exactEncoding: + # Use the exact same hex values for the source font as for the symbol font. + # Problem is we do not know the codepoint of the sym_glyph and because it + # came from a selection.byGlyphs there might be skipped over glyphs. + # The iteration is still in the order of the selection by codepoint, + # so we take the next allowed codepoint of the current glyph + possible_codes = [ ] + if sym_glyph.unicode > currentSourceFontGlyph: + possible_codes += [ sym_glyph.unicode ] + if sym_glyph.altuni: + possible_codes += [ v for v, s, r in sym_glyph.altuni if v > currentSourceFontGlyph ] + if len(possible_codes) == 0: + logger.warning("Can not determine codepoint of %X. Skipping...", sym_glyph.unicode) + continue + currentSourceFontGlyph = min(possible_codes) + else: + # use source font defined hex values based on passed in start (fills gaps; symbols are packed) + currentSourceFontGlyph = sourceFontStart + sourceFontCounter + sourceFontCounter += 1 + + # For debugging process only limited glyphs + # if currentSourceFontGlyph != 0xe7bd: + # continue + + ypadding = sym_attr['params'].get('ypadding') + self.font_dim['ypadding'] = ypadding or 0.0 + + if not self.args.quiet: + if self.args.progressbars: + update_progress(round(float(index + 1) / glyphSetLength, 2)) + else: + progressText = "\nUpdating glyph: {} {} putting at: {:X}".format(sym_glyph, sym_glyph.glyphname, currentSourceFontGlyph) + sys.stdout.write(progressText) + sys.stdout.flush() + + # check if a glyph already exists in this location + do_careful = sym_attr['params'].get('careful', careful) # params take precedence + if do_careful or currentSourceFontGlyph in self.essential: + if currentSourceFontGlyph in self.sourceFont: + careful_type = 'essential' if currentSourceFontGlyph in self.essential else 'existing' + logger.debug("Found %s Glyph at %X. Skipping...", careful_type, currentSourceFontGlyph) + # We don't want to touch anything so move to next Glyph + continue + else: + # If we overwrite an existing glyph all subtable entries regarding it will be wrong + # (Probably; at least if we add a symbol and do not substitute a ligature or such) + if currentSourceFontGlyph in self.sourceFont: + self.sourceFont[currentSourceFontGlyph].removePosSub("*") + + stretch = sym_attr['stretch'] + dont_copy = sym_attr['params'].get('dont_copy') + + if dont_copy: + # Just prepare scaling of existing glyphs + glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, stretch, self.sourceFont, currentSourceFontGlyph) if scaleRules is not None else None + else: + # Break apart multiple unicodes linking to one glyph + if currentSourceFontGlyph in self.sourceFont: + altuni = self.sourceFont[currentSourceFontGlyph].altuni + if altuni: + codes = { v for v, s, r in altuni if v >= 0 } + codes.add(self.sourceFont[currentSourceFontGlyph].unicode) + codes.remove(currentSourceFontGlyph) + codes = [ "{:04X}".format(c) for c in sorted(list(codes)) ] + logger.debug("Removing alternate unicode on %X (%s)", currentSourceFontGlyph, ' '.join(codes)); + self.sourceFont[currentSourceFontGlyph].altuni = None + self.sourceFont.encoding = 'UnicodeFull' # Rebuild encoding table (needed after altuni changes) + + # This will destroy any content currently in currentSourceFontGlyph, so do it first + glyph_scale_data = self.get_glyph_scale(sym_glyph.encoding, scaleRules, stretch, symbolFont, currentSourceFontGlyph) if scaleRules is not None else None + + # Select and copy symbol from its encoding point + # We need to do this select after the careful check, this way we don't + # reset our selection before starting the next loop + symbolFont.selection.select(sym_glyph.encoding) + symbolFont.copy() + + # Paste it + self.sourceFont.selection.select(currentSourceFontGlyph) + self.sourceFont.paste() + self.sourceFont[currentSourceFontGlyph].glyphname = \ + self.glyphnames.get(currentSourceFontGlyph, sym_glyph.glyphname) if setName != 'Custom' else sym_glyph.glyphname + self.sourceFont[currentSourceFontGlyph].manualHints = True # No autohints for symbols + + # Prepare symbol glyph dimensions + sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph]) + overlap = sym_attr['params'].get('overlap') + if overlap and ypadding: + logger.critical("Conflicting params: overlap and ypadding") + sys.exit(1) + + if glyph_scale_data is not None: + if glyph_scale_data[1] is not None: + sym_dim = glyph_scale_data[1] # Use combined bounding box + (scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, stretch, overlap) + else: + # This is roughly alike get_scale_factors(glyph_scale_data[1], 'pa') + # Except we do not have glyph_scale_data[1] always... + (scale_ratio_x, scale_ratio_y) = (glyph_scale_data[0], glyph_scale_data[0]) + if overlap: + scale_ratio_x *= 1.0 + (self.font_dim['width'] / (sym_dim['width'] * scale_ratio_x)) * overlap + y_overlap = min(0.01, overlap) # never aggressive vertical overlap + scale_ratio_y *= 1.0 + (self.font_dim['height'] / (sym_dim['height'] * scale_ratio_y)) * y_overlap + else: + (scale_ratio_x, scale_ratio_y) = self.get_scale_factors(sym_dim, stretch, overlap) + + + # Size in x to size in y ratio limit (to prevent over-wide glyphs) + xy_ratio_max = sym_attr['params'].get('xy-ratio') + if (xy_ratio_max): + xy_ratio = sym_dim['width'] * scale_ratio_x / (sym_dim['height'] * scale_ratio_y) + if xy_ratio > xy_ratio_max: + scale_ratio_x = scale_ratio_x * xy_ratio_max / xy_ratio + + if scale_ratio_x != 1.0 or scale_ratio_y != 1.0: + scale_ratio_x *= self.sourceFont.em / (self.sourceFont.em + 1) # scale a tiny bit too small to avoid rounding problems + self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y)) + + # Drop nonintegral part of nodes' coordinates; ttf will do it anyhow, otf will be much smaller + self.sourceFont[currentSourceFontGlyph].round() + + if self.args.single: + # Check and correct the scaling after rounding (if all 3 tries fail we will get a warning later on) + destmaxsize = self.font_dim['width'] * max(1, 1 + (overlap or 0)) + for increaser in range(3): + (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() + sizeerror = (xmax - xmin) - destmaxsize + if sizeerror <= 0: + break + # Start from scratch with a new unscaled glyph + scale_ratio_x /= 1 + ((sizeerror + increaser) / destmaxsize) + self.sourceFont.paste() + self.sourceFont[currentSourceFontGlyph].transform(psMat.scale(scale_ratio_x, scale_ratio_y)) + self.sourceFont[currentSourceFontGlyph].round() + + # We pasted and scaled now we want to align/move + # Use the dimensions from the newly pasted and stretched glyph to avoid any rounding errors + sym_dim = get_glyph_dimensions(self.sourceFont[currentSourceFontGlyph]) + # Use combined bounding box? + if glyph_scale_data is not None and glyph_scale_data[1] is not None: + scaleglyph_dim = scale_bounding_box(glyph_scale_data[1], scale_ratio_x, scale_ratio_y) + if scaleglyph_dim['advance'] is None: + # On monospaced symbol collections use their advance with, otherwise align horizontally individually + scaleglyph_dim['xmin'] = sym_dim['xmin'] + scaleglyph_dim['xmax'] = sym_dim['xmax'] + scaleglyph_dim['width'] = sym_dim['width'] + sym_dim = scaleglyph_dim + + y_align_distance = 0 + if sym_attr['valign'] == 'c': + # Center the symbol vertically by matching the center of the line height and center of symbol + sym_ycenter = sym_dim['ymax'] - (sym_dim['height'] / 2) + font_ycenter = self.font_dim['ymax'] - (self.font_dim['height'] / 2) + y_align_distance = font_ycenter - sym_ycenter + + # Handle glyph l/r/c alignment + x_align_distance = 0 + simple_nonmono = self.args.nonmono and sym_dim['advance'] is None + if simple_nonmono: + # Remove left side bearing + # (i.e. do not remove left side bearing when combined BB is in use) + x_align_distance = -self.sourceFont[currentSourceFontGlyph].left_side_bearing + elif sym_attr['align']: + # First find the baseline x-alignment (left alignment amount) + x_align_distance = self.font_dim['xmin'] - sym_dim['xmin'] + if self.args.nonmono and 'pa' in stretch: + cell_width = sym_dim['advance'] or sym_dim['width'] + else: + cell_width = self.font_dim['width'] + if sym_attr['align'] == 'c': + # Center align + x_align_distance += (cell_width / 2) - (sym_dim['width'] / 2) + elif sym_attr['align'] == 'r': + # Right align + # (not really supported with pa scaling and 2x stretch in NFP) + x_align_distance += cell_width * self.get_target_width(stretch) - sym_dim['width'] + if not overlap: + # If symbol glyph is wider than target font cell, just left-align + x_align_distance = max(self.font_dim['xmin'] - sym_dim['xmin'], x_align_distance) + + if overlap: + overlap_width = self.font_dim['width'] * overlap + if sym_attr['align'] == 'l': + x_align_distance -= overlap_width + elif sym_attr['align'] == 'c': + # center aligned keeps being center aligned even with overlap + if overlap_width < 0 and simple_nonmono: # Keep positive bearing due to negative overlap (propo) + x_align_distance -= overlap_width / 2 + elif sym_attr['align'] == 'r' and not simple_nonmono: + # Check and correct overlap; it can go wrong if we have a xy-ratio limit + target_xmax = (self.font_dim['xmin'] + self.font_dim['width']) * self.get_target_width(stretch) + target_xmax += overlap_width + glyph_xmax = sym_dim['xmax'] + x_align_distance + correction = target_xmax - glyph_xmax + x_align_distance += correction + + align_matrix = psMat.translate(x_align_distance, y_align_distance) + self.sourceFont[currentSourceFontGlyph].transform(align_matrix) + + # Ensure after horizontal adjustments and centering that the glyph + # does not overlap the bearings (edges) + if not overlap: + self.remove_glyph_neg_bearings(self.sourceFont[currentSourceFontGlyph]) + + # Needed for setting 'advance width' on each glyph so they do not overlap, + # also ensures the font is considered monospaced on Windows by setting the + # same width for all character glyphs. This needs to be done for all glyphs, + # even the ones that are empty and didn't go through the scaling operations. + # It should come after setting the glyph bearings + if not self.args.nonmono: + self.set_glyph_width_mono(self.sourceFont[currentSourceFontGlyph]) + else: + # Target font with variable advance width get the icons with their native widths + # and keeping possible (right and/or negative) bearings in effect + if sym_dim['advance'] is not None: + # 'Width' from monospaced scale group + width = sym_dim['advance'] + else: + width = sym_dim['width'] + # If we have overlap we need to subtract that to keep/get negative bearings + if overlap: + width -= overlap_width + # Fontforge handles the width change like this: + # - Keep existing left_side_bearing + # - Set width + # - Calculate and set new right_side_bearing + self.sourceFont[currentSourceFontGlyph].width = int(width) + + # Check if the inserted glyph is scaled correctly for monospace + if self.args.single: + (xmin, _, xmax, _) = self.sourceFont[currentSourceFontGlyph].boundingBox() + if (xmax - xmin) > self.font_dim['width'] * max(1, 1 + (overlap or 0)): + logger.warning("Scaled glyph %X wider than one monospace width (%d / %d (overlap %s))", + currentSourceFontGlyph, int(xmax - xmin), self.font_dim['width'], repr(overlap)) + + # end for + + if not self.args.quiet: + sys.stdout.write("\n") + + + def set_sourcefont_glyph_widths(self): + """ Makes self.sourceFont monospace compliant """ + + for glyph in self.sourceFont.glyphs(): + if (glyph.width == self.font_dim['width']): + # Don't touch the (negative) bearings if the width is ok + # Ligatures will have these. + continue + + if (glyph.width != 0): + # If the width is zero this glyph is intended to be printed on top of another one. + # In this case we need to keep the negative bearings to shift it 'left'. + # Things like Ä have these: composed of U+0041 'A' and U+0308 'double dot above' + # + # If width is not zero, correct the bearings such that they are within the width: + self.remove_glyph_neg_bearings(glyph) + + self.set_glyph_width_mono(glyph) + + + def remove_glyph_neg_bearings(self, glyph): + """ Sets passed glyph's bearings 0 if they are negative. """ + try: + if glyph.left_side_bearing < 0: + glyph.left_side_bearing = 0 + if glyph.right_side_bearing < 0: + glyph.right_side_bearing = 0 + except: + pass + + + def set_glyph_width_mono(self, glyph): + """ Sets passed glyph.width to self.font_dim.width. + + self.font_dim.width is set with self.get_sourcefont_dimensions(). + """ + try: + # Fontforge handles the width change like this: + # - Keep existing left_side_bearing + # - Set width + # - Calculate and set new right_side_bearing + glyph.width = self.font_dim['width'] + except: + pass + + def prepareScaleRules(self, scaleRules, stretch, symbolFont, destGlyph): + """ Prepare raw ScaleRules data for use """ + # The scaleRules is/will be a dict with these (possible) entries: + # 'ScaleGroups': List of ((lists of glyph codes) or (ranges of glyph codes)) that shall be scaled + # 'scales': List of associated scale factors, one for each entry in 'ScaleGroups' (generated by this function) + # 'bbdims': List of associated sym_dim dicts, one for each entry in 'ScaleGroups' (generated by this function) + # Each dim_dict describes the combined bounding box of all glyphs in one ScaleGroups group + # Example: + # { 'ScaleGroups': [ range(1, 3), [ 7, 10 ], ], + # 'scales': [ 1.23, 1.33, ], + # 'bbdims': [ dim_dict1, dim_dict2, ] } + # + # Each item in 'ScaleGroups' (a range or an explicit list) forms a group of glyphs that shall be + # as rescaled all with the same and maximum possible (for the included glyphs) 'pa' factor. + # If the 'bbdims' is present they all shall be shifted in the same way. + # + # Previously this structure has been used: + # 'ScaleGlyph' Lead glyph, which scaling factor is taken + # 'GlyphsToScale': List of ((glyph code) or (tuple of two glyph codes that form a closed range)) that shall be scaled + # Note that this allows only one group for the whle symbol font, and that the scaling factor is defined by + # a specific character, which needs to be manually selected (on each symbol font update). + # Previous entries are automatically rewritten to the new style. + # + # Note that scaleRules is overwritten with the added data. + if 'scales' in scaleRules: + # Already prepared... must not happen, ignore call + return + + scaleRules['scales'] = [] + scaleRules['bbdims'] = [] + if 'ScaleGroups' not in scaleRules: + scaleRules['ScaleGroups'] = [] + + mode = scaleRules['ShiftMode'] # Mode is only documentary + for group in scaleRules['ScaleGroups']: + sym_dim = get_multiglyph_boundingBox([ symbolFont[g] if g in symbolFont else None for g in group ], destGlyph) + scale = self.get_scale_factors(sym_dim, stretch)[0] + scaleRules['scales'].append(scale) + scaleRules['bbdims'].append(sym_dim) + if (mode): + if ('x' in mode) != (sym_dim['advance'] is not None): + d = '0x{:X} - 0x{:X}'.format(group[0], group[-1]) + if ('x' in mode) : + logger.critical("Scaling in group %s is expected to do horizontal shifts but can not", d) + else: + logger.critical("Scaling in group %s is expected to not do horizontal shifts but will", d) + sys.exit(1) + + if 'ScaleGlyph' in scaleRules: + # Rewrite to equivalent ScaleGroup + group_list = [] + if 'GlyphsToScale+' in scaleRules: + key = 'GlyphsToScale+' + plus = True + else: + key = 'GlyphsToScale' + plus = False + for i in scaleRules[key]: + if isinstance(i, tuple): + group_list.append(range(i[0], i[1] + 1)) + else: + group_list.append(i) + sym_dim = get_glyph_dimensions(symbolFont[scaleRules['ScaleGlyph']]) + scale = self.get_scale_factors(sym_dim, stretch)[0] + scaleRules['ScaleGroups'].append(group_list) + scaleRules['scales'].append(scale) + if plus: + scaleRules['bbdims'].append(sym_dim) + else: + scaleRules['bbdims'].append(None) # The 'old' style keeps just the scale, not the positioning + + def get_glyph_scale(self, symbol_unicode, scaleRules, stretch, symbolFont, dest_unicode): + """ Determines whether or not to use scaled glyphs for glyph in passed symbol_unicode """ + # Potentially destroys the contents of self.sourceFont[dest_unicode] + if not 'scales' in scaleRules: + if not dest_unicode in self.sourceFont: + self.sourceFont.createChar(dest_unicode) + self.prepareScaleRules(scaleRules, stretch, symbolFont, self.sourceFont[dest_unicode]) + for glyph_list, scale, box in zip(scaleRules['ScaleGroups'], scaleRules['scales'], scaleRules['bbdims']): + for e in glyph_list: + if isinstance(e, range): + if symbol_unicode in e: + return (scale, box) + elif symbol_unicode == e: + return (scale, box) + return None + + +def half_gap(gap, top): + """ Divides integer value into two new integers """ + # Line gap add extra space on the bottom of the line which + # doesn't allow the powerline glyphs to fill the entire line. + # Put half of the gap into the 'cell', each top and bottom + if gap <= 0: + return 0 + gap_top = int(gap / 2) + gap_bottom = gap - gap_top + if top: + logger.info("Redistributing line gap of %d (%d top and %d bottom)", gap, gap_top, gap_bottom) + return gap_top + return gap_bottom + +def replace_font_name(font_name, replacement_dict): + """ Replaces all keys with vals from replacement_dict in font_name. """ + for key, val in replacement_dict.items(): + font_name = font_name.replace(key, val) + return font_name + + +def make_sure_path_exists(path): + """ Verifies path passed to it exists. """ + try: + os.makedirs(path) + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + +def sanitize_filename(filename, allow_dirs = False): + """ Enforces to not use forbidden characters in a filename/path. """ + if filename == '.' and not allow_dirs: + return '_' + restore_colon = sys.platform == 'win32' and re.match('[a-z]:', filename, re.I) + trans = filename.maketrans('<>:"|?*', '_______') + for i in range(0x00, 0x20): + trans[i] = ord('_') + if not allow_dirs: + trans[ord('/')] = ord('_') + trans[ord('\\')] = ord('_') + else: + trans[ord('\\')] = ord('/') # We use Posix paths + new_filename = filename.translate(trans) + if restore_colon: + new_filename = new_filename[ :1] + ':' + new_filename[2: ] + return new_filename + +def get_multiglyph_boundingBox(glyphs, destGlyph = None): + """ Returns dict of the dimensions of multiple glyphs combined(, as if they are copied into destGlyph) """ + # If destGlyph is given the glyph(s) are first copied over into that + # glyph and measured in that font (to avoid rounding errors) + # Leaves the destGlyph in unknown state! + bbox = [ None, None, None, None, None ] + for glyph in glyphs: + if glyph is None: + # Glyph has been in defining range but is not in the actual font + continue + if destGlyph and glyph.font != destGlyph.font: + glyph.font.selection.select(glyph) + glyph.font.copy() + destGlyph.font.selection.select(destGlyph) + destGlyph.font.paste() + glyph = destGlyph + gbb = glyph.boundingBox() + gadvance = glyph.width + if len(glyphs) > 1 and gbb[0] == gbb[2] and gbb[1] == gbb[3]: + # Ignore empty glyphs if we examine more than one glyph + continue + bbox[0] = gbb[0] if bbox[0] is None or bbox[0] > gbb[0] else bbox[0] + bbox[1] = gbb[1] if bbox[1] is None or bbox[1] > gbb[1] else bbox[1] + bbox[2] = gbb[2] if bbox[2] is None or bbox[2] < gbb[2] else bbox[2] + bbox[3] = gbb[3] if bbox[3] is None or bbox[3] < gbb[3] else bbox[3] + if not bbox[4]: + bbox[4] = -gadvance # Negative for one/first glyph + else: + if abs(bbox[4]) != gadvance: + bbox[4] = -1 # Marker for not-monospaced + else: + bbox[4] = gadvance # Positive for 2 or more glyphs + if bbox[4] and bbox[4] < 0: + # Not monospaced when only one glyph is used or multiple glyphs with different advance widths + bbox[4] = None + return { + 'xmin' : bbox[0], + 'ymin' : bbox[1], + 'xmax' : bbox[2], + 'ymax' : bbox[3], + 'width' : bbox[2] + (-bbox[0]), + 'height' : bbox[3] + (-bbox[1]), + 'advance': bbox[4], # advance width if monospaced + } + +def get_glyph_dimensions(glyph): + """ Returns dict of the dimensions of the glyph passed to it. """ + return get_multiglyph_boundingBox([ glyph ]) + +def scale_bounding_box(bbox, scale_x, scale_y): + """ Return a scaled version of a glyph dimensions dict """ + # Simulate scaling on combined bounding box, round values for better simulation + new_dim = { + 'xmin' : int(bbox['xmin'] * scale_x), + 'ymin' : int(bbox['ymin'] * scale_y), + 'xmax' : int(bbox['xmax'] * scale_x), + 'ymax' : int(bbox['ymax'] * scale_y), + 'advance': int(bbox['advance'] * scale_x) if bbox['advance'] is not None else None, + } + new_dim['width'] = new_dim['xmax'] + (-new_dim['xmin']) + new_dim['height'] = new_dim['ymax'] + (-new_dim['ymin']) + return new_dim + +def update_progress(progress): + """ Updates progress bar length. + + Accepts a float between 0.0 and 1.0. Any int will be converted to a float. + A value at 1 or bigger represents 100% + modified from: https://stackoverflow.com/questions/3160699/python-progress-bar + """ + barLength = 40 # Modify this to change the length of the progress bar + if isinstance(progress, int): + progress = float(progress) + if progress >= 1: + progress = 1 + status = "Done...\r\n" # NOTE: status initialized and never used + block = int(round(barLength * progress)) + text = "\râ•¢{0}╟ {1}%".format("â–ˆ" * block + "â–‘" * (barLength - block), int(progress * 100)) + sys.stdout.write(text) + sys.stdout.flush() + + +def check_fontforge_min_version(): + """ Verifies installed FontForge version meets minimum requirement. """ + minimumVersion = 20141231 + actualVersion = int(fontforge.version()) + + # un-comment following line for testing invalid version error handling + # actualVersion = 20120731 + + # versions tested: 20150612, 20150824 + if actualVersion < minimumVersion: + logger.critical("You seem to be using an unsupported (old) version of fontforge: %d", actualVersion) + logger.critical("Please use at least version: %d", minimumVersion) + sys.exit(1) + +def check_version_with_git(version): + """ Upgraded the version to the current git tag version (starting with 'v') """ + git = subprocess.run("git describe --tags", + cwd=os.path.dirname(__file__), + shell=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ).stdout.decode('utf-8') + if len(git) == 0: + return False + tag = git.strip() + if len(tag) == 0 or not tag.startswith('v'): + return False + tag = tag[1:] + r = re.search('(.*?)(-[0-9]+)-g[0-9a-fA-F]+$', tag) + if r: + tag = r.group(1) + patchlevel = r.group(2) + else: + patchlevel = "" + # Inspired by Phaxmohdem's versiontuple https://stackoverflow.com/a/28568003 + + versiontuple = lambda v: tuple( p.zfill(8) for p in v.split(".") ) + if versiontuple(tag) > versiontuple(version): + return tag + patchlevel + if versiontuple(tag) == versiontuple(version) and len(patchlevel) > 0: + return tag + patchlevel + return False + +def setup_arguments(): + """ Parse the command line parameters and load the config file if needed """ + parser = argparse.ArgumentParser( + description=( + 'Nerd Fonts Font Patcher: patches a given font with programming and development related glyphs\n\n' + '* Website: https://www.nerdfonts.com\n' + '* Version: ' + version + '\n' + '* Development Website: https://github.com/ryanoasis/nerd-fonts\n' + '* Changelog: https://github.com/ryanoasis/nerd-fonts/blob/-/changelog.md'), + formatter_class=RawTextHelpFormatter, + add_help=False, + ) + + parser.add_argument('font', help='The path to the font to patch (e.g., Inconsolata.otf)') + # optional arguments + parser.add_argument('--careful', dest='careful', default=False, action='store_true', help='Do not overwrite existing glyphs if detected') + parser.add_argument('--debug', dest='debugmode', default=0, type=int, nargs='?', help='Verbose mode (optional: 1=just to file; 2*=just to terminal; 3=display and file)', const=2, choices=range(0, 3 + 1)) + parser.add_argument('--extension', '-ext', dest='extension', default="", type=str, help='Change font file type to create (e.g., ttf, otf)') + parser.add_argument('--help', '-h', action='help', default=argparse.SUPPRESS, help='Show this help message and exit') + parser.add_argument('--makegroups', dest='makegroups', default=1, type=int, nargs='?', help='Use alternative method to name patched fonts (default=1)', const=1, choices=range(-1, 6 + 1)) + parser.add_argument('--mono', '-s', dest='forcemono', default=False, action='count', help='Create monospaced font, existing and added glyphs are single-width (implies --single-width-glyphs)') + parser.add_argument('--outputdir', '-out', dest='outputdir', default=".", type=str, help='The directory to output the patched font file to') + parser.add_argument('--quiet', '-q', dest='quiet', default=False, action='store_true', help='Do not generate verbose output') + parser.add_argument('--single-width-glyphs', dest='single', default=False, action='store_true', help='Whether to generate the glyphs as single-width not double-width (default is double-width) (Nerd Font Mono)') + parser.add_argument('--use-single-width-glyphs', dest='forcemono', default=False, action='count', help=argparse.SUPPRESS) + parser.add_argument('--variable-width-glyphs', dest='nonmono', default=False, action='store_true', help='Do not adjust advance width (no "overhang") (Nerd Font Propo)') + parser.add_argument('--version', '-v', action='version', version=projectName + ': %(prog)s (' + version + ')', help='Show program\'s version number and exit') + # --makegroup has an additional undocumented numeric specifier. '--makegroup' is in fact '--makegroup 1'. + # Original font name: Hugo Sans Mono ExtraCondensed Light Italic + # NF Fam agg. + # -1 no renaming at all (keep old names and versions etc) --- --- --- + # 0 turned off, use old naming scheme [-] [-] [-] + # 1 HugoSansMono Nerd Font ExtraCondensed Light Italic [ ] [ ] [ ] + # 2 HugoSansMono Nerd Font ExtCn Light Italic [ ] [X] [ ] + # 3 HugoSansMono Nerd Font XCn Lt It [ ] [X] [X] + # 4 HugoSansMono NF ExtraCondensed Light Italic [X] [ ] [ ] + # 5 HugoSansMono NF ExtCn Light Italic [X] [X] [ ] + # 6 HugoSansMono NF XCn Lt It [X] [X] [X] + + sym_font_group = parser.add_argument_group('Symbol Fonts') + sym_font_group.add_argument('--complete', '-c', dest='complete', default=False, action='store_true', help='Add all available Glyphs') + sym_font_group.add_argument('--codicons', dest='codicons', default=False, action='store_true', help='Add Codicons Glyphs (https://github.com/microsoft/vscode-codicons)') + sym_font_group.add_argument('--fontawesome', dest='fontawesome', default=False, action='store_true', help='Add Font Awesome Glyphs (http://fontawesome.io/)') + sym_font_group.add_argument('--fontawesomeext', dest='fontawesomeextension', default=False, action='store_true', help='Add Font Awesome Extension Glyphs (https://andrelzgava.github.io/font-awesome-extension/)') + sym_font_group.add_argument('--fontlogos', dest='fontlogos', default=False, action='store_true', help='Add Font Logos Glyphs (https://github.com/Lukas-W/font-logos)') + sym_font_group.add_argument('--material', '--mdi', dest='material', default=False, action='store_true', help='Add Material Design Icons (https://github.com/templarian/MaterialDesign)') + sym_font_group.add_argument('--octicons', dest='octicons', default=False, action='store_true', help='Add Octicons Glyphs (https://octicons.github.com)') + sym_font_group.add_argument('--pomicons', dest='pomicons', default=False, action='store_true', help='Add Pomicon Glyphs (https://github.com/gabrielelana/pomicons)') + sym_font_group.add_argument('--powerline', dest='powerline', default=False, action='store_true', help='Add Powerline Glyphs') + sym_font_group.add_argument('--powerlineextra', dest='powerlineextra', default=False, action='store_true', help='Add Powerline Extra Glyphs (https://github.com/ryanoasis/powerline-extra-symbols)') + sym_font_group.add_argument('--powersymbols', dest='powersymbols', default=False, action='store_true', help='Add IEC Power Symbols (https://unicodepowersymbol.com/)') + sym_font_group.add_argument('--weather', dest='weather', default=False, action='store_true', help='Add Weather Icons (https://github.com/erikflowers/weather-icons)') + + expert_group = parser.add_argument_group('Expert Options') + expert_group.add_argument('--adjust-line-height', '-l', dest='adjustLineHeight', default=False, action='store_true', help='Whether to adjust line heights (attempt to center powerline separators more evenly)') + expert_group.add_argument('--boxdrawing', dest='forcebox', default=False, action='store_true', help='Force patching in (over existing) box drawing glyphs') + expert_group.add_argument('--cell', dest='cellopt', default=None, type=str, help='Adjust or query the cell size, e.g. use "0:1000:-200:800" or "?"') + expert_group.add_argument('--configfile', dest='configfile', default=False, type=str, help='Specify a file path for configuration file (see sample: src/config.sample.cfg)') + expert_group.add_argument('--custom', dest='custom', default=False, type=str, help='Specify a custom symbol font, all glyphs will be copied; absolute path suggested') + expert_group.add_argument('--dry', dest='dry_run', default=False, action='store_true', help='Do neither patch nor store the font, to check naming') + expert_group.add_argument('--glyphdir', dest='glyphdir', default=__dir__ + "/src/glyphs/", type=str, help='Path to glyphs to be used for patching') + expert_group.add_argument('--has-no-italic', dest='noitalic', default=False, action='store_true', help='Font family does not have Italic (but Oblique), to help create correct RIBBI set') + expert_group.add_argument('--metrics', dest='metrics', default=None, choices=get_metrics_names(), help='Select vertical metrics source (for problematic cases)') + expert_group.add_argument('--name', dest='force_name', default=None, type=str, help='Specify naming source (\'full\', \'postscript\', \'filename\', or concrete free name-string)') + expert_group.add_argument('--postprocess', dest='postprocess', default=False, type=str, help='Specify a Script for Post Processing') + progressbars_group_parser = expert_group.add_mutually_exclusive_group(required=False) + expert_group.add_argument('--removeligs', '--removeligatures', dest='removeligatures', default=False, action='store_true', help='Removes ligatures specified in configuration file (needs --configfile)') + expert_group.add_argument('--xavgcharwidth', dest='xavgwidth', default=None, type=int, nargs='?', help='Adjust xAvgCharWidth (optional: concrete value)', const=True) + # --xavgcharwidth for compatibility with old applications like notepad and non-latin fonts + # Possible values with examples: + # - copy from sourcefont (default) + # 0 - calculate from font according to OS/2-version-2 + # 500 - set to 500 + + # progress bar arguments - https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse + progressbars_group_parser.add_argument('--progressbars', dest='progressbars', action='store_true', help='Show percentage completion progress bars per Glyph Set (default)') + progressbars_group_parser.add_argument('--no-progressbars', dest='progressbars', action='store_false', help='Don\'t show percentage completion progress bars per Glyph Set') + expert_group.set_defaults(progressbars=True) + + args = parser.parse_args() + setup_global_logger(args) + + # if we have a config file: fetch commandline arguments from there and process again with all arguments + config = configparser.ConfigParser(empty_lines_in_values=False, allow_no_value=True) + if args.configfile: + if not os.path.isfile(args.configfile): + logger.critical("Configfile does not exist: %s", args.configfile) + sys.exit(1) + if not os.access(args.configfile, os.R_OK): + logger.critical("Can not open configfile for reading: %s", args.configfile) + sys.exit(1) + config.read(args.configfile) + extraflags = config.get("Config", "commandline", fallback='') + if len(extraflags): + logger.info("Adding config commandline options: %s", extraflags) + extraflags += ' ' + args.font # Need to re-add the mandatory argument + args = parser.parse_args(extraflags.split(), args) + + if args.makegroups > 0 and not FontnameParserOK: + logger.critical("FontnameParser module missing (bin/scripts/name_parser/Fontname*), specify --makegroups 0") + sys.exit(1) + + # if you add a new font, set it to True here inside the if condition + if args.complete: + args.fontawesome = True + args.fontawesomeextension = True + args.fontlogos = True + args.octicons = True + args.codicons = True + args.powersymbols = True + args.pomicons = True + args.powerline = True + args.powerlineextra = True + args.material = True + args.weather = True + + if not args.complete: + sym_font_args = [] + # add the list of arguments for each symbol font to the list sym_font_args + for action in sym_font_group._group_actions: + sym_font_args.append(action.__dict__['option_strings']) + + # determine whether or not all symbol fonts are to be used + font_complete = True + for sym_font_arg_aliases in sym_font_args: + found = False + for alias in sym_font_arg_aliases: + if alias in sys.argv: + found = True + if not found: + font_complete = False + args.complete = font_complete + + if args.forcemono: + args.single = True + if args.nonmono and args.single: + logger.warning("Specified contradicting --variable-width-glyphs together with --mono or --single-width-glyphs. Ignoring --variable-width-glyphs.") + args.nonmono = False + + if args.cellopt: + if args.cellopt != '?': + try: + parts = [ int(v) for v in args.cellopt.split(':') ] + if len(parts) != 4: + raise + except: + logger.critical("Parameter for --cell is not 4 colon separated integer numbers: '%s'", args.cellopt) + sys.exit(2) + if parts[0] >= parts[1] or parts[2] >= parts[3]: + logger.critical("Parameter for --cell do not result in positive cell size: %d x %d", + parts[1] - parts[0], parts[3] - parts[2]) + sys.exit(2) + if parts[0] != 0: + logger.warn("First parameter for --cell should be zero, this is probably not working") + args.cellopt = parts + + make_sure_path_exists(args.outputdir) + if not os.path.isfile(args.font): + logger.critical("Font file does not exist: %s", args.font) + sys.exit(1) + if not os.access(args.font, os.R_OK): + logger.critical("Can not open font file for reading: %s", args.font) + sys.exit(1) + is_ttc = len(fontforge.fontsInFile(args.font)) > 1 + try: + source_font_test = TableHEADWriter(args.font) + args.is_variable = source_font_test.find_table([b'avar', b'cvar', b'fvar', b'gvarb', b'HVAR', b'MVAR', b'VVAR'], 0) + if args.is_variable: + logger.warning("Source font is a variable open type font (VF), opening might fail...") + except: + args.is_variable = False + finally: + try: + source_font_test.close() + except: + pass + + if args.extension == "": + args.extension = os.path.splitext(args.font)[1] + else: + args.extension = '.' + args.extension + if re.match(r'\.ttc$', args.extension, re.IGNORECASE): + if not is_ttc: + logger.critical("Can not create True Type Collections from single font files") + sys.exit(1) + else: + if is_ttc: + logger.critical("Can not create single font files from True Type Collections") + sys.exit(1) + + # The if might look ridiculous, but isinstance(False, int) is True! + if isinstance(args.xavgwidth, int) and not isinstance(args.xavgwidth, bool): + if args.xavgwidth < 0: + logger.critical("--xavgcharwidth takes no negative numbers") + sys.exit(2) + if args.xavgwidth > 16384: + logger.critical("--xavgcharwidth takes only numbers up to 16384") + sys.exit(2) + + return (args, config) + +def setup_global_logger(args): + """ Set up the logger and take options into account """ + global logger + logger = logging.getLogger(os.path.basename(args.font)) + logger.setLevel(logging.DEBUG) + log_to_file = (args.debugmode & 1 == 1) + if log_to_file: + try: + f_handler = logging.FileHandler('font-patcher-log.txt') + f_handler.setFormatter(logging.Formatter('%(levelname)s: %(name)s %(message)s')) + logger.addHandler(f_handler) + except: + log_to_file = False + logger.debug(allversions) + logger.debug("Options %s", repr(sys.argv[1:])) + c_handler = logging.StreamHandler(stream=sys.stdout) + c_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + if not (args.debugmode & 2 == 2): + c_handler.setLevel(logging.INFO) + logger.addHandler(c_handler) + if (args.debugmode & 1 == 1) and not log_to_file: + logger.info("Can not write logfile, disabling") + +def main(): + global logger + logger = logging.getLogger("start") # Use start logger until we can set up something sane + s_handler = logging.StreamHandler(stream=sys.stdout) + s_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + logger.addHandler(s_handler) + + global version + git_version = check_version_with_git(version) + global allversions + allversions = "Patcher v{} ({}) (ff {})".format( + git_version if git_version else version, script_version, fontforge.version()) + print("{} {}".format(projectName, allversions)) + if git_version: + version = git_version + check_fontforge_min_version() + (args, conf) = setup_arguments() + logger.debug("Naming mode %d", args.makegroups) + + patcher = font_patcher(args, conf) + + sourceFonts = [] + all_fonts = fontforge.fontsInFile(args.font) + if not all_fonts: + if re.match(".*\\.woff2?", args.font, re.I): + all_fonts=[ "" ] + else: + logger.critical("Can not find any fonts in '%s'", args.font) + sys.exit(1) + for i, subfont in enumerate(all_fonts): + if len(all_fonts) > 1: + print("\n") + logger.info("Processing %s (%d/%d)", subfont, i + 1, len(all_fonts)) + try: + sourceFonts.append(fontforge.open("{}({})".format(args.font, i), 1)) # 1 = ("fstypepermitted",)) + except Exception: + logger.critical("Can not open font '%s', try to open with fontforge interactively to get more information", + subfont) + sys.exit(1) + + patcher.setup_name_backup(sourceFonts[-1]) + patcher.patch(sourceFonts[-1]) + + print("Done with Patch Sets, generating font...") + for f in sourceFonts: + patcher.setup_font_names(f) + patcher.generate(sourceFonts) + + for f in sourceFonts: + f.close() + + +if __name__ == "__main__": + __dir__ = os.path.dirname(os.path.abspath(__file__)) + main()