From 0d2c8eedf9617991d24704c28059fc9558338d45 Mon Sep 17 00:00:00 2001 From: Peterino2 Date: Fri, 18 Sep 2026 15:21:15 -0700 Subject: [PATCH] Add a pinned 16px Roboto bitmap and tiny Linux software canvas --- shell.nix | 2 +- src/linux/Bitmap.zig | 96 +++++++++++++++++++++++++++++++++ src/linux/font/OFL.txt | 93 ++++++++++++++++++++++++++++++++ src/linux/font/README.md | 32 +++++++++++ src/linux/font/manifest.json | 23 ++++++++ src/linux/font/roboto-16.bin | Bin 0 -> 24191 bytes src/linux/tools/bitmap-font.py | 75 ++++++++++++++++++++++++++ src/root.zig | 1 + 8 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 src/linux/Bitmap.zig create mode 100644 src/linux/font/OFL.txt create mode 100644 src/linux/font/README.md create mode 100644 src/linux/font/manifest.json create mode 100644 src/linux/font/roboto-16.bin create mode 100644 src/linux/tools/bitmap-font.py diff --git a/shell.nix b/shell.nix index 61b5538..cb6f1af 100644 --- a/shell.nix +++ b/shell.nix @@ -3,7 +3,7 @@ }: pkgs.mkShell { packages = [ - (pkgs.python3.withPackages (python: [ python.dbus-next ])) + (pkgs.python3.withPackages (python: [ python.dbus-next python.pillow ])) pkgs.dbus pkgs.wayland-scanner pkgs.clang-tools diff --git a/src/linux/Bitmap.zig b/src/linux/Bitmap.zig new file mode 100644 index 0000000..55d4dcc --- /dev/null +++ b/src/linux/Bitmap.zig @@ -0,0 +1,96 @@ +//! Fixed 16px menu font and a clipped, opaque software canvas. No runtime font +//! parsing, allocation, GPU work, or dependency on an application's renderer. +const std = @import("std"); +const data = @embedFile("font/roboto-16.bin"); +fn word(offset: usize) u32 { + return std.mem.readInt(u32, data[offset..][0..4], .little); +} +const count = word(4); +const Glyph = struct { offset: usize, width: u8, height: u8, x: i8, y: i8, advance: u8 }; +fn glyph(codepoint: u21) Glyph { + var lo: usize = 0; + var hi: usize = count; + while (lo < hi) { + const mid = (lo + hi) / 2; + const key = word(8 + mid * 16); + if (key < codepoint) lo = mid + 1 else hi = mid; + } + const record = 8 + (if (lo < count and word(8 + lo * 16) == codepoint) lo else count - 1) * 16; + return .{ .offset = word(record + 4), .width = data[record + 8], .height = data[record + 9], .x = @bitCast(data[record + 10]), .y = @bitCast(data[record + 11]), .advance = data[record + 12] }; +} +pub fn width(text: []const u8) i32 { + var it = std.unicode.Utf8View.initUnchecked(text).iterator(); + var result: i32 = 0; + while (it.nextCodepoint()) |cp| { + result += glyph(cp).advance; + if (result >= 1024) break; // Menus constrain/ellipsize long labels. + } + return result; +} +pub const Canvas = struct { + pixels: []u32, + width: i32, + height: i32, + scale: i32, + pub fn rect(self: Canvas, x: i32, y: i32, w: i32, h: i32, color: u32) void { + const x0 = std.math.clamp(x, 0, self.width) * self.scale; + const y0 = std.math.clamp(y, 0, self.height) * self.scale; + const x1 = std.math.clamp(x + w, 0, self.width) * self.scale; + const y1 = std.math.clamp(y + h, 0, self.height) * self.scale; + if (x1 <= x0 or y1 <= y0) return; + var row = y0; + while (row < y1) : (row += 1) { + const offset: usize = @intCast(row * self.width * self.scale); + @memset(self.pixels[offset + @as(usize, @intCast(x0)) .. offset + @as(usize, @intCast(x1))], color | 0xff000000); + } + } + fn pixel(self: Canvas, x: i32, y: i32, color: u32, alpha: u32) void { + if (x < 0 or y < 0 or x >= self.width or y >= self.height or alpha == 0) return; + const index: usize = @intCast((y * self.width * self.scale + x) * self.scale); + const background = self.pixels[index]; + var blended: u32 = 0xff000000; + inline for (.{ 0, 8, 16 }) |shift| { + blended |= ((((color >> shift) & 255) * alpha + ((background >> shift) & 255) * (255 - alpha) + 127) / 255) << shift; + } + self.rect(x, y, 1, 1, blended); + } + fn drawGlyph(self: Canvas, value: Glyph, x: i32, baseline: i32, color: u32) void { + for (0..value.height) |row| for (0..value.width) |column| { + self.pixel(x + value.x + @as(i32, @intCast(column)), baseline + value.y + @as(i32, @intCast(row)), color, data[value.offset + row * value.width + column]); + }; + } + pub fn text(self: Canvas, value: []const u8, x: i32, baseline: i32, limit: i32, color: u32) void { + var it = std.unicode.Utf8View.initUnchecked(value).iterator(); + var cursor = x; + const ellipsis = glyph(0x2026); + const clipped = width(value) > limit; + while (it.nextCodepoint()) |cp| { + const g = glyph(cp); + if (cursor + g.advance > x + limit - (if (clipped) @as(i32, ellipsis.advance) else 0)) { + if (limit >= ellipsis.advance) self.drawGlyph(ellipsis, cursor, baseline, color); + return; + } + self.drawGlyph(g, cursor, baseline, color); + cursor += g.advance; + } + } +}; +test "bitmap bounds, coverage and clipping" { + try std.testing.expectEqualStrings("UF16", data[0..4]); + var previous: u32 = 0; + for (0..count) |i| { + const cp = word(8 + i * 16); + try std.testing.expect(cp > previous); + previous = cp; + const g = glyph(@intCast(cp)); + try std.testing.expect(g.offset + @as(usize, g.width) * g.height <= data.len); + } + try std.testing.expectEqual(@as(u32, 0xfffd), previous); + try std.testing.expectEqual(width("�"), width("漢")); + var pixels: [64 * 32]u32 = @splat(0xff000000); + const canvas: Canvas = .{ .pixels = &pixels, .width = 64, .height = 32, .scale = 1 }; + canvas.text("Roboto café", -2, 17, 60, 0xffffff); + try std.testing.expect(!std.mem.allEqual(u32, &pixels, 0xff000000)); + canvas.rect(-100, -100, 500, 500, 0x123456); + try std.testing.expect(std.mem.allEqual(u32, &pixels, 0xff123456)); +} diff --git a/src/linux/font/OFL.txt b/src/linux/font/OFL.txt new file mode 100644 index 0000000..9c48e05 --- /dev/null +++ b/src/linux/font/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +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: +https://openfontlicense.org + + +----------------------------------------------------------- +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/src/linux/font/README.md b/src/linux/font/README.md new file mode 100644 index 0000000..4f8af7a --- /dev/null +++ b/src/linux/font/README.md @@ -0,0 +1,32 @@ +# Linux menu bitmap + +`roboto-16.bin` contains Roboto Regular rasterized at 16 pixels, with 8-bit +antialiased coverage. It is embedded into the Linux backend; no font parser, +font discovery, image decoder, or runtime rasterizer is linked into UHDK. + +Coverage: printable ASCII, Latin-1, selected punctuation/currency/arrows, and +U+FFFD. Other codepoints use the replacement glyph. There is no shaping, bidi, +kerning, or combining-mark placement. Larger output scales enlarge the bitmap; +they do not rerasterize the font. This is an intentionally fixed, small menu font. + +The source is the unmodified Google Fonts Roboto variable TTF, pinned in +`manifest.json`, with weight 400 and width 100. The generated bitmap is a font +derivative distributed under the accompanying SIL Open Font License (`OFL.txt`). +The TTF is downloaded only for maintainer regeneration and is not shipped here. + +Binary format, little-endian: `UF16`, u32 glyph count, sorted 16-byte records +(u32 codepoint, u32 absolute pixel offset, u8 width/height, i8 x/y bearing relative +to the baseline, u8 advance, three padding bytes), followed by tightly packed, +row-major grayscale glyph bitmaps. + +From the package directory: + +```sh +python3 src/linux/tools/bitmap-font.py --check +# Optional nix-shell provides pinned Pillow/FreeType: +python3 src/linux/tools/bitmap-font.py --verify-generation +python3 src/linux/tools/bitmap-font.py --regenerate +``` + +Source URLs/checksums, rasterizer versions and output checksum are recorded in +`manifest.json`. Normal builds require only the checked-in bitmap and Zig. diff --git a/src/linux/font/manifest.json b/src/linux/font/manifest.json new file mode 100644 index 0000000..8e518c9 --- /dev/null +++ b/src/linux/font/manifest.json @@ -0,0 +1,23 @@ +{ + "revision": "1c627bfa375fc51cf86fabeca4f6e08a95f0aa5c", + "size": 16, + "sources": [ + { + "url": "https://raw.githubusercontent.com/google/fonts/1c627bfa375fc51cf86fabeca4f6e08a95f0aa5c/ofl/roboto/Roboto%5Bwdth%2Cwght%5D.ttf", + "sha256": "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134", + "name": "Roboto.ttf" + }, + { + "url": "https://raw.githubusercontent.com/google/fonts/1c627bfa375fc51cf86fabeca4f6e08a95f0aa5c/ofl/roboto/OFL.txt", + "sha256": "061402327a96aadb0bfb694a960ed289ecd38d383e396243831ab81feb109c41", + "name": "OFL.txt" + } + ], + "rasterizer": { + "pillow": "12.2.0", + "freetype": "2.13.3" + }, + "generated": { + "roboto-16.bin": "23bfe858a9ff398b3826d836bb815255ced0e50ac7f695b2d9309a15b32d335e" + } +} diff --git a/src/linux/font/roboto-16.bin b/src/linux/font/roboto-16.bin new file mode 100644 index 0000000000000000000000000000000000000000..6e652e61380e3b1da1de11e3c33f97a9c2ad6e07 GIT binary patch literal 24191 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 literal 0 HcmV?d00001 diff --git a/src/linux/tools/bitmap-font.py b/src/linux/tools/bitmap-font.py new file mode 100644 index 0000000..4701f95 --- /dev/null +++ b/src/linux/tools/bitmap-font.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Maintainer-only generation/checking of the Linux menu's 16px Roboto bitmap. + +Ordinary builds embed the checked-in bytes. Generation needs Pillow/FreeType +from the optional shell.nix and downloads only checksum-pinned font sources. +""" +import argparse +import hashlib +import io +import json +from pathlib import Path +import struct +import urllib.request + +ROOT = Path(__file__).resolve().parents[1] / 'font' + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def generate(manifest, font_path): + import PIL + from PIL import Image, ImageDraw, ImageFont, features + versions = {'pillow': PIL.__version__, 'freetype': features.version('freetype2')} + if versions != manifest['rasterizer']: + raise SystemExit('Use the pinned shell.nix rasterizer: ' + str(manifest['rasterizer'])) + source = manifest['sources'][0] + data = Path(font_path).read_bytes() if font_path else urllib.request.urlopen(source['url'], timeout=30).read() + assert digest(data) == source['sha256'], 'Font source checksum differs' + font = ImageFont.truetype(io.BytesIO(data), manifest['size'], layout_engine=ImageFont.Layout.BASIC) + # Explicit variation axes: regular weight and normal width, in font order. + axes = font.get_variation_axes() + font.set_variation_by_axes([400 if a['name'] == b'Weight' else 100 if a['name'] == b'Width' else a['default'] for a in axes]) + codepoints = sorted(set(range(32, 127)) | set(range(160, 256)) | set(map(ord, '–—‘’“”…•€←↑→↓�'))) + header_size = 8 + len(codepoints) * 16 + records, pixels = bytearray(), bytearray() + for cp in codepoints: + character = chr(cp) + x, y, right, bottom = font.getbbox(character, anchor='ls') + width, height = right - x, bottom - y + image = Image.new('L', (width, height)) + if width and height: + ImageDraw.Draw(image).text((-x, -y), character, font=font, fill=255, anchor='ls') + advance = int(font.getlength(character) + .5) + records += struct.pack('