🖨️ esc/pos implementation in gleam

feat: image apis and pbm support

okk.moe fe2f0093 753ff539

verified
+154 -146
-3
dev/dev.gleam dev/escpos_dev.gleam
··· 1 1 import escpos 2 - import escpos/document 3 2 import escpos/image 4 3 import escpos/printer 5 - import escpos/protocol 6 4 import simplifile 7 5 8 6 pub fn main() { ··· 13 11 img 14 12 // |> image.dither_ign 15 13 |> image.dither_bayer4x4(0) 16 - |> echo 17 14 18 15 let assert Ok(printer) = printer.connect("10.13.141.62", 9100) 19 16
+4 -4
src/escpos.gleam
··· 92 92 append(cb, protocol.init) 93 93 } 94 94 95 - pub fn image(cb: CommandBuffer, image: image.MonochromeImage) -> CommandBuffer { 95 + pub fn image(cb: CommandBuffer, image: image.PrintableImage) -> CommandBuffer { 96 96 ensure_new_line(cb) 97 97 |> append(protocol.image_to_graphics_buffer( 98 - image.pixels, 99 - image.width, 100 - image.height, 98 + image.pixels(image), 99 + image.width(image), 100 + image.height(image), 101 101 protocol.Monochrome, 102 102 protocol.Scale1x, 103 103 protocol.Scale1x,
+60 -42
src/escpos/document.gleam
··· 1 + import escpos/image 1 2 import escpos/printer.{type CommandBuffer, CommandBuffer} 2 3 import escpos/protocol 3 4 import gleam/bit_array ··· 10 11 Writeln(String) 11 12 LineFeed(Int) 12 13 Cut(protocol.Cut) 14 + Image(image: image.PrintableImage) 13 15 Styled(modifiers: Set(Modifier), commands: List(Command)) 14 16 Custom(BitArray) 15 17 } ··· 33 35 DoWrite(String) 34 36 DoLineFeed(Int) 35 37 DoCut(protocol.Cut) 38 + DoImage(image.PrintableImage) 36 39 DoCustom(BitArray) 37 40 SetBold(Bool) 38 41 SetUnderline(Bool) ··· 77 80 ) 78 81 } 79 82 83 + fn ensure_new_line(state: State, acc: List(AST)) -> #(List(AST), State) { 84 + case state.new_line { 85 + True -> #(acc, state) 86 + False -> #([DoLineFeed(1), ..acc], State(..state, new_line: True)) 87 + } 88 + } 89 + 80 90 pub fn build(document: List(Command)) -> CommandBuffer { 81 91 upside_down_pass(document) 82 92 |> build_ast ··· 100 110 LineFeed(lines) 101 111 } 102 112 113 + pub fn image(image: image.PrintableImage) -> Command { 114 + Image(image) 115 + } 116 + 103 117 pub fn cut() -> Command { 104 118 Cut(protocol.Full) 105 119 } ··· 125 139 } 126 140 127 141 pub fn flip() -> Modifier { 128 - Reverse 142 + Flip 129 143 } 130 144 131 145 pub fn upside_down() -> Modifier { ··· 235 249 list.prepend(acc, DoWrite(x)) |> list.prepend(DoLineFeed(1)) 236 250 do_build_ast(rest, new_acc, State(..state, new_line: True)) 237 251 } 252 + Image(i) -> { 253 + let #(acc, state) = ensure_new_line(state, acc) 254 + do_build_ast( 255 + rest, 256 + [DoImage(i), ..acc], 257 + State(..state, new_line: True), 258 + ) 259 + } 238 260 Custom(b) -> 239 261 do_build_ast( 240 262 rest, ··· 290 312 [] -> #(acc, state) 291 313 [style, ..rest] -> 292 314 case style { 293 - SetJustify(_) | SetUpsideDown(_) -> 294 - case state.new_line { 295 - True -> do_revert_styles(rest, [style, ..acc], state) 296 - False -> 297 - do_revert_styles( 298 - rest, 299 - list.prepend(acc, DoLineFeed(1)) |> list.prepend(style), 300 - State(..state, new_line: True), 301 - ) 302 - } 315 + SetJustify(_) | SetUpsideDown(_) -> { 316 + let #(acc, state) = ensure_new_line(state, acc) 317 + do_revert_styles(rest, [style, ..acc], state) 318 + } 303 319 _ -> do_revert_styles(rest, [style, ..acc], state) 304 320 } 305 321 } ··· 351 367 [SetFlip(True), ..acc], 352 368 State(..state, flip: True), 353 369 ) 354 - UpsideDown -> 355 - case state.new_line { 356 - True -> 357 - do_apply_modifiers( 358 - rest, 359 - [SetUpsideDown(True), ..acc], 360 - State(..state, upside_down: True, new_line: True), 361 - ) 362 - False -> 363 - do_apply_modifiers( 364 - rest, 365 - list.prepend(acc, DoLineFeed(1)) 366 - |> list.prepend(SetUpsideDown(True)), 367 - State(..state, upside_down: True), 368 - ) 369 - } 370 + UpsideDown -> { 371 + let #(acc, state) = ensure_new_line(state, acc) 372 + do_apply_modifiers( 373 + rest, 374 + [SetUpsideDown(True), ..acc], 375 + State(..state, upside_down: True), 376 + ) 377 + } 370 378 TextSize(size) -> 371 379 do_apply_modifiers( 372 380 rest, ··· 385 393 [SetTextSize(width: state.text_width, height: size), ..acc], 386 394 State(..state, text_height: size), 387 395 ) 388 - Justify(j) -> 389 - case state.new_line { 390 - True -> 391 - do_apply_modifiers( 392 - rest, 393 - [SetJustify(j), ..acc], 394 - State(..state, justify: j, new_line: True), 395 - ) 396 - False -> 397 - do_apply_modifiers( 398 - rest, 399 - list.prepend(acc, DoLineFeed(1)) |> list.prepend(SetJustify(j)), 400 - State(..state, justify: j), 401 - ) 402 - } 396 + Justify(j) -> { 397 + let #(acc, state) = ensure_new_line(state, acc) 398 + do_apply_modifiers( 399 + rest, 400 + [SetJustify(j), ..acc], 401 + State(..state, justify: j), 402 + ) 403 + } 403 404 Font(f) -> 404 405 do_apply_modifiers(rest, [SetFont(f), ..acc], State(..state, font: f)) 405 406 } ··· 419 420 Init -> do_compile_ast(rest, bit_array.append(acc, protocol.init)) 420 421 DoWrite(x) -> 421 422 do_compile_ast(rest, bit_array.append(acc, bit_array.from_string(x))) 423 + DoImage(i) -> 424 + do_compile_ast( 425 + rest, 426 + bit_array.append( 427 + acc, 428 + protocol.image_to_graphics_buffer( 429 + image.pixels(i), 430 + image.width(i), 431 + image.height(i), 432 + protocol.Monochrome, 433 + protocol.Scale1x, 434 + protocol.Scale1x, 435 + protocol.Color1, 436 + ), 437 + ) 438 + |> bit_array.append(protocol.print_graphics_buffer()), 439 + ) 422 440 DoLineFeed(n) -> 423 441 do_compile_ast(rest, bit_array.append(acc, protocol.line_feed(n))) 424 442 DoCut(c) -> do_compile_ast(rest, bit_array.append(acc, protocol.cut(c)))
+90 -97
src/escpos/image.gleam
··· 4 4 import gleam/int 5 5 import gleam/result 6 6 7 - pub type Image { 7 + pub opaque type Image { 8 8 Image(width: Int, height: Int, pixels: BitArray) 9 9 } 10 10 11 - pub type MonochromeImage { 12 - MonochromeImage(width: Int, height: Int, pixels: BitArray) 11 + pub opaque type PrintableImage { 12 + PrintableImage(width: Int, height: Int, pixels: BitArray) 13 13 } 14 14 15 15 pub opaque type ImageError { 16 16 ParseError 17 17 } 18 18 19 - pub fn from_pgm(pgm: BitArray) -> Result(Image, ImageError) { 20 - case pgm { 21 - <<"P5\n", rest:bits>> -> pgm_width(rest, 0) 19 + pub fn pixels(image: PrintableImage) -> BitArray { 20 + image.pixels 21 + } 22 + 23 + pub fn width(image: PrintableImage) -> Int { 24 + image.width 25 + } 26 + 27 + pub fn height(image: PrintableImage) -> Int { 28 + image.height 29 + } 30 + 31 + pub fn from_pbm(pbm: BitArray) -> Result(PrintableImage, ImageError) { 32 + case pbm { 33 + <<"P4\n", rest:bits>> -> { 34 + use #(width, rest) <- result.try(parse_number(rest, 0)) 35 + use #(height, pixels) <- result.try(parse_number(rest, 0)) 36 + Ok(PrintableImage(width:, height:, pixels:)) 37 + } 22 38 _ -> Error(ParseError) 23 39 } 24 40 } 25 41 26 - fn pgm_width(pgm: BitArray, width: Int) -> Result(Image, ImageError) { 42 + pub fn from_pgm(pgm: BitArray) -> Result(Image, ImageError) { 27 43 case pgm { 28 - <<"0", rest:bits>> -> pgm_width(rest, width * 10 + 0) 29 - <<"1", rest:bits>> -> pgm_width(rest, width * 10 + 1) 30 - <<"2", rest:bits>> -> pgm_width(rest, width * 10 + 2) 31 - <<"3", rest:bits>> -> pgm_width(rest, width * 10 + 3) 32 - <<"4", rest:bits>> -> pgm_width(rest, width * 10 + 4) 33 - <<"5", rest:bits>> -> pgm_width(rest, width * 10 + 5) 34 - <<"6", rest:bits>> -> pgm_width(rest, width * 10 + 6) 35 - <<"7", rest:bits>> -> pgm_width(rest, width * 10 + 7) 36 - <<"8", rest:bits>> -> pgm_width(rest, width * 10 + 8) 37 - <<"9", rest:bits>> -> pgm_width(rest, width * 10 + 9) 38 - <<" ", rest:bits>> -> pgm_height(rest, width, 0) 44 + <<"P5\n", rest:bits>> -> { 45 + use #(width, rest) <- result.try(parse_number(rest, 0)) 46 + use #(height, rest) <- result.try(parse_number(rest, 0)) 47 + case rest { 48 + <<"255\n", pixels:bytes>> -> Ok(Image(width:, height:, pixels:)) 49 + _ -> Error(ParseError) 50 + } 51 + } 39 52 _ -> Error(ParseError) 40 53 } 41 54 } 42 55 43 - fn pgm_height( 44 - pgm: BitArray, 45 - width: Int, 46 - height: Int, 47 - ) -> Result(Image, ImageError) { 48 - case pgm { 49 - <<"0", rest:bits>> -> pgm_height(rest, width, height * 10 + 0) 50 - <<"1", rest:bits>> -> pgm_height(rest, width, height * 10 + 1) 51 - <<"2", rest:bits>> -> pgm_height(rest, width, height * 10 + 2) 52 - <<"3", rest:bits>> -> pgm_height(rest, width, height * 10 + 3) 53 - <<"4", rest:bits>> -> pgm_height(rest, width, height * 10 + 4) 54 - <<"5", rest:bits>> -> pgm_height(rest, width, height * 10 + 5) 55 - <<"6", rest:bits>> -> pgm_height(rest, width, height * 10 + 6) 56 - <<"7", rest:bits>> -> pgm_height(rest, width, height * 10 + 7) 57 - <<"8", rest:bits>> -> pgm_height(rest, width, height * 10 + 8) 58 - <<"9", rest:bits>> -> pgm_height(rest, width, height * 10 + 9) 59 - <<"\n255\n", pixels:bytes>> -> Ok(Image(width:, height:, pixels:)) 60 - 56 + fn parse_number( 57 + data: BitArray, 58 + acc: Int, 59 + ) -> Result(#(Int, BitArray), ImageError) { 60 + case data { 61 + <<"0", rest:bits>> -> parse_number(rest, acc * 10 + 0) 62 + <<"1", rest:bits>> -> parse_number(rest, acc * 10 + 1) 63 + <<"2", rest:bits>> -> parse_number(rest, acc * 10 + 2) 64 + <<"3", rest:bits>> -> parse_number(rest, acc * 10 + 3) 65 + <<"4", rest:bits>> -> parse_number(rest, acc * 10 + 4) 66 + <<"5", rest:bits>> -> parse_number(rest, acc * 10 + 5) 67 + <<"6", rest:bits>> -> parse_number(rest, acc * 10 + 6) 68 + <<"7", rest:bits>> -> parse_number(rest, acc * 10 + 7) 69 + <<"8", rest:bits>> -> parse_number(rest, acc * 10 + 8) 70 + <<"9", rest:bits>> -> parse_number(rest, acc * 10 + 9) 71 + <<" ", rest:bits>> | <<"\n", rest:bits>> -> Ok(#(acc, rest)) 61 72 _ -> Error(ParseError) 62 73 } 63 74 } 64 75 65 - pub fn dither_ign(image: Image) -> MonochromeImage { 76 + pub fn dither_ign(image: Image) -> PrintableImage { 66 77 use pixel, row, column <- dither_map(image) 67 78 let ign = 68 79 float.modulo( ··· 84 95 } 85 96 } 86 97 87 - pub fn dither_bayer2x2(image: Image, bias: Int) -> MonochromeImage { 98 + pub fn dither_bayer2x2(image: Image, bias: Int) -> PrintableImage { 88 99 dither_bayer(image, bias, bayer_2x2_matrix) 89 100 } 90 101 91 - pub fn dither_bayer4x4(image: Image, bias: Int) -> MonochromeImage { 102 + pub fn dither_bayer4x4(image: Image, bias: Int) -> PrintableImage { 92 103 dither_bayer(image, bias, bayer_4x4_matrix) 93 104 } 94 105 ··· 133 144 image: Image, 134 145 bias: Int, 135 146 matrix: fn(Int, Int) -> Int, 136 - ) -> MonochromeImage { 147 + ) -> PrintableImage { 137 148 use pixel, row, column <- dither_map(image) 138 149 case pixel > matrix(row, column) + bias { 139 150 True -> 0 ··· 141 152 } 142 153 } 143 154 144 - fn dither_map(image: Image, with fun: fn(Int, Int, Int) -> Int) { 145 - MonochromeImage( 146 - width: image.width, 147 - height: image.height, 148 - pixels: dither_loop(image.pixels, image.width, 0, 0, 0, 0, <<>>, fun), 149 - ) 155 + fn dither_map(image: Image, fun: fn(Int, Int, Int) -> Int) -> PrintableImage { 156 + let pixels = 157 + dither_loop(image.pixels, image.width, 0, 0, <<>>, fun) 158 + |> pack(image.width) 159 + PrintableImage(width: image.width, height: image.height, pixels:) 150 160 } 151 161 152 - /// TODO: refactor this 153 162 fn dither_loop( 154 163 pixels: BitArray, 155 164 width: Int, 156 165 row: Int, 157 166 column: Int, 158 - current_byte: Int, 159 - bit_position: Int, 160 167 acc: BitArray, 161 168 fun: fn(Int, Int, Int) -> Int, 162 169 ) -> BitArray { 163 170 case pixels { 164 171 <<pixel, rest:bits>> -> { 165 - // Get dithered bit (0 or 1) 166 172 let bit = fun(pixel, row, column) 167 - 168 - // Pack bit into current byte (MSB first: bit 7 = leftmost pixel) 169 - let new_byte = case bit { 170 - 1 -> 171 - int.bitwise_or( 172 - current_byte, 173 - int.bitwise_shift_left(1, 7 - bit_position), 173 + let next_column = column + 1 174 + case next_column == width { 175 + True -> 176 + dither_loop(rest, width, row + 1, 0, <<acc:bits, bit:size(1)>>, fun) 177 + False -> 178 + dither_loop( 179 + rest, 180 + width, 181 + row, 182 + next_column, 183 + <<acc:bits, bit:size(1)>>, 184 + fun, 174 185 ) 175 - _ -> current_byte 176 186 } 187 + } 188 + _ -> acc 189 + } 190 + } 177 191 178 - let next_column = column + 1 179 - let next_bit_position = bit_position + 1 192 + pub fn pack(pixels, width) -> BitArray { 193 + let padding = { 8 - width % 8 } % 8 194 + pack_rows(pixels, width, padding, <<>>) 195 + } 180 196 181 - case next_column == width { 182 - // End of row - flush current byte (with padding) and start new row 183 - True -> { 184 - let new_acc = <<acc:bits, new_byte>> 185 - dither_loop(rest, width, row + 1, 0, 0, 0, new_acc, fun) 186 - } 187 - // Continue row 188 - False -> { 189 - case next_bit_position == 8 { 190 - // Byte complete - flush and start new byte 191 - True -> { 192 - let new_acc = <<acc:bits, new_byte>> 193 - dither_loop(rest, width, row, next_column, 0, 0, new_acc, fun) 194 - } 195 - // Continue filling current byte 196 - False -> { 197 - dither_loop( 198 - rest, 199 - width, 200 - row, 201 - next_column, 202 - new_byte, 203 - next_bit_position, 204 - acc, 205 - fun, 206 - ) 207 - } 208 - } 209 - } 210 - } 197 + fn pack_rows( 198 + pixels: BitArray, 199 + width: Int, 200 + padding: Int, 201 + acc: BitArray, 202 + ) -> BitArray { 203 + case pixels { 204 + <<row:bits-size(width), rest:bits>> -> { 205 + let padded_row = << 206 + row:bits-size(width), 207 + 0:size(padding), 208 + >> 209 + pack_rows(rest, width, padding, <<acc:bits, padded_row:bits>>) 211 210 } 212 - // Done - flush any remaining bits 213 - _ -> { 214 - case bit_position > 0 { 215 - True -> <<acc:bits, current_byte>> 216 - False -> acc 217 - } 218 - } 211 + _ -> acc 219 212 } 220 213 }