add dedicated font generation exe for the build

This commit is contained in:
Emil Lerch 2023-04-06 13:26:42 -07:00
parent f1e635d786
commit 08b30a336f
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
2 changed files with 258 additions and 0 deletions

View File

@ -46,6 +46,16 @@ pub fn build(b: *std.build.Builder) !void {
exe.addIncludePath("lib/i2cdriver");
exe.install();
const exe_fontgen = b.addExecutable(.{
.name = "fontgen",
.root_source_file = .{ .path = "src/fontgen.zig" },
.target = target,
.optimize = optimize,
});
exe_fontgen.linkLibrary(im_dep.artifact("MagickWand"));
exe_fontgen.linkLibrary(z_dep.artifact("z"));
exe.step.dependOn(&exe_fontgen.run().step);
// TODO: I believe we can use runArtifact on a second
// exe with a different source file for font generation
// taking us to a series of 5 byte arrays for each

248
src/fontgen.zig Normal file
View File

@ -0,0 +1,248 @@
const std = @import("std");
// The package manager will install headers from our dependency in zig's build
// cache and include the cache directory as a "-I" option on the build command
// automatically.
const c = @cImport({
@cInclude("MagickWand/MagickWand.h");
});
// This is set in two places. If this needs adjustment be sure to change the
// magick CLI command (where it is a string)
const GLYPH_WIDTH = 5;
const GLYPH_HEIGHT = 8;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
//
// var env = try std.process.getEnvMap(alloc);
// defer env.deinit();
// var env_iterator = env.iterator();
// std.debug.print("\n", .{});
// while (env_iterator.next()) |entry| {
// std.debug.print("{s}={s}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
// }
// cwd is the root of the project - yay!
const proj_path = std.fs.cwd(); //.realpath(".", &path_buf);
// We will assume we own the src/fonts dir in entirety
proj_path.makeDir("src/fonts/") catch {};
if (!std.meta.isError(proj_path.statFile("src/fonts/fonts.zig"))) return;
const generated_file = try proj_path.createFile("src/fonts/fonts.zig", .{
.read = false,
.truncate = true,
.lock = .Exclusive,
.lock_nonblocking = false,
.mode = 0o666,
.intended_io_mode = .blocking,
});
defer generated_file.close();
// We need a temp file for the glyph bmp
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const temp_file = try std.fs.path.joinZ(alloc, &[_][]const u8{ try proj_path.realpath("src/fonts/", &path_buf), ".tmp.bmp" });
defer alloc.free(temp_file);
defer std.fs.deleteFileAbsolute(temp_file) catch {};
const file_writer = generated_file.writer();
var buffered_writer = std.io.bufferedWriter(file_writer);
defer buffered_writer.flush() catch unreachable;
const writer = buffered_writer.writer();
// std.debug.print("cwd: {s}", .{try std.fs.cwd().realpath(".", &path_buf)});
// const args = try std.process.argsAlloc(alloc);
// defer std.process.argsFree(alloc, args);
//
// const stdout_file = std.io.getStdOut().writer();
// var bw = std.io.bufferedWriter(stdout_file);
// const stdout = bw.writer();
// defer bw.flush() catch unreachable; // don't forget to flush!
//
// // try stdout.print("Run `zig build test` to run the tests.\n", .{});
var pixels: [GLYPH_WIDTH * GLYPH_HEIGHT]u8 = undefined;
try writer.print("pub const @\"{s}\" = .{{\n", .{"Hack-Regular"});
// TODO: Read and cache
for (32..127) |i| {
// if (i == 32) {
// try writer.print(" \"\",\n", .{});
// continue;
// }
const char_str = [_]u8{@intCast(u8, i)};
// Need to escape the following chars: 32 (' ') 92 ('\')
const label_param = parm: {
switch (i) {
32 => break :parm "label:\\ ",
92 => break :parm "label:\\\\",
else => break :parm "label:" ++ char_str,
}
};
// generate the file
// 36 ($) and 81 (Q) are widest and only 9 wide
// We are chopping the right pixel
try run(alloc, &[_][]const u8{
"magick",
"-background",
"white",
"-fill",
"black",
"-font",
"Hack-Regular",
"-density",
"72",
"-pointsize",
"8",
label_param,
"-extent",
"5x8",
temp_file,
});
// Grab pixels from the file
try convertImage(temp_file, &pixels);
packBits(&pixels);
// unpackBits(&pixels);
try writer.print(" .@\"{d}\" = &[_]u8{{ ", .{i});
var first = true;
for (pixels[0..(GLYPH_WIDTH * GLYPH_HEIGHT / 8)]) |byte| {
// for (pixels) |byte| { // unpacked only
if (!first) try writer.print(", ", .{});
try writer.print("0x{s}", .{std.fmt.bytesToHex(&[_]u8{byte}, .lower)});
first = false;
}
try writer.print(" }},\n", .{});
}
try writer.print("}};\n", .{});
}
const hi = .{
.there = &[_]u8{0xff},
};
fn run(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var env_map = try std.process.getEnvMap(allocator);
defer env_map.deinit();
var child = std.ChildProcess.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
child.cwd = null; //std.fs.cwd();
child.env_map = &env_map;
try child.spawn();
const result = try child.wait();
switch (result) {
.Exited => |code| if (code != 0) {
std.log.err("command failed with exit code {}", .{code});
{
var msg = std.ArrayList(u8).init(allocator);
defer msg.deinit();
const writer = msg.writer();
var prefix: []const u8 = "";
for (argv) |arg| {
try writer.print("{s}\"{s}\"", .{ prefix, arg });
prefix = " ";
}
std.log.debug("[RUN] {s}", .{msg.items});
}
std.os.exit(0xff);
},
else => {
std.log.err("command failed with: {}", .{result});
std.os.exit(0xff);
},
}
}
pub fn unpackBits(pixels: *[GLYPH_WIDTH * GLYPH_HEIGHT]u8) void {
// bits packed: 0000 0001
// ^
// \- most significant bit
// Need to start at the end and work forward to avoid
// overwrites
var i: isize = (GLYPH_WIDTH * GLYPH_HEIGHT / 8 - 1);
while (i >= 0) {
const start = @intCast(usize, i) * 8;
const packed_byte = pixels[@intCast(usize, i)];
pixels[start + 7] = ((packed_byte & 0b10000000) >> 7) * 0xFF;
pixels[start + 6] = ((packed_byte & 0b01000000) >> 6) * 0xFF;
pixels[start + 5] = ((packed_byte & 0b00100000) >> 5) * 0xFF;
pixels[start + 4] = ((packed_byte & 0b00010000) >> 4) * 0xFF;
pixels[start + 3] = ((packed_byte & 0b00001000) >> 3) * 0xFF;
pixels[start + 2] = ((packed_byte & 0b00000100) >> 2) * 0xFF;
pixels[start + 1] = ((packed_byte & 0b00000010) >> 1) * 0xFF;
pixels[start + 0] = ((packed_byte & 0b00000001) >> 0) * 0xFF;
i -= 1;
}
}
fn packBits(pixels: *[GLYPH_WIDTH * GLYPH_HEIGHT]u8) void {
for (0..(GLYPH_WIDTH * GLYPH_HEIGHT / 8)) |i| {
const start = i * 8;
pixels[i] = (pixels[start] & 0b00000001) |
(pixels[(start + 1)] & 0b00000010) |
(pixels[(start + 2)] & 0b00000100) |
(pixels[(start + 3)] & 0b00001000) |
(pixels[(start + 4)] & 0b00010000) |
(pixels[(start + 5)] & 0b00100000) |
(pixels[(start + 6)] & 0b01000000) |
(pixels[(start + 7)] & 0b10000000);
}
}
fn convertImage(filename: [:0]u8, pixels: *[GLYPH_WIDTH * GLYPH_HEIGHT]u8) !void {
c.MagickWandGenesis();
defer c.MagickWandTerminus();
var mw = c.NewMagickWand();
defer {
if (mw) |w| mw = c.DestroyMagickWand(w);
}
// Reading an image into ImageMagick is problematic if it isn't a bmp
// as the library needs a bunch of dependencies available
var status = c.MagickReadImage(mw, filename);
if (status == c.MagickFalse) {
// try reportMagickError(mw);
return error.CouldNotReadImage;
}
// We make the image monochrome by quantizing the image with 2 colors in the
// gray colorspace. See:
// https://www.imagemagick.org/Usage/quantize/#monochrome
// and
// https://stackoverflow.com/questions/18267432/using-the-c-api-for-imagemagick-on-iphone-to-convert-to-monochrome
//
// We do this at the end so we have pure black and white. Otherwise the
// resizing oprations will generate some greyscale that we don't want
status = c.MagickQuantizeImage(mw, // MagickWand
2, // Target number colors
c.GRAYColorspace, // Colorspace
1, // Optimal depth
c.MagickTrue, // Dither
c.MagickFalse // Quantization error
);
if (status == c.MagickFalse)
return error.CouldNotQuantizeImage;
status = c.MagickExportImagePixels(mw, 0, 0, GLYPH_WIDTH, GLYPH_HEIGHT, "I", c.CharPixel, @ptrCast(*anyopaque, pixels));
if (status == c.MagickFalse)
return error.CouldNotExportImage;
for (0..GLYPH_WIDTH * GLYPH_HEIGHT) |i| {
switch (pixels[i]) {
0x00 => pixels[i] = 0xFF,
0xFF => pixels[i] = 0x00,
else => {},
}
}
}