-
Notifications
You must be signed in to change notification settings - Fork 3
/
ecaptcha_gif.erl
122 lines (108 loc) · 3.91 KB
/
ecaptcha_gif.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
%% @doc Basic GIF encoder
%%
%% [https://www.w3.org/Graphics/GIF/spec-gif89a.txt]
%% Originally based on [https://github.com/huacnlee/rucaptcha/tree/master/ext/rucaptcha]
-module(ecaptcha_gif).
-export([encode/4]).
%% For tests
-export([min_bits_to_fit/1]).
-spec encode(
binary(),
pos_integer(),
pos_integer(),
ecaptcha:color_name() | ecaptcha:color_rgb()
) -> iodata().
encode(Pixels, Width, Height, ColorName) when is_atom(ColorName) ->
encode(Pixels, Width, Height, ecaptcha_color:by_name(ColorName));
encode(Pixels, Width, Height, Color) when byte_size(Pixels) =:= (Width * Height) ->
Palette = ecaptcha_color:new_palette(Pixels, Color),
[
header(Width, Height, Palette),
encode_raster_data(Pixels, Palette),
trailer()
].
%% Header
header(Width, Height, Palette) ->
NumColors = ecaptcha_color:palette_size(Palette),
[
screen_descriptor(Width, Height, NumColors),
palette(Palette),
image_descriptor(Width, Height)
].
%% erlfmt-ignore
screen_descriptor(Width, Height, PaletteSize) ->
Bits = min_bits_to_fit(PaletteSize) - 1,
<<
"GIF89a",
Width:16/little, % Logical screen width
Height:16/little, % Logical screen height
1:1, % GCTFlag:1 - 1 - Global Color Table exists
Bits:3, % ColorResolution:3
0:1, % SortFlag:1 - 0 - not sorted
Bits:3, % GCTSize:3 - `2^(3+1)' - size of GCT
0, % BackgroundColor index in palette
0 % Aspect ratio
>>.
palette(Palette) ->
%% Palette size must be power of 2. If number of colors is not exactly power of 2, we fill
%% extra space with dumy color (white).
NumColors = ecaptcha_color:palette_size(Palette),
ColorsRGB = ecaptcha_color:palette_colors_by_frequency(Palette),
PaletteLenBits = min_bits_to_fit(NumColors),
% 2 ^ PaletteLenBits
PaletteSize = 1 bsl PaletteLenBits,
NumDummyColors = PaletteSize - NumColors,
White = ecaptcha_color:by_name(white),
PaletteAndDummy = ColorsRGB ++ lists:duplicate(NumDummyColors, White),
lists:map(fun ecaptcha_color:bin_3b/1, PaletteAndDummy).
min_bits_to_fit(Size) ->
if
Size =< 2 -> 1;
Size =< 4 -> 2;
Size =< 8 -> 3;
Size =< 16 -> 4;
Size =< 32 -> 5;
Size =< 64 -> 6;
Size =< 128 -> 7;
Size =< 256 -> 8;
true -> error(size_overflow)
end.
%% erlfmt-ignore
image_descriptor(Width, Height) ->
<<
",",
0:16/little, 0:16/little, % (x0, y0) - start of image
Width:16/little, Height:16/little, % (xN, yN) - end of image
0:1, % no local color table
0:1, % sequential order (not interlaced)
0:3, % reserved
0:3 % Local color table size
>>.
%% Body
encode_raster_data(Pixels, Palette) ->
PaletteSize = ecaptcha_color:palette_size(Palette),
PalettePixels = map_to_palette(Pixels, Palette),
CodeSize = max(min_bits_to_fit(PaletteSize), 2),
Compressed = ecaptcha_gif_lzw:compress(PalettePixels, CodeSize),
% LZW minimum code size
[CodeSize | encode_chunks(Compressed)].
map_to_palette(Pixels, Palette) ->
<<
<<(ecaptcha_color:palette_get_index(Pixel, Palette))>>
|| <<Pixel>> <= Pixels
>>.
encode_chunks(<<Chunk:255/binary, Tail/binary>>) ->
[255, Chunk | encode_chunks(Tail)];
encode_chunks(<<>>) ->
%Stream terminator
[0];
encode_chunks(Remaining) ->
[
byte_size(Remaining),
Remaining,
%Stream terminator
0
].
%% Trailer
trailer() ->
<<";">>.