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