-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpdf.lua
More file actions
3353 lines (2958 loc) · 110 KB
/
Copy pathpdf.lua
File metadata and controls
3353 lines (2958 loc) · 110 KB
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Pure Lua PDF Library
-- Simple and efficient PDF generation without external dependencies
local PDF = {}
PDF.__index = PDF
local Utils = {}
local Helper = {}
local QuickRef = {}
local unpack_values = table.unpack or unpack
-- Basic scalar and byte helpers used throughout layout, parsing, and serialization.
local function mm_to_pt(mm)
return mm * 2.83464567 -- 1 mm = 2.83464567 points
end
local function normalize_rgb(r, g, b)
if r > 1 or g > 1 or b > 1 then
return r / 255, g / 255, b / 255
end
return r, g, b
end
local function read_u32_be(data, pos)
local a, b, c, d = string.byte(data, pos, pos + 3)
return ((a * 256 + b) * 256 + c) * 256 + d
end
local function read_u16_be(data, pos)
local a, b = string.byte(data, pos, pos + 1)
return a * 256 + b
end
local function hex_encode(data)
return (data:gsub(".", function(byte)
return string.format("%02X", string.byte(byte))
end))
end
local function decode_utf8(text)
local codepoints = {}
local i = 1
while i <= #text do
local b1 = string.byte(text, i)
local codepoint
local count
if b1 < 0x80 then
codepoint = b1
count = 1
elseif b1 >= 0xC2 and b1 <= 0xDF then
local b2 = string.byte(text, i + 1)
if not b2 or b2 < 0x80 or b2 > 0xBF then
return nil
end
codepoint = (b1 - 0xC0) * 0x40 + (b2 - 0x80)
count = 2
elseif b1 >= 0xE0 and b1 <= 0xEF then
local b2 = string.byte(text, i + 1)
local b3 = string.byte(text, i + 2)
if not b2 or not b3 or b2 < 0x80 or b2 > 0xBF or b3 < 0x80 or b3 > 0xBF then
return nil
end
if (b1 == 0xE0 and b2 < 0xA0) or (b1 == 0xED and b2 > 0x9F) then
return nil
end
codepoint = (b1 - 0xE0) * 0x1000 + (b2 - 0x80) * 0x40 + (b3 - 0x80)
count = 3
elseif b1 >= 0xF0 and b1 <= 0xF4 then
local b2 = string.byte(text, i + 1)
local b3 = string.byte(text, i + 2)
local b4 = string.byte(text, i + 3)
if not b2 or not b3 or not b4 or
b2 < 0x80 or b2 > 0xBF or
b3 < 0x80 or b3 > 0xBF or
b4 < 0x80 or b4 > 0xBF then
return nil
end
if (b1 == 0xF0 and b2 < 0x90) or (b1 == 0xF4 and b2 > 0x8F) then
return nil
end
codepoint = (b1 - 0xF0) * 0x40000 + (b2 - 0x80) * 0x1000 + (b3 - 0x80) * 0x40 + (b4 - 0x80)
count = 4
else
return nil
end
codepoints[#codepoints + 1] = codepoint
i = i + count
end
return codepoints
end
local function encode_utf16be(codepoints)
local parts = {string.char(0xFE, 0xFF)}
for i = 1, #codepoints do
local codepoint = codepoints[i]
if codepoint <= 0xFFFF then
parts[#parts + 1] = string.char(
math.floor(codepoint / 256) % 256,
codepoint % 256
)
else
codepoint = codepoint - 0x10000
local high = 0xD800 + math.floor(codepoint / 0x400)
local low = 0xDC00 + (codepoint % 0x400)
parts[#parts + 1] = string.char(
math.floor(high / 256) % 256,
high % 256,
math.floor(low / 256) % 256,
low % 256
)
end
end
return table.concat(parts)
end
local WINANSI_EXTRA_BYTES = {
[0x20AC] = 0x80,
[0x201A] = 0x82,
[0x0192] = 0x83,
[0x201E] = 0x84,
[0x2026] = 0x85,
[0x2020] = 0x86,
[0x2021] = 0x87,
[0x02C6] = 0x88,
[0x2030] = 0x89,
[0x0160] = 0x8A,
[0x2039] = 0x8B,
[0x0152] = 0x8C,
[0x017D] = 0x8E,
[0x2018] = 0x91,
[0x2019] = 0x92,
[0x201C] = 0x93,
[0x201D] = 0x94,
[0x2022] = 0x95,
[0x2013] = 0x96,
[0x2014] = 0x97,
[0x02DC] = 0x98,
[0x2122] = 0x99,
[0x0161] = 0x9A,
[0x203A] = 0x9B,
[0x0153] = 0x9C,
[0x017E] = 0x9E,
[0x0178] = 0x9F,
}
local function split_utf8_chars(text)
local chars = {}
local i = 1
while i <= #text do
local b1 = string.byte(text, i)
local length = 1
if b1 >= 0xC2 and b1 <= 0xDF then
length = 2
elseif b1 >= 0xE0 and b1 <= 0xEF then
length = 3
elseif b1 >= 0xF0 and b1 <= 0xF4 then
length = 4
end
chars[#chars + 1] = text:sub(i, i + length - 1)
i = i + length
end
return chars
end
local function reverse_bits(value, width)
local reversed = 0
for _ = 1, width do
reversed = reversed * 2 + (value % 2)
value = math.floor(value / 2)
end
return reversed
end
-- Canonical Huffman decode-table builder used by the PNG inflate implementation.
local function build_huffman(lengths)
local max_len = 0
for i = 1, #lengths do
if lengths[i] > max_len then
max_len = lengths[i]
end
end
local counts = {}
for len = 0, max_len do
counts[len] = 0
end
for i = 1, #lengths do
counts[lengths[i]] = counts[lengths[i]] + 1
end
local next_code = {}
local code = 0
counts[0] = counts[0] or 0
for bits = 1, max_len do
code = (code + (counts[bits - 1] or 0)) * 2
next_code[bits] = code
end
local lookup = {}
for len = 1, max_len do
lookup[len] = {}
end
for symbol = 0, #lengths - 1 do
local len = lengths[symbol + 1]
if len > 0 then
local assigned = next_code[len]
next_code[len] = assigned + 1
lookup[len][reverse_bits(assigned, len)] = symbol
end
end
return {
lookup = lookup,
max_len = max_len,
}
end
-- Canonical Huffman code builder used by the built-in fixed-Huffman deflate encoder.
local function build_huffman_codes(lengths)
local max_len = 0
for i = 1, #lengths do
if lengths[i] > max_len then
max_len = lengths[i]
end
end
local counts = {}
for len = 0, max_len do
counts[len] = 0
end
for i = 1, #lengths do
counts[lengths[i]] = counts[lengths[i]] + 1
end
local next_code = {}
local code = 0
counts[0] = counts[0] or 0
for bits = 1, max_len do
code = (code + (counts[bits - 1] or 0)) * 2
next_code[bits] = code
end
local codes = {}
for symbol = 0, #lengths - 1 do
local len = lengths[symbol + 1]
if len > 0 then
local assigned = next_code[len]
next_code[len] = assigned + 1
codes[symbol] = {
code = reverse_bits(assigned, len),
bit_length = len,
}
end
end
return codes
end
local FIXED_LITERAL_LENGTHS = {}
for i = 0, 287 do
if i <= 143 then
FIXED_LITERAL_LENGTHS[i + 1] = 8
elseif i <= 255 then
FIXED_LITERAL_LENGTHS[i + 1] = 9
elseif i <= 279 then
FIXED_LITERAL_LENGTHS[i + 1] = 7
else
FIXED_LITERAL_LENGTHS[i + 1] = 8
end
end
local FIXED_DISTANCE_LENGTHS = {}
for i = 0, 31 do
FIXED_DISTANCE_LENGTHS[i + 1] = 5
end
local FIXED_LITERAL_CODES = build_huffman_codes(FIXED_LITERAL_LENGTHS)
local FIXED_DISTANCE_CODES = build_huffman_codes(FIXED_DISTANCE_LENGTHS)
local LENGTH_BASES = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}
local LENGTH_EXTRAS = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}
local DISTANCE_BASES = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}
local DISTANCE_EXTRAS = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}
local DEFLATE_WINDOW_SIZE = 32768
local DEFLATE_MIN_MATCH = 3
local DEFLATE_MAX_MATCH = 258
local DEFLATE_MAX_CHAIN = 10
local DEFLATE_HASH_SIZE = 16384
-- Adler-32 checksum for the zlib wrapper around compressed PDF streams.
local function adler32(data)
local s1 = 1
local s2 = 0
for i = 1, #data do
s1 = s1 + string.byte(data, i)
if s1 >= 65521 then
s1 = s1 - 65521
end
s2 = s2 + s1
if s2 >= 65521 then
s2 = s2 % 65521
end
end
return s2 * 65536 + s1
end
-- Pack a 32-bit integer as big-endian binary.
local function pack_u32_be(value)
local b1 = math.floor(value / 16777216) % 256
local b2 = math.floor(value / 65536) % 256
local b3 = math.floor(value / 256) % 256
local b4 = value % 256
return string.char(b1, b2, b3, b4)
end
-- Lightweight bit writer for LSB-first deflate payload emission.
local function make_bit_writer()
return {
chunks = {},
bytes = {},
byte_count = 0,
bit_buffer = 0,
bit_count = 0,
}
end
local function writer_append_byte(writer, byte)
writer.byte_count = writer.byte_count + 1
writer.bytes[writer.byte_count] = string.char(byte)
if writer.byte_count >= 4096 then
writer.chunks[#writer.chunks + 1] = table.concat(writer.bytes)
writer.bytes = {}
writer.byte_count = 0
end
end
-- Append a variable-width value to the bit stream one bit at a time.
local function writer_write_bits(writer, value, bit_length)
local bit_buffer = writer.bit_buffer
local bit_count = writer.bit_count
for _ = 1, bit_length do
if value % 2 == 1 then
bit_buffer = bit_buffer + (2 ^ bit_count)
end
value = math.floor(value / 2)
bit_count = bit_count + 1
if bit_count == 8 then
writer_append_byte(writer, bit_buffer)
bit_buffer = 0
bit_count = 0
end
end
writer.bit_buffer = bit_buffer
writer.bit_count = bit_count
end
-- Flush any pending bits and concatenate buffered byte chunks.
local function finish_bit_writer(writer)
if writer.bit_count > 0 then
writer_append_byte(writer, writer.bit_buffer)
end
if writer.byte_count > 0 then
writer.chunks[#writer.chunks + 1] = table.concat(writer.bytes)
end
return table.concat(writer.chunks)
end
-- Map a match length to the corresponding deflate symbol and extra bits.
local function get_length_code(length)
for i = 1, #LENGTH_BASES do
local base = LENGTH_BASES[i]
local extra_bits = LENGTH_EXTRAS[i]
local max_length = base + ((2 ^ extra_bits) - 1)
if length <= max_length then
return 256 + i, extra_bits, length - base
end
end
return 285, 0, 0
end
-- Map a back-reference distance to the corresponding deflate symbol and extra bits.
local function get_distance_code(distance)
for i = 1, #DISTANCE_BASES do
local base = DISTANCE_BASES[i]
local extra_bits = DISTANCE_EXTRAS[i]
local max_distance = base + ((2 ^ extra_bits) - 1)
if distance <= max_distance then
return i - 1, extra_bits, distance - base
end
end
return 29, 13, distance - DISTANCE_BASES[30]
end
-- Hash a 3-byte sliding window so repeated substrings can be located cheaply.
local function deflate_hash(data, pos)
local b1, b2, b3 = string.byte(data, pos, pos + 2)
return (((b1 or 0) * 251 + (b2 or 0)) * 251 + (b3 or 0)) % DEFLATE_HASH_SIZE + 1
end
-- Remember recent positions for later match searches within the deflate window.
local function deflate_store_position(hash_table, data, pos, data_len)
if pos > data_len - 2 then
return
end
local hash = deflate_hash(data, pos)
local bucket = hash_table[hash]
if not bucket then
hash_table[hash] = {pos}
return
end
while #bucket > 0 and pos - bucket[1] > DEFLATE_WINDOW_SIZE do
table.remove(bucket, 1)
end
if #bucket >= DEFLATE_MAX_CHAIN then
table.remove(bucket, 1)
end
bucket[#bucket + 1] = pos
end
-- Search recent history for the best match at the current position.
local function deflate_find_match(hash_table, data, pos, data_len)
if pos > data_len - 2 then
return 0, 0
end
local bucket = hash_table[deflate_hash(data, pos)]
if not bucket then
return 0, 0
end
local best_length = 0
local best_distance = 0
local max_length = math.min(DEFLATE_MAX_MATCH, data_len - pos + 1)
for i = #bucket, 1, -1 do
local candidate = bucket[i]
local distance = pos - candidate
if distance > 0 and distance <= DEFLATE_WINDOW_SIZE then
local length = 0
while length < max_length and
string.byte(data, candidate + length) == string.byte(data, pos + length) do
length = length + 1
end
if length >= DEFLATE_MIN_MATCH and length > best_length then
best_length = length
best_distance = distance
if length == DEFLATE_MAX_MATCH then
break
end
end
end
end
return best_length, best_distance
end
-- Compress stream payloads with a compact fixed-Huffman deflate implementation.
local function compress_flate(data)
if data == "" then
return string.char(0x78, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01)
end
local writer = make_bit_writer()
local hash_table = {}
local data_len = #data
local pos = 1
-- Single final block using fixed Huffman codes.
writer_write_bits(writer, 1, 1)
writer_write_bits(writer, 1, 2)
while pos <= data_len do
local match_length, match_distance = deflate_find_match(hash_table, data, pos, data_len)
if match_length >= DEFLATE_MIN_MATCH then
local length_symbol, length_extra_bits, length_extra = get_length_code(match_length)
local distance_symbol, distance_extra_bits, distance_extra = get_distance_code(match_distance)
local literal_code = FIXED_LITERAL_CODES[length_symbol]
local distance_code = FIXED_DISTANCE_CODES[distance_symbol]
writer_write_bits(writer, literal_code.code, literal_code.bit_length)
if length_extra_bits > 0 then
writer_write_bits(writer, length_extra, length_extra_bits)
end
writer_write_bits(writer, distance_code.code, distance_code.bit_length)
if distance_extra_bits > 0 then
writer_write_bits(writer, distance_extra, distance_extra_bits)
end
for offset = 0, match_length - 1 do
deflate_store_position(hash_table, data, pos + offset, data_len)
end
pos = pos + match_length
else
local literal = string.byte(data, pos)
local literal_code = FIXED_LITERAL_CODES[literal]
writer_write_bits(writer, literal_code.code, literal_code.bit_length)
deflate_store_position(hash_table, data, pos, data_len)
pos = pos + 1
end
end
local end_code = FIXED_LITERAL_CODES[256]
writer_write_bits(writer, end_code.code, end_code.bit_length)
return string.char(0x78, 0x01) .. finish_bit_writer(writer) .. pack_u32_be(adler32(data))
end
-- Build a PDF stream object and only enable FlateDecode when it saves space.
local function build_stream_object(dictionary_entries, stream_data, enable_compression)
local dict = dictionary_entries or ""
if dict ~= "" and not dict:match("%s$") then
dict = dict .. " "
end
local payload = stream_data
local filter_part = ""
if enable_compression and #stream_data > 32 then
local compressed = compress_flate(stream_data)
if #compressed < #stream_data then
payload = compressed
filter_part = "/Filter /FlateDecode "
end
end
return string.format("<<%s%s/Length %d>>\nstream\n%s\nendstream", dict, filter_part, #payload, payload)
end
-- Inflate zlib-compressed PNG payloads without any external dependency.
local function inflate_zlib(data)
if #data < 2 then
error("Invalid zlib stream")
end
local cmf = string.byte(data, 1)
local flg = string.byte(data, 2)
if cmf % 16 ~= 8 then
error("Unsupported zlib compression method")
end
if ((cmf * 256) + flg) % 31 ~= 0 then
error("Invalid zlib header checksum")
end
if math.floor(flg / 32) % 2 == 1 then
error("Preset zlib dictionaries are not supported")
end
local bit_pos = 17
local function read_bits(count)
local value = 0
local factor = 1
for _ = 1, count do
local byte_index = math.floor((bit_pos - 1) / 8) + 1
local byte = string.byte(data, byte_index)
if not byte then
error("Unexpected end of deflate stream")
end
local bit_index = (bit_pos - 1) % 8
local bit = math.floor(byte / (2 ^ bit_index)) % 2
value = value + bit * factor
factor = factor * 2
bit_pos = bit_pos + 1
end
return value
end
local function align_to_byte()
local mod = (bit_pos - 1) % 8
if mod ~= 0 then
bit_pos = bit_pos + (8 - mod)
end
end
local function decode_symbol(tree)
local code = 0
local factor = 1
for len = 1, tree.max_len do
local bit = read_bits(1)
code = code + bit * factor
local symbol = tree.lookup[len][code]
if symbol ~= nil then
return symbol
end
factor = factor * 2
end
error("Invalid huffman code")
end
local fixed_literal_lengths = {}
for i = 0, 287 do
if i <= 143 then
fixed_literal_lengths[i + 1] = 8
elseif i <= 255 then
fixed_literal_lengths[i + 1] = 9
elseif i <= 279 then
fixed_literal_lengths[i + 1] = 7
else
fixed_literal_lengths[i + 1] = 8
end
end
local fixed_distance_lengths = {}
for i = 1, 32 do
fixed_distance_lengths[i] = 5
end
local fixed_literal_tree = build_huffman(fixed_literal_lengths)
local fixed_distance_tree = build_huffman(fixed_distance_lengths)
local length_bases = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}
local length_extras = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}
local distance_bases = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}
local distance_extras = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}
local out = {}
local out_len = 0
local function append_byte(byte)
out_len = out_len + 1
out[out_len] = byte
end
local function append_bytes(bytes)
for i = 1, #bytes do
append_byte(string.byte(bytes, i))
end
end
local last_block = 0
while last_block == 0 do
last_block = read_bits(1)
local block_type = read_bits(2)
local literal_tree
local distance_tree
if block_type == 0 then
align_to_byte()
local byte_index = math.floor((bit_pos - 1) / 8) + 1
local len1, len2, nlen1, nlen2 = string.byte(data, byte_index, byte_index + 3)
local len = len1 + len2 * 256
local nlen = nlen1 + nlen2 * 256
if len ~= 65535 - nlen then
error("Invalid uncompressed deflate block")
end
bit_pos = bit_pos + 32
local start = math.floor((bit_pos - 1) / 8) + 1
append_bytes(data:sub(start, start + len - 1))
bit_pos = bit_pos + len * 8
else
if block_type == 1 then
literal_tree = fixed_literal_tree
distance_tree = fixed_distance_tree
elseif block_type == 2 then
local hlit = read_bits(5) + 257
local hdist = read_bits(5) + 1
local hclen = read_bits(4) + 4
local code_length_order = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}
local code_length_lengths = {}
for i = 1, 19 do
code_length_lengths[i] = 0
end
for i = 1, hclen do
code_length_lengths[code_length_order[i] + 1] = read_bits(3)
end
local code_length_tree = build_huffman(code_length_lengths)
local lengths = {}
local target = hlit + hdist
local index = 1
while index <= target do
local symbol = decode_symbol(code_length_tree)
if symbol <= 15 then
lengths[index] = symbol
index = index + 1
elseif symbol == 16 then
local repeat_count = read_bits(2) + 3
local previous = lengths[index - 1] or 0
for _ = 1, repeat_count do
lengths[index] = previous
index = index + 1
end
elseif symbol == 17 then
local repeat_count = read_bits(3) + 3
for _ = 1, repeat_count do
lengths[index] = 0
index = index + 1
end
elseif symbol == 18 then
local repeat_count = read_bits(7) + 11
for _ = 1, repeat_count do
lengths[index] = 0
index = index + 1
end
else
error("Invalid code length symbol")
end
end
local literal_lengths = {}
local distance_lengths = {}
for i = 1, hlit do
literal_lengths[i] = lengths[i] or 0
end
for i = 1, hdist do
distance_lengths[i] = lengths[hlit + i] or 0
end
literal_tree = build_huffman(literal_lengths)
distance_tree = build_huffman(distance_lengths)
else
error("Reserved deflate block type")
end
while true do
local symbol = decode_symbol(literal_tree)
if symbol < 256 then
append_byte(symbol)
elseif symbol == 256 then
break
else
local len_index = symbol - 257 + 1
local length = length_bases[len_index] + read_bits(length_extras[len_index])
local dist_symbol = decode_symbol(distance_tree)
local distance = distance_bases[dist_symbol + 1] + read_bits(distance_extras[dist_symbol + 1])
for _ = 1, length do
append_byte(out[out_len - distance + 1])
end
end
end
end
end
local chunks = {}
local chunk = {}
for i = 1, out_len do
chunk[#chunk + 1] = string.char(out[i])
if #chunk >= 4096 then
chunks[#chunks + 1] = table.concat(chunk)
chunk = {}
end
end
if #chunk > 0 then
chunks[#chunks + 1] = table.concat(chunk)
end
return table.concat(chunks)
end
-- Normalize PNG samples of varying bit depths into 8-bit channel values.
local function scale_sample(sample, bit_depth)
if bit_depth == 8 then
return sample
elseif bit_depth == 16 then
return math.floor(sample / 257)
end
local max = (2 ^ bit_depth) - 1
if max == 0 then
return 0
end
return math.floor((sample * 255) / max + 0.5)
end
-- Expand packed PNG samples into a flat sample array for one decoded row.
local function unpack_samples(row, width, samples_per_pixel, bit_depth)
local samples = {}
local index = 1
if bit_depth == 8 then
for i = 1, width * samples_per_pixel do
samples[i] = string.byte(row, index)
index = index + 1
end
elseif bit_depth == 16 then
for i = 1, width * samples_per_pixel do
samples[i] = read_u16_be(row, index)
index = index + 2
end
else
local samples_per_byte = 8 / bit_depth
local mask = (2 ^ bit_depth) - 1
local out_index = 1
for i = 1, #row do
local byte = string.byte(row, i)
for offset = samples_per_byte - 1, 0, -1 do
if out_index > width * samples_per_pixel then
break
end
samples[out_index] = math.floor(byte / (2 ^ (offset * bit_depth))) % (mask + 1)
out_index = out_index + 1
end
end
end
return samples
end
-- Reverse PNG row filters so raw pixel samples can be read pass by pass.
local function unfilter_scanlines(data, width, height, bits_per_pixel)
local bpp = math.max(1, math.ceil(bits_per_pixel / 8))
local row_bytes = math.ceil(width * bits_per_pixel / 8)
local rows = {}
local pos = 1
local previous = nil
for _ = 1, height do
local filter_type = string.byte(data, pos)
local row = {string.byte(data, pos + 1, pos + row_bytes)}
pos = pos + 1 + row_bytes
if filter_type == 1 then
for i = 1, #row do
local left = i > bpp and row[i - bpp] or 0
row[i] = (row[i] + left) % 256
end
elseif filter_type == 2 then
for i = 1, #row do
local up = previous and previous[i] or 0
row[i] = (row[i] + up) % 256
end
elseif filter_type == 3 then
for i = 1, #row do
local left = i > bpp and row[i - bpp] or 0
local up = previous and previous[i] or 0
row[i] = (row[i] + math.floor((left + up) / 2)) % 256
end
elseif filter_type == 4 then
local function paeth(a, b, c)
local p = a + b - c
local pa = math.abs(p - a)
local pb = math.abs(p - b)
local pc = math.abs(p - c)
if pa <= pb and pa <= pc then
return a
elseif pb <= pc then
return b
end
return c
end
for i = 1, #row do
local left = i > bpp and row[i - bpp] or 0
local up = previous and previous[i] or 0
local up_left = (previous and i > bpp) and previous[i - bpp] or 0
row[i] = (row[i] + paeth(left, up, up_left)) % 256
end
elseif filter_type ~= 0 then
error("Unsupported PNG filter type: " .. tostring(filter_type))
end
previous = row
rows[#rows + 1] = string.char(unpack_values(row))
end
return rows
end
-- Decode parsed PNG state into RGB bytes plus an optional alpha plane.
local function decode_png_pixels(png)
local channel_count_by_color = {
[0] = 1,
[2] = 3,
[3] = 1,
[4] = 2,
[6] = 4,
}
local samples_per_pixel = channel_count_by_color[png.color_type]
if not samples_per_pixel then
error("Unsupported PNG color type: " .. tostring(png.color_type))
end
local bits_per_pixel = samples_per_pixel * png.bit_depth
local width = png.width
local height = png.height
local rgb = {}
local alpha = {}
local has_alpha = png.color_type == 4 or png.color_type == 6 or png.trns ~= nil
local function set_pixel(x, y, r, g, b, a)
local pixel_index = y * width + x
local rgb_index = pixel_index * 3 + 1
rgb[rgb_index] = string.char(r)
rgb[rgb_index + 1] = string.char(g)
rgb[rgb_index + 2] = string.char(b)
if has_alpha then
alpha[pixel_index + 1] = string.char(a or 255)
end
end
local function process_pass(pass_data, pass_width, pass_height, start_x, start_y, step_x, step_y)
if pass_width == 0 or pass_height == 0 then
return
end
local rows = unfilter_scanlines(pass_data, pass_width, pass_height, bits_per_pixel)
for row_index = 1, #rows do
local samples = unpack_samples(rows[row_index], pass_width, samples_per_pixel, png.bit_depth)
local sample_index = 1
for column = 0, pass_width - 1 do
local dest_x = start_x + column * step_x
local dest_y = start_y + (row_index - 1) * step_y
local r, g, b, a = 0, 0, 0, 255
if png.color_type == 0 then
local gray = samples[sample_index]
sample_index = sample_index + 1
local gray8 = scale_sample(gray, png.bit_depth)
r, g, b = gray8, gray8, gray8
if png.trns and gray == png.trns.gray then
a = 0
end
elseif png.color_type == 2 then
local raw_r = samples[sample_index]
local raw_g = samples[sample_index + 1]
local raw_b = samples[sample_index + 2]
sample_index = sample_index + 3
r = scale_sample(raw_r, png.bit_depth)
g = scale_sample(raw_g, png.bit_depth)
b = scale_sample(raw_b, png.bit_depth)
if png.trns and raw_r == png.trns.r and raw_g == png.trns.g and raw_b == png.trns.b then
a = 0
end
elseif png.color_type == 3 then
local idx = samples[sample_index]
sample_index = sample_index + 1
local palette_offset = idx * 3 + 1
r = string.byte(png.palette, palette_offset) or 0
g = string.byte(png.palette, palette_offset + 1) or 0
b = string.byte(png.palette, palette_offset + 2) or 0
if png.trns then
a = string.byte(png.trns, idx + 1) or 255
end
elseif png.color_type == 4 then
local gray = samples[sample_index]
local alpha_sample = samples[sample_index + 1]
sample_index = sample_index + 2
local gray8 = scale_sample(gray, png.bit_depth)
r, g, b = gray8, gray8, gray8
a = scale_sample(alpha_sample, png.bit_depth)
elseif png.color_type == 6 then
r = scale_sample(samples[sample_index], png.bit_depth)
g = scale_sample(samples[sample_index + 1], png.bit_depth)
b = scale_sample(samples[sample_index + 2], png.bit_depth)
a = scale_sample(samples[sample_index + 3], png.bit_depth)
sample_index = sample_index + 4
end
set_pixel(dest_x, dest_y, r, g, b, a)
end
end
end
if png.interlace == 0 then
process_pass(png.raw_data, width, height, 0, 0, 1, 1)
elseif png.interlace == 1 then
local passes = {
{0, 0, 8, 8},
{4, 0, 8, 8},
{0, 4, 4, 8},
{2, 0, 4, 4},
{0, 2, 2, 4},
{1, 0, 2, 2},
{0, 1, 1, 2},
}
local pos = 1
for _, pass in ipairs(passes) do
local start_x, start_y, step_x, step_y = pass[1], pass[2], pass[3], pass[4]
local pass_width = width <= start_x and 0 or math.floor((width - start_x + step_x - 1) / step_x)
local pass_height = height <= start_y and 0 or math.floor((height - start_y + step_y - 1) / step_y)
if pass_width > 0 and pass_height > 0 then
local row_bytes = math.ceil(pass_width * bits_per_pixel / 8)
local pass_size = pass_height * (1 + row_bytes)
process_pass(png.raw_data:sub(pos, pos + pass_size - 1), pass_width, pass_height, start_x, start_y, step_x, step_y)
pos = pos + pass_size
end
end
else
error("Unsupported PNG interlace method")
end
local rgb_data = table.concat(rgb)
local alpha_data = has_alpha and table.concat(alpha) or nil
return rgb_data, alpha_data
end
-- Parse PNG chunks and collect the metadata needed for raster decoding.
local function parse_png(data)
local signature = "\137PNG\r\n\26\n"
if data:sub(1, 8) ~= signature then
error("Not a PNG file")