lpdf-grp.lmt /size: 15 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-grp'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.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
9local type, tonumber = type, tonumber
10local formatters, gsub = string.formatters, string.gsub
11local sort = table.sort
12local round = math.round
13
14local colors         = attributes.colors
15local basepoints     = number.dimenfactors.bp
16
17local pdfbackend     = backends.registered.pdf
18local nodeinjections = pdfbackend.nodeinjections
19local codeinjections = pdfbackend.codeinjections
20local registrations  = pdfbackend.registrations
21
22local lpdf           = lpdf
23local pdfdictionary  = lpdf.dictionary
24local pdfarray       = lpdf.array
25local pdfconstant    = lpdf.constant
26local pdfboolean     = lpdf.boolean
27local pdfreference   = lpdf.reference
28local pdfflushobject = lpdf.flushobject
29
30local createimage    = images.create
31local wrapimage      = images.wrap
32local embedimage     = images.embed
33
34-- can also be done indirectly:
35--
36-- 12 : << /AntiAlias false /ColorSpace  8 0 R /Coords [ 0.0 0.0 1.0 0.0 ] /Domain [ 0.0 1.0 ] /Extend [ true true ] /Function 22 0 R /ShadingType 2 >>
37-- 22 : << /Bounds [ ] /Domain [ 0.0 1.0 ] /Encode [ 0.0 1.0 ] /FunctionType 3 /Functions [ 31 0 R ] >>
38-- 31 : << /C0 [ 1.0 0.0 ] /C1 [ 0.0 1.0 ] /Domain [ 0.0 1.0 ] /FunctionType 2 /N 1.0 >>
39
40local function hasopacities(opacities)
41    if opacities then
42        for i=1,#opacities do
43            if type(opacities[i]) ~= "number" then
44                return false
45            end
46        end
47        return true
48    end
49    return false
50end
51
52local function shade(stype,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions,opacities)
53    local func = nil
54    --
55    -- domain has to be consistently added in all dictionaries here otherwise
56    -- acrobat fails with a drawing error
57    --
58    domain = pdfarray(domain)
59    n      = tonumber(n)
60    --
61    if steps then
62        local list   = pdfarray()
63        local bounds = pdfarray()
64        local encode = pdfarray()
65
66-- if not opacities then
67--     opacities = { }
68-- end
69-- local hasops = hasopacities(opacities)
70-- if hasops then
71--     for i=1,steps do
72--         local o = opacities[i]
73--         local a = color_a[i]
74--         for i=1,#a do
75--             local c = o * a[i]
76--             a[i] = c > 1 and 1 or c
77--         end
78--     end
79--     color_b[1] = 1
80--     for i=2,steps do
81--         local color_b[i] = color_a[i-1]
82--     end
83--     inspect(color_a)
84--     inspect(color_b)
85-- end
86
87        -- The bounds need to be sorted and we can have illustrator output
88        -- that violates this rule.
89        local tmp = { }
90        for i=1,steps do
91            tmp[i] = { fractions[i], color_a[i], color_b[i] }
92        end
93        sort(tmp, function(a,b)
94            return a[1] < b[1]
95        end)
96        for i=1,steps do
97            local t = tmp[i]
98            fractions[i] = t[1]
99            color_a  [i] = t[2]
100            color_b  [i] = t[3]
101        end
102        -- So far for a fix.
103        for i=1,steps do
104            if i < steps then
105                bounds[i] = fractions[i] or 1
106            end
107            encode[2*i-1] = 0
108            encode[2*i]   = 1
109            list  [i]     = pdfdictionary {
110                FunctionType = 2,
111                Domain       = domain,
112                C0           = pdfarray(color_a[i]),
113                C1           = pdfarray(color_b[i]),
114                N            = n,
115            }
116        end
117        func = pdfdictionary {
118            FunctionType = 3,
119            Bounds       = bounds,
120            Encode       = encode,
121            Functions    = list,
122            Domain       = domain,
123        }
124    else
125        func = pdfdictionary {
126            FunctionType = 2,
127            Domain       = domain,
128            C0           = pdfarray(color_a),
129            C1           = pdfarray(color_b),
130            N            = n,
131        }
132    end
133    separation = separation and registrations.getspotcolorreference(separation)
134    local s = pdfdictionary {
135        ShadingType = stype,
136        ColorSpace  = separation and pdfreference(separation) or pdfconstant(colorspace),
137        Domain      = domain,
138        Function    = pdfreference(pdfflushobject(func)),
139        Coords      = pdfarray(coordinates),
140        Extend      = pdfarray { true, true },
141        AntiAlias   = pdfboolean(true),
142    }
143    lpdf.adddocumentshade(name,pdfreference(pdfflushobject(s)))
144end
145
146function lpdf.circularshade(name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions,opacities)
147    shade(3,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions,opacities)
148end
149
150function lpdf.linearshade(name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions,opacities)
151    shade(2,name,domain,color_a,color_b,n,colorspace,coordinates,separation,steps,fractions,opacities)
152end
153
154-- inline bitmaps but xform'd
155--
156-- we could derive the colorspace if we strip the data
157-- and divide by x*y
158
159-- todo: map onto png
160
161
162do
163
164    local template = "q BI %s ID %s > EI Q"
165    local factor   = 72/300
166
167    local methods = { }
168
169    methods.hex = function(t)
170        -- encoding is ascii hex, no checking here
171        local xresolution = t.xresolution or 0
172        local yresolution = t.yresolution or 0
173        if xresolution == 0 or yresolution == 0 then
174            return -- fatal error
175        end
176        local colorspace = t.colorspace
177        if colorspace ~= "rgb" and colorspace ~= "cmyk" and colorspace ~= "gray" then
178            -- not that efficient but ok
179            local d = gsub(t.data,"[^0-9a-f]","")
180            local b = round(#d / (xresolution * yresolution))
181            if b == 2 then
182                colorspace = "gray"
183            elseif b == 6 then
184                colorspace = "rgb"
185            elseif b == 8 then
186                colorspace = "cmyk"
187            end
188        end
189        colorspace = lpdf.colorspaceconstants[colorspace]
190        if not colorspace then
191            return -- fatal error
192        end
193        --the original length L is required for pdf 2.0 (4096 max)
194        local d = pdfdictionary {
195            W   = xresolution,
196            H   = yresolution,
197            CS  = colorspace,
198            BPC = 8,
199            F   = pdfconstant("AHx"),
200         -- CS  = nil,
201         -- BPC = 1,
202         -- IM = true,
203        }
204        -- for some reasons it only works well if we take a 1bp boundingbox
205        local urx, ury = 1/basepoints, 1/basepoints
206     -- urx = (xresolution/300)/basepoints
207     -- ury = (yresolution/300)/basepoints
208        local width, height = t.width or 0, t.height or 0
209        if width == 0 and height == 0 then
210            width  = factor * xresolution / basepoints
211            height = factor * yresolution / basepoints
212        elseif width == 0 then
213            width  = height * xresolution / yresolution
214        elseif height == 0 then
215            height = width  * yresolution / xresolution
216        end
217        local a = pdfdictionary {
218            BBox = pdfarray { 0, 0, round(urx * basepoints), round(ury * basepoints) }
219        }
220        local image = createimage {
221            stream = formatters[template](d(),t.data),
222            width  = width,
223            height = height,
224            bbox   = { 0, 0, round(urx), round(ury) },
225            attr   = a(),
226            nobbox = true,
227        }
228        return wrapimage(image)
229    end
230
231 -- local lpegmatch     = lpeg.match
232 -- local pattern       = lpeg.Cs((lpeg.patterns.space/"" + lpeg.patterns.hextobyte)^0)
233
234    local zlibcompress    = xzip.compress
235    local hextocharacters = string.hextocharacters
236    local compresslevel   = 3
237
238    methods.png = function(t)
239        -- encoding is ascii hex, no checking here
240        local xresolution = t.xresolution or 0
241        local yresolution = t.yresolution or 0
242        local data        = t.data or ""
243        if xresolution == 0 or yresolution == 0 or data == "" then
244            return -- fatal error
245        end
246        data = hextocharacters(data)
247        if not data then
248            return
249        end
250        local colorspace = t.colorspace
251        local colordepth = 8
252        local colors     = 1
253--         if colorspace ~= "rgb" and colorspace ~= "gray" then
254--             -- not that efficient but ok
255--             local d = gsub(t.data,"[^0-9a-f]","")
256--             local b = round(#d / (xresolution * yresolution))
257--             if b == 2 then
258--                 colorspace = "gray"
259--                 colors     = 1
260--             elseif b == 6 then
261--                 colorspace = "rgb"
262--                 colors     = 3
263--             elseif b == 8 then
264--                 return -- for now, todo: convert
265--             end
266--         end
267        if colorspace ~= "rgb" and colorspace ~= "gray" then
268            local b = round(#data / (xresolution * yresolution))
269            if b == 1 then
270                colorspace = "gray"
271                colors     = 1
272            elseif b == 3 then
273                colorspace = "rgb"
274                colors     = 3
275            elseif b == 4 then
276                return -- for now, todo: convert
277            end
278        end
279        colorspace = lpdf.colorspaceconstants[colorspace]
280        if not colorspace then
281            return -- fatal error
282        end
283        local width  = t.width
284        local height = t.height
285        if width == 0 and height == 0 then
286            width  = factor * xresolution / basepoints
287            height = factor * yresolution / basepoints
288        elseif width == 0 then
289            width  = height * xresolution / yresolution
290        elseif height == 0 then
291            height = width  * yresolution / xresolution
292        end
293     -- data = zlibcompress(lpegmatch(pattern,data),compresslevel)
294        data = zlibcompress(data,compresslevel)
295        local xobject = pdfdictionary {
296            Type             = pdfconstant("XObject"),
297            Subtype          = pdfconstant("Image"),
298            Width            = xresolution,
299            Height           = yresolution,
300            BitsPerComponent = 8,
301            ColorSpace       = colorspace,
302            Length           = #data,
303            Filter           = pdfconstant("FlateDecode"),
304        }
305        local image = createimage {
306--             bbox     = { 0, 0, round(width/xresolution), round(height/yresolution) }, -- mandate
307            bbox     = { 0, 0, round(width), round(height) }, -- mandate
308            width    = round(width),
309            height   = round(height),
310            nolength = true,
311            nobbox   = true,
312            notype   = true,
313            stream   = data,
314            attr     = xobject(),
315        }
316        return wrapimage(image)
317    end
318
319    function nodeinjections.injectbitmap(t)
320        if t.colorspace == "cmyk" then
321            return methods.hex(t)
322        else
323            return (methods[t.format or "hex"] or methods.hex)(t)
324        end
325    end
326
327end
328
329function codeinjections.setfigurealternative(data,figure)
330    local request = data.request
331    local display = request.display
332    if display and display ~= ""  then
333        local nested = figures.push {
334            name   = display,
335            page   = request.page,
336            size   = request.size,
337            prefix = request.prefix,
338            cache  = request.cache,
339            width  = request.width,
340            height = request.height,
341        }
342        figures.identify()
343        local displayfigure = figures.check()
344        if displayfigure then
345        --  figure.aform = true
346            embedimage(figure)
347            local a = pdfarray {
348                pdfdictionary {
349                    Image              = pdfreference(figure.objnum),
350                    DefaultForPrinting = true,
351                }
352            }
353            local d = pdfdictionary {
354                Alternates = pdfreference(pdfflushobject(a)),
355            }
356            displayfigure.attr = d()
357            figures.pop()
358            return displayfigure, nested
359        else
360            figures.pop()
361        end
362    end
363end
364
365function codeinjections.getpreviewfigure(request)
366    local figure = figures.initialize(request)
367    if not figure then
368        return
369    end
370    figure = figures.identify(figure)
371    if not (figure and figure.status and figure.status.fullname) then
372        return
373    end
374    figure = figures.check(figure)
375    if not (figure and figure.status and figure.status.fullname) then
376        return
377    end
378    local image = figure.status.private
379    if image then
380        embedimage(image)
381    end
382    return figure
383end
384
385local masks = {
386    demomask = {
387        {   0,  63,   0 },
388        {  64, 127, 127 },
389        { 128, 195, 195 },
390        { 196, 255, 255 },
391    }
392}
393
394local ranges = {
395--     [".75"] = .75,
396--     [".50"] = .50,
397--     [".25"] = .25,
398}
399
400function codeinjections.registerfiguremask(name,specification)
401    masks[name] = specification
402end
403
404function codeinjections.registerfigurerange(name,specification)
405    ranges[name] = specification
406end
407
408function codeinjections.setfiguremask(data,figure) -- mark
409    local request = data.request
410    local mask    = request.mask
411    local range   = request.range
412    if mask and mask ~= ""  then
413        local m = masks[mask]
414        if m then
415            if type(m) == "function" then
416                m = m()
417            end
418            figure.newmask = m
419        else
420            figures.push {
421                name   = mask,
422                page   = request.page,
423                size   = request.size,
424                prefix = request.prefix,
425                cache  = request.cache,
426                width  = request.width,
427                height = request.height,
428            }
429            mask = figures.identify()
430            mask = figures.check(mask)
431            if mask then
432                local image = mask.status.private
433                if image then
434                    figures.include(mask)
435                    embedimage(image)
436                    local d = pdfdictionary {
437                        Interpolate  = false,
438                        SMask        = pdfreference(mask.status.objectnumber),
439                    }
440                    figure.attr = d()
441                end
442            end
443            figures.pop()
444        end
445    end
446    if range and range ~= "" then
447        local r = ranges[range]
448        if not r then
449            r = tonumber(range)
450        end
451        if type(r) == "function" then
452            r = r()
453        end
454        figure.newranges = r
455    end
456end
457
458-- experimental (q Q is not really needed)
459
460local saveboxresource = tex.boxresources.save
461local nofpatterns     = 0
462local f_pattern       = formatters["q /Pattern cs /%s scn 0 0 %.6N %.6N re f Q"]
463
464function lpdf.registerpattern(specification)
465    nofpatterns = nofpatterns + 1
466    local d = pdfdictionary {
467        Type        = pdfconstant("Pattern"),
468        PatternType = 1,
469        PaintType   = 1,
470        TilingType  = 2,
471        XStep       = (specification.width  or 10) * basepoints,
472        YStep       = (specification.height or 10) * basepoints,
473        Matrix      = {
474            1, 0, 0, 1,
475            (specification.hoffset or 0) * basepoints,
476            (specification.voffset or 0) * basepoints,
477        },
478    }
479
480 -- local resources  = lpdf.collectedresources{ patterns = false } -- we don't want duplicates, so no serialize here:
481    local resources  = lpdf.collectedresources{ patterns = false, serialize = false }
482    local attributes = d -- () -- we need to check for patterns
483    local onlybounds = 1
484    local patternobj = saveboxresource(specification.number,attributes,resources,true,onlybounds)
485    lpdf.adddocumentpattern("Pt" .. nofpatterns,lpdf.reference(patternobj ))
486    return nofpatterns
487end
488
489function lpdf.patternstream(n,width,height)
490    return f_pattern("Pt" .. n,width*basepoints,height*basepoints)
491end
492
493codeinjections.registerpattern = lpdf.registerpattern
494