diff --git a/src/fontgen.zig b/src/fontgen.zig index ff51d73..e176bc5 100644 --- a/src/fontgen.zig +++ b/src/fontgen.zig @@ -7,10 +7,12 @@ const c = @cImport({ @cInclude("MagickWand/MagickWand.h"); }); +const display = @import("display.zig"); + // 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; +const GLYPH_WIDTH = display.FONT_WIDTH; +const GLYPH_HEIGHT = display.FONT_HEIGHT; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; diff --git a/src/main.zig b/src/main.zig index 3e672f2..64b6061 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,19 @@ const std = @import("std"); const display = @import("display.zig"); const chars = @import("images/images.zig").chars; +const fonts = @import("fonts/fonts.zig"); +const unpackBits = @import("fontgen.zig").unpackBits; + +const DEFAULT_FONT = "Hack-Regular"; + +const supported_fonts = @typeInfo(fonts).Struct.decls; +// Specifying the size here works, but will cause problems later if we want, for example, +// 2x heigh font size. But...we would need a slice, which is runtime known, and will +// require an array -> slice conversion and additional allocations, etc, etc. +// So for now, we'll keep this specified so we can simply use a pointer +const FontInnerHash = std.AutoHashMap(u21, *const [display.FONT_WIDTH * display.FONT_HEIGHT / 8]u8); +var font_map: ?std.StringHashMap(?*FontInnerHash) = null; +var font_arena: ?std.heap.ArenaAllocator = null; // 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 @@ -24,6 +37,7 @@ const Options = struct { device_file: [:0]u8, }; pub fn main() !void { + defer deinit(); const alloc = std.heap.c_allocator; //defer alloc.deinit(); const args = try std.process.argsAlloc(alloc); @@ -66,12 +80,12 @@ pub fn main() !void { line_inx += 1; } } - std.debug.print("delme: {s}\n", .{lines[0].*}); const opts = try processArgs(alloc, args, &lines); defer alloc.destroy(opts); if (opts.background_filename.len > 0) try stdout.print("Converting {s}\n", .{opts.background_filename}); var pixels: [display.WIDTH * display.HEIGHT]u8 = undefined; - try convertImage(opts.background_filename, &pixels, textForLine); + try convertImage(opts.background_filename, &pixels, noTextForLine); + try addTextToImage(alloc, &pixels, &lines); try bw.flush(); // We should take the linux device file here, then inspect for ttyUSB vs @@ -81,6 +95,22 @@ pub fn main() !void { // try stdout.print("Run `zig build test` to run the tests.\n", .{}); } +fn deinit() void { + if (font_map == null or font_arena == null) return; + // var it = font_map.?.keyIterator(); + // while (it.next()) |key| { + // // if (font_map.?.get(key.*).?) |font| { + // // std.debug.print("delme: {s}: {*}\n", .{ key.*, font }); + // // font.deinit(); + // // } + // } + font_map.?.deinit(); + font_map = null; + + font_arena.?.deinit(); + font_arena = null; +} + fn processArgs(allocator: std.mem.Allocator, args: [][:0]u8, line_array: *[display.LINES]*const [:0]u8) !*Options { if (args.len < 2) try usage(args); const prefix = "/dev/ttyUSB"; @@ -128,6 +158,117 @@ fn areDigits(bytes: []u8) bool { return true; } +fn addTextToImage(allocator: std.mem.Allocator, pixels: *[display.WIDTH * display.HEIGHT]u8, data: []*[]u8) !void { + var maybe_font_data = try getFontData(allocator, DEFAULT_FONT); + + if (maybe_font_data == null) return error.FontNotFound; + var font_data = maybe_font_data.?; + for (data, 0..) |line, starting_display_line| { + var utf8 = (try std.unicode.Utf8View.init(line.*)).iterator(); + const starting_display_row = display.HEIGHT / display.LINES * starting_display_line; + var starting_display_column: usize = 0; + while (utf8.nextCodepoint()) |cp| { + var glyph: [display.FONT_WIDTH * display.FONT_HEIGHT]u8 = undefined; + std.debug.assert(font_data.get(cp).?.*.len == (display.FONT_WIDTH * display.FONT_HEIGHT / 8)); + // Is .? appropriate here? this is a hard fail if our codepoint isn't in the font... + for (font_data.get(cp).?.*, 0..) |b, i| { + glyph[i] = b; + } + unpackBits(&glyph); // this will fill the rest of our array + // std.log.debug("====" ++ "=" ** display.FONT_WIDTH, .{}); + // for (0..display.FONT_HEIGHT) |i| { + // std.log.debug( + // "{d:0>2}: {s}", + // .{ i, fmtSliceGreyscaleImage(glyph[(i * display.FONT_WIDTH)..((i + 1) * display.FONT_WIDTH)]) }, + // ); + // } + // std.log.debug("====" ++ "=" ** display.FONT_WIDTH ++ "\n", .{}); + // unpacked - time to ram this in + for (glyph, 0..) |b, i| { + const column = i % display.FONT_WIDTH + starting_display_column + display.BORDER_LEFT; + const row = i / display.FONT_WIDTH + starting_display_row; + pixels[(row * display.WIDTH) + column] = b; + } + starting_display_column += display.FONT_WIDTH; + } + } +} + +fn getFontData(allocator: std.mem.Allocator, font_name: []const u8) !?FontInnerHash { + if (font_arena == null) { + font_arena = std.heap.ArenaAllocator.init(allocator); + } + var alloc = font_arena.?.allocator(); + // The bit lookup will be a bit tricky because we have runtime value to look up + // We can use an inline for but compute complexity is a bit crazy. Best to use a + // hashmap here + // + // We aren't generating Unicode at the moment, but to handle it appropriately, let's + // assign our key value to u21 + if (font_map == null) { + font_map = std.hash_map.StringHashMap(?*FontInnerHash).init(alloc); + try font_map.?.ensureTotalCapacity(supported_fonts.len); + inline for (supported_fonts) |font| { + font_map.?.putAssumeCapacity(font.name, null); + } + // defer font_map.deinit(); + } + if (!font_map.?.contains(font_name)) return null; // this font not in fonts/fonts.zig. This must be addressed in compilation + + const font = font_map.?.get(font_name).?; + if (font) |f| return f.*; // Font exists and is fully populated + + // Font exists, but has not been populated yet. Build it now + // idk if actual map needs to be on the heap here? + // I think it does... let's try without and see how we leak + inline for (supported_fonts) |supported_font| { + if (std.mem.eql(u8, supported_font.name, font_name)) { + font_map.?.putAssumeCapacity(supported_font.name, try getFontMap(alloc, supported_font.name)); + } + } + std.log.debug( + "All fonts added. Outer hash map capacity: {d} count: {d}\n", + .{ font_map.?.capacity(), font_map.?.count() }, + ); + std.log.debug( + "Inner hash map capacity for default font: {d} count: {d}\n", + .{ font_map.?.get(DEFAULT_FONT).?.?.capacity(), font_map.?.get(DEFAULT_FONT).?.?.count() }, + ); + return font_map.?.get(font_name).?.?.*; +} +/// gets a font map. All memory owned by caller +/// The inner hash map is key type u21, value type of pointer to u8 array +/// The values are used directly, but all keys will be allocated at the time +/// of call +fn getFontMap(allocator: std.mem.Allocator, comptime font_name: []const u8) !*FontInnerHash { + // supported_font is a comptime value due to inline for + // font_name is a runtime value + const font_struct = @field(fonts, font_name); + const code_points = @typeInfo(@TypeOf(font_struct)).Struct.fields; + // found font - populate map and return + var map = FontInnerHash.init(allocator); + try map.ensureTotalCapacity(code_points.len); + inline for (code_points) |point| { + if (std.mem.eql(u8, "120", point.name)) { + std.log.debug( + "CodePoint 120 Added. Data:{s}\n", + .{fmtSliceHexLower(@field(font_struct, point.name))}, + ); + } + var key_ptr = try allocator.create(u21); + key_ptr.* = std.fmt.parseInt(u21, point.name, 10) catch unreachable; + map.putAssumeCapacity( + key_ptr.*, + @field(font_struct, point.name), + ); + } + std.log.debug( + "All codepoints added. {*} capacity: {d} count: {d}\n", + .{ &map, map.capacity(), map.count() }, + ); + return ↦ +} + fn sendPixels(pixels: []const u8, file: [:0]const u8, device_id: u8) !void { if (std.mem.eql(u8, file, "-")) return sendPixelsToStdOut(pixels); @@ -265,6 +406,35 @@ fn i2cWrite(i2c: *c.I2CDriver, bytes: []const u8) !void { return error.BadWrite; } +const Case = enum { lower, upper }; +fn formatSliceHexImpl(comptime case: Case) type { + const charset = "0123456789" ++ if (case == .upper) "ABCDEF" else "abcdef"; + + return struct { + pub fn formatSliceHexImpl( + bytes: []const u8, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + var buf: [2]u8 = undefined; + + for (bytes) |ch| { + buf[0] = charset[ch >> 4]; + buf[1] = charset[ch & 15]; + try writer.print(" 0x", .{}); + try writer.writeAll(&buf); + } + } + }; +} + +const formatSliceHexLower = formatSliceHexImpl(.lower).formatSliceHexImpl; +fn fmtSliceHexLower(bytes: []const u8) std.fmt.Formatter(formatSliceHexLower) { + return .{ .data = bytes }; +} fn fmtSliceGreyscaleImage(bytes: []const u8) std.fmt.Formatter(formatSliceGreyscaleImage) { return .{ .data = bytes }; } @@ -294,6 +464,9 @@ fn reportMagickError(mw: ?*c.MagickWand) !void { fn textForLine(line: usize) []u8 { return lines[line].*; } +fn noTextForLine(_: usize) []u8 { + return ""; +} fn convertImage(filename: [:0]u8, pixels: *[display.WIDTH * display.HEIGHT]u8, text_fn: *const fn (usize) []u8) !void { c.MagickWandGenesis(); defer c.MagickWandTerminus(); @@ -505,8 +678,26 @@ fn getNewDimensions(width: usize, height: usize, desired_width: usize, desired_h .height = @floatToInt(usize, @intToFloat(f64, height) / resize_ratio), // 64, }; } +test "gets proper font data" { + // std.testing.log_level = .debug; + std.log.debug("\n", .{}); + defer deinit(); + var maybe_font_data = try getFontData(std.testing.allocator, DEFAULT_FONT); + try std.testing.expect(maybe_font_data != null); + var font_data = maybe_font_data.?; + try std.testing.expect(font_data.capacity() > 90); + try std.testing.expect(font_data.count() > 0); + try std.testing.expect(font_data.get(33) != null); + try std.testing.expectEqualSlices(u8, &[_]u8{ 0x80, 0x10, 0x42, 0x00, 0x20 }, font_data.get(33).?); +} +test "deinit" { + defer deinit(); +} test "gets correct bytes" { + defer deinit(); + std.testing.log_level = .debug; + std.log.debug("\n", .{}); const bg_file: [:0]u8 = @constCast("logo:"); const opts = .{ .background_filename = bg_file, .device_file = "-" }; var empty: [:0]u8 = @constCast(""); @@ -520,7 +711,38 @@ test "gets correct bytes" { var expected_pixels: *const [display.WIDTH * display.HEIGHT]u8 = @embedFile("testExpectedBytes.bin"); // [_]u8{..,..,..} - try convertImage(opts.background_filename, &pixels, textForLine); + try convertImage(opts.background_filename, &pixels, noTextForLine); + try addTextToImage(std.testing.allocator, &pixels, &lines); + const fmt = "{d:0>2}: {s}"; + for (0..display.HEIGHT) |i| { + const actual = try std.fmt.allocPrint( + std.testing.allocator, + fmt, + .{ i, fmtSliceGreyscaleImage(pixels[(i * display.WIDTH)..((i + 1) * display.WIDTH)]) }, + ); + defer std.testing.allocator.free(actual); + const expected = try std.fmt.allocPrint( + std.testing.allocator, + fmt, + .{ i, fmtSliceGreyscaleImage(expected_pixels[(i * display.WIDTH)..((i + 1) * display.WIDTH)]) }, + ); + defer std.testing.allocator.free(expected); + std.testing.expectEqualSlices(u8, expected, actual) catch |err| { + for (0..display.HEIGHT) |r| { + std.log.debug( + fmt, + .{ r, fmtSliceGreyscaleImage(pixels[(r * display.WIDTH)..((r + 1) * display.WIDTH)]) }, + ); + } + for (0..display.HEIGHT) |r| { + std.log.debug( + fmt, + .{ r, fmtSliceGreyscaleImage(expected_pixels[(r * display.WIDTH)..((r + 1) * display.WIDTH)]) }, + ); + } + return err; + }; + } // try writeBytesToFile("testExpectedBytes.bin", &pixels); try std.testing.expectEqualSlices(u8, expected_pixels, &pixels); } @@ -538,9 +760,3 @@ fn writeBytesToFile(filename: []const u8, bytes: []u8) !void { try writer.writeAll(bytes); // try writer.print("pub const chars = &[_][]const u8{{\n", .{}); } -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} diff --git a/src/testExpectedBytes.bin b/src/testExpectedBytes.bin index 90546a5..85f2a3a 100644 Binary files a/src/testExpectedBytes.bin and b/src/testExpectedBytes.bin differ