Add a pinned 16px Roboto bitmap and tiny Linux software canvas
This commit is contained in:
parent
b0da270a67
commit
0d2c8eedf9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("<EFBFBD>"), 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));
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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, '–—‘’“”…•€←↑→↓<EFBFBD>')))
|
||||
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('<IIBBbbB3x', cp, header_size + len(pixels), width, height, x, y, advance)
|
||||
pixels += image.tobytes()
|
||||
return b'UF16' + struct.pack('<I', len(codepoints)) + records + pixels
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument('--check', action='store_true')
|
||||
mode.add_argument('--regenerate', action='store_true')
|
||||
mode.add_argument('--verify-generation', action='store_true')
|
||||
parser.add_argument('--font', help='Use a local copy of the pinned TTF')
|
||||
args = parser.parse_args()
|
||||
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||
if args.regenerate or args.verify_generation:
|
||||
data = generate(manifest, args.font)
|
||||
if args.verify_generation:
|
||||
assert data == (ROOT / 'roboto-16.bin').read_bytes(), 'Generated bitmap differs'
|
||||
else:
|
||||
(ROOT / 'roboto-16.bin').write_bytes(data)
|
||||
manifest['generated'] = {'roboto-16.bin': digest(data)}
|
||||
(ROOT / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
for filename, checksum in manifest['generated'].items():
|
||||
assert digest((ROOT / filename).read_bytes()) == checksum, filename
|
||||
assert digest((ROOT / 'OFL.txt').read_bytes()) == manifest['sources'][1]['sha256']
|
||||
print('Pinned Roboto bitmap and license verified.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -25,6 +25,7 @@ test {
|
|||
_ = @import("linux/Text.zig");
|
||||
_ = @import("linux/Watch.zig");
|
||||
_ = @import("linux/Desktop.zig");
|
||||
_ = @import("linux/Bitmap.zig");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue