grph-img.lua /size: 28 Kb    last modification: 2025-02-21 11:03
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
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, -- gray
47        [3] = 2, -- rgb
48        [4] = 3, -- cmyk
49    }
50
51    local tags = {
52        [0xC0] = { name = "SOF0",                    }, -- baseline DCT
53        [0xC1] = { name = "SOF1",                    }, -- extended sequential DCT
54        [0xC2] = { name = "SOF2",                    }, -- progressive DCT
55        [0xC3] = { name = "SOF3",  supported = false }, -- lossless (sequential)
56
57        [0xC5] = { name = "SOF5",  supported = false }, -- differential sequential DCT
58        [0xC6] = { name = "SOF6",  supported = false }, -- differential progressive DCT
59        [0xC7] = { name = "SOF7",  supported = false }, -- differential lossless (sequential)
60
61        [0xC8] = { name = "JPG",                     }, -- reserved for JPEG extensions
62        [0xC9] = { name = "SOF9",                    }, -- extended sequential DCT
63        [0xCA] = { name = "SOF10", supported = false }, -- progressive DCT
64        [0xCB] = { name = "SOF11", supported = false }, -- lossless (sequential)
65
66        [0xCD] = { name = "SOF13", supported = false }, -- differential sequential DCT
67        [0xCE] = { name = "SOF14", supported = false }, -- differential progressive DCT
68        [0xCF] = { name = "SOF15", supported = false }, -- differential lossless (sequential)
69
70        [0xC4] = { name = "DHT"                      }, -- define Huffman table(s)
71
72        [0xCC] = { name = "DAC"                      }, -- define arithmetic conditioning table
73
74        [0xD0] = { name = "RST0", zerolength = true }, -- restart
75        [0xD1] = { name = "RST1", zerolength = true }, -- restart
76        [0xD2] = { name = "RST2", zerolength = true }, -- restart
77        [0xD3] = { name = "RST3", zerolength = true }, -- restart
78        [0xD4] = { name = "RST4", zerolength = true }, -- restart
79        [0xD5] = { name = "RST5", zerolength = true }, -- restart
80        [0xD6] = { name = "RST6", zerolength = true }, -- restart
81        [0xD7] = { name = "RST7", zerolength = true }, -- restart
82
83        [0xD8] = { name = "SOI",  zerolength = true }, -- start of image
84        [0xD9] = { name = "EOI",  zerolength = true }, -- end of image
85        [0xDA] = { name = "SOS"                     }, -- start of scan
86        [0xDB] = { name = "DQT"                     }, -- define quantization tables
87        [0xDC] = { name = "DNL"                     }, -- define number of lines
88        [0xDD] = { name = "DRI"                     }, -- define restart interval
89        [0xDE] = { name = "DHP"                     }, -- define hierarchical progression
90        [0xDF] = { name = "EXP"                     }, -- expand reference image(s)
91
92        [0xE0] = { name = "APP0"                    }, -- application marker, used for JFIF
93        [0xE1] = { name = "APP1"                    }, -- application marker
94        [0xE2] = { name = "APP2"                    }, -- application marker
95        [0xE3] = { name = "APP3"                    }, -- application marker
96        [0xE4] = { name = "APP4"                    }, -- application marker
97        [0xE5] = { name = "APP5"                    }, -- application marker
98        [0xE6] = { name = "APP6"                    }, -- application marker
99        [0xE7] = { name = "APP7"                    }, -- application marker
100        [0xE8] = { name = "APP8"                    }, -- application marker
101        [0xE9] = { name = "APP9"                    }, -- application marker
102        [0xEA] = { name = "APP10"                   }, -- application marker
103        [0xEB] = { name = "APP11"                   }, -- application marker
104        [0xEC] = { name = "APP12"                   }, -- application marker
105        [0xED] = { name = "APP13"                   }, -- application marker
106        [0xEE] = { name = "APP14"                   }, -- application marker, used by Adobe
107        [0xEF] = { name = "APP15"                   }, -- application marker
108
109        [0xF0] = { name = "JPG0"                    }, -- reserved for JPEG extensions
110        [0xFD] = { name = "JPG13"                   }, -- reserved for JPEG extensions
111        [0xFE] = { name = "COM"                     }, -- comment
112
113        [0x01] = { name = "TEM",  zerolength = true }, -- temporary use
114    }
115
116    setmetatableindex(tags, function(t,k)
117        -- we can add some tracing if needed (global) to get an idea
118        local v = "tag " .. k
119        t[k] = v
120        return v
121    end)
122
123    -- More can be found in http://www.exif.org/Exif2-2.PDF but basically we have
124    -- good old tiff tags here.
125
126    local function read_APP1_Exif(f, xres, yres, orientation) -- untested
127        local position     = false
128        local littleendian = false
129        -- endian II|MM
130        while true do
131            position = f:getposition()
132            local b  = f:readbyte()
133            if b == 0 then
134                -- next one
135            elseif b == 0x4D and f:readbyte() == 0x4D then -- M
136                -- big endian
137                break
138            elseif b == 0x49 and f:readbyte() == 0x49 then -- I
139                -- little endian
140                littleendian = true
141                break
142            else
143                -- warning "bad exif data"
144                return xres, yres, orientation
145            end
146        end
147        -- version
148        local version = littleendian and f:readcardinal2le() or f:readcardinal2()
149        if version ~= 42 then
150            return xres, yres, orientation
151        end
152        -- offset to records
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 -- byte | undefined
172                value = f:readbyte()
173                f:skip(3)
174            elseif kind == 3 or kind == 8 then -- (un)signed short
175                value = littleendian and f:readcardinal2le() or f:readcardinal2()
176                f:skip(2)
177            elseif kind == 4 or kind == 9 then -- (un)signed long
178                value = littleendian and f:readcardinal4le() or f:readcardinal4()
179            elseif kind == 5 or kind == 10 then -- (s)rational
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 -- 2 -- ascii
187                f:skip(4)
188            end
189            if tag == 274 then         -- orientation
190                orientation = value
191            elseif tag == 282 then     -- x resolution
192                if den ~= 0 then
193                    x_res = num/den
194                end
195            elseif tag == 283 then     -- y resolution
196                if den ~= 0 then
197                    y_res = num/den
198                end
199            elseif tag == 296 then     -- resolution unit
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  -- pixel unit
206                res_unit_ms = value == 1
207            elseif tag == 0x5111 then  -- x pixels per unit
208                x_res_ms = value
209            elseif tag == 0x5112 then  -- y pixels per unit
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) -- in meters
218            y_temp = round(y_res_ms * 0.0254) -- in meters
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                -- exif resolution differs from already found resolution
223            elseif x_temp == 1 or y_temp == 1 then
224                -- exif resolution is kind of weird
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 -- error
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 -- error
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 -- error
257        end
258        local xres         = 0
259        local yres         = 0
260        local orientation  = 1
261        local okay         = false
262        local filesize     = f:getsize() -- seek end
263     -- local majorversion = pdfmajorversion and pdfmajorversion() or 1
264     -- local minorversion = pdfminorversion and pdfminorversion() or 7
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                    -- or check for size
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                -- It makes no sense to support pdf < 1.3 so we now just omit this
291                -- test. There is no need to polute the code with useless tests.
292                --
293             -- if majorversion == 1 and minorversion <= 2 then
294             --     specification.error = "no progressive DCT in PDF <= 1.2"
295             --     break
296             -- end
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                            -- pixels per inch
318                            if xres == 1 or yres == 1 then
319                                -- warning
320                            end
321                        elseif units == 2 then
322                            -- pixels per cm */
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  -- move this
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) -- nc
391        specification.colordepth = f:readcardinal() + 1
392        f:skip(3) -- c unkc ipr
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 -- error
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 -- error
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() -- seek end
493        --
494        local pos = 0
495        --  signature
496        local kind, size = read_boxhdr(specification,f)
497        pos = pos + size
498        f:setposition(pos)
499        -- filetype
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() -- seek end
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    -- 0 = gray               "image b"
543    -- 2 = rgb                "image c"
544    -- 3 = palette            "image c" + "image i"
545    -- 4 = gray + alpha       "image b"
546    -- 6 = rgb + alpha        "image c"
547
548    -- for i=1,length/3 do
549    --     palette[i] = f:readstring3)
550    -- end
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            -- t wiped in caller
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 -- error
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 -- error
587        end
588        local filesize = f:getsize() -- seek end
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 -- error
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 -- metadata
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 -- meters
634                    -- there was a reason why this was commented
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) -- #size #kind #crc
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 -- 1 .. 2
691        local colorspace = specification.colorspace or 1 -- 1 .. 3
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            -- not yet
715            return
716        end
717        if data then
718            -- assume correct data
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", -- indexed
786    [2] = "rgb",
787    [3] = "cmyk",
788}
789
790-- inspect(identifiers.jpg("t:/sources/hacker.jpg"))
791-- inspect(identifiers.png("t:/sources/mill.png"))
792
793return graphics
794