🖨️ esc/pos implementation in gleam

wip: images

okk.moe 7ce0fed0 265fa379

verified
+315 -88
+3 -9
gleam.toml
··· 1 1 name = "escpos" 2 2 version = "1.0.0" 3 3 4 - # Fill out these fields if you intend to generate HTML documentation or publish 5 - # your project to the Hex package manager. 6 - # 7 - # description = "" 8 - # licences = ["Apache-2.0"] 9 - # repository = { type = "github", user = "", repo = "" } 4 + description = "🖨️ ESC/POS library for Gleam" 5 + licences = ["Apache-2.0"] 6 + repository = { type = "tangled", user = "did:plc:jwgnraovgs3eeenh23tlllyk", repo = "escpos" } 10 7 # links = [{ title = "Website", href = "" }] 11 - # 12 - # For a full reference of all the available options, you can have a look at 13 - # https://gleam.run/writing-gleam/gleam-toml/. 14 8 15 9 [dependencies] 16 10 gleam_stdlib = ">= 0.44.0 and < 2.0.0"
+7 -7
src/escpos.gleam
··· 16 16 |> append(protocol.lf) 17 17 } 18 18 19 - pub fn bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { 19 + pub fn set_bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { 20 20 append(cb, protocol.bold(b)) 21 21 } 22 22 23 - pub fn underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { 23 + pub fn set_underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { 24 24 append(cb, protocol.underline(b)) 25 25 } 26 26 27 - pub fn double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { 27 + pub fn set_double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { 28 28 append(cb, protocol.double_strike(b)) 29 29 } 30 30 31 - pub fn reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { 31 + pub fn set_reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { 32 32 append(cb, protocol.reverse(b)) 33 33 } 34 34 35 - pub fn upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { 35 + pub fn set_upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { 36 36 append(cb, protocol.upside_down(b)) 37 37 } 38 38 39 - pub fn smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { 39 + pub fn set_smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { 40 40 append(cb, protocol.smooth(b)) 41 41 } 42 42 43 - pub fn flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { 43 + pub fn set_flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { 44 44 append(cb, protocol.flip(b)) 45 45 } 46 46
+79 -9
src/escpos/document.gleam
··· 19 19 Underline 20 20 DoubleStrike 21 21 Reverse 22 + Flip 22 23 UpsideDown 24 + TextSize(Int) 25 + TextWidth(Int) 26 + TextHeight(Int) 23 27 Justify(protocol.Justify) 24 28 Font(protocol.Font) 25 29 } ··· 34 38 SetUnderline(Bool) 35 39 SetDoubleStrike(Bool) 36 40 SetReverse(Bool) 41 + SetFlip(Bool) 37 42 SetUpsideDown(Bool) 43 + SetTextSize(width: Int, height: Int) 38 44 SetJustify(protocol.Justify) 39 45 SetFont(protocol.Font) 40 46 } ··· 46 52 underline: Bool, 47 53 double_strike: Bool, 48 54 reverse: Bool, 55 + flip: Bool, 49 56 upside_down: Bool, 57 + text_width: Int, 58 + text_height: Int, 50 59 justify: protocol.Justify, 51 60 font: protocol.Font, 52 61 ) ··· 59 68 underline: False, 60 69 double_strike: False, 61 70 reverse: False, 71 + flip: False, 62 72 upside_down: False, 73 + text_width: 0, 74 + text_height: 0, 63 75 justify: protocol.Left, 64 76 font: protocol.FontA, 65 77 ) ··· 112 124 Reverse 113 125 } 114 126 127 + pub fn flip() -> Modifier { 128 + Reverse 129 + } 130 + 115 131 pub fn upside_down() -> Modifier { 116 132 UpsideDown 117 133 } ··· 124 140 Font(f) 125 141 } 126 142 143 + pub fn text_size(size: Int) -> Modifier { 144 + TextSize(size) 145 + } 146 + 147 + pub fn text_width(size: Int) -> Modifier { 148 + TextWidth(size) 149 + } 150 + 151 + pub fn text_height(size: Int) -> Modifier { 152 + TextHeight(size) 153 + } 154 + 127 155 @internal 128 156 pub fn upside_down_pass(commands: List(Command)) -> List(Command) { 129 - do_upside_down_pass(commands, []) 157 + do_upside_down_pass(commands, [], False) 130 158 |> list.reverse 131 159 } 132 160 133 161 fn do_upside_down_pass( 134 162 commands: List(Command), 135 163 acc: List(Command), 164 + upside_down: Bool, 136 165 ) -> List(Command) { 137 166 case commands { 138 167 [] -> acc 139 168 [cmd, ..rest] -> { 140 169 case cmd { 141 170 Styled(mods, cmds) -> { 142 - let styled_acc = 143 - case set.contains(mods, UpsideDown) { 144 - True -> do_upside_down_pass(list.reverse(cmds), []) 145 - False -> do_upside_down_pass(cmds, []) 146 - } 147 - |> list.reverse 148 - do_upside_down_pass(rest, [Styled(mods, styled_acc), ..acc]) 171 + let styled_acc = case set.contains(mods, UpsideDown), upside_down { 172 + True, False | False, True -> do_upside_down_pass(cmds, [], True) 173 + _, _ -> 174 + do_upside_down_pass(cmds, [], upside_down) 175 + |> list.reverse 176 + } 177 + do_upside_down_pass( 178 + rest, 179 + [Styled(mods, styled_acc), ..acc], 180 + upside_down, 181 + ) 149 182 } 150 - _ -> do_upside_down_pass(rest, [cmd, ..acc]) 183 + _ -> do_upside_down_pass(rest, [cmd, ..acc], upside_down) 151 184 } 152 185 } 153 186 } ··· 230 263 SetDoubleStrike(state.double_strike), 231 264 ), 232 265 changed(state.reverse != nested_state.reverse, SetReverse(state.reverse)), 266 + changed(state.flip != nested_state.flip, SetFlip(state.flip)), 233 267 changed( 234 268 state.upside_down != nested_state.upside_down, 235 269 SetUpsideDown(state.upside_down), 236 270 ), 271 + changed( 272 + state.text_width != nested_state.text_width 273 + || state.text_height != nested_state.text_height, 274 + SetTextSize(width: state.text_width, height: state.text_height), 275 + ), 237 276 changed(state.justify != nested_state.justify, SetJustify(state.justify)), 238 277 changed(state.font != nested_state.font, SetFont(state.font)), 239 278 ] ··· 306 345 [SetReverse(True), ..acc], 307 346 State(..state, reverse: True), 308 347 ) 348 + Flip -> 349 + do_apply_modifiers( 350 + rest, 351 + [SetFlip(True), ..acc], 352 + State(..state, flip: True), 353 + ) 309 354 UpsideDown -> 310 355 case state.new_line { 311 356 True -> ··· 322 367 State(..state, upside_down: True), 323 368 ) 324 369 } 370 + TextSize(size) -> 371 + do_apply_modifiers( 372 + rest, 373 + [SetTextSize(width: size, height: size), ..acc], 374 + State(..state, text_height: size, text_width: size), 375 + ) 376 + TextWidth(size) -> 377 + do_apply_modifiers( 378 + rest, 379 + [SetTextSize(width: size, height: state.text_height), ..acc], 380 + State(..state, text_width: size), 381 + ) 382 + TextHeight(size) -> 383 + do_apply_modifiers( 384 + rest, 385 + [SetTextSize(width: state.text_width, height: size), ..acc], 386 + State(..state, text_height: size), 387 + ) 325 388 Justify(j) -> 326 389 case state.new_line { 327 390 True -> ··· 371 434 ) 372 435 SetReverse(on) -> 373 436 do_compile_ast(rest, bit_array.append(acc, protocol.reverse(on))) 437 + SetFlip(on) -> 438 + do_compile_ast(rest, bit_array.append(acc, protocol.flip(on))) 374 439 SetUpsideDown(on) -> 375 440 do_compile_ast(rest, bit_array.append(acc, protocol.upside_down(on))) 441 + SetTextSize(width:, height:) -> 442 + do_compile_ast( 443 + rest, 444 + bit_array.append(acc, protocol.character_size(width, height)), 445 + ) 376 446 SetJustify(j) -> 377 447 do_compile_ast(rest, bit_array.append(acc, protocol.justify(j))) 378 448 SetFont(f) ->
+149
src/escpos/image.gleam
··· 1 + import gleam/float 2 + import gleam/int 3 + import gleam/result 4 + 5 + pub type Image { 6 + Image(width: Int, height: Int, pixels: BitArray) 7 + } 8 + 9 + pub fn from_pgm(pgm: BitArray) -> Result(Image, Nil) { 10 + case pgm { 11 + <<"P5\n", rest:bits>> -> pgm_width(rest, 0) 12 + 13 + _ -> Error(Nil) 14 + } 15 + } 16 + 17 + fn pgm_width(pgm: BitArray, width: Int) -> Result(Image, Nil) { 18 + case pgm { 19 + <<"0", rest:bits>> -> pgm_width(rest, width * 10 + 0) 20 + <<"1", rest:bits>> -> pgm_width(rest, width * 10 + 1) 21 + <<"2", rest:bits>> -> pgm_width(rest, width * 10 + 2) 22 + <<"3", rest:bits>> -> pgm_width(rest, width * 10 + 3) 23 + <<"4", rest:bits>> -> pgm_width(rest, width * 10 + 4) 24 + <<"5", rest:bits>> -> pgm_width(rest, width * 10 + 5) 25 + <<"6", rest:bits>> -> pgm_width(rest, width * 10 + 6) 26 + <<"7", rest:bits>> -> pgm_width(rest, width * 10 + 7) 27 + <<"8", rest:bits>> -> pgm_width(rest, width * 10 + 8) 28 + <<"9", rest:bits>> -> pgm_width(rest, width * 10 + 9) 29 + <<" ", rest:bits>> -> pgm_height(rest, width, 0) 30 + _ -> Error(Nil) 31 + } 32 + } 33 + 34 + fn pgm_height(pgm: BitArray, width: Int, height: Int) -> Result(Image, Nil) { 35 + case pgm { 36 + <<"0", rest:bits>> -> pgm_height(rest, width, height * 10 + 0) 37 + <<"1", rest:bits>> -> pgm_height(rest, width, height * 10 + 1) 38 + <<"2", rest:bits>> -> pgm_height(rest, width, height * 10 + 2) 39 + <<"3", rest:bits>> -> pgm_height(rest, width, height * 10 + 3) 40 + <<"4", rest:bits>> -> pgm_height(rest, width, height * 10 + 4) 41 + <<"5", rest:bits>> -> pgm_height(rest, width, height * 10 + 5) 42 + <<"6", rest:bits>> -> pgm_height(rest, width, height * 10 + 6) 43 + <<"7", rest:bits>> -> pgm_height(rest, width, height * 10 + 7) 44 + <<"8", rest:bits>> -> pgm_height(rest, width, height * 10 + 8) 45 + <<"9", rest:bits>> -> pgm_height(rest, width, height * 10 + 9) 46 + <<"\n255\n", pixels:bytes>> -> Ok(Image(width:, height:, pixels:)) 47 + 48 + _ -> Error(Nil) 49 + } 50 + } 51 + 52 + pub fn map(image: Image, with fun: fn(Int, Int, Int) -> Int) { 53 + Image(..image, pixels: map_loop(image.pixels, image.width, 1, 1, <<>>, fun)) 54 + } 55 + 56 + fn map_loop( 57 + pixels: BitArray, 58 + width: Int, 59 + current_row: Int, 60 + current_column: Int, 61 + acc: BitArray, 62 + fun: fn(Int, Int, Int) -> Int, 63 + ) -> BitArray { 64 + case pixels { 65 + <<pixel, pixels:bits>> -> { 66 + let new_pixel = fun(pixel, current_row, current_column) 67 + let #(new_row, new_column) = case current_column == width { 68 + True -> #(current_row + 1, 1) 69 + False -> #(current_row, current_column + 1) 70 + } 71 + map_loop(pixels, width, new_row, new_column, <<acc:bits, new_pixel>>, fun) 72 + } 73 + _ -> acc 74 + } 75 + } 76 + 77 + pub fn dither_ign(image: Image) -> Image { 78 + use pixel, row, column <- map(image) 79 + let ign = 80 + float.modulo( 81 + 52.9829189 82 + *. { 83 + float.modulo( 84 + 0.06711056 *. int.to_float(row) +. 0.00583715 *. int.to_float(column), 85 + 1.0, 86 + ) 87 + |> result.unwrap(1.0) 88 + }, 89 + 1.0, 90 + ) 91 + |> result.unwrap(1.0) 92 + 93 + case int.to_float(pixel) /. 255.0 >. ign { 94 + True -> 1 95 + False -> 0 96 + } 97 + } 98 + 99 + pub fn dither_bayer2x2(image: Image, bias: Int) -> Image { 100 + dither_bayer(image, bias, bayer_2x2_matrix) 101 + } 102 + 103 + pub fn dither_bayer4x4(image: Image, bias: Int) -> Image { 104 + dither_bayer(image, bias, bayer_4x4_matrix) 105 + } 106 + 107 + fn dither_bayer(image: Image, bias: Int, matrix: fn(Int, Int) -> Int) -> Image { 108 + use pixel, row, column <- map(image) 109 + case pixel > matrix(row, column) + bias { 110 + True -> 1 111 + False -> 0 112 + } 113 + } 114 + 115 + pub fn bayer_2x2_matrix(row: Int, column: Int) -> Int { 116 + case row % 2 + 1, column % 2 + 1 { 117 + 1, 1 -> 0 118 + 1, 2 -> 127 119 + 2, 1 -> 191 120 + 2, 2 -> 64 121 + _, _ -> panic 122 + } 123 + } 124 + 125 + pub fn bayer_4x4_matrix(row: Int, column: Int) -> Int { 126 + case row % 4 + 1, column % 4 + 1 { 127 + 1, 1 -> 0 128 + 1, 2 -> 127 129 + 1, 3 -> 32 130 + 1, 4 -> 159 131 + 132 + 2, 1 -> 191 133 + 2, 2 -> 64 134 + 2, 3 -> 223 135 + 2, 4 -> 96 136 + 137 + 3, 1 -> 48 138 + 3, 2 -> 175 139 + 3, 3 -> 16 140 + 3, 4 -> 143 141 + 142 + 4, 1 -> 239 143 + 4, 2 -> 112 144 + 4, 3 -> 207 145 + 4, 4 -> 80 146 + 147 + _, _ -> panic 148 + } 149 + }
+4
src/escpos/protocol.gleam
··· 119 119 let h = int.clamp(height, min: 1, max: 8) |> int.subtract(1) 120 120 <<gs, "!", 0:1, w:3, 0:1, h:3>> 121 121 } 122 + 123 + pub fn image(data: BitArray, width: Int, height: Int) -> BitArray { 124 + <<gs, "(", "L", data:bits>> 125 + }
+73 -63
test/escpos_test.gleam
··· 32 32 document.write("Hello, World!"), 33 33 document.styled([document.upside_down()], [ 34 34 document.styled([document.bold()], [ 35 - document.write("Hello"), 36 - document.write("Joe!"), 37 35 document.styled([document.upside_down()], [ 36 + document.write("Hello"), 38 37 document.write("New Zealand!"), 39 - document.write("Hello"), 40 38 ]), 39 + document.write("Joe!"), 40 + document.write("Hello"), 41 41 ]), 42 42 document.write("Australia!"), 43 43 document.write("Hello"), ··· 46 46 assert document.upside_down_pass(input) == result 47 47 } 48 48 49 - // fn test_print( 50 - // printer: printer.Printer, 51 - // name: String, 52 - // commands: fn(printer.CommandBuffer) -> escpos.CommandBuffer, 53 - // ) { 54 - // escpos.new() 55 - // |> escpos.writeln("----- " <> name) 56 - // |> commands 57 - // |> escpos.new_line 58 - // |> printer.print(printer) 59 - // } 60 - // 61 - // pub fn print_test() { 62 - // let assert Ok(printer) = setup_printer() 63 - // 64 - // let assert Ok(Nil) = 65 - // test_print(printer, "writeln", escpos.write(_, "Hello, World!")) 66 - // 67 - // let assert Ok(Nil) = 68 - // test_print(printer, "font B", fn(b) { 69 - // escpos.set_font(b, protocol.FontB) 70 - // |> escpos.write("Hello, World!") 71 - // |> escpos.reset_font 72 - // }) 73 - // 74 - // let assert Ok(Nil) = 75 - // test_print(printer, "font C", fn(b) { 76 - // escpos.set_font(b, protocol.FontC) 77 - // |> escpos.write("Hello, World!") 78 - // |> escpos.reset_font 79 - // }) 80 - // 81 - // let assert Ok(Nil) = 82 - // test_print(printer, "high text size", fn(b) { 83 - // escpos.set_text_size(b, 1, 8) 84 - // |> escpos.write("Hello, World!") 85 - // |> escpos.reset_text_size 86 - // }) 87 - // 88 - // let assert Ok(Nil) = 89 - // test_print(printer, "wide text size", fn(b) { 90 - // escpos.set_text_size(b, 8, 1) 91 - // |> escpos.write("Hello, World!") 92 - // |> escpos.reset_text_size 93 - // }) 94 - // 95 - // let assert Ok(Nil) = 96 - // test_print(printer, "large text size", fn(b) { 97 - // escpos.set_text_size(b, 3, 3) 98 - // |> escpos.write("Hello, World!") 99 - // |> escpos.reset_text_size 100 - // }) 101 - // 102 - // let assert Ok(Nil) = 103 - // test_print(printer, "line feed 5", escpos.line_feed(_, 1)) 104 - // 105 - // let assert Ok(Nil) = test_print(printer, "partial cut", escpos.partial_cut) 106 - // 107 - // printer.disconnect(printer) 108 - // } 49 + fn test_print( 50 + printer: printer.Printer, 51 + name: String, 52 + commands: fn(printer.CommandBuffer) -> printer.CommandBuffer, 53 + ) { 54 + escpos.new() 55 + |> escpos.writeln("----- " <> name) 56 + |> commands 57 + |> escpos.new_line 58 + |> printer.print(printer) 59 + } 60 + 61 + pub fn imp_print_test() { 62 + let assert Ok(printer) = setup_printer() 63 + 64 + let assert Ok(Nil) = 65 + test_print(printer, "writeln", escpos.write(_, "Hello, World!")) 66 + 67 + let assert Ok(Nil) = 68 + test_print(printer, "font B", fn(b) { 69 + escpos.set_font(b, protocol.FontB) 70 + |> escpos.write("Hello, World!") 71 + |> escpos.reset_font 72 + }) 73 + 74 + let assert Ok(Nil) = 75 + test_print(printer, "font C", fn(b) { 76 + escpos.set_font(b, protocol.FontC) 77 + |> escpos.write("Hello, World!") 78 + |> escpos.reset_font 79 + }) 80 + 81 + let assert Ok(Nil) = 82 + test_print(printer, "large text size", fn(b) { 83 + escpos.set_text_size(b, 3, 3) 84 + |> escpos.write("Hello, World!") 85 + |> escpos.reset_text_size 86 + }) 87 + 88 + let assert Ok(Nil) = test_print(printer, "line feed", escpos.line_feed(_, 1)) 89 + 90 + let assert Ok(Nil) = test_print(printer, "partial cut", escpos.partial_cut) 91 + 92 + printer.disconnect(printer) 93 + } 94 + 95 + pub fn decl_print_test() { 96 + let assert Ok(printer) = setup_printer() 97 + 98 + let assert Ok(Nil) = 99 + document.build([ 100 + document.writeln("hello"), 101 + document.styled([document.bold()], [ 102 + document.writeln("world"), 103 + document.styled([document.justify(protocol.Center)], [ 104 + document.writeln("center and bold"), 105 + ]), 106 + ]), 107 + document.styled([document.justify(protocol.Right)], [ 108 + document.writeln("right"), 109 + ]), 110 + document.styled([document.upside_down()], [ 111 + document.write("Hello"), 112 + document.write("Australia!"), 113 + ]), 114 + ]) 115 + |> printer.print(printer) 116 + 117 + printer.disconnect(printer) 118 + } 109 119 110 120 pub fn protocol_character_size_test() { 111 121 assert protocol.character_size(2, 2) == <<29, "!", 0:1, 1:3, 0:1, 1:3>>