diff --git a/build.zig b/build.zig index c9a75aa..9c348a6 100644 --- a/build.zig +++ b/build.zig @@ -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 diff --git a/src/fontgen.zig b/src/fontgen.zig new file mode 100644 index 0000000..ff51d73 --- /dev/null +++ b/src/fontgen.zig @@ -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 => {}, + } + } +}