1 if not modules then modules = { } end modules ['grph-img'] = {
2 version = 1.001,
3 comment = "companion to grph-inc.mkiv",
4 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5 copyright = "PRAGMA ADE / ConTeXt Development Team",
6 license = "see context related readme files"
7}
8
9
10
11
12
13
14local lower, strip = string.lower, string.strip
15local round = math.round
16local concat = table.concat
17local suffixonly = file.suffix
18
19local newreader = io.newreader
20local setmetatableindex = table.setmetatableindex
21local setmetatablecall = table.setmetatablecall
22
23local graphics = graphics or { }
24local identifiers = { }
25graphics.identifiers = identifiers
26
27if not resolvers.loadbinfile then
28 function resolvers.loadbinfile(filename)
29 local data = io.loaddata(filename)
30 return type(data) == "string", data
31 end
32end
33
34local function checkedmethod(filename,method)
35 if method ~= "string" then
36 local found, data = resolvers.loadbinfile(filename)
37 return data, "string"
38 else
39 return filename, method
40 end
41end
42
43do
44
45 local colorspaces = {
46 [1] = 1,
47 [3] = 2,
48 [4] = 3,
49 }
50
51 local tags = {
52 [0xC0] = { name = "SOF0", },
53 [0xC1] = { name = "SOF1", },
54 [0xC2] = { name = "SOF2", },
55 [0xC3] = { name = "SOF3", supported = false },
56
57 [0xC5] = { name = "SOF5", supported = false },
58 [0xC6] = { name = "SOF6", supported = false },
59 [0xC7] = { name = "SOF7", supported = false },
60
61 [0xC8] = { name = "JPG", },
62 [0xC9] = { name = "SOF9", },
63 [0xCA] = { name = "SOF10", supported = false },
64 [0xCB] = { name = "SOF11", supported = false },
65
66 [0xCD] = { name = "SOF13", supported = false },
67 [0xCE] = { name = "SOF14", supported = false },
68 [0xCF] = { name = "SOF15", supported = false },
69
70 [0xC4] = { name = "DHT" },
71
72 [0xCC] = { name = "DAC" },
73
74 [0xD0] = { name = "RST0", zerolength = true },
75 [0xD1] = { name = "RST1", zerolength = true },
76 [0xD2] = { name = "RST2", zerolength = true },
77 [0xD3] = { name = "RST3", zerolength = true },
78 [0xD4] = { name = "RST4", zerolength = true },
79 [0xD5] = { name = "RST5", zerolength = true },
80 [0xD6] = { name = "RST6", zerolength = true },
81 [0xD7] = { name = "RST7", zerolength = true },
82
83 [0xD8] = { name = "SOI", zerolength = true },
84 [0xD9] = { name = "EOI", zerolength = true },
85 [0xDA] = { name = "SOS" },
86 [0xDB] = { name = "DQT" },
87 [0xDC] = { name = "DNL" },
88 [0xDD] = { name = "DRI" },
89 [0xDE] = { name = "DHP" },
90 [0xDF] = { name = "EXP" },
91
92 [0xE0] = { name = "APP0" },
93 [0xE1] = { name = "APP1" },
94 [0xE2] = { name = "APP2" },
95 [0xE3] = { name = "APP3" },
96 [0xE4] = { name = "APP4" },
97 [0xE5] = { name = "APP5" },
98 [0xE6] = { name = "APP6" },
99 [0xE7] = { name = "APP7" },
100 [0xE8] = { name = "APP8" },
101 [0xE9] = { name = "APP9" },
102 [0xEA] = { name = "APP10" },
103 [0xEB] = { name = "APP11" },
104 [0xEC] = { name = "APP12" },
105 [0xED] = { name = "APP13" },
106 [0xEE] = { name = "APP14" },
107 [0xEF] = { name = "APP15" },
108
109 [0xF0] = { name = "JPG0" },
110 [0xFD] = { name = "JPG13" },
111 [0xFE] = { name = "COM" },
112
113 [0x01] = { name = "TEM", zerolength = true },
114 }
115
116 setmetatableindex(tags, function(t,k)
117
118 local v = "tag " .. k
119 t[k] = v
120 return v
121 end)
122
123
124
125
126 local function read_APP1_Exif(f, xres, yres, orientation)
127 local position = false
128 local littleendian = false
129
130 while true do
131 position = f:getposition()
132 local b = f:readbyte()
133 if b == 0 then
134
135 elseif b == 0x4D and f:readbyte() == 0x4D then
136
137 break
138 elseif b == 0x49 and f:readbyte() == 0x49 then
139
140 littleendian = true
141 break
142 else
143
144 return xres, yres, orientation
145 end
146 end
147
148 local version = littleendian and f:readcardinal2le() or f:readcardinal2()
149 if version ~= 42 then
150 return xres, yres, orientation
151 end
152
153 local offset = littleendian and f:readcardinal4le() or f:readcardinal4()
154 if not offset then
155 return xres, yres, orientation
156 end
157 f:setposition(position + offset)
158 local entries = littleendian and f:readcardinal2le() or f:readcardinal2()
159 if not entries or entries == 0 then
160 return xres, yres, orientation
161 end
162 local x_res, y_res, x_res_ms, y_res_ms, x_temp, y_temp
163 local res_unit, res_unit_ms
164 for i=1,entries do
165 local tag = littleendian and f:readcardinal2le() or f:readcardinal2()
166 local kind = littleendian and f:readcardinal2le() or f:readcardinal2()
167 local size = littleendian and f:readcardinal4le() or f:readcardinal4()
168 local value = 0
169 local num = 0
170 local den = 0
171 if kind == 1 or kind == 7 then
172 value = f:readbyte()
173 f:skip(3)
174 elseif kind == 3 or kind == 8 then
175 value = littleendian and f:readcardinal2le() or f:readcardinal2()
176 f:skip(2)
177 elseif kind == 4 or kind == 9 then
178 value = littleendian and f:readcardinal4le() or f:readcardinal4()
179 elseif kind == 5 or kind == 10 then
180 local offset = littleendian and f:readcardinal4le() or f:readcardinal4()
181 local saved = f:getposition()
182 f:setposition(position+offset)
183 num = littleendian and f:readcardinal4le() or f:readcardinal4()
184 den = littleendian and f:readcardinal4le() or f:readcardinal4()
185 f:setposition(saved)
186 else
187 f:skip(4)
188 end
189 if tag == 274 then
190 orientation = value
191 elseif tag == 282 then
192 if den ~= 0 then
193 x_res = num/den
194 end
195 elseif tag == 283 then
196 if den ~= 0 then
197 y_res = num/den
198 end
199 elseif tag == 296 then
200 if value == 2 then
201 res_unit = 1
202 elseif value == 3 then
203 res_unit = 2.54
204 end
205 elseif tag == 0x5110 then
206 res_unit_ms = value == 1
207 elseif tag == 0x5111 then
208 x_res_ms = value
209 elseif tag == 0x5112 then
210 y_res_ms = value
211 end
212 end
213 if x_res and y_res and res_unit and res_unit > 0 then
214 x_temp = round(x_res * res_unit)
215 y_temp = round(y_res * res_unit)
216 elseif x_res_ms and y_res_ms and res_unit_ms then
217 x_temp = round(x_res_ms * 0.0254)
218 y_temp = round(y_res_ms * 0.0254)
219 end
220 if x_temp and a_temp and x_temp > 0 and y_temp > 0 then
221 if (x_temp ~= x_res or y_temp ~= y_res) and x_res ~= 0 and y_res ~= 0 then
222
223 elseif x_temp == 1 or y_temp == 1 then
224
225 else
226 return x_temp, y_temp, orientation
227 end
228 end
229 return round(xres), round(yres), orientation
230 end
231
232 function identifiers.jpg(filename,method)
233 local specification = {
234 filename = filename,
235 filetype = "jpg",
236 }
237 if not filename or filename == "" then
238 specification.error = "invalid filename"
239 return specification
240 end
241 filename, method = checkedmethod(filename,method)
242 local f = newreader(filename,method)
243 if not f then
244 specification.error = "unable to open file"
245 return specification
246 end
247 specification.xres = 0
248 specification.yres = 0
249 specification.orientation = 1
250 specification.totalpages = 1
251 specification.pagenum = 1
252 specification.length = 0
253 local banner = f:readcardinal2()
254 if banner ~= 0xFFD8 then
255 specification.error = "no jpeg file"
256 return specification
257 end
258 local xres = 0
259 local yres = 0
260 local orientation = 1
261 local okay = false
262 local filesize = f:getsize()
263
264
265 while f:getposition() < filesize do
266 local b = f:readbyte()
267 if not b then
268 break
269 elseif b ~= 0xFF then
270 if not okay then
271
272 specification.error = "incomplete file"
273 end
274 break
275 end
276 local category = f:readbyte()
277 local position = f:getposition()
278 local length = 0
279 local tagdata = tags[category]
280 if not tagdata then
281 specification.error = "invalid tag " .. (category or "?")
282 break
283 elseif tagdata.supported == false then
284 specification.error = "unsupported " .. tagdata.comment
285 break
286 end
287 local name = tagdata.name
288 if name == "SOF0" or name == "SOF1" or name == "SOF2" then
289
290
291
292
293
294
295
296
297 length = f:readcardinal2()
298 specification.colordepth = f:readcardinal()
299 specification.ysize = f:readcardinal2()
300 specification.xsize = f:readcardinal2()
301 specification.colorspace = colorspaces[f:readcardinal()]
302 if not specification.colorspace then
303 specification.error = "unsupported color space"
304 break
305 end
306 okay = true
307 elseif name == "APP0" then
308 length = f:readcardinal2()
309 if length > 6 then
310 local format = f:readstring(5)
311 if format == "JFIF\000" then
312 f:skip(2)
313 units = f:readcardinal()
314 xres = f:readcardinal2()
315 yres = f:readcardinal2()
316 if units == 1 then
317
318 if xres == 1 or yres == 1 then
319
320 end
321 elseif units == 2 then
322
323 xres = xres * 2.54
324 yres = yres * 2.54
325 else
326 xres = 0
327 yres = 0
328 end
329 end
330 end
331 elseif name == "APP1" then
332 length = f:readcardinal2()
333 if length > 7 then
334 local format = f:readstring(5)
335 if format == "Exif\000" then
336 xres, yres, orientation = read_APP1_Exif(f,xres,yres,orientation)
337 end
338 end
339 elseif not tagdata.zerolength then
340 length = f:readcardinal2()
341 end
342 if length > 0 then
343 f:setposition(position+length)
344 end
345 end
346 f:close()
347 if not okay then
348 specification.error = specification.error or "invalid file"
349 elseif not specification.error then
350 if xres == 0 and yres ~= 0 then
351 xres = yres
352 end
353 if yres == 0 and xres ~= 0 then
354 yres = xres
355 end
356 end
357 specification.xres = xres
358 specification.yres = yres
359 specification.orientation = orientation
360 specification.length = filesize
361 return specification
362 end
363
364 identifiers.jpeg = identifiers.jpg
365
366end
367
368do
369
370 local function read_boxhdr(specification,f)
371 local size = f:readcardinal4()
372 local kind = f:readstring(4)
373 if kind then
374 kind = strip(lower(kind))
375 else
376 kind = ""
377 end
378 if size == 1 then
379 size = f:readcardinal4() * 0xFFFF0000 + f:readcardinal4()
380 end
381 if size == 0 and kind ~= "jp2c" then
382 specification.error = "invalid size"
383 end
384 return kind, size
385 end
386
387 local function scan_ihdr(specification,f)
388 specification.ysize = f:readcardinal4()
389 specification.xsize = f:readcardinal4()
390 f:skip(2)
391 specification.colordepth = f:readcardinal() + 1
392 f:skip(3)
393 end
394
395 local function scan_resc_resd(specification,f)
396 local vr_n = f:readcardinal2()
397 local vr_d = f:readcardinal2()
398 local hr_n = f:readcardinal2()
399 local hr_d = f:readcardinal2()
400 local vr_e = f:readcardinal()
401 local hr_e = f:readcardinal()
402 specification.xres = math.round((hr_n / hr_d) * math.exp(hr_e * math.log(10.0)) * 0.0254)
403 specification.yres = math.round((vr_n / vr_d) * math.exp(vr_e * math.log(10.0)) * 0.0254)
404 end
405
406 local function scan_res(specification,f,last)
407 local pos = f:getposition()
408 while true do
409 local kind, size = read_boxhdr(specification,f)
410 pos = pos + size
411 if kind == "resc" then
412 if specification.xres == 0 and specification.yres == 0 then
413 scan_resc_resd(specification,f)
414 if f:getposition() ~= pos then
415 specification.error = "invalid resc"
416 return
417 end
418 end
419 elseif tpos == "resd" then
420 scan_resc_resd(specification,f)
421 if f:getposition() ~= pos then
422 specification.error = "invalid resd"
423 return
424 end
425 elseif pos > last then
426 specification.error = "invalid res"
427 return
428 elseif pos == last then
429 break
430 end
431 if specification.error then
432 break
433 end
434 f:setposition(pos)
435 end
436 end
437
438 local function scan_jp2h(specification,f,last)
439 local okay = false
440 local pos = f:getposition()
441 while true do
442 local kind, size = read_boxhdr(specification,f)
443 pos = pos + size
444 if kind == "ihdr" then
445 scan_ihdr(specification,f)
446 if f:getposition() ~= pos then
447 specification.error = "invalid ihdr"
448 return false
449 end
450 okay = true
451 elseif kind == "res" then
452 scan_res(specification,f,pos)
453 elseif pos > last then
454 specification.error = "invalid jp2h"
455 return false
456 elseif pos == last then
457 break
458 end
459 if specification.error then
460 break
461 end
462 f:setposition(pos)
463 end
464 return okay
465 end
466
467 function identifiers.jp2(filename,method)
468 local specification = {
469 filename = filename,
470 filetype = "jp2",
471 }
472 if not filename or filename == "" then
473 specification.error = "invalid filename"
474 return specification
475 end
476 filename, method = checkedmethod(filename,method)
477 local f = newreader(filename,method)
478 if not f then
479 specification.error = "unable to open file"
480 return specification
481 end
482 specification.xres = 0
483 specification.yres = 0
484 specification.orientation = 1
485 specification.totalpages = 1
486 specification.pagenum = 1
487 specification.length = 0
488 local xres = 0
489 local yres = 0
490 local orientation = 1
491 local okay = false
492 local filesize = f:getsize()
493
494 local pos = 0
495
496 local kind, size = read_boxhdr(specification,f)
497 pos = pos + size
498 f:setposition(pos)
499
500 local kind, size = read_boxhdr(specification,f)
501 if kind ~= "ftyp" then
502 specification.error = "missing ftyp box"
503 return specification
504 end
505 pos = pos + size
506 f:setposition(pos)
507 while not okay do
508 local kind, size = read_boxhdr(specification,f)
509 pos = pos + size
510 if kind == "jp2h" then
511 okay = scan_jp2h(specification,f,pos)
512 elseif kind == "jp2c" and not okay then
513 specification.error = "no ihdr box found"
514 return specification
515 end
516 f:setposition(pos)
517 end
518
519 local filesize = f:getsize()
520 f:close()
521 if not okay then
522 specification.error = "invalid file"
523 elseif not specification.error then
524 if xres == 0 and yres ~= 0 then
525 xres = yres
526 end
527 if yres == 0 and xres ~= 0 then
528 yres = xres
529 end
530 end
531 specification.xres = xres
532 specification.yres = yres
533 specification.orientation = orientation
534 specification.length = filesize
535 return specification
536 end
537
538end
539
540do
541
542
543
544
545
546
547
548
549
550
551
552 local function grab(t,f,once)
553 if once then
554 for i=1,#t do
555 local l = t[i]
556 f:setposition(l.offset)
557 t[i] = f:readstring(l.length)
558 end
559 local data = concat(t)
560
561 return data
562 else
563 local data = { }
564 for i=1,#t do
565 local l = t[i]
566 f:setposition(l.offset)
567 data[i] = f:readstring(l.length)
568 end
569 return concat(data)
570 end
571 end
572
573 function identifiers.png(filename,method)
574 local specification = {
575 filename = filename,
576 filetype = "png",
577 }
578 if not filename or filename == "" then
579 specification.error = "invalid filename"
580 return specification
581 end
582 filename, method = checkedmethod(filename,method)
583 local f = newreader(filename,method)
584 if not f then
585 specification.error = "unable to open file"
586 return specification
587 end
588 local filesize = f:getsize()
589 specification.xres = 0
590 specification.yres = 0
591 specification.orientation = 1
592 specification.totalpages = 1
593 specification.pagenum = 1
594 specification.offset = 0
595 specification.length = filesize
596 local tables = { }
597 local banner = f:readstring(8)
598 if banner ~= "\137PNG\013\010\026\010" then
599 specification.error = "no png file"
600 return specification
601 end
602 while true do
603 local position = f:getposition()
604 if position >= filesize then
605 break
606 end
607 local length = f:readcardinal4()
608 if not length then
609 break
610 end
611 local kind = f:readstring(4)
612 if kind then
613 kind = lower(kind)
614 else
615 break
616 end
617 if kind == "ihdr" then
618 specification.xsize = f:readcardinal4()
619 specification.ysize = f:readcardinal4()
620 specification.colordepth = f:readcardinal()
621 specification.colorspace = f:readcardinal()
622 specification.compression = f:readcardinal()
623 specification.filter = f:readcardinal()
624 specification.interlace = f:readcardinal()
625 tables[kind] = true
626 elseif kind == "iend" then
627 tables[kind] = true
628 break
629 elseif kind == "phys" then
630 local x = f:readcardinal4()
631 local y = f:readcardinal4()
632 local u = f:readcardinal()
633 if u == 1 then
634
635 x = round(0.0254 * x)
636 y = round(0.0254 * y)
637 if x == 0 then x = 1 end
638 if y == 0 then y = 1 end
639 end
640 specification.xres = x
641 specification.yres = y
642 tables[kind] = true
643 elseif kind == "idat" or kind == "plte" or kind == "gama" or kind == "trns" then
644 local t = tables[kind]
645 if not t then
646 t = setmetatablecall(grab)
647 tables[kind] = t
648 end
649 t[#t+1] = {
650 offset = f:getposition(),
651 length = length,
652 }
653 else
654 tables[kind] = true
655 end
656 f:setposition(position+length+12)
657 end
658 f:close()
659 specification.tables = tables
660 return specification
661 end
662
663end
664
665do
666
667 local function gray(t,k)
668 local v = 0
669 t[k] = v
670 return v
671 end
672
673 local function rgb(t,k)
674 local v = { 0, 0, 0 }
675 t[k] = v
676 return v
677 end
678
679 local function cmyk(t,k)
680 local v = { 0, 0, 0, 0 }
681 t[k] = v
682 return v
683 end
684
685 function identifiers.bitmap(specification)
686 local xsize = specification.xsize or 0
687 local ysize = specification.ysize or 0
688 local width = specification.width or xsize * 65536
689 local height = specification.height or ysize * 65536
690 local colordepth = specification.colordepth or 1
691 local colorspace = specification.colorspace or 1
692 local pixel = false
693 local data = specification.data
694 local mask = specification.mask
695 local index = specification.index
696 if colorspace == 1 or colorspace == "gray" then
697 pixel = gray
698 colorspace = 1
699 elseif colorspace == 2 or colorspace == "rgb" then
700 pixel = rgb
701 colorspace = 2
702 elseif colorspace == 3 or colorspace == "cmyk" then
703 pixel = cmyk
704 colorspace = 3
705 else
706 return
707 end
708 if colordepth == 8 then
709 colordepth = 1
710 elseif colordepth == 16 then
711 colordepth = 2
712 end
713 if colordepth > 1 then
714
715 return
716 end
717 if data then
718
719 else
720 data = { }
721 for i=1,ysize do
722 data[i] = setmetatableindex(pixel)
723 end
724 end
725 if mask == true then
726 mask = { }
727 for i=1,ysize do
728 mask[i] = setmetatableindex(gray)
729 end
730 end
731 if index then
732 index = setmetatableindex(pixel)
733 end
734 local specification = {
735 xsize = xsize,
736 ysize = ysize,
737 width = width,
738 height = height,
739 colordepth = colordepth,
740 colorspace = colorspace,
741 data = data,
742 mask = mask,
743 index = index,
744 }
745 return specification
746 end
747
748end
749
750function graphics.identify(filename,filetype)
751 local identify = filetype and identifiers[filetype]
752 if not identify then
753 identify = identifiers[suffixonly(filename)]
754 end
755 if identify then
756 return identify(filename)
757 else
758 return {
759 filename = filename,
760 filetype = filetype,
761 error = "identification failed",
762 }
763 end
764end
765
766function graphics.bpsize(specification,factor)
767 local xsize = specification.xsize or 0
768 local ysize = specification.ysize or 0
769 local xres = specification.xres or 0
770 local yres = specification.yres or 0
771 if not factor then
772 factor = 72
773 end
774 if xres == 0 then
775 xres = factor
776 end
777 if yres == 0 then
778 yres = factor
779 end
780 return round(xsize/(xres/factor)), round(ysize/(yres/factor))
781end
782
783graphics.colorspaces = {
784 [0] = "gray",
785 [1] = "gray",
786 [2] = "rgb",
787 [3] = "cmyk",
788}
789
790
791
792
793return graphics
794 |