if not modules then modules = { } end modules ['lpdf-img'] = { version = 1.001, optimize = true, comment = "companion to lpdf-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- This started as an experiment but has potential for some (cached) optimizations. -- At some point we can also use it for fonts. For small images performance is ok -- with pure lua but for bigger images we can use some helpers. Normally in a -- typesetting workflow non-interlaced images are used. One should convert -- interlaced images to more efficient non-interlaced ones (ok, we can cache them if -- needed). -- -- The \LUA\ code is slightly optimized so we could have done with less lines if we -- wanted but best gain a little. The idea is that we collect striped (in stages) so -- that we can play with substitutions. We keep this variant commented but not -- embedding it saves some 14K bytecode in the format. -- -- We keep the \LUA\ code commented because it is what I started with from the \PNG\ -- specification. It was one fo the first things needed for dropping the backend so -- actually this was part of the first \LUA\ based \PDF\ backend, the one that for a -- while was part of \MKIV. That bit of development was not widely advertized and -- just for me to make the transition and prove that it could be done. At some point -- I decided to not provide a generic backend so that cdoe went away. Reminder: -- there ended up some code here that was needed for font related png too (and I'd -- already forgotten about: I need to document that). -- The \LUA\ code is now gone and can be found in the archives. We still prototype -- in \LUA\ but carrying around that code makes no sense. In the end it's also a -- bit entertaining to go from the standard to this mix of helpers and \LUA. local type = type local concat, move = table.concat, table.move local ceil, min = math.ceil, math.min local char, byte = string.char, string.byte local loaddata = io.loaddata local setmetatableindex = table.setmetatableindex local formatters = string.formatters local streams = utilities.streams local openstring = streams.openstring local readstring = streams.readstring local readbytetable = streams.readbytetable local newreader = io.newreader local tobytetable = string.bytetable local lpdf = lpdf or { } local pdfdictionary = lpdf.dictionary local pdfarray = lpdf.array local pdfconstant = lpdf.constant local pdfreference = lpdf.reference local pdfverbose = lpdf.verbose local pdfflushobject = lpdf.flushobject local pdfflushstreamobject = lpdf.flushstreamobject local pdfmajorversion = lpdf.majorversion local pdfminorversion = lpdf.minorversion local codeinjections = backends.registered.pdf.codeinjections local createimage = images.create local zlibcompress = xzip.compress local zlibdecompress = xzip.decompress local trace = false local cleanvirtual = resolvers.cleaners.virtual -- false -- for now local report_jpg = logs.reporter("graphics","jpg") local report_jp2 = logs.reporter("graphics","jp2") local report_png = logs.reporter("graphics","png") trackers.register("graphics.backend", function(v) trace = v end) directives.register("graphics.cleanvirtuals", function(v) cleanvirtual = v and resolvers.cleaners.virtual or false end) local injectors = { } lpdf.injectors = injectors local function loadcontent(filename,method,wipe) if method == "string" then return filename else local found, data = resolvers.loadbinfile(filename) if wipe then resolvers.cleanupbinfile(filename) end return data end end local function newcontent(filename,method,wipe) if method == "string" then return newreader(filename,method) else local found, data = resolvers.loadbinfile(filename) if wipe then resolvers.cleanupbinfile(filename) end return newreader(data or "", "string") end end -- this likely is never needed (see convert) local chars = setmetatableindex(function(t,k) -- share this one local v = (k <= 0 and "\000") or (k >= 255 and "\255") or char(k) t[k] = v return v end) -- newmask = 150 -- -- newmask = { -- { 0, 100, 0x00 }, -- { 101, 200, 0x7F }, -- { 201, 255, 0xFF }, -- } -- newranges = .75 -- -- newranges = { 0, 0.4, 0, 0.5, 0, 0.6 } } -- newranges = { 0.4, 0.5, 0.6 } -- newranges = { 0.5 } local function todecode(newranges,colors) local kind = type(newranges) if colors == 1 then if kind == "table" then local n = #newranges if n == 1 then return { 0, newranges[1] } elseif n == 2 then return { newranges[1], newranges[2] } end else return { 0, newranges } end elseif colors == 3 then if kind == "table" then local n = #newranges if n == 1 then return { 0, newranges, 0, newranges, 0, newranges } elseif n == 2 then local n1, n2 = newranges[1], newranges[2] return { n1, n2, n1, n2, n1, n2 } elseif n == 6 then return newranges end else return { 0, newranges, 0, newranges, 0, newranges } end elseif colors == 4 then if kind == "table" then local n = #newranges if n == 1 then return { 0, newranges, 0, newranges, 0, newranges, 0, newranges } elseif n == 2 then local n1, n2 = newranges[1], newranges[2] return { n1, n2, n1, n2, n1, n2, n1, n2 } elseif n == 8 then return newranges end else return { 0, newranges, 0, newranges, 0, newranges, 0, newranges } end end end -- do local compresslevel = 3 local pngtogray = pngdecode.togray function injectors.jpg(specification,method) if specification.error then return end local filename = specification.filename if not filename then return end local colorspace = specification.colorspace or jpg_gray local decodearray = nil local colors = 1 ----- procset = colorspace == 0 and "image b" or "image c" if colorspace == 1 then colorspace = "DeviceGray" elseif colorspace == 2 then colorspace = "DeviceRGB" colors = 3 elseif colorspace == 3 then colorspace = "DeviceCMYK" colors = 4 decodearray = pdfarray { 1, 0, 1, 0, 1, 0, 1, 0 } end -- todo: set filename local newranges = specification.newranges if newranges then decodearray = newranges and pdfarray(todecode(newranges,colors)) or nil end -- todo: set filename local xsize = specification.xsize local ysize = specification.ysize local colordepth = specification.colordepth local content = loadcontent(filename,method,true) local xobject = nil if specification.enforcegray and colors == 3 then local w, h, d local filter = compresslevel > 0 and pdfconstant("FlateDecode") or nil w, h, d, content = nanojpeg.decode(content) content = pngtogray(content,colordepth) if filter then content = zlibcompress(content,compresslevel) end local lmtxdata = pdfdictionary { Format = pdfconstant("PNG"), XSize = xsize, YSize = ysize, Compresslevel = compresslevel, Original = pdfdictionary { Format = pdfconstant("JPEG"), Colors = colors, } } xobject = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), Width = xsize, Height = ysize, BitsPerComponent = colordepth, ColorSpace = pdfconstant("DeviceGray"), Filter = filter, Length = #content, LMTX_Graphicdata = pdfreference(pdfflushobject(lmtxdata)), } + specification.attr else xobject = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), -- BBox = pdfarray { 0, 0, xsize, ysize }, Width = xsize, Height = ysize, BitsPerComponent = colordepth, Filter = pdfconstant("DCTDecode"), ColorSpace = pdfconstant(colorspace), Decode = decodearray, Length = #content, -- specification.length } + specification.attr end if trace then report_jpg("%s: width %i, height %i, colordepth %i, size %i",filename,xsize,ysize,colordepth,#content) end if cleanvirtual then cleanvirtual(filename) end return createimage { bbox = { 0, 0, specification.width/xsize, specification.height/ysize }, -- mandate transform = specification.transform, nolength = true, nobbox = true, notype = true, stream = content, attr = xobject(), } end end do function injectors.jp2(specification,method) if specification.error then return end local filename = specification.filename if not filename then return end -- todo: set filename local xsize = specification.xsize local ysize = specification.ysize local content = loadcontent(filename,method,true) local xobject = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), BBox = pdfarray { 0, 0, xsize, ysize }, Width = xsize, Height = ysize, Filter = pdfconstant("JPXDecode"), Length = #content, -- specification.length } + specification.attr if trace then report_jp2("%s: width %i, height %i, size %i",filename,xsize,ysize,#content) end if cleanvirtual then cleanvirtual(filename) end return createimage { bbox = { 0, 0, specification.width/xsize, specification.height/ysize }, -- mandate transform = specification.transform, nolength = true, nobbox = true, notype = true, stream = content, attr = xobject(), } end end do -- We don't like interlaced files. You can deinterlace them beforehand because otherwise -- each run you add runtime. Actually, even masked images can best be converted to PDF -- beforehand. -- The amount of code is larger that I like and looks somewhat redundant but we sort of -- optimize a few combinations that happen often. -- Per 2026 we started experimenting with \LUA's 5.5 feature to use external string which -- for this kind of graphics is more efficient (we can gain some 10 percent). The same is -- true for bytesmaps etc. pngdecode.setexperiment(1) local pngapplyfilter = pngdecode.applyfilter local pngsplitmask = pngdecode.splitmask local pnginterlace = pngdecode.interlace local pngexpand = pngdecode.expand local pngtocmyk = pngdecode.tocmyk local pngtogray = pngdecode.togray local pngtomask = pngdecode.tomask local pngmakemask = pngdecode.makemask local pngfrompalette = pngdecode.frompalette local pngpalettemask = pngdecode.palettemask local transparentmask = pngdecode.transparentmask local newindex = lua.newindex local newtable = lua.newtable local trace_png = false trackers.register("graphics.png", function(v) trace_png = v end) local function newoutput(size) if newindex then return newindex(size,char(0)) end local t = newtable and newtable(size,0) or { } for i=1,size do t[i] = 0 end return t end local function convert(t) if type(t) == "table" then -- this likely never happens for i=1,#t do local ti = t[i] if ti ~= "" then -- soon gone t[i] = chars[ti] end end return concat(t) else return t end end local function filtermask(content,xsize,ysize,colordepth,colorspace) local bytes = colordepth == 16 and 2 or 1 local bpp = colorspace == "DeviceRGB" and 3 or 1 return pngsplitmask(content,xsize,ysize,bpp,bytes) end local function decodemask(content,xsize,ysize,colordepth,colorspace) local mask = true local filter = false local bytes = colordepth == 16 and 2 or 1 local bpp = colorspace == "DeviceRGB" and 3 or 1 local slice = bytes * (bpp + 1) -- always a mask content = pngapplyfilter(content,xsize,ysize,slice) return pngsplitmask(content,xsize,ysize,bpp,bytes,mask,filter) end local function decodestrip(s,nx,ny,slice,parts) local length = ny * (nx * slice + 1) local input = readstring(s,length) if parts then nx = ceil(nx*parts/8) end input = pngapplyfilter(input,nx,ny,slice) return input, false end local xstart = { 0, 4, 0, 2, 0, 1, 0 } local ystart = { 0, 0, 4, 0, 2, 0, 1 } local xstep = { 8, 8, 4, 4, 2, 2, 1 } local ystep = { 8, 8, 8, 4, 4, 2, 2 } local xblock = { 8, 4, 4, 2, 2, 1, 1 } local yblock = { 8, 8, 4, 4, 2, 2, 1 } local function analyze(colordepth,colorspace,palette,mask) -- return bytes, parts, factor if palette then if colordepth == 16 then return 2, false, false elseif colordepth == 8 then return 1, false, false elseif colordepth == 4 then return 1, 4, false elseif colordepth == 2 then return 1, 2, false elseif colordepth == 1 then return 1, 1, false end elseif colorspace == "DeviceGray" then if colordepth == 16 then return mask and 4 or 2, false, false elseif colordepth == 8 then return mask and 2 or 1, false, false elseif colordepth == 4 then return 1, 4, true elseif colordepth == 2 then return 1, 2, true elseif colordepth == 1 then return 1, 1, true end else if colordepth == 16 then return mask and 8 or 6, false, false elseif colordepth == 8 then return mask and 4 or 3, false, false elseif colordepth == 4 then return 3, 4, true elseif colordepth == 2 then return 3, 2, true elseif colordepth == 1 then return 3, 1, true end end return false, false, false end -- 1 6 4 6 2 6 4 6 -- 7 7 7 7 7 7 7 7 -- 5 6 5 6 5 6 5 6 -- 7 7 7 7 7 7 7 7 -- 3 6 4 6 3 6 4 6 -- 7 7 7 7 7 7 7 7 -- 5 6 5 6 5 6 5 6 -- 7 7 7 7 7 7 7 7 local function deinterlace(content,xsize,ysize,colordepth,colorspace,palette,mask) local slice, parts, factor = analyze(colordepth,colorspace,palette,mask) if slice then content = openstring(zlibdecompress(content)) local filter = false local output = false for pass=1,7 do local xstart = xstart[pass] local xstep = xstep[pass] local ystart = ystart[pass] local ystep = ystep[pass] local nx = (xsize + xstep - xstart - 1) // xstep local ny = (ysize + ystep - ystart - 1) // ystep if nx > 0 and ny > 0 then local input, filter if parts then local nxx = ceil(nx*parts/8) input, filter = decodestrip(content,nxx,ny,slice) input, filter = pngexpand(input,nx,ny,parts,nxx,factor,filter) else input, filter = decodestrip(content,nx,ny,slice) end output = pnginterlace(xsize,ysize,slice,pass,input,output,filter) end -- if pass == 3 then -- break -- still looks ok, could be nice for a preroll -- end end return output, parts and 8 or false end end -- 1 (palette used), 2 (color used), and 4 (alpha channel used) -- paeth: -- -- p = a + b - c -- pa = abs(p - a) => a + b - c - a => b - c -- pb = abs(p - b) => a + b - c - b => a - c -- pc = abs(p - c) => a + b - c - c => a + b - c - c => a - c + b - c => pa + pb local function converttocmyk(content,colorspace,colordepth) if colorspace == "DeviceRGB" and (colordepth == 8 or colordepth == 16) then local done = pngtocmyk(content,colordepth) if done then content = done colorspace = "DeviceCMYK" end end return content, colorspace end local function converttogray(content,colorspace,colordepth,average) if colorspace == "DeviceRGB" and (colordepth == 8 or colordepth == 16) then local done = pngtogray(content,colordepth,average) if done then content = done colorspace = "DeviceGray" end end return content, colorspace end local alwaysdecode = false -- tricky with palettes local compresslevel = 3 local expandpalette = false directives.register("graphics.png.recompress", function(v) alwaysdecode = v end) directives.register("graphics.png.compresslevel", function(v) v = tonumber(v) if compresslevel >= 0 or compresslevel <= 9 then compresslevel = v else -- compresslevel = 3 end end) directives.register("graphics.png.expandpalette", function(v) expandpalette = v end) -- can be combined local function extractmask_from_transparent_0( content,colordepth,xsize,ysize,devicespace,transparent ) local content = zlibdecompress(content) if type(transparent) == "string" and #transparent == 2 then local bytes, parts = analyze(colordepth,devicespace) local xxsize = xsize if parts then xxsize = ceil(xsize*parts/8) end content = decodestrip(openstring(content),xxsize,ysize,bytes) local mask, maskdepth = transparentmask(content,bytes,colordepth,transparent) if mask then return mask, maskdepth, content end end return false, 0, content end local function extractmask_from_transparent_2( content,colordepth,xsize,ysize,devicespace,transparent ) local content = zlibdecompress(content) if type(transparent) == "string" and #transparent == 6 then local bytes = analyze(colordepth,devicespace) if bytes then content = decodestrip(openstring(content),xsize,ysize,bytes) end local mask, maskdepth = transparentmask(content,bytes,colordepth,transparent) if mask then return mask, maskdepth, content end end return false, 0, content end local function extractmask_from_transparent_3( content,colordepth,xsize,ysize,devicespace,transparent,palette ) content = zlibdecompress(content) if type(transparent) == "string" then -- decodestrip local len = ceil(xsize*colordepth/8) content = pngapplyfilter(content,len,ysize,1) -- nostrip (saves copy) local mask = pngtomask(content,transparent,xsize,ysize,colordepth) if mask then return mask, 8, content end end return false, 0, content end function injectors.png(specification,method) -- todo: method in specification if specification.error then return end local filename = specification.filename if not filename then return end local colortype = specification.colorspace -- actually more colortype if not colortype then return end local interlace = specification.interlace or 0 if interlace == 1 then interlace = true elseif interlace == 0 then interlace = false else report_png("unknown interlacing %i",interlace) return end local tables = specification.tables if not tables then return end local idat = tables.idat if not idat then return end local pngfile = newcontent(filename,method,true) if not pngfile then return end local content = idat(pngfile,true) tables.idat = false -- -- if tables.gama then -- report_png("ignoring gamma correction") -- end -- local xsize = specification.xsize local ysize = specification.ysize local colordepth = specification.colordepth or 8 local devicespace = false local colorspace = false local mask = false local transparent = false local palette = false local enforcecmyk = specification.enforcecmyk local enforcegray = specification.enforcegray local averagegray = false -- local averagegray = true local colors = 1 local zipcontent = false -- local lmtxcmyk = false local lmtxgray = false local lmtxmask = false local lmtxcomp = false local lmtxdata = pdfdictionary { Format = pdfconstant("PNG"), Colors = colors, XSize = xsize, YSize = ysize, } if colortype == 0 then -- gray | image b devicespace = "DeviceGray" colorspace = "DeviceGray" -- colors = 1 transparent = true elseif colortype == 2 then -- rgb | image c devicespace = "DeviceRGB" colorspace = "DeviceRGB" colors = 3 transparent = true elseif colortype == 3 then -- palette | image c+i devicespace = "DeviceRGB" colorspace = "DeviceRGB" -- colors = 1 palette = true transparent = true elseif colortype == 4 then -- gray | alpha | image b devicespace = "DeviceGray" colorspace = "DeviceGray" -- colors = 1 mask = true elseif colortype == 6 then -- rgb | alpha | image c devicespace = "DeviceRGB" colorspace = "DeviceRGB" colors = 3 mask = true else report_png("unknown colorspace, type %i",colortype) return end -- if transparent then local trns = tables.trns if trns then transparent = trns(pngfile,true) if transparent == "" then transparent = false end tables.trns = false else transparent = false end if transparent then lmtxdata.Transparent = true end end -- local decode = alwaysdecode -- tricky, might go away local filter = pdfconstant("FlateDecode") local major = pdfmajorversion() local minor = pdfminorversion() if major > 1 then -- we're okay elseif minor < 5 and colordepth == 16 then report_png("16 bit colordepth not supported in pdf < 1.5") return elseif minor < 4 and (mask or transparent) then report_png("alpha channels not supported in pdf < 1.4") return elseif minor < 2 then report_png("you'd better use a version > 1.2") return -- decode = true end -- -- todo: compresslevel (or delegate) -- if palette then local plte = tables.plte if plte then palette = plte(pngfile,true) if palette == "" then palette = false end tables.plte = false else palette = false end if palette then lmtxdata.Palette = true end end -- local newmask = specification.newmask local newranges = specification.newranges -- if newranges then newranges = todecode(newranges,colors) lmtxdata.Remapped = true end -- local maskdepth = 0 local keepbytes = enforcecmyk or enforcegray -- -- local statictics = { -- filename = filename, -- colortype = colortype, -- interlace = interlace and true or false, -- mask = mask and true or false, -- transparent = transparent and true or false, -- colorspace = colorspace, -- colors = colors, -- } if trace_png then report_png("filename : %s",filename) report_png(" colortype : %s",colortype) report_png(" interlace : %s",interlace and "yes" or "no") report_png(" mask : %s",mask and "yes" or "no") report_png(" transparent : %s",transparent and "yes" or "no") report_png(" colorspace : %s",colorspace) report_png(" colors : %s",colors) end if interlace then local r, p = deinterlace(content,xsize,ysize,colordepth,devicespace,palette,mask) if not r then return end lmtxdata.Deinterlaced = true if p then colordepth = p end -- We are now unzipped and have much in common with the other -- branches so we can share ... todo. if mask then if not (colordepth == 8 or colordepth == 16) then report_png("mask can't be split from the image (%s)","depth") return end -- get rid of bpp: content, mask = filtermask(r,xsize,ysize,colordepth,devicespace,false) elseif transparent and palette then if devicespace == "DeviceRGB" and colordepth == 8 then local bytes = 3 -- index -> mapped -> r g b local parts = 1 -- whole byte if expandpalette then local c, m = pngfrompalette(r,palette,bytes,parts,transparent) if c and m then content = c palette = false mask = m lmtxmask = "Expanded" else report_png("mask can't be split from the image (%s)","error") content = convert(r) -- can be in deinterlace if needed lmtxmask = "Discarded" end else mask = pngpalettemask(r,bytes,parts,transparent) if mask then lmtxmask = "Separated" else report_png("mask can't be split from the image (%s)","error") lmtxmask = "Discarded" end content = convert(r) -- can be in deinterlace if needed end else report_png("mask can't be split from the image (%s)","colorspace") content = convert(r) -- can be in deinterlace if needed lmtxmask = "Discarded" end else content = convert(r) -- can be in deinterlace if needed end if enforcecmyk then content, devicespace = converttocmyk(content,devicespace,colordepth) lmtxcmyk = devicespace == "DeviceCMYK" elseif enforcegray and colors == 3 then content, devicespace = converttogray(content,devicespace,colordepth,averagegray) lmtxgray = devicespace == "DeviceGray" if lmtxgray then colors = 1 end end zipcontent = true decode = true maskdepth = palette and 8 or colordepth elseif mask then if not (colordepth == 8 or colordepth == 16) then report_png("mask can't be split from the image") return end content = zlibdecompress(content) content, mask = decodemask(content,xsize,ysize,colordepth,devicespace) if palette then -- ignore elseif enforcecmyk then content, devicespace = converttocmyk(content,devicespace,colordepth) lmtxcmyk = devicespace == "DeviceCMYK" elseif enforcegray and colors == 3 then content, devicespace = converttogray(content,devicespace,colordepth,averagegray) lmtxgray = devicespace == "DeviceGray" if lmtxgray then colors = 1 end end lmtxmask = "Decoded" decode = true -- we don't copy the filter byte zipcontent = true maskdepth = palette and 8 or colordepth elseif transparent then local c = false if palette then mask, maskdepth, c = extractmask_from_transparent_3( content,colordepth,xsize,ysize,devicespace,transparent,palette ) elseif colortype == 0 then mask, maskdepth, c = extractmask_from_transparent_0( content,colordepth,xsize,ysize,devicespace,transparent ) elseif colortype == 2 then mask, maskdepth, c = extractmask_from_transparent_2( content,colordepth,xsize,ysize,devicespace,transparent ) end if keepbytes and c then content = c decode = true zipcontent = true end if enforcecmyk then content, devicespace = converttocmyk(content,devicespace,colordepth) lmtxcmyk = devicespace == "DeviceCMYK" elseif enforcegray and colors == 3 then content, devicespace = converttogray(content,devicespace,colordepth,averagegray) lmtxgray = devicespace == "DeviceGray" if lmtxgray then colors = 1 end end lmtxmask = mask and "Created" or "Ignored" elseif decode or (keepbytes and not palette) then -- this one needs checking local bytes, parts = analyze(colordepth,devicespace) if bytes then content = zlibdecompress(content) content = decodestrip(openstring(content),xsize,ysize,bytes,parts) if palette then -- ignore elseif enforcecmyk then content, devicespace = converttocmyk(content,devicespace,colordepth) lmtxcmyk = devicespace == "DeviceCMYK" elseif enforcegray and colors == 3 then content, devicespace = converttogray(content,devicespace,colordepth,averagegray) lmtxgray = devicespace == "DeviceGray" if lmtxgray then colors = 1 end end zipcontent = true decode = true -- due to enforcecmyk else return end elseif newmask and colordepth == 8 and devicespace == "DeviceGray" then local bytes = analyze(colordepth,devicespace) if bytes then content = zlibdecompress(content) content = decodestrip(openstring(content),xsize,ysize,bytes) mask = pngmakemask(content,newmask) zipcontent = true lmtxmask = "Created" decode = true -- due to enforcecmyk else return end maskdepth = colordepth -- to be checked else -- print("PASS ON") end if enforcecmyk then content, devicespace = converttocmyk(content,devicespace,colordepth) lmtxcmyk = devicespace == "DeviceCMYK" elseif enforcegray and colors == 3 then content, devicespace = converttogray(content,devicespace,colordepth,averagegray) lmtxgray = devicespace == "DeviceGray" if lmtxgray then colors = 1 end end if not zipcontent then -- we're fine elseif compresslevel > 0 then content = zlibcompress(content,compresslevel) lmtxcomp = compresslevel else filter = nil end if decode then lmtxdata.Decoded = true end if palette then local devicespace = "DeviceRGB" local nofbytes = 3 if enforcecmyk then palette = converttocmyk(palette,devicespace,8,averagegray) devicespace = "DeviceCMYK" nofbytes = 4 lmtxcmyk = true elseif enforcegray then -- todo palette = converttogray(palette,devicespace,8,averagegray) devicespace = "DeviceGray" nofbytes = 1 lmtxgray = false end palette = pdfarray { pdfconstant("Indexed"), pdfconstant(devicespace), #palette // nofbytes, pdfreference(pdfflushstreamobject(palette)), } end if lmtxcmyk then lmtxdata.CMYK = true elseif lmtxgray then lmtxdata.Gray = true end if lmtxmask then lmtxdata.Mask = pdfconstant(lmtxmask) end if lmtxcomp then lmtxdata.Compresslevel = compresslevel end pngfile:close() local xobject = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), -- BBox = pdfarray { 0, 0, xsize, ysize }, Width = xsize, Height = ysize, BitsPerComponent = colordepth, Filter = filter, ColorSpace = palette or pdfconstant(devicespace), Length = #content, Decode = newranges and pdfarray(newranges) or nil, LMTX_Graphicdata = pdfreference(pdfflushobject(lmtxdata)), } + specification.attr if mask then local d = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), Width = xsize, Height = ysize, BitsPerComponent = maskdepth, ColorSpace = pdfconstant("DeviceGray"), Decode = newranges and pdfarray(newranges) or nil, } xobject.SMask = pdfreference(pdfflushstreamobject(mask,d())) end if not decode then -- not if Decode set? xobject.DecodeParms = pdfdictionary { Colors = colors, Columns = xsize, BitsPerComponent = colordepth, Predictor = 15, } end if trace then report_png("%s: width %i, height %i, colordepth %i, size %i, palette %l, mask %l, transparent %l, decode %l",filename,xsize,ysize,colordepth,#content,palette,mask,transparent,decode) end if specification.colorref then xobject.ColorSpace = pdfreference(specification.colorref) end local width = specification.width or xsize * 65536 local height = specification.height or ysize * 65536 if cleanvirtual then cleanvirtual(filename) end return createimage { bbox = { 0, 0, width/xsize, height/ysize }, -- mandate transform = specification.transform, nolength = true, nobbox = true, notype = true, stream = content, attr = xobject(), } end end do -- we could have a helper for this local function pack(specification,what) local t = { } local n = 0 local s = specification.colorspace local d = specification.data local x = specification.xsize local y = specification.ysize if what == "mask" then d = specification.mask s = 1 elseif what == "indexed" then s = 1 elseif what == "index" then d = specification.index s = - s end if s > 0 then return string.packrowscolumns(d) else local z = d[0] and 0 or 1 if s == -1 then local f = formatters["%02X"] for i=z,#d do n = n + 1 ; t[n] = f(d[i]) end elseif s == -2 then local f = formatters["%02X%02X%02X"] for i=z,#d do local c = d[i] n = n + 1 ; t[n] = f(c[1],c[2],c[3]) end elseif s == -3 then local f = formatters["%02X%02X%02X%02X"] for i=z,#d do local c = d[i] n = n + 1 ; t[n] = f(c[1],c[2],c[3],c[4]) end end return "<" .. concat(t," ") .. ">" end return "" end function injectors.bitmap(specification) local data = specification.data if not data then return end local xsize = specification.xsize or 0 local ysize = specification.ysize or 0 if xsize == 0 or ysize == 0 then return end local colorspace = specification.colorspace or 1 if colorspace == 1 then colorspace = "DeviceGray" elseif colorspace == 2 then colorspace = "DeviceRGB" elseif colorspace == 3 then colorspace = "DeviceCMYK" end local colordepth = (specification.colordepth or 2) == 16 or 8 local index = specification.index local content = pack(specification,index and "indexed" or "data") local mask = specification.mask local colorspace = pdfconstant(colorspace) if index then colorspace = pdfarray { pdfconstant("Indexed"), colorspace, #index + (index[0] and 0 or -1), -- upper index pdfverbose(pack(specification,"index")) } end local xobject = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), BBox = pdfarray { 0, 0, xsize, ysize }, Width = xsize, Height = ysize, BitsPerComponent = colordepth, ColorSpace = colorspace, Length = #content, -- specification.length } if mask then local d = pdfdictionary { Type = pdfconstant("XObject"), Subtype = pdfconstant("Image"), Width = xsize, Height = ysize, BitsPerComponent = colordepth, ColorSpace = pdfconstant("DeviceGray"), } xobject.SMask = pdfreference(pdfflushstreamobject(pack(specification,"mask"),d())) end local w = specification.width local h = specification.height return createimage { bbox = { 0, 0, w and (w/xsize) or xsize, h and (h/ysize) or ysize }, -- mandate -- nolength = true, nobbox = true, notype = true, stream = content, attr = xobject(), } end codeinjections.bitmap = injectors.bitmap end codeinjections.jpg = lpdf.injectors.jpg codeinjections.jp2 = lpdf.injectors.jp2 codeinjections.png = lpdf.injectors.png