grph-con.lua /size: 15 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['grph-con'] = {
2    version   = 1.001,
3    comment   = "companion to grph-inc.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9local P, R, S, Cc, C, Cs, Ct, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.Cc, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.match
10
11local tonumber          = tonumber
12local find              = string.find
13local longtostring      = string.longtostring
14local formatters        = string.formatters
15local expandfilename    = dir.expandname
16local isfile            = lfs.isfile
17
18local settings_to_array = utilities.parsers.settings_to_array
19local settings_to_hash  = utilities.parsers.settings_to_hash
20local allocate          = utilities.storage.allocate
21local setmetatableindex = table.setmetatableindex
22
23local codeinjections    = backends.codeinjections
24local nodeinjections    = backends.nodeinjections
25
26local report_figures    = logs.reporter("system","graphics")
27
28local variables         = interfaces.variables
29local v_high            = variables.high
30local v_low             = variables.low
31local v_medium          = variables.medium
32local v_yes             = variables.yes
33
34local figures           = figures
35
36local converters        = figures.converters
37local programs          = figures.programs
38
39local runprogram        = programs.run
40
41do -- eps | ps
42
43    -- \externalfigure[cow.eps]
44    -- \externalfigure[cow.pdf][conversion=stripped]
45
46    -- todo: colorspace
47    -- todo: lowres
48
49    local epsconverter = converters.eps
50    converters.ps      = epsconverter
51
52    local function gscrop(specification)
53        return (specification and specification.crop == v_yes) and "-dEPSCrop" or ""
54    end
55
56    local resolutions = {
57        [v_low]    = "screen",
58        [v_medium] = "ebook",
59        [v_high]   = "prepress",
60    }
61
62    local runner = sandbox.registerrunner {
63        name     = "eps to pdf",
64        program  = {
65            windows = os.platform == "win64" and "gswin64c" or "gswin32c",
66            unix    = "gs",
67        },
68        template = longtostring [[
69            -q
70            -sDEVICE=pdfwrite
71            -dNOPAUSE
72            -dNOCACHE
73            -dBATCH
74            -dAutoRotatePages=/None
75            -dPDFSETTINGS=/%presets%
76            %crop%
77            -dCompatibilityLevel=%level%
78            -sOutputFile=%newname%
79            %colorspace%
80            %oldname%
81            -c quit
82        ]],
83        checkers = {
84            oldname    = "readable",
85            newname    = "writable",
86            presets    = "string",
87            level      = "string",
88            colorspace = "string",
89         -- crop       = "string",
90        },
91    }
92
93    programs.epstopdf = { resolutions = epstopdf, runner = runner  }
94    programs.gs       = programs.epstopdf
95
96    local cleanups    = { }
97    local cleaners    = { }
98
99    local whitespace  = lpeg.patterns.whitespace
100    local quadruple   = Ct((whitespace^0 * lpeg.patterns.number/tonumber * whitespace^0)^4)
101    local betterbox   = P("%%BoundingBox:")      * quadruple
102                      * P("%%HiResBoundingBox:") * quadruple
103                      * P("%AI3_Cropmarks:")     * quadruple
104                      * P("%%CropBox:")          * quadruple
105                      / function(b,h,m,c)
106                             return formatters["%%%%BoundingBox: %r %r %r %r\n%%%%HiResBoundingBox: %F %F %F %F\n%%%%CropBox: %F %F %F %F\n"](
107                                 m[1],m[2],m[3],m[4], -- rounded integer
108                                 m[1],m[2],m[3],m[4], -- real number
109                                 m[1],m[2],m[3],m[4]
110                             )
111                         end
112    local nocrap      = P("%") / "" * (
113                             (P("AI9_PrivateDataBegin") * P(1)^0)                            / "%%%%EOF"
114                           + (P("%EOF") * whitespace^0 * P("%AI9_PrintingDataEnd") * P(1)^0) / "%%%%EOF"
115                           + (P("AI7_Thumbnail") * (1-P("%%EndData"))^0 * P("%%EndData"))    / ""
116                        )
117    local whatever    = nocrap + P(1)
118    local pattern     = Cs((betterbox * whatever^1 + whatever)^1)
119
120    directives.register("graphics.conversion.eps.cleanup.ai",function(v) cleanups.ai = v end)
121
122    cleaners.ai = function(name)
123        local tmpname = name .. ".tmp"
124        io.savedata(tmpname,lpegmatch(pattern,io.loaddata(name) or ""))
125        return tmpname
126    end
127
128    function epsconverter.pdf(oldname,newname,resolution,colorspace,specification) -- the resolution interface might change
129        local presets  = resolutions[resolution or "high"] or resolutions.high
130        local level    = codeinjections.getformatoption("pdf_level") or "1.3"
131        local tmpname  = oldname
132        if not tmpname or tmpname == "" or not isfile(tmpname) then
133            return
134        end
135        if cleanups.ai then
136            tmpname = cleaners.ai(oldname)
137        end
138        if colorspace == "gray" then
139            colorspace = "-sColorConversionStrategy=Gray -sProcessColorModel=DeviceGray"
140         -- colorspace = "-sColorConversionStrategy=Gray"
141        else
142            colorspace = nil
143        end
144        runner {
145            newname    = newname,
146            oldname    = tmpname,
147            presets    = presets,
148            level      = tostring(level),
149            colorspace = colorspace,
150            crop       = gscrop(specification),
151        }
152        if tmpname ~= oldname then
153            os.remove(tmpname)
154        end
155    end
156
157    epsconverter["gray.pdf"] = function(oldname,newname,resolution,_,specification) -- the resolution interface might change
158        epsconverter.pdf(oldname,newname,resolution,"gray")
159    end
160
161    epsconverter.default = epsconverter.pdf
162
163end
164
165-- do -- pdf
166--
167--     local pdfconverter = converters.pdf
168--
169--     programs.pdftoeps = {
170--         runner = sandbox.registerrunner {
171--             name     = "pdf to ps",
172--             command  = "pdftops",
173--             template = [[-eps "%oldname%" "%newname%"]],
174--             checkers = {
175--                 oldname = "readable",
176--                 newname = "writable",
177--             }
178--         }
179--     }
180--
181--     pdfconverter.stripped = function(oldname,newname)
182--         local pdftoeps = programs.pdftoeps -- can be changed
183--         local epstopdf = programs.epstopdf -- can be changed
184--         local presets  = epstopdf.resolutions[resolution or ""] or epstopdf.resolutions.high
185--         local level    = codeinjections.getformatoption("pdf_level") or "1.3"
186--         local tmpname  = newname .. ".tmp"
187--         pdftoeps.runner { oldname = oldname, newname = tmpname, presets = presets, level = level }
188--         epstopdf.runner { oldname = tmpname, newname = newname, presets = presets, level = level }
189--         os.remove(tmpname)
190--     end
191--
192--     figures.registersuffix("stripped","pdf")
193--
194-- end
195
196do -- svg
197
198    local svgconverter = converters.svg
199    converters.svgz    = svgconverter
200
201    -- inkscape on windows only works with complete paths .. did the command line
202    -- arguments change again? Ok, it's weirder, with -A then it's a name only when
203    -- not . (current)
204
205    -- Beware: the order of printed output lines is a bit random depending on the
206    -- method of calling (bin or pipe) because part of the message prints to stdout
207    -- and part to stderr. Also, on Windows, a second call to the old binaries
208    -- doesn't return anything at all, so that is also a signal of it being old.
209    -- This test will be dropped in 2021 anyway.
210
211    local new = nil
212
213    local function inkscapeformat(suffix)
214        if new == nil then
215            new = os.resultof("inkscape --version") or ""
216            new = new == "" or not find(new,"Inkscape%s*0")
217        end
218        return new and "filename" or suffix
219    end
220
221    local function inkscapecrop(specification)
222        return (specification and specification.crop == v_yes) and "--export-area-drawing" or ""
223    end
224
225    local runner = sandbox.registerrunner {
226        name     = "svg to something",
227        program  = "inkscape",
228        template = longtostring [[
229            %oldname%
230            %crop%
231            --export-dpi=%resolution%
232            --export-%format%=%newname%
233        ]],
234        checkers = {
235            oldname    = "readable",
236            newname    = "writable",
237            format     = "string",
238            resolution = "string",
239        },
240        defaults = {
241            format     = "pdf",
242            resolution = "600",
243        }
244    }
245
246    programs.inkscape = {
247        runner = runner,
248    }
249
250    function svgconverter.pdf(oldname,newname,resolution,arguments,specification)
251        runner {
252            format     = inkscapeformat("pdf"),
253            resolution = "600",
254            crop       = inkscapecrop(specification),
255            newname    = expandfilename(newname),
256            oldname    = expandfilename(oldname),
257        }
258    end
259
260    function svgconverter.png(oldname,newname)
261        runner {
262            format     = inkscapeformat("png"),
263            resolution = "600",
264            crop       = inkscapecrop(specification),
265            newname    = expandfilename(newname),
266            oldname    = expandfilename(oldname),
267        }
268    end
269
270    svgconverter.default = svgconverter.pdf
271
272end
273
274do -- gif | tif
275
276    local gifconverter = converters.gif
277    local tifconverter = converters.tif
278    local bmpconverter = converters.bmp
279
280    programs.convert = {
281        command  = "gm", -- graphicmagick
282        argument = [[convert "%oldname%" "%newname%"]],
283    }
284
285    local function converter(oldname,newname)
286        local convert = programs.convert
287        runprogram(convert.command, convert.argument, {
288            newname = newname,
289            oldname = oldname,
290        } )
291    end
292
293    tifconverter.pdf = converter
294    gifconverter.pdf = converter
295    bmpconverter.pdf = converter
296
297    gifconverter.default = converter
298    tifconverter.default = converter
299    bmpconverter.default = converter
300
301end
302
303do -- png | jpg | profiles
304
305    -- ecirgb_v2.icc
306    -- ecirgb_v2_iccv4.icc
307    -- isocoated_v2_300_eci.icc
308    -- isocoated_v2_eci.icc
309    -- srgb.icc
310    -- srgb_v4_icc_preference.icc
311
312    -- [[convert %?colorspace: -colorspace "%colorspace%" ?%]]
313
314    local rgbprofile  = "srgb_v4_icc_preference.icc" -- srgb.icc
315    local cmykprofile = "isocoated_v2_eci.icc"       -- isocoated_v2_300_eci.icc
316
317    directives.register("graphics.conversion.rgbprofile", function(v) rgbprofile  = type(v) == "string" and v or rgbprofile  end)
318    directives.register("graphics.conversion.cmykprofile",function(v) cmykprofile = type(v) == "string" and v or cmykprofile end)
319
320    local jpgconverters = converters.jpg
321    local pngconverters = converters.png
322
323    local findfile = resolvers.findfile
324
325    local function profiles()
326        if not isfile(rgbprofile) then
327            local found = findfile(rgbprofile)
328            if not found or found == "" then
329                found = findfile("colo-imp-"..rgbprofile)
330            end
331            if found and found ~= "" then
332                rgbprofile = found
333            else
334                report_figures("unknown profile %a",rgbprofile)
335            end
336        end
337        if not isfile(cmykprofile) then
338            local found = resolvers.findfile(cmykprofile)
339            if not found or found == "" then
340                found = findfile("colo-imp-"..cmykprofile)
341            end
342            if found and found ~= "" then
343                cmykprofile = found
344            else
345                report_figures("unknown profile %a",cmykprofile)
346            end
347        end
348        return rgbprofile, cmykprofile
349    end
350
351    local checkers = {
352        oldname     = "readable",
353        newname     = "writable",
354        rgbprofile  = "string",
355        cmykprofile = "string",
356        resolution  = "string",
357        color       = "string",
358    }
359
360    local defaults = {
361        resolution = "600",
362    }
363
364    local pngtocmykpdf = sandbox.registerrunner {
365        name     = "png to cmyk pdf",
366        program  = "gm",
367        template = [[convert -compress Zip  -strip +profile "*" -profile %rgbprofile% -profile %cmykprofile% -sampling-factor 1x1 %oldname% %newname%]],
368        checkers = checkers,
369        defaults = defaults,
370    }
371
372    local jpgtocmykpdf = sandbox.registerrunner {
373        name     = "jpg to cmyk pdf",
374        program  = "gm",
375        template = [[convert -compress JPEG -strip +profile "*" -profile %rgbprofile% -profile %cmykprofile% -sampling-factor 1x1 %oldname% %newname%]],
376        checkers = checkers,
377        defaults = defaults,
378    }
379
380    local pngtograypdf = sandbox.registerrunner {
381        name     = "png to gray pdf",
382        program  = "gm",
383        template = [[convert -colorspace gray -compress Zip -sampling-factor 1x1 %oldname% %newname%]],
384        checkers = checkers,
385        defaults = defaults,
386    }
387
388    local jpgtograypdf = sandbox.registerrunner {
389        name     = "jpg to gray pdf",
390        program  = "gm",
391        template = [[convert -colorspace gray -compress Zip -sampling-factor 1x1 %oldname% %newname%]],
392        checkers = checkers,
393        defaults = defaults,
394    }
395
396    programs.pngtocmykpdf = { runner = pngtocmykpdf }
397    programs.jpgtocmykpdf = { runner = jpgtocmykpdf }
398    programs.pngtograypdf = { runner = pngtograypdf }
399    programs.jpgtograypdf = { runner = jpgtograypdf }
400
401    pngconverters["cmyk.pdf"] = function(oldname,newname,resolution)
402        local rgbprofile, cmykprofile = profiles()
403        pngtocmykpdf {
404            oldname     = oldname,
405            newname     = newname,
406            rgbprofile  = rgbprofile,
407            cmykprofile = cmykprofile,
408            resolution  = resolution,
409        }
410    end
411
412    pngconverters["gray.pdf"] = function(oldname,newname,resolution)
413        pngtograypdf {
414            oldname    = oldname,
415            newname    = newname,
416            resolution = resolution,
417        }
418    end
419
420    jpgconverters["cmyk.pdf"] = function(oldname,newname,resolution)
421        local rgbprofile, cmykprofile = profiles()
422        jpgtocmykpdf {
423            oldname     = oldname,
424            newname     = newname,
425            rgbprofile  = rgbprofile,
426            cmykprofile = cmykprofile,
427            resolution  = resolution,
428        }
429    end
430
431    jpgconverters["gray.pdf"] = function(oldname,newname,resolution)
432        jpgtograypdf {
433            oldname    = oldname,
434            newname    = newname,
435            resolution = resolution,
436        }
437    end
438
439    -- recolor
440
441    local recolorpng = sandbox.registerrunner {
442        name     = "recolor png",
443        program  = "gm",
444        template = [[convert -recolor %color% %oldname% %newname%]],
445        checkers = checkers,
446        defaults = defaults,
447    }
448
449    -- this is now built in so not really needed any more
450
451    programs.recolor = { runner = recolorpng }
452
453    pngconverters["recolor.png"] = function(oldname,newname,resolution,arguments)
454        recolorpng {
455            oldname    = oldname,
456            newname    = newname,
457            resolution = resolution,
458            color      = arguments or ".5 0 0 .7 0 0 .9 0 0",
459        }
460    end
461
462end
463
464if CONTEXTLMTXMODE > 0 then
465
466    -- This might also work ok in mkiv but is yet untested. Anyway, it's experimental as we
467    -- go through TeX which is is inefficient. I'll improve the buffer trick.
468
469    local function remap(specification)
470        local fullname = specification.fullname
471        if fullname then
472            local only = file.nameonly(fullname)
473            local name = formatters["svg-%s-inclusion"](only)
474            local code = formatters["\\includesvgfile[%s]\\resetbuffer[%s]"](fullname,name)
475            buffers.assign(name,code)
476            specification.format   = "buffer"
477            specification.fullname = name
478        end
479        return specification
480    end
481
482    figures.remappers.svg = { mp = remap }
483
484end
485