Current section

Files

Jump to
starprnt src starprnt.gleam
Raw

src/starprnt.gleam

import gleam/bit_array
import gleam/bool
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import starprnt/font.{type Font, Font}
pub type Speed {
Slow
Normal
Fast
}
pub fn print_speed(speed speed: Speed) -> BitArray {
let num = case speed {
Slow -> 2
Normal -> 1
Fast -> 0
}
<<0x1b, 0x1e, "r", num>>
}
pub type Wrap {
NoWrap
WrapAnywhere
WrapWords(wrap_overflow: Bool)
}
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 const cut = <<start_continuous_raster:bits, "b", 0, 0, end_raster:bits>>
fn raster_line(bits: BitArray, byte_size: Int) -> BitArray {
<<"b", byte_size:little-16, bits:bits>>
}
fn padded_raster_line(bits: BitArray) -> BitArray {
let bits = bit_array.pad_to_bytes(bits)
raster_line(bits, bit_array.byte_size(bits))
}
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
<<data:bytes-size(max_bytes_per_line), image_data:bits>> -> {
let line = raster_line(data, max_bytes_per_line)
do_image_raster(image_data, <<acc:bits, line:bits>>)
}
<<data:bits>> -> <<acc:bits, padded_raster_line(data):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] -> {
let graphemes = [" ", " ", ..graphemes]
do_process_graphemes(graphemes, default_glyph, render_glyph, acc)
}
["\f", ..graphemes] -> {
let graphemes = ["\n", "\n", ..graphemes]
do_process_graphemes(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, [], [])
WrapWords(wrap_overflow:) ->
do_wrap_words(chars, wrap_overflow, line_limit, None, 0, [], 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_words(
chars: List(#(String, BitArray)),
wrap: Bool,
line_limit: Int,
break: Option(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 break {
Some(break) -> reverse_and_prepend(line, [break, ..list.reverse(word)])
None -> 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 break {
Some(break) -> reverse_and_prepend(line, [break, ..list.reverse(word)])
None -> list.reverse(word)
}
let lines = [line, ..lines]
do_wrap_words(chars, wrap, line_limit, None, 0, [], 0, [], lines)
}
[#("\n", _), ..chars] -> {
let lines = [list.reverse(word), list.reverse(line), ..lines]
do_wrap_words(chars, wrap, line_limit, None, 0, [], 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 break {
Some(break) -> #(
reverse_and_prepend([break, ..list.reverse(word)], line),
line_len + word_len + 1,
)
None -> #(
reverse_and_prepend(list.reverse(word), line),
line_len + word_len,
)
}
do_wrap_words(
chars,
wrap,
line_limit,
Some(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] ->
do_wrap_words(
chars,
wrap,
line_limit,
Some(break),
0,
[],
word_len,
word,
[list.reverse(line), ..lines],
)
[#(_, bits), ..chars] if word_len < line_limit ->
do_wrap_words(
chars,
wrap,
line_limit,
break,
word_len + 1,
[bits, ..word],
line_len,
line,
lines,
)
[#(_, bits), ..chars] if wrap -> {
let lines = case line {
[] -> [list.reverse(word), ..lines]
_ -> [list.reverse(word), list.reverse(line), ..lines]
}
do_wrap_words(chars, wrap, line_limit, None, 1, [bits], 0, [], lines)
}
[_, ..chars] ->
do_wrap_words(
chars,
wrap,
line_limit,
break,
word_len,
word,
line_len,
line,
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>>
let lines = [chars, ..lines]
let rem_height = rem_height - 1
do_render_lines(lines, padding, width, height, rem_height, 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 {
<<data:bytes-size(max_bytes_per_line), _:bits>> ->
raster_line(data, max_bytes_per_line)
_ -> padded_raster_line(acc)
}
|> repeat_bits(scale),
list.reverse(leftover_chars),
)
}
[char, ..chars] ->
case char {
<<char_line:size(width)-bits, _:size(padding), leftover_char:bits>> -> {
let acc = <<acc:bits, scale_bits(char_line, scale):bits>>
let leftover_chars = [leftover_char, ..leftover_chars]
do_render_char_line(chars, padding, width, scale, acc, leftover_chars)
}
_ -> {
let acc = <<acc:bits, 0:size(width)>>
let leftover_chars = [char, ..leftover_chars]
do_render_char_line(chars, padding, width, scale, acc, 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>>)
}
}