switch to manual text overlay

This commit is contained in:
Emil Lerch 2023-04-08 16:57:55 -07:00
parent 8c9df71080
commit b05ac7bae7
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
3 changed files with 229 additions and 11 deletions

View File

@ -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(.{}){};

View File

@ -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 &map;
}
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());
}

Binary file not shown.