🖨️ esc/pos implementation in gleam
1//// A low-level imperative API for building ESC/POS printer commands.
2////
3//// This module provides a builder-style interface where each function takes
4//// a `CommandBuffer` and returns a modified buffer. For a declarative
5//// alternative, see `escpos/document`.
6////
7//// ## Example
8////
9//// ```gleam
10//// import escpos.{Center, Left}
11//// import escpos/printer
12////
13//// escpos.new()
14//// |> escpos.reset()
15//// |> escpos.align(Center)
16//// |> escpos.bold(True)
17//// |> escpos.writeln("Receipt")
18//// |> escpos.bold(False)
19//// |> escpos.align(Left)
20//// |> escpos.writeln("Item 1 ... $5.00")
21//// |> escpos.cut()
22//// |> printer.print(my_printer)
23//// ```
24
25import escpos/image
26import escpos/printer.{type CommandBuffer, CommandBuffer}
27import escpos/protocol
28import gleam/bit_array
29import gleam/bool
30
31/// Text justification. Re-exported from `escpos/protocol`.
32pub type Justify =
33 protocol.Justify
34
35/// Printer font. Re-exported from `escpos/protocol`.
36pub type Font =
37 protocol.Font
38
39/// Creates a new empty command buffer.
40pub fn new() -> CommandBuffer {
41 CommandBuffer(<<>>)
42}
43
44/// Writes text to the buffer.
45pub fn write(cb: CommandBuffer, text: String) -> CommandBuffer {
46 append(cb, bit_array.from_string(text))
47}
48
49/// Writes text to the buffer followed by a newline.
50pub fn writeln(cb: CommandBuffer, text: String) -> CommandBuffer {
51 append(cb, bit_array.from_string(text))
52 |> append(protocol.lf)
53}
54
55/// Enables or disables bold text.
56pub fn bold(cb: CommandBuffer, b: Bool) -> CommandBuffer {
57 append(cb, protocol.bold(b))
58}
59
60/// Enables or disables underlined text.
61pub fn underline(cb: CommandBuffer, b: Bool) -> CommandBuffer {
62 append(cb, protocol.underline(b))
63}
64
65/// Enables or disables double-strike text.
66pub fn double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer {
67 append(cb, protocol.double_strike(b))
68}
69
70/// Enables or disables reverse (white on black) text.
71pub fn reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer {
72 append(cb, protocol.reverse(b))
73}
74
75/// Enables or disables upside-down text.
76pub fn upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer {
77 append(cb, protocol.upside_down(b))
78}
79
80/// Enables or disables character smoothing.
81pub fn smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer {
82 append(cb, protocol.smooth(b))
83}
84
85/// Enables or disables 180-degree rotation.
86pub fn flip(cb: CommandBuffer, b: Bool) -> CommandBuffer {
87 append(cb, protocol.flip(b))
88}
89
90/// Sets the printer font.
91pub fn font(cb: CommandBuffer, font: Font) -> CommandBuffer {
92 append(cb, protocol.font(font))
93}
94
95/// Resets the font to the default (FontA).
96pub fn reset_font(cb: CommandBuffer) -> CommandBuffer {
97 append(cb, protocol.font(protocol.FontA))
98}
99
100/// Sets text alignment (Left, Center, or Right).
101pub fn align(cb: CommandBuffer, justify: Justify) -> CommandBuffer {
102 ensure_new_line(cb)
103 |> append(protocol.justify(justify))
104}
105
106/// Sets text size multiplier (1-8 for width and height).
107pub fn text_size(
108 cb: CommandBuffer,
109 width: Int,
110 height: Int,
111) -> CommandBuffer {
112 append(cb, protocol.character_size(width, height))
113}
114
115/// Resets text size to normal (1x1).
116pub fn reset_text_size(cb: CommandBuffer) -> CommandBuffer {
117 append(cb, protocol.character_size(1, 1))
118}
119
120/// Performs a full paper cut.
121pub fn cut(cb: CommandBuffer) -> CommandBuffer {
122 append(cb, protocol.cut(protocol.Full))
123}
124
125/// Performs a partial paper cut, leaving a small portion connected.
126pub fn partial_cut(cb: CommandBuffer) -> CommandBuffer {
127 append(cb, protocol.cut(protocol.Partial))
128}
129
130/// Appends a single newline.
131pub fn new_line(cb: CommandBuffer) -> CommandBuffer {
132 append(cb, protocol.lf)
133}
134
135/// Feeds the specified number of lines.
136pub fn line_feed(cb: CommandBuffer, lines: Int) -> CommandBuffer {
137 append(cb, protocol.line_feed(lines))
138}
139
140/// Resets the printer to its initial state.
141pub fn reset(cb: CommandBuffer) -> CommandBuffer {
142 append(cb, protocol.init)
143}
144
145/// Prints a monochrome image prepared by the `image` module.
146///
147/// ## Example
148///
149/// ```gleam
150/// let assert Ok(pgm) = simplifile.read_bits(from: "./lucy.pgm")
151/// let assert Ok(img) = image.from_pgm(raw_pgm)
152/// let img = image.dither_bayer4x4(img, 0)
153///
154/// escpos.new()
155/// |> escpos.image(img)
156/// |> printer.print(printer)
157/// ```
158pub fn image(cb: CommandBuffer, image: image.PrintableImage) -> CommandBuffer {
159 ensure_new_line(cb)
160 |> append(protocol.image_to_graphics_buffer(
161 image.pixels(image),
162 image.width(image),
163 image.height(image),
164 protocol.Monochrome,
165 protocol.Scale1x,
166 protocol.Scale1x,
167 protocol.Color1,
168 ))
169 |> append(protocol.print_graphics_buffer())
170}
171
172/// Appends raw bytes to the buffer.
173pub fn raw(cb: CommandBuffer, data: BitArray) -> CommandBuffer {
174 append(cb, data)
175}
176
177fn append(cb: CommandBuffer, data: BitArray) -> CommandBuffer {
178 bit_array.append(cb.data, data)
179 |> CommandBuffer
180}
181
182fn ensure_new_line(cb: CommandBuffer) -> CommandBuffer {
183 case is_new_line(cb) {
184 True -> cb
185 False -> new_line(cb)
186 }
187}
188
189fn is_new_line(cb: CommandBuffer) -> Bool {
190 use <- bool.guard(when: cb.data == <<>>, return: True)
191 case
192 bit_array.slice(from: cb.data, at: bit_array.byte_size(cb.data), take: -3)
193 {
194 Ok(<<27, "d", _>>) -> True
195 Ok(<<_, _, 10>>) -> True
196 _ -> False
197 }
198}