-module(escpos@image). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/escpos/image.gleam"). -export([pixels/1, width/1, height/1, from_pbm/1, from_pgm/1, bayer_2x2_matrix/2, bayer_4x4_matrix/2, dither_ign/1, dither_bayer2x2/2, dither_bayer4x4/2]). -export_type([grayscale_image/0, printable_image/0, image_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Module to parse and dither PBM/PGM images to be printed on monochrome\n" " printers.\n" " There are printers that can print grayscale or even color, if\n" " you want to do that, you'll need to implement that yourself\n" " and/or leverage the `protocol` module directly.\n" " \n" " 80mm printers usually have a maximum of 640 dots width.\n" " There is no validation so please check your image beforehand.\n" "\n" " Thank you jak for the pgm reading and bayer dithering 💕\n" ). -opaque grayscale_image() :: {grayscale_image, integer(), integer(), bitstring()}. -opaque printable_image() :: {printable_image, integer(), integer(), bitstring()}. -opaque image_error() :: parse_error. -file("src/escpos/image.gleam", 31). ?DOC(false). -spec pixels(printable_image()) -> bitstring(). pixels(Image) -> erlang:element(4, Image). -file("src/escpos/image.gleam", 36). ?DOC(false). -spec width(printable_image()) -> integer(). width(Image) -> erlang:element(2, Image). -file("src/escpos/image.gleam", 41). ?DOC(false). -spec height(printable_image()) -> integer(). height(Image) -> erlang:element(3, Image). -file("src/escpos/image.gleam", 73). -spec parse_number(bitstring(), integer()) -> {ok, {integer(), bitstring()}} | {error, image_error()}. parse_number(Data, Acc) -> case Data of <<"0"/utf8, Rest/bitstring>> -> parse_number(Rest, (Acc * 10) + 0); <<"1"/utf8, Rest@1/bitstring>> -> parse_number(Rest@1, (Acc * 10) + 1); <<"2"/utf8, Rest@2/bitstring>> -> parse_number(Rest@2, (Acc * 10) + 2); <<"3"/utf8, Rest@3/bitstring>> -> parse_number(Rest@3, (Acc * 10) + 3); <<"4"/utf8, Rest@4/bitstring>> -> parse_number(Rest@4, (Acc * 10) + 4); <<"5"/utf8, Rest@5/bitstring>> -> parse_number(Rest@5, (Acc * 10) + 5); <<"6"/utf8, Rest@6/bitstring>> -> parse_number(Rest@6, (Acc * 10) + 6); <<"7"/utf8, Rest@7/bitstring>> -> parse_number(Rest@7, (Acc * 10) + 7); <<"8"/utf8, Rest@8/bitstring>> -> parse_number(Rest@8, (Acc * 10) + 8); <<"9"/utf8, Rest@9/bitstring>> -> parse_number(Rest@9, (Acc * 10) + 9); <<" "/utf8, Rest@10/bitstring>> -> {ok, {Acc, Rest@10}}; <<"\n"/utf8, Rest@10/bitstring>> -> {ok, {Acc, Rest@10}}; _ -> {error, parse_error} end. -file("src/escpos/image.gleam", 46). ?DOC(" Parse a PBM image. PBM is already monocrhome and ready to be printed.\n"). -spec from_pbm(bitstring()) -> {ok, printable_image()} | {error, image_error()}. from_pbm(Pbm) -> case Pbm of <<"P4\n"/utf8, Rest/bitstring>> -> gleam@result:'try'( parse_number(Rest, 0), fun(_use0) -> {Width, Rest@1} = _use0, gleam@result:'try'( parse_number(Rest@1, 0), fun(_use0@1) -> {Height, Pixels} = _use0@1, {ok, {printable_image, Width, Height, Pixels}} end ) end ); _ -> {error, parse_error} end. -file("src/escpos/image.gleam", 58). ?DOC(" Parse a PGM image. PGM is grayscale and needs to be dithered.\n"). -spec from_pgm(bitstring()) -> {ok, grayscale_image()} | {error, image_error()}. from_pgm(Pgm) -> case Pgm of <<"P5\n"/utf8, Rest/bitstring>> -> gleam@result:'try'( parse_number(Rest, 0), fun(_use0) -> {Width, Rest@1} = _use0, gleam@result:'try'( parse_number(Rest@1, 0), fun(_use0@1) -> {Height, Rest@2} = _use0@1, case Rest@2 of <<"255\n"/utf8, Pixels/binary>> -> {ok, {grayscale_image, Width, Height, Pixels}}; _ -> {error, parse_error} end end ) end ); _ -> {error, parse_error} end. -file("src/escpos/image.gleam", 129). -spec bayer_2x2_matrix(integer(), integer()) -> integer(). bayer_2x2_matrix(Row, Column) -> case {(Row rem 2) + 1, (Column rem 2) + 1} of {1, 1} -> 0; {1, 2} -> 127; {2, 1} -> 191; {2, 2} -> 64; {_, _} -> 0 end. -file("src/escpos/image.gleam", 140). -spec bayer_4x4_matrix(integer(), integer()) -> integer(). bayer_4x4_matrix(Row, Column) -> case {(Row rem 4) + 1, (Column rem 4) + 1} of {1, 1} -> 0; {1, 2} -> 127; {1, 3} -> 32; {1, 4} -> 159; {2, 1} -> 191; {2, 2} -> 64; {2, 3} -> 223; {2, 4} -> 96; {3, 1} -> 48; {3, 2} -> 175; {3, 3} -> 16; {3, 4} -> 143; {4, 1} -> 239; {4, 2} -> 112; {4, 3} -> 207; {4, 4} -> 80; {_, _} -> 0 end. -file("src/escpos/image.gleam", 188). -spec dither_loop( bitstring(), integer(), integer(), integer(), bitstring(), fun((integer(), integer(), integer()) -> integer()) ) -> bitstring(). dither_loop(Pixels, Width, Row, Column, Acc, Fun) -> case Pixels of <> -> Bit = Fun(Pixel, Row, Column), Next_column = Column + 1, case Next_column =:= Width of true -> dither_loop( Rest, Width, Row + 1, 0, <>, Fun ); false -> dither_loop( Rest, Width, Row, Next_column, <>, Fun ) end; _ -> Acc end. -file("src/escpos/image.gleam", 223). -spec do_byte_align_rows(bitstring(), integer(), integer(), bitstring()) -> bitstring(). do_byte_align_rows(Pixels, Width, Padding, Acc) -> case Pixels of <> -> Padded_row = <>, do_byte_align_rows( Rest, Width, Padding, <> ); _ -> Acc end. -file("src/escpos/image.gleam", 218). -spec byte_align_rows(bitstring(), integer()) -> bitstring(). byte_align_rows(Pixels, Width) -> Padding = (8 - (Width rem 8)) rem 8, do_byte_align_rows(Pixels, Width, Padding, <<>>). -file("src/escpos/image.gleam", 178). -spec dither_map( grayscale_image(), fun((integer(), integer(), integer()) -> integer()) ) -> printable_image(). dither_map(Image, Fun) -> Pixels = begin _pipe = dither_loop( erlang:element(4, Image), erlang:element(2, Image), 0, 0, <<>>, Fun ), byte_align_rows(_pipe, erlang:element(2, Image)) end, {printable_image, erlang:element(2, Image), erlang:element(3, Image), Pixels}. -file("src/escpos/image.gleam", 97). ?DOC( " Interleaved Gradient Noise dithering\n" " More natural look for photos\n" " Nice blog post explaining it:\n" " https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/\n" ). -spec dither_ign(grayscale_image()) -> printable_image(). dither_ign(Image) -> dither_map( Image, fun(Pixel, Row, Column) -> Ign = begin _pipe@1 = gleam@float:modulo( 52.9829189 * begin _pipe = gleam@float:modulo( (0.06711056 * erlang:float(Row)) + (0.00583715 * erlang:float( Column )), 1.0 ), gleam@result:unwrap(_pipe, 1.0) end, 1.0 ), gleam@result:unwrap(_pipe@1, 1.0) end, case (erlang:float(Pixel) / 255.0) > Ign of true -> 0; false -> 1 end end ). -file("src/escpos/image.gleam", 166). -spec dither_bayer( grayscale_image(), integer(), fun((integer(), integer()) -> integer()) ) -> printable_image(). dither_bayer(Image, Bias, Matrix) -> dither_map( Image, fun(Pixel, Row, Column) -> case Pixel > (Matrix(Row, Column) + Bias) of true -> 0; false -> 1 end end ). -file("src/escpos/image.gleam", 120). ?DOC(" Bayer dithering using a 2x2 matrix\n"). -spec dither_bayer2x2(grayscale_image(), integer()) -> printable_image(). dither_bayer2x2(Image, Bias) -> dither_bayer(Image, Bias, fun bayer_2x2_matrix/2). -file("src/escpos/image.gleam", 125). ?DOC(" Bayer dithering using a 4x4 matrix\n"). -spec dither_bayer4x4(grayscale_image(), integer()) -> printable_image(). dither_bayer4x4(Image, Bias) -> dither_bayer(Image, Bias, fun bayer_4x4_matrix/2).