diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua new file mode 100644 index 000000000..225abb6f7 --- /dev/null +++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua @@ -0,0 +1,107 @@ +--- Provides utilities for working with "nft" images. +-- +-- nft ("Nitrogen Fingers Text") is a file format for drawing basic images. +-- Unlike the images that @{paintutils.parseImage} uses, nft supports coloured +-- text. +-- +-- @module cc.image.nft +-- @usage Load an image from `example.nft` and draw it. +-- +-- local nft = require "cc.image.nft" +-- local image = assert(nft.load("example.nft")) +-- nft.draw(image) + +local expect = require "cc.expect".expect + +--- Parse an nft image from a string. +-- +-- @tparam string image The image contents. +-- @return table The parsed image. +local function parse(image) + expect(1, image, "string") + + local result = {} + local line = 1 + local foreground = "0" + local background = "f" + + local i, len = 1, #image + while i <= len do + local c = image:sub(i, i) + if c == "\31" and i < len then + i = i + 1 + foreground = image:sub(i, i) + elseif c == "\30" and i < len then + i = i + 1 + background = image:sub(i, i) + elseif c == "\n" then + if result[line] == nil then + result[line] = { text = "", foreground = "", background = "" } + end + + line = line + 1 + else + local next = image:find("[\n\30\31]", i) or #image + 1 + local seg_len = next - i + + local this_line = result[line] + if this_line == nil then + this_line = { foreground = "", background = "", text = "" } + result[line] = this_line + end + + this_line.text = this_line.text .. image:sub(i, next - 1) + this_line.foreground = this_line.foreground .. foreground:rep(seg_len) + this_line.background = this_line.background .. background:rep(seg_len) + + i = next - 1 + end + + i = i + 1 + end + return result +end + +--- Load an nft image from a file. +-- +-- @tparam string path The file to load. +-- @treturn[1] table The parsed image. +-- @treturn[2] nil If the file does not exist or could not be loaded. +-- @treturn[2] string An error message explaining why the file could not be +-- loaded. +local function load(path) + expect(1, path, "string") + local file, err = io.open(path, "r") + if not file then return nil, err end + + local result = file:read("*a") + file:close() + return parse(result) +end + +--- Draw an nft image to the screen. +-- +-- @tparam table image An image, as returned from @{load} or @{draw}. +-- @tparam number xPos The x position to start drawing at. +-- @tparam number xPos The y position to start drawing at. +-- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the +-- current terminal. +local function draw(image, xPos, yPos, target) + expect(1, image, "table") + expect(2, xPos, "number") + expect(3, yPos, "number") + expect(4, target, "table", "nil") + + if not target then target = term end + + for y, line in ipairs(image) do + target.setCursorPos(xPos, yPos + y - 1) + target.blit(line.text, line.foreground, line.background) + end +end + +return { + parse = parse, + load = load, + draw = draw, +} diff --git a/src/test/resources/test-rom/spec/modules/cc/image/nft_spec.lua b/src/test/resources/test-rom/spec/modules/cc/image/nft_spec.lua new file mode 100644 index 000000000..f67c40898 --- /dev/null +++ b/src/test/resources/test-rom/spec/modules/cc/image/nft_spec.lua @@ -0,0 +1,91 @@ +local helpers = require "test_helpers" + +describe("cc.image.nft", function() + local nft = require("cc.image.nft") + + describe("parse", function() + it("validates arguments", function() + nft.parse("") + expect.error(nft.parse, nil):eq("bad argument #1 (expected string, got nil)") + end) + + it("parses an empty string", function() + expect(nft.parse("")):same {} + end) + + it("parses a string with no colours", function() + expect(nft.parse("Hello")):same { { text = "Hello", foreground = "00000", background = "fffff" } } + end) + + it("handles background and foreground colours", function() + expect(nft.parse("\30a\31bHello")) + :same { { text = "Hello", foreground = "bbbbb", background = "aaaaa" } } + end) + + it("parses multi-line files", function() + expect(nft.parse("Hello\nWorld")):same { + { text = "Hello", foreground = "00000", background = "fffff" }, + { text = "World", foreground = "00000", background = "fffff" }, + } + end) + + it("handles empty lines", function() + expect(nft.parse("\n\n")):same { + { text = "", foreground = "", background = "" }, + { text = "", foreground = "", background = "" }, + } + end) + end) + + describe("load", function() + it("validates arguments", function() + nft.load("") + expect.error(nft.load, nil):eq("bad argument #1 (expected string, got nil)") + end) + + it("loads from a file", function() + local image = fs.open("/test-files/example.nft", "w") + image.write("\30aHello, world!") + image.close() + + expect(nft.load("/test-files/example.nft")):same { + { background = "aaaaaaaaaaaaa", foreground = "0000000000000", text = "Hello, world!" }, + } + end) + + it("fails on missing files", function() + expect({ nft.load("/test-files/not_a_file.nft") }) + :same { nil, "/test-files/not_a_file.nft: No such file" } + end) + end) + + describe("draw", function() + it("validates arguments", function() + expect.error(nft.draw, nil):eq("bad argument #1 (expected table, got nil)") + expect.error(nft.draw, {}, nil):eq("bad argument #2 (expected number, got nil)") + expect.error(nft.draw, {}, 1, nil):eq("bad argument #3 (expected number, got nil)") + expect.error(nft.draw, {}, 1, 1, false):eq("bad argument #4 (expected table, got boolean)") + end) + + it("draws an image", function() + local win = helpers.with_window(7, 3, function() + nft.draw({ + { background = "aaaaa", foreground = "f000f", text = "Hello" }, + }, 2, 2) + end) + + expect(win.getLine(1)):eq(" ") + expect({ win.getLine(2) }):same { " Hello ", "0f000f0", "faaaaaf" } + expect(win.getLine(3)):eq(" ") + end) + + it("draws an image to a custom redirect", function() + local win = window.create(term.current(), 1, 1, 5, 1, false) + nft.draw({ + { background = "aaaaa", foreground = "f000f", text = "Hello" }, + }, 1, 1, win) + + expect({ win.getLine(1) }):same { "Hello", "f000f", "aaaaa" } + end) + end) +end)