import gleam/bit_array import gleam/bool import gleam/int import gleam/list import gleam/string import starprnt/font.{type Font, Font} pub type Speed { Slow Normal Fast } pub fn print_speed(speed speed: Speed) -> BitArray { << 0x1b, 0x1e, "r", case speed { Slow -> 2 Normal -> 1 Fast -> 0 }, >> } pub type Wrap { NoWrap WrapAnywhere WrapOnlyWords WrapPreferWords } const max_bytes_per_line = 72 const esc = 0x1b const raster_cmd = <> const start_raster = <> const continuous_raster = <> const start_continuous_raster = <> const disable_cut = <> const start_continuous_raster_no_cut = << start_continuous_raster:bits, disable_cut:bits, >> const end_raster = <> pub fn image_raster(image image_data: BitArray, cut cut: Bool) -> BitArray { let start_bits = case cut { True -> start_continuous_raster False -> start_continuous_raster_no_cut } do_image_raster(image_data, start_bits) |> bit_array.append(end_raster) } fn do_image_raster(image_data: BitArray, acc: BitArray) -> BitArray { case image_data { <<>> -> acc <> -> do_image_raster(image_data, << acc:bits, "b", max_bytes_per_line, 0, line:bits, >>) <> -> { let line = bit_array.pad_to_bytes(line) <> } } } pub fn text_raster( text text: String, font font: Font, scale scale: Int, wrap wrap: Wrap, cut cut: Bool, ) -> BitArray { use <- bool.guard(scale == 0, return: <<>>) let Font(width:, height:, default_glyph:, render_glyph:) = font let line_limit = 8 * max_bytes_per_line / { width * int.absolute_value(scale) } let padding = case width % 8 { 0 -> 0 rem -> 8 - rem } let start_bits = case cut { True -> start_continuous_raster False -> start_continuous_raster_no_cut } string.to_graphemes(text) |> do_process_graphemes(default_glyph, render_glyph, []) |> layout_chars(wrap, line_limit) |> do_render_lines(padding, width, height, height, scale, start_bits) |> bit_array.append(end_raster) } fn do_process_graphemes( graphemes: List(String), default_glyph: BitArray, render_glyph: fn(String) -> Result(BitArray, Nil), acc: List(#(String, BitArray)), ) -> List(#(String, BitArray)) { case graphemes { [] -> list.reverse(acc) ["\r", ..graphemes] -> do_process_graphemes(graphemes, default_glyph, render_glyph, acc) ["\t", ..graphemes] -> do_process_graphemes( [" ", " ", ..graphemes], default_glyph, render_glyph, acc, ) ["\f", ..graphemes] -> do_process_graphemes( ["\n", "\n", ..graphemes], default_glyph, render_glyph, acc, ) [grapheme, ..graphemes] -> { let acc = case render_glyph(grapheme) { Ok(bits) -> [#(grapheme, bits), ..acc] Error(Nil) -> string.to_utf_codepoints(grapheme) |> do_process_codepoints(default_glyph, render_glyph, acc) } do_process_graphemes(graphemes, default_glyph, render_glyph, acc) } } } fn do_process_codepoints( codepoints: List(UtfCodepoint), default_glyph: BitArray, render_glyph: fn(String) -> Result(BitArray, Nil), acc: List(#(String, BitArray)), ) -> List(#(String, BitArray)) { case codepoints { [] -> acc [codepoint, ..codepoints] -> { let char = string.from_utf_codepoints([codepoint]) let acc = case render_glyph(char) { Ok(bits) -> [#(char, bits), ..acc] Error(Nil) -> [#(char, default_glyph), ..acc] } do_process_codepoints(codepoints, default_glyph, render_glyph, acc) } } } fn layout_chars(chars: List(#(String, BitArray)), wrap: Wrap, line_limit: Int) { case wrap { NoWrap -> do_no_wrap(chars, line_limit, 0, [], []) WrapAnywhere -> do_wrap_anywhere(chars, line_limit, 0, [], []) WrapOnlyWords -> do_wrap_only_words_unbroken(chars, line_limit, 0, [], []) WrapPreferWords -> do_wrap_prefer_words_unbroken(chars, line_limit, 0, [], []) } } fn do_no_wrap( chars: List(#(String, BitArray)), line_limit: Int, line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] -> list.reverse([list.reverse(line), ..lines]) [#("\n", _), ..chars] -> do_no_wrap(chars, line_limit, 0, [], [list.reverse(line), ..lines]) [#(_, bits), ..chars] if line_len < line_limit -> do_no_wrap(chars, line_limit, line_len + 1, [bits, ..line], lines) [_, ..chars] -> do_no_wrap(chars, line_limit, line_len, line, lines) } } fn do_wrap_anywhere( chars: List(#(String, BitArray)), line_limit: Int, line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] -> list.reverse([list.reverse(line), ..lines]) [#("\n", _), ..chars] -> { let acc = [list.reverse(line), ..lines] do_wrap_anywhere(chars, line_limit, 0, [], acc) } [#(_, bits), ..chars] if line_len < line_limit -> { let line = [bits, ..line] do_wrap_anywhere(chars, line_limit, line_len + 1, line, lines) } [#(" ", _), ..chars] | [#("\u{1680}", _), ..chars] | [#("\u{2000}", _), ..chars] | [#("\u{2001}", _), ..chars] | [#("\u{2002}", _), ..chars] | [#("\u{2003}", _), ..chars] | [#("\u{2004}", _), ..chars] | [#("\u{2005}", _), ..chars] | [#("\u{2006}", _), ..chars] | [#("\u{2008}", _), ..chars] | [#("\u{2009}", _), ..chars] | [#("\u{200a}", _), ..chars] | [#("\u{205f}", _), ..chars] | [#("\u{3000}", _), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_anywhere(chars, line_limit, 0, [], lines) } [#(_, bits), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_anywhere(chars, line_limit, 1, [bits], lines) } } } fn do_wrap_only_words_unbroken( chars: List(#(String, BitArray)), line_limit: Int, line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] -> list.reverse([list.reverse(line), ..lines]) [#("\n", _), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_only_words_unbroken(chars, line_limit, 0, [], lines) } [#(" ", break), ..chars] | [#("\u{1680}", break), ..chars] | [#("\u{2000}", break), ..chars] | [#("\u{2001}", break), ..chars] | [#("\u{2002}", break), ..chars] | [#("\u{2003}", break), ..chars] | [#("\u{2004}", break), ..chars] | [#("\u{2005}", break), ..chars] | [#("\u{2006}", break), ..chars] | [#("\u{2008}", break), ..chars] | [#("\u{2009}", break), ..chars] | [#("\u{200a}", break), ..chars] | [#("\u{205f}", break), ..chars] | [#("\u{3000}", break), ..chars] -> { do_wrap_only_words(chars, line_limit, break, 0, [], line_len, line, lines) } [#(_, bits), ..chars] if line_len < line_limit -> do_wrap_only_words_unbroken( chars, line_limit, line_len + 1, [bits, ..line], lines, ) [_, ..chars] -> do_wrap_only_words_unbroken(chars, line_limit, line_len, line, lines) } } fn do_wrap_only_words( chars: List(#(String, BitArray)), line_limit: Int, break: BitArray, word_len: Int, word: List(BitArray), line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] if line_len + word_len < line_limit -> { let line = case line { [] -> list.reverse(word) _ -> reverse_and_prepend(line, [break, ..list.reverse(word)]) } list.reverse([line, ..lines]) } [] -> list.reverse([list.reverse(word), list.reverse(line), ..lines]) [#("\n", _), ..chars] if line_len + word_len < line_limit -> { let line = case line { [] -> list.reverse(word) _ -> reverse_and_prepend(line, [break, ..list.reverse(word)]) } let lines = [line, ..lines] do_wrap_only_words(chars, line_limit, break, 0, [], 0, [], lines) } [#("\n", _), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_only_words(chars, line_limit, break, word_len, word, 0, [], lines) } [#(" ", new_break), ..chars] | [#("\u{1680}", new_break), ..chars] | [#("\u{2000}", new_break), ..chars] | [#("\u{2001}", new_break), ..chars] | [#("\u{2002}", new_break), ..chars] | [#("\u{2003}", new_break), ..chars] | [#("\u{2004}", new_break), ..chars] | [#("\u{2005}", new_break), ..chars] | [#("\u{2006}", new_break), ..chars] | [#("\u{2008}", new_break), ..chars] | [#("\u{2009}", new_break), ..chars] | [#("\u{200a}", new_break), ..chars] | [#("\u{205f}", new_break), ..chars] | [#("\u{3000}", new_break), ..chars] if line_len + word_len < line_limit -> { let #(line, line_len) = case line { [] -> #(word, word_len) _ -> #( reverse_and_prepend([break, ..list.reverse(word)], line), line_len + word_len + 1, ) } do_wrap_only_words( chars, line_limit, new_break, 0, [], line_len, line, lines, ) } [#(" ", break), ..chars] | [#("\u{1680}", break), ..chars] | [#("\u{2000}", break), ..chars] | [#("\u{2001}", break), ..chars] | [#("\u{2002}", break), ..chars] | [#("\u{2003}", break), ..chars] | [#("\u{2004}", break), ..chars] | [#("\u{2005}", break), ..chars] | [#("\u{2006}", break), ..chars] | [#("\u{2008}", break), ..chars] | [#("\u{2009}", break), ..chars] | [#("\u{200a}", break), ..chars] | [#("\u{205f}", break), ..chars] | [#("\u{3000}", break), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_only_words(chars, line_limit, break, 0, [], word_len, word, lines) } [#(_, bits), ..chars] if word_len < line_limit -> do_wrap_only_words( chars, line_limit, break, word_len + 1, [bits, ..word], line_len, line, lines, ) [_, ..chars] -> do_wrap_only_words( chars, line_limit, break, word_len, word, line_len, line, lines, ) } } fn do_wrap_prefer_words_unbroken( chars: List(#(String, BitArray)), line_limit: Int, line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] -> list.reverse([list.reverse(line), ..lines]) [#("\n", _), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_prefer_words_unbroken(chars, line_limit, 0, [], lines) } [#(" ", break), ..chars] | [#("\u{1680}", break), ..chars] | [#("\u{2000}", break), ..chars] | [#("\u{2001}", break), ..chars] | [#("\u{2002}", break), ..chars] | [#("\u{2003}", break), ..chars] | [#("\u{2004}", break), ..chars] | [#("\u{2005}", break), ..chars] | [#("\u{2006}", break), ..chars] | [#("\u{2008}", break), ..chars] | [#("\u{2009}", break), ..chars] | [#("\u{200a}", break), ..chars] | [#("\u{205f}", break), ..chars] | [#("\u{3000}", break), ..chars] -> { do_wrap_prefer_words( chars, line_limit, break, 0, [], line_len, line, lines, ) } [#(_, bits), ..chars] if line_len < line_limit -> do_wrap_prefer_words_unbroken( chars, line_limit, line_len + 1, [bits, ..line], lines, ) [#(_, bits), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_prefer_words_unbroken(chars, line_limit, 1, [bits], lines) } } } fn do_wrap_prefer_words( chars: List(#(String, BitArray)), line_limit: Int, break: BitArray, word_len: Int, word: List(BitArray), line_len: Int, line: List(BitArray), lines: List(List(BitArray)), ) -> List(List(BitArray)) { case chars { [] if line_len + word_len < line_limit -> { let line = case line { [] -> list.reverse(word) _ -> reverse_and_prepend(line, [break, ..list.reverse(word)]) } list.reverse([line, ..lines]) } [] -> list.reverse([list.reverse(word), list.reverse(line), ..lines]) [#("\n", _), ..chars] if line_len + word_len < line_limit -> { let line = case line { [] -> list.reverse(word) _ -> reverse_and_prepend(line, [break, ..list.reverse(word)]) } let lines = [line, ..lines] do_wrap_prefer_words(chars, line_limit, break, 0, [], 0, [], lines) } [#("\n", _), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_prefer_words( chars, line_limit, break, word_len, word, 0, [], lines, ) } [#(" ", new_break), ..chars] | [#("\u{1680}", new_break), ..chars] | [#("\u{2000}", new_break), ..chars] | [#("\u{2001}", new_break), ..chars] | [#("\u{2002}", new_break), ..chars] | [#("\u{2003}", new_break), ..chars] | [#("\u{2004}", new_break), ..chars] | [#("\u{2005}", new_break), ..chars] | [#("\u{2006}", new_break), ..chars] | [#("\u{2008}", new_break), ..chars] | [#("\u{2009}", new_break), ..chars] | [#("\u{200a}", new_break), ..chars] | [#("\u{205f}", new_break), ..chars] | [#("\u{3000}", new_break), ..chars] if line_len + word_len < line_limit -> { let #(line, line_len) = case line { [] -> #(word, word_len) _ -> #( reverse_and_prepend([break, ..list.reverse(word)], line), line_len + word_len + 1, ) } do_wrap_prefer_words( chars, line_limit, new_break, 0, [], line_len, line, lines, ) } [#(" ", break), ..chars] | [#("\u{1680}", break), ..chars] | [#("\u{2000}", break), ..chars] | [#("\u{2001}", break), ..chars] | [#("\u{2002}", break), ..chars] | [#("\u{2003}", break), ..chars] | [#("\u{2004}", break), ..chars] | [#("\u{2005}", break), ..chars] | [#("\u{2006}", break), ..chars] | [#("\u{2008}", break), ..chars] | [#("\u{2009}", break), ..chars] | [#("\u{200a}", break), ..chars] | [#("\u{205f}", break), ..chars] | [#("\u{3000}", break), ..chars] -> { let lines = [list.reverse(line), ..lines] do_wrap_prefer_words( chars, line_limit, break, 0, [], word_len, word, lines, ) } [#(_, bits), ..chars] if word_len < line_limit -> do_wrap_prefer_words( chars, line_limit, break, word_len + 1, [bits, ..word], line_len, line, lines, ) [#(_, bits), ..chars] -> { let lines = case line { [] -> [list.reverse(word), ..lines] _ -> [list.reverse(word), list.reverse(line), ..lines] } do_wrap_prefer_words(chars, line_limit, break, 1, [bits], 0, [], lines) } } } @external(erlang, "lists", "reverse") fn reverse_and_prepend(list prefix: List(a), to suffix: List(a)) -> List(a) { case prefix { [] -> suffix [first, ..rest] -> reverse_and_prepend(list: rest, to: [first, ..suffix]) } } fn do_render_lines( lines: List(List(BitArray)), padding: Int, width: Int, height: Int, rem_height: Int, scale: Int, acc: BitArray, ) -> BitArray { case lines { [] -> acc [chars, ..lines] -> case rem_height { 0 -> do_render_lines(lines, padding, width, height, height, scale, acc) _ -> { let #(line_data, chars) = do_render_char_line(chars, padding, width, scale, <<>>, []) let acc = <> do_render_lines( [chars, ..lines], padding, width, height, rem_height - 1, scale, acc, ) } } } } fn do_render_char_line( chars: List(BitArray), padding: Int, width: Int, scale: Int, acc: BitArray, leftover_chars: List(BitArray), ) -> #(BitArray, List(BitArray)) { case chars { [] -> { #( case acc { <> -> << "b", max_bytes_per_line, 0, line_data:bits, >> _ -> { let line_data = bit_array.pad_to_bytes(acc) <<"b", bit_array.byte_size(line_data), 0, line_data:bits>> } } |> repeat_bits(scale), list.reverse(leftover_chars), ) } [char, ..chars] -> case char { <> -> do_render_char_line( chars, padding, width, scale, <>, [leftover_char, ..leftover_chars], ) _ -> do_render_char_line( chars, padding, width, scale, <>, [char, ..leftover_chars], ) } } } fn scale_bits(bits: BitArray, scale: Int) -> BitArray { case scale { 0 -> <<>> 1 | -1 -> bits _ -> do_scale_bits(bits, int.absolute_value(scale), <<>>) } } fn do_scale_bits(bits: BitArray, scale: Int, acc: BitArray) -> BitArray { case bits { <<0:1, bits:bits>> -> do_scale_bits(bits, scale, <>) <<1:1, bits:bits>> -> do_scale_bits(bits, scale, <>) _ -> acc } } fn repeat_bits(bits: BitArray, times: Int) -> BitArray { case times { 0 -> <<>> 1 | -1 -> bits _ -> do_repeat_bits(bits, int.absolute_value(times) - 1, bits) } } fn do_repeat_bits(bits: BitArray, times: Int, acc: BitArray) -> BitArray { case times { 0 -> acc _ -> do_repeat_bits(bits, times - 1, <>) } }