Current section
Files
Jump to
Current section
Files
src/starprnt.gleam
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 = <<esc, "*r">>
const start_raster = <<raster_cmd:bits, "A">>
const continuous_raster = <<raster_cmd:bits, "P0", 0>>
const start_continuous_raster = <<start_raster:bits, continuous_raster:bits>>
const disable_cut = <<raster_cmd:bits, "E1", 0>>
const start_continuous_raster_no_cut = <<
start_continuous_raster:bits,
disable_cut:bits,
>>
const end_raster = <<raster_cmd:bits, "B">>
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
<<line:bytes-size(max_bytes_per_line), image_data:bits>> ->
do_image_raster(image_data, <<
acc:bits,
"b",
max_bytes_per_line,
0,
line:bits,
>>)
<<line:bits>> -> {
let line = bit_array.pad_to_bytes(line)
<<acc:bits, "b", bit_array.byte_size(line), 0, line:bits>>
}
}
}
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 = <<acc:bits, line_data:bits>>
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 {
<<line_data:bytes-size(max_bytes_per_line), _:bits>> -> <<
"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 {
<<char_line:size(width)-bits, _:size(padding), leftover_char:bits>> ->
do_render_char_line(
chars,
padding,
width,
scale,
<<acc:bits, scale_bits(char_line, scale):bits>>,
[leftover_char, ..leftover_chars],
)
_ ->
do_render_char_line(
chars,
padding,
width,
scale,
<<acc:bits, 0:size(width)>>,
[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, <<acc:bits, 0:size(scale)>>)
<<1:1, bits:bits>> ->
do_scale_bits(bits, scale, <<acc:bits, -1:size(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, <<acc:bits, bits:bits>>)
}
}