//TODO: re-implement sorting //c library usage based on https://medium.com/@eddo2626/lets-learn-zig-4-using-c-libraries-in-zig-5fcc3206f0dc // const std = @import("std"); const clap = @import("clap"); const c = @cImport({ @cInclude("pdfgen.h"); }); const print = std.debug.print; const io = std.io; const fs = std.fs; const json = std.json; const cwd = fs.cwd(); const indexOf = std.mem.indexOf; const expect = std.testing.expect; const assert = std.debug.assert; const Card = struct { name: []const u8 = "", //string mana_cost: []const u8 = "", //string cmc: f32 = 0, //technically a float? but I think we can always cast safely cast. EDIT: NOPE type_line: []const u8 = "", //string oracle_text: []const u8 = "", //string power: []const u8 = "", //coerced to string toughness: []const u8 = "", //coerced to string card_faces: ?[]Card = null, //array of cards }; const TextCard = struct { lines: [][]const u8 = undefined, }; //dimension constants and defaults const cardWidth = 30; const cardHeight = 32; const minCardHeight = 5; const pageHeight = 66; var heightMayVary = false; const formatString = "{s: <" ++ std.fmt.digits2(cardWidth) ++ "}"; const lineFormatter = "|" ++ formatString; const spacer: []const u8 = "|" ++ (" " ** cardWidth); const oracleFileName = "/home/lumenk/Documents/code/zig/proxy-print/src/oracle-cards-20240701090158.json"; //"oracle-cards-20240205220208.js"; const fullWidthSpacer = (spacer ** 3) ++ "\n"; const pageWidth = fullWidthSpacer.len; test "Check constants" { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const paddedLine = try std.fmt.allocPrint(gpa.allocator(), lineFormatter, .{"hello> woooooooooooooooooooorl"}); try expect(paddedLine.len == spacer.len); } pub fn main() !void { var args = try std.process.argsWithAllocator(std.heap.page_allocator); //TODO: properly handle program arguments _ = args.next(); //handle program name (args[0]) const listFileName: []const u8 = args.next() orelse { return error.ExpectedArgument; }; heightMayVary = stringToBool(args.next()); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const pdf_doc: *c.pdf_doc = @ptrCast(c.pdf_create(c.PDF_A4_WIDTH, c.PDF_A4_HEIGHT, &c.pdf_info{.creator = ("My Software" ++ " " ** 53).* }).?); _ = c.pdf_set_font(pdf_doc, "Times-Roman"); const page = c.pdf_append_page(pdf_doc); _ = c.pdf_add_text(pdf_doc, page, "This is text", 12, 10, 100, c.PDF_BLACK); // _ = c.pdf_add_line(pdf_doc, null, 50, 24, 150, 24, 3, c.PDF_BLACK); _ = c.pdf_save(pdf_doc, "output.pdf"); _ = c.pdf_destroy(pdf_doc); const oracleFile = try cwd.openFile(oracleFileName, .{}); var jsonReader = json.reader(allocator, oracleFile.reader()); const parsedJson = try json.parseFromTokenSource([]Card, allocator, &jsonReader, .{ .ignore_unknown_fields = true }); var cardNames = std.ArrayList([]const u8).init(allocator); const listReader = (try cwd.openFile(listFileName, .{})).reader(); var line = std.ArrayList(u8).init(allocator); while (listReader.streamUntilDelimiter(line.writer(), '\n', null)) { defer line.clearRetainingCapacity(); const cardName = line.items[indexOf(u8, line.items, " ").? + 1 .. indexOf(u8, line.items, "(").? - 1]; try cardNames.append(try allocator.dupe(u8, cardName)); } else |err| switch (err) { error.EndOfStream => {}, else => return err, } var cards = std.StringArrayHashMap(TextCard).init(allocator); for (parsedJson.value) |cardObj| { for (cardNames.items, 0..) |cardName, i| { if (std.mem.eql(u8, cardName, cardObj.name)) { const printableCard = try card(cardObj, allocator, false); try cards.put(cardObj.name, printableCard); _ = cardNames.orderedRemove(i); } } } var allPrinted = std.ArrayList(u8).init(allocator); var rowToPrint = std.ArrayList(TextCard).init(allocator); // var cardIterator = cards.valueIterator(); for (cards.values()) |cardText| { try rowToPrint.append(cardText); if (rowToPrint.items.len >= 3) { try cardRow.print(allocator, rowToPrint.items, &allPrinted); rowToPrint.clearAndFree(); } } else { try cardRow.print(allocator, rowToPrint.items, &allPrinted); std.debug.print("{s}", .{allPrinted.items}); rowToPrint.clearAndFree(); } } fn card( cardObj: Card, allocator: std.mem.Allocator, isFace: bool, ) !TextCard { var cardText = std.ArrayList([]const u8).init(allocator); var fullUnformattedText = std.ArrayList(u8).init(allocator); if (!isFace) { try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{ cardObj.name, if (cardObj.mana_cost.len > 0) " " else "", cardObj.mana_cost, " (", cardObj.type_line, ") >> ", })); } try fullUnformattedText.appendSlice(try std.mem.concat(allocator, u8, &[_][]const u8{ cardObj.oracle_text, if (cardObj.power.len > 0) " (" else "", cardObj.power, if (cardObj.power.len > 0) "/" else "", cardObj.toughness, if (cardObj.power.len > 0) ") " else "", })); if (cardObj.card_faces) |faces| { for (faces, 0..) |face, idx| { const faceText = (try card(face, allocator, true)).lines; try fullUnformattedText.appendSlice(std.mem.trim(u8, try std.mem.join(allocator, " ", faceText), "\n")); if (idx == 0) try fullUnformattedText.appendSlice(" // "); } } var line = std.ArrayList(u8).init(allocator); var word = std.ArrayList(u8).init(allocator); for (fullUnformattedText.items) |char| { try switch (char) { '\n', ' ' => addWord(&word, &line, &cardText), else => if (std.ascii.isASCII(char)) { try word.append(char); }, }; } else { try addWord(&word, &line, &cardText); const lineToBeAdded = std.mem.trimRight(u8, try line.toOwnedSlice(), " "); // const lineToBeAdded = try line.toOwnedSlice(); try cardText.append(lineToBeAdded); line.clearAndFree(); } while (wrongCardHeight(cardText.items.len)) { try cardText.append(" " ** (cardWidth - 2)); } try cardText.append(" " ** (cardWidth - 2)); // return cardText.items; return TextCard{ .lines = try cardText.toOwnedSlice() }; } fn wrongCardHeight(length: usize) bool { return (!heightMayVary and length < cardHeight) or length < minCardHeight; } fn addWord(word: *std.ArrayList(u8), line: *std.ArrayList(u8), cardText: *std.ArrayList([]const u8)) !void { if (line.items.len + word.items.len >= (cardWidth)) { const lineToBeAdded = std.mem.trimRight(u8, try line.toOwnedSlice(), " "); assert(lineToBeAdded.len < 30); try cardText.append(lineToBeAdded); line.clearAndFree(); } try line.appendSlice(word.items); try line.append(' '); word.clearAndFree(); } const linesList = std.MultiArrayList(cardRow); const cardRow = struct { first: []const u8 = spacer, second: []const u8 = spacer, third: []const u8 = spacer, last: []const u8 = "\n", fn print(allocator: std.mem.Allocator, cards: []TextCard, allPrinted: *std.ArrayList(u8)) !void { _ = allocator; var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var lines = linesList{}; defer lines.deinit(gpa.allocator()); for (cards, 0..) |cardObj, cardNo| { //const cardText = try card(cardObj); const cardText = cardObj.lines; for (cardText, 0..) |line, idx| { //this step is probably unnecessary const strippedLine = std.mem.trimRight(u8, line, " "); const paddedLine = try std.fmt.allocPrint(gpa.allocator(), lineFormatter, .{strippedLine}); const theoreticalLength = (paddedLine.len * 3) + 1; assert(paddedLine.len == spacer.len); assert(theoreticalLength == pageWidth); const placeholder = if (idx < lines.items(.first).len) lines.get(idx) else cardRow{}; const new: cardRow = switch (cardNo) { 0 => .{ .first = paddedLine }, 1 => .{ .first = placeholder.first, .second = paddedLine }, 2 => .{ .first = placeholder.first, .second = placeholder.second, .third = paddedLine }, else => unreachable, }; if (idx < lines.items(.first).len) { lines.set(idx, new); } else { try lines.append(gpa.allocator(), new); } } } const rowHeight = lines.items(.first).len; while (remainingPage(allPrinted.items.len) <= rowHeight) { try allPrinted.appendSlice(fullWidthSpacer); } for (lines.items(.first), 0..) |_, idx| { const line = lines.get(idx); const printedWidth = line.first.len + line.second.len + line.third.len + line.last.len; assert(printedWidth == pageWidth); const fullLine = try std.mem.concat(gpa.allocator(), u8, &[_][]const u8{ line.first, line.second, line.third, line.last }); try allPrinted.appendSlice(fullLine); } } }; test "Remaining page length" { try expect(remainingPage(67) == 0); try expect(remainingPage(1) == 0); } fn remainingPage(length: usize) usize { assert(length % pageWidth == 0); return pageHeight - ((length / pageWidth) % pageHeight); } fn stringToBool(str: ?[]const u8) bool { return std.mem.eql(u8, (str orelse "false"), "true"); }