Current section
Files
Jump to
Current section
Files
src/escpos@image.erl
-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, 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 around 600 dots width.\n"
" There is no validation so please check your image width beforehand.\n"
"\n"
" Images need to be supplied in PBM or PGM format. You can use tools like\n"
" Imagemagick to convert your image. `magick img.png img.pgm`\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import escpos/document\n"
" import escpos/image\n"
" import simplifile\n"
"\n"
" // PBM images are already monochrome — just parse and print\n"
" let assert Ok(pbm) = simplifile.read_bits(from: \"./logo.pbm\")\n"
" let assert Ok(img) = image.from_pbm(pbm)\n"
"\n"
" document.render([document.image(img), document.cut()])\n"
"\n"
" // PGM images are grayscale and need dithering first\n"
" let assert Ok(pgm) = simplifile.read_bits(from: \"./photo.pgm\")\n"
" let assert Ok(img) = image.from_pgm(pgm)\n"
" let img = image.dither_ign(img)\n"
"\n"
" document.render([document.image(img), document.cut()])\n"
" ```\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", 55).
?DOC(false).
-spec pixels(printable_image()) -> bitstring().
pixels(Image) ->
erlang:element(4, Image).
-file("src/escpos/image.gleam", 60).
?DOC(false).
-spec width(printable_image()) -> integer().
width(Image) ->
erlang:element(2, Image).
-file("src/escpos/image.gleam", 65).
?DOC(false).
-spec height(printable_image()) -> integer().
height(Image) ->
erlang:element(3, Image).
-file("src/escpos/image.gleam", 97).
-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", 70).
?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", 82).
?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", 153).
-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", 164).
-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", 212).
-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
<<Pixel, Rest/bitstring>> ->
Bit = Fun(Pixel, Row, Column),
Next_column = Column + 1,
case Next_column =:= Width of
true ->
dither_loop(
Rest,
Width,
Row + 1,
0,
<<Acc/bitstring, Bit:1>>,
Fun
);
false ->
dither_loop(
Rest,
Width,
Row,
Next_column,
<<Acc/bitstring, Bit:1>>,
Fun
)
end;
_ ->
Acc
end.
-file("src/escpos/image.gleam", 247).
-spec do_byte_align_rows(bitstring(), integer(), integer(), bitstring()) -> bitstring().
do_byte_align_rows(Pixels, Width, Padding, Acc) ->
case Pixels of
<<Row:Width/bitstring, Rest/bitstring>> ->
Padded_row = <<Row:(lists:max([(Width), 0]))/bitstring,
0:(lists:max([(Padding), 0]))>>,
do_byte_align_rows(
Rest,
Width,
Padding,
<<Acc/bitstring, Padded_row/bitstring>>
);
_ ->
Acc
end.
-file("src/escpos/image.gleam", 242).
-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", 202).
-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", 121).
?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", 190).
-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", 144).
?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", 149).
?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).