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