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
