lpdf-fmt.lmt /size: 47 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['lpdf-fmt'] = {
2    version   = 1.001,
3    comment   = "companion to lpdf-ini.mkiv",
4    author    = "Peter Rolf and Hans Hagen",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files",
7}
8
9-- Thanks to Luigi and Steffen for testing.
10
11-- context --directives="backend.format=PDF/X-1a:2001" --trackers=backend.format yourfile
12
13local tonumber = tonumber
14local lower, gmatch, format, find, gsub = string.lower, string.gmatch, string.format, string.find, string.gsub
15local concat, serialize, sortedhash = table.concat, table.serialize, table.sortedhash
16
17local trace_format    = false  trackers.register("backend.format",    function(v) trace_format    = v end)
18local trace_variables = false  trackers.register("backend.variables", function(v) trace_variables = v end)
19
20local report_backend = logs.reporter("backend","profiles")
21
22local pdfbackend               = backends.registered.pdf
23----- nodeinjections           = pdfbackend.nodeinjections
24local codeinjections           = pdfbackend.codeinjections
25
26local variables                = interfaces.variables
27local viewerlayers             = attributes.viewerlayers
28local colors                   = attributes.colors
29local transparencies           = attributes.transparencies
30
31local lpdf                     = lpdf
32local pdfdictionary            = lpdf.dictionary
33local pdfarray                 = lpdf.array
34local pdfconstant              = lpdf.constant
35local pdfreference             = lpdf.reference
36local pdfflushobject           = lpdf.flushobject
37local pdfstring                = lpdf.string
38local pdfverbose               = lpdf.verbose
39local pdfflushstreamfileobject = lpdf.flushstreamfileobject
40
41local addtoinfo                = lpdf.addtoinfo
42local injectxmpinfo            = lpdf.injectxmpinfo
43local insertxmpinfo            = lpdf.insertxmpinfo
44local replacexmpinfo           = lpdf.replacexmpinfo
45
46local settings_to_array        = utilities.parsers.settings_to_array
47local settings_to_hash         = utilities.parsers.settings_to_hash
48
49--[[
50    Comments by Peter:
51
52    output intent       : only one profile per color space (and device class)
53    default color space : (theoretically) several profiles per color space possible
54
55    The default color space profiles define the current gamuts (part of/all the
56    colors we have in the document), while the output intent profile declares the
57    gamut of the output devices (the colors that we get normally a printer or
58    monitor).
59
60    Example:
61
62    I have two RGB pictures (both 'painted' in /DeviceRGB) and I declare sRGB as
63    default color space for one picture and AdobeRGB for the other. As output
64    intent I use ISO_coated_v2_eci.icc.
65
66    If I had more than one output intent profile for the combination CMYK/printer I
67    can't decide which one to use. But it is no problem to use several default color
68    space profiles for the same color space as it's just a different color
69    transformation. The relation between picture and profile is clear.
70]]--
71
72local channels = {
73    gray = 1,
74    grey = 1,
75    rgb  = 3,
76    cmyk = 4,
77}
78
79local prefixes = {
80    gray = "DefaultGray",
81    grey = "DefaultGray",
82    rgb  = "DefaultRGB",
83    cmyk = "DefaultCMYK",
84}
85
86local formatspecification = nil
87local formatname          = nil
88
89-- * correspondent document wide flags (write once) needed for permission tests
90
91-- defaults as mt
92
93local pdfformats = {
94
95   ["x1a2001"] = {
96        pdf_version             = 1.3,
97        format_name             = "PDF/X-1a:2001",
98        xmp_file                = "lpdf-pdx.xml",
99        gts_flag                = "GTS_PDFX",
100        gray_scale              = true,
101        cmyk_colors             = true,
102        spot_colors             = true,
103        internal_icc_profiles   = true,
104        include_cidsets         = true,
105        include_charsets        = true,
106        include_procsets        = true,
107        attachments             = false,
108        inject_metadata         = function()
109            addtoinfo("GTS_PDFXVersion","PDF/X-1a:2001")
110            replacexmpinfo(
111                "xml://rdf:RDF/pdfaid-placeholder",
112[[
113<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'>
114      <pdfxid:GTS_PDFXVersion>PDF/X-1a:2001</pdfxid:GTS_PDFXVersion>
115    </rdf:Description>
116]]
117            )
118        end
119    },
120
121    ["x1a2003"] = {
122        pdf_version             = 1.4,
123        format_name             = "PDF/X-1a:2003",
124        xmp_file                = "lpdf-pdx.xml",
125        gts_flag                = "GTS_PDFX",
126        gray_scale              = true,
127        cmyk_colors             = true,
128        spot_colors             = true,
129        internal_icc_profiles   = true,
130        include_cidsets         = true,
131        include_charsets        = true,
132        include_procsets        = true,
133        attachments             = false,
134        inject_metadata         = function()
135            addtoinfo("GTS_PDFXVersion","PDF/X-1a:2003")
136            replacexmpinfo(
137                "xml://rdf:RDF/pdfaid-placeholder",
138[[
139<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'>
140      <pdfxid:GTS_PDFXVersion>PDF/X-1a:2003</pdfxid:GTS_PDFXVersion>
141    </rdf:Description>
142]]
143            )
144        end
145    },
146
147    ["x32002"] = {
148        pdf_version             = 1.3,
149        format_name             = "PDF/X-3:2002",
150        xmp_file                = "lpdf-pdx.xml",
151        gts_flag                = "GTS_PDFX",
152        gray_scale              = true,
153        cmyk_colors             = true,
154        rgb_colors              = true,
155        calibrated_rgb_colors   = true,
156        spot_colors             = true,
157        cielab_colors           = true,
158        internal_icc_profiles   = true,
159        include_intents         = true,
160        include_cidsets         = true,
161        include_charsets        = true,
162        include_procsets        = true,
163        attachments             = false,
164        inject_metadata         = function()
165            addtoinfo("GTS_PDFXVersion","PDF/X-3:2002")
166        end
167    },
168
169    ["x32003"] = {
170        pdf_version             = 1.4,
171        format_name             = "PDF/X-3:2003",
172        xmp_file                = "lpdf-pdx.xml",
173        gts_flag                = "GTS_PDFX",
174        gray_scale              = true,
175        cmyk_colors             = true,
176        rgb_colors              = true,
177        calibrated_rgb_colors   = true,
178        spot_colors             = true,
179        cielab_colors           = true,
180        internal_icc_profiles   = true,
181        include_intents         = true,
182        jbig2_compression       = true,
183        include_cidsets         = true,
184        include_charsets        = true,
185        include_procsets        = true,
186        attachments             = false,
187        inject_metadata         = function()
188            addtoinfo("GTS_PDFXVersion","PDF/X-3:2003")
189        end
190    },
191
192    ["x4"] = {
193        pdf_version             = 1.6,
194        format_name             = "PDF/X-4",
195        xmp_file                = "lpdf-pdx.xml",
196        gts_flag                = "GTS_PDFX",
197        gray_scale              = true,
198        cmyk_colors             = true,
199        rgb_colors              = true,
200        calibrated_rgb_colors   = true,
201        spot_colors             = true,
202        cielab_colors           = true,
203        internal_icc_profiles   = true,
204        include_intents         = true,
205        optional_content        = true,
206        transparency            = true,
207        jbig2_compression       = true,
208        jpeg2000_compression    = true,
209        object_compression      = true,
210        include_cidsets         = true,
211        include_charsets        = true,
212        include_procsets        = true,
213        attachments             = false,
214        inject_metadata         = function()
215            replacexmpinfo(
216                "xml://rdf:RDF/pdfaid-placeholder",
217[[
218<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'>
219      <pdfxid:GTS_PDFXVersion>PDF/X-4</pdfxid:GTS_PDFXVersion>
220    </rdf:Description>
221]]
222                )
223            insertxmpinfo(
224                "xml://rdf:Description/xmpMM:InstanceID",
225                [[<xmpMM:VersionID>1</xmpMM:VersionID>]],
226                false
227            )
228            insertxmpinfo(
229                "xml://rdf:Description/xmpMM:InstanceID",
230                [[<xmpMM:RenditionClass>default</xmpMM:RenditionClass>]],
231                false
232            )
233        end
234    },
235
236    ["x4p"] = {
237        pdf_version             = 1.6,
238        format_name             = "PDF/X-4p",
239        xmp_file                = "lpdf-pdx.xml",
240        gts_flag                = "GTS_PDFX",
241        gray_scale              = true,
242        cmyk_colors             = true,
243        rgb_colors              = true,
244        calibrated_rgb_colors   = true,
245        spot_colors             = true,
246        cielab_colors           = true,
247        internal_icc_profiles   = true,
248        external_icc_profiles   = true,
249        include_intents         = true,
250        optional_content        = true,
251        transparency            = true,
252        jbig2_compression       = true,
253        jpeg2000_compression    = true,
254        object_compression      = true,
255        include_cidsets         = true,
256        include_charsets        = true,
257        include_procsets        = true,
258        attachments             = false,
259        inject_metadata         = function()
260            replacexmpinfo(
261                "xml://rdf:RDF/pdfaid-placeholder",
262[[
263<rdf:Description rdf:about='' xmlns:pdfxid='http://www.npes.org/pdfx/ns/id/'>
264      <pdfxid:GTS_PDFXVersion>PDF/X-4p</pdfxid:GTS_PDFXVersion>
265    </rdf:Description>
266]]
267            )
268            insertxmpinfo(
269                "xml://rdf:Description/xmpMM:InstanceID",
270                [[<xmpMM:VersionID>1</xmpMM:VersionID>]],
271                false
272            )
273            insertxmpinfo(
274                "xml://rdf:Description/xmpMM:InstanceID",
275                [[<xmpMM:RenditionClass>default</xmpMM:RenditionClass>]],
276                false
277            )
278        end
279    },
280
281    ["x5g"] = {
282        pdf_version             = 1.6,
283        format_name             = "PDF/X-5g",
284        xmp_file                = "lpdf-pdx.xml",
285        gts_flag                = "GTS_PDFX",
286        gray_scale              = true,
287        cmyk_colors             = true,
288        rgb_colors              = true,
289        calibrated_rgb_colors   = true,
290        spot_colors             = true,
291        cielab_colors           = true,
292        internal_icc_profiles   = true,
293        include_intents         = true,
294        open_prepress_interface = true,
295        optional_content        = true,
296        transparency            = true,
297        jbig2_compression       = true,
298        jpeg2000_compression    = true,
299        object_compression      = true,
300        include_cidsets         = true,
301        include_charsets        = true,
302        include_procsets        = true,
303        attachments             = false,
304        inject_metadata         = function()
305            -- todo
306        end
307    },
308
309    ["x5pg"] = {
310        pdf_version             = 1.6,
311        format_name             = "PDF/X-5pg",
312        xmp_file                = "lpdf-pdx.xml",
313        gts_flag                = "GTS_PDFX",
314        gray_scale              = true,
315        cmyk_colors             = true,
316        rgb_colors              = true,
317        calibrated_rgb_colors   = true,
318        spot_colors             = true,
319        cielab_colors           = true,
320        internal_icc_profiles   = true,
321        external_icc_profiles   = true,
322        include_intents         = true,
323        open_prepress_interface = true,
324        optional_content        = true,
325        transparency            = true,
326        jbig2_compression       = true,
327        jpeg2000_compression    = true,
328        object_compression      = true,
329        include_cidsets         = true,
330        include_charsets        = true,
331        include_procsets        = true,
332        attachments             = false,
333        inject_metadata         = function()
334            -- todo
335        end
336    },
337
338    ["x5n"] = {
339        pdf_version             = 1.6,
340        format_name             = "PDF/X-5n",
341        xmp_file                = "lpdf-pdx.xml",
342        gts_flag                = "GTS_PDFX",
343        gray_scale              = true,
344        cmyk_colors             = true,
345        rgb_colors              = true,
346        calibrated_rgb_colors   = true,
347        spot_colors             = true,
348        cielab_colors           = true,
349        internal_icc_profiles   = true,
350        include_intents         = true,
351        optional_content        = true,
352        transparency            = true,
353        jbig2_compression       = true,
354        jpeg2000_compression    = true,
355        nchannel_colorspace     = true,
356        object_compression      = true,
357        include_cidsets         = true,
358        include_charsets        = true,
359        include_procsets        = true,
360        attachments             = false,
361        inject_metadata         = function()
362            -- todo
363        end
364    },
365
366    ["a1a2005"] = {
367        pdf_version             = 1.4,
368        format_name             = "pdf/a-1a:2005",
369        xmp_file                = "lpdf-pda.xml",
370        gts_flag                = "GTS_PDFA1",
371        gray_scale              = true,
372        cmyk_colors             = true,
373        rgb_colors              = true,
374        spot_colors             = true,
375        calibrated_rgb_colors   = true, -- unknown
376        cielab_colors           = true, -- unknown
377        include_intents         = true,
378        forms                   = true, -- new: forms are allowed (with limitations); no JS,  other restrictions are unknown (TODO)
379        tagging                 = true, -- new: the only difference to PDF/A-1b
380        internal_icc_profiles   = true,
381        include_cidsets         = true,
382        include_charsets        = true,
383        include_procsets        = true,
384        attachments             = false,
385        inject_metadata         = function()
386            replacexmpinfo(
387                "xml://rdf:RDF/pdfaid-placeholder",
388[[
389<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
390      <pdfaid:part>1</pdfaid:part>
391      <pdfaid:conformance>A</pdfaid:conformance>
392    </rdf:Description>
393]]
394            )
395        end
396    },
397
398    ["a1b2005"] = {
399        pdf_version             = 1.4,
400        format_name             = "pdf/a-1b:2005",
401        xmp_file                = "lpdf-pda.xml",
402        gts_flag                = "GTS_PDFA1",
403        gray_scale              = true,
404        cmyk_colors             = true,
405        rgb_colors              = true,
406        spot_colors             = true,
407        calibrated_rgb_colors   = true, -- unknown
408        cielab_colors           = true, -- unknown
409        include_intents         = true,
410        forms                   = true,
411        internal_icc_profiles   = true,
412        include_cidsets         = true,
413        include_charsets        = true,
414        include_procsets        = true,
415        attachments             = false,
416        inject_metadata         = function()
417            replacexmpinfo(
418                "xml://rdf:RDF/pdfaid-placeholder",
419[[
420<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
421      <pdfaid:part>1</pdfaid:part>
422      <pdfaid:conformance>B</pdfaid:conformance>
423    </rdf:Description>
424]]
425            )
426        end
427    },
428
429    -- Only PDF/A Attachments are allowed but we don't check the attachments
430    -- for any quality: they are just blobs.
431    ["a2a"] = {
432        pdf_version             = 1.7,
433        format_name             = "pdf/a-2a",
434        xmp_file                = "lpdf-pda.xml",
435        gts_flag                = "GTS_PDFA1",
436        gray_scale              = true,
437        cmyk_colors             = true,
438        rgb_colors              = true,
439        spot_colors             = true,
440        calibrated_rgb_colors   = true, -- unknown
441        cielab_colors           = true, -- unknown
442        include_intents         = true,
443        forms                   = true,
444        tagging                 = true,
445        internal_icc_profiles   = true,
446        transparency            = true, -- new
447        jbig2_compression       = true,
448        jpeg2000_compression    = true, -- new
449        object_compression      = true, -- new
450     -- include_cidsets         = false,
451     -- include_charsets        = false,
452     -- include_procsets        = false,
453        attachments             = true, -- new
454        inject_metadata         = function()
455            replacexmpinfo(
456                "xml://rdf:RDF/pdfaid-placeholder",
457[[
458<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
459      <pdfaid:part>2</pdfaid:part>
460      <pdfaid:conformance>A</pdfaid:conformance>
461    </rdf:Description>
462]]
463            )
464        end
465    },
466
467    ["a2b"] = {
468        pdf_version             = 1.7,
469        format_name             = "pdf/a-2b",
470        xmp_file                = "lpdf-pda.xml",
471        gts_flag                = "GTS_PDFA1",
472        gray_scale              = true,
473        cmyk_colors             = true,
474        rgb_colors              = true,
475        spot_colors             = true,
476        calibrated_rgb_colors   = true, -- unknown
477        cielab_colors           = true, -- unknown
478        include_intents         = true,
479        forms                   = true,
480        tagging                 = false,
481        internal_icc_profiles   = true,
482        transparency            = true,
483        jbig2_compression       = true,
484        jpeg2000_compression    = true,
485        object_compression      = true,
486     -- include_cidsets         = false,
487     -- include_charsets        = false,
488     -- include_procsets        = false,
489        attachments             = true,
490        inject_metadata         = function()
491            replacexmpinfo(
492                "xml://rdf:RDF/pdfaid-placeholder",
493[[
494<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
495      <pdfaid:part>2</pdfaid:part>
496      <pdfaid:conformance>B</pdfaid:conformance>
497    </rdf:Description>
498]]
499            )
500        end
501    },
502
503    -- This is like the b variant, but it requires Unicode mapping of fonts
504    -- which we do anyway.
505    ["a2u"] = {
506        pdf_version             = 1.7,
507        format_name             = "pdf/a-2u",
508        xmp_file                = "lpdf-pda.xml",
509        gts_flag                = "GTS_PDFA1",
510        gray_scale              = true,
511        cmyk_colors             = true,
512        rgb_colors              = true,
513        spot_colors             = true,
514        calibrated_rgb_colors   = true, -- unknown
515        cielab_colors           = true, -- unknown
516        include_intents         = true,
517        forms                   = true,
518     -- tagging                 = false,
519        internal_icc_profiles   = true,
520        transparency            = true,
521        jbig2_compression       = true,
522        jpeg2000_compression    = true,
523        object_compression      = true,
524     -- include_cidsets         = false,
525     -- include_charsets        = false,
526     -- include_procsets        = false,
527        attachments             = true,
528        inject_metadata         = function()
529            replacexmpinfo(
530                "xml://rdf:RDF/pdfaid-placeholder",
531[[
532<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
533      <pdfaid:part>2</pdfaid:part>
534      <pdfaid:conformance>U</pdfaid:conformance>
535    </rdf:Description>
536]]
537            )
538        end
539    },
540
541    -- Any type of attachment is allowed but we don't check the quality
542    -- of them.
543    ["a3a"] = {
544        pdf_version             = 1.7,
545        format_name             = "pdf/a-3a",
546        xmp_file                = "lpdf-pda.xml",
547        gts_flag                = "GTS_PDFA1",
548        gray_scale              = true,
549        cmyk_colors             = true,
550        rgb_colors              = true,
551        spot_colors             = true,
552        calibrated_rgb_colors   = true, -- unknown
553        cielab_colors           = true, -- unknown
554        include_intents         = true,
555        forms                   = true,
556        tagging                 = true,
557        internal_icc_profiles   = true,
558        transparency            = true,
559        jbig2_compression       = true,
560        jpeg2000_compression    = true,
561        object_compression      = true,
562     -- include_cidsets         = false,
563     -- include_charsets        = false,
564     -- include_procsets        = false,
565        attachments             = true,
566        inject_metadata         = function()
567            replacexmpinfo(
568                "xml://rdf:RDF/pdfaid-placeholder",
569[[
570<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
571      <pdfaid:part>3</pdfaid:part>
572      <pdfaid:conformance>A</pdfaid:conformance>
573    </rdf:Description>
574]]
575            )
576        end
577    },
578
579    ["a3b"] = {
580        pdf_version             = 1.7,
581        format_name             = "pdf/a-3b",
582        xmp_file                = "lpdf-pda.xml",
583        gts_flag                = "GTS_PDFA1",
584        gray_scale              = true,
585        cmyk_colors             = true,
586        rgb_colors              = true,
587        spot_colors             = true,
588        calibrated_rgb_colors   = true, -- unknown
589        cielab_colors           = true, -- unknown
590        include_intents         = true,
591        forms                   = true,
592     -- tagging                 = false,
593        internal_icc_profiles   = true,
594        transparency            = true,
595        jbig2_compression       = true,
596        jpeg2000_compression    = true,
597        object_compression      = true,
598     -- include_cidsets         = false,
599     -- include_charsets        = false,
600     -- include_procsets        = false,
601        attachments             = true,
602        inject_metadata         = function()
603            replacexmpinfo(
604                "xml://rdf:RDF/pdfaid-placeholder",
605[[
606<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
607      <pdfaid:part>3</pdfaid:part>
608      <pdfaid:conformance>B</pdfaid:conformance>
609    </rdf:Description>
610]]
611            )
612        end
613    },
614
615    ["a3u"] = {
616        pdf_version             = 1.7,
617        format_name             = "pdf/a-3u",
618        xmp_file                = "lpdf-pda.xml",
619        gts_flag                = "GTS_PDFA1",
620        gray_scale              = true,
621        cmyk_colors             = true,
622        rgb_colors              = true,
623        spot_colors             = true,
624        calibrated_rgb_colors   = true, -- unknown
625        cielab_colors           = true, -- unknown
626        include_intents         = true,
627        forms                   = true,
628        tagging                 = false,
629        internal_icc_profiles   = true,
630        transparency            = true,
631        jbig2_compression       = true,
632        jpeg2000_compression    = true,
633        object_compression      = true,
634     -- include_cidsets         = false,
635     -- include_charsets        = false,
636     -- include_procsets        = false,
637        attachments             = true,
638        inject_metadata         = function()
639            replacexmpinfo(
640                "xml://rdf:RDF/pdfaid-placeholder",
641[[
642<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
643      <pdfaid:part>3</pdfaid:part>
644      <pdfaid:conformance>U</pdfaid:conformance>
645    </rdf:Description>
646]]
647        )
648        end
649    },
650
651    ["ua1"] = { -- based on PDF/A-3a, but no 'gts_flag'
652        pdf_version             = 1.7,
653        format_name             = "pdf/ua-1",
654        xmp_file                = "lpdf-pua.xml",
655        gray_scale              = true,
656        cmyk_colors             = true,
657        rgb_colors              = true,
658        spot_colors             = true,
659        calibrated_rgb_colors   = true, -- unknown
660        cielab_colors           = true, -- unknown
661        include_intents         = true,
662        forms                   = true,
663        tagging                 = true,
664        internal_icc_profiles   = true,
665        transparency            = true,
666        jbig2_compression       = true,
667        jpeg2000_compression    = true,
668        object_compression      = true,
669     -- include_cidsets         = false,
670     -- include_charsets        = false,
671     -- include_procsets        = false,
672        attachments             = true,
673        inject_metadata         = function()
674            replacexmpinfo(
675                "xml://rdf:RDF/pdfaid-placeholder",
676[[
677<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
678      <pdfaid:part>3</pdfaid:part>
679      <pdfaid:conformance>A</pdfaid:conformance>
680    </rdf:Description>
681]]
682                )
683                replacexmpinfo(
684                    "xml://rdf:RDF/pdfuaid-placeholder",
685[[
686<rdf:Description rdf:about='' xmlns:pdfuaid='http://www.aiim.org/pdfua/ns/id/'>
687      <pdfuaid:part>1</pdfuaid:part>
688      <pdfuaid:rev>2024</pdfuaid:rev>
689    </rdf:Description>
690]]
691            )
692        end
693    },
694
695    ["ua2"] = {
696        pdf_version             = 2.0,
697        format_name             = "pdf/ua-2",
698        xmp_file                = "lpdf-pua.xml",
699        gray_scale              = true,
700        cmyk_colors             = true,
701        rgb_colors              = true,
702        spot_colors             = true,
703        calibrated_rgb_colors   = true, -- unknown
704        cielab_colors           = true, -- unknown
705        include_intents         = true,
706        forms                   = true,
707        tagging                 = true,
708        internal_icc_profiles   = true,
709        transparency            = true,
710        jbig2_compression       = true,
711        jpeg2000_compression    = true,
712        object_compression      = true,
713     -- include_cidsets         = false,
714     -- include_charsets        = false,
715     -- include_procsets        = false,
716        attachments             = true,
717        inject_metadata         = function()
718            replacexmpinfo(
719                "xml://rdf:RDF/pdfaid-placeholder",
720[[
721<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/'>
722      <pdfaid:part>3</pdfaid:part>
723      <pdfaid:conformance>A</pdfaid:conformance>
724    </rdf:Description>
725]]
726            )
727            replacexmpinfo(
728                "xml://rdf:RDF/pdfuaid-placeholder",
729[[
730<rdf:Description rdf:about='' xmlns:pdfuaid='http://www.aiim.org/pdfua/ns/id/'>
731      <pdfuaid:part>2</pdfuaid:part>
732      <pdfuaid:rev>2024</pdfuaid:rev>
733    </rdf:Description>
734]]
735            )
736        end,
737        inject_conformance      = function(indeed)
738            return replacexmpinfo(
739                "xml://rdf:RDF/wtpdf-placeholder",
740indeed and [[
741<rdf:Description rdf:about='' xmlns:pdfd='http://pdfa.org/declarations/'>
742      <pdfd:declarations>
743        <rdf:Bag>
744          <rdf:li rdf:parseType='Resource'>
745             <pdfd:conformsTo>http://pdfa.org/declarations/wtpdf#accessibility1.0</pdfd:conformsTo>
746          </rdf:li>
747          <rdf:li rdf:parseType='Resource'>
748             <pdfd:conformsTo>http://pdfa.org/declarations/wtpdf#reuse1.0</pdfd:conformsTo>
749          </rdf:li>
750        </rdf:Bag>
751      </pdfd:declarations>
752    </rdf:Description>
753]] or ""
754                )
755        end
756    },
757
758}
759
760pdfformats["2"]   = pdfformats["ua2"]
761pdfformats["2.0"] = pdfformats["ua2"]
762
763local formats = utilities.storage.allocate {
764    version = {
765        external_icc_profiles   = 1.4, -- 'p' in name; URL reference of output intent
766        jbig2_compression       = 1.4,
767        jpeg2000_compression    = 1.5, -- not supported yet
768        nchannel_colorspace     = 1.6, -- 'n' in name; n-channel colorspace support
769        open_prepress_interface = 1.3, -- 'g' in name; reference to external graphics
770        optional_content        = 1.5,
771        transparency            = 1.4,
772        object_compression      = 1.5,
773        attachments             = 1.7,
774    },
775    default = {
776        pdf_version             = 1.7,  -- todo: block tex primitive
777        format_name             = "default",
778        xmp_file                = "lpdf-pdx.xml",
779        gray_scale              = true,
780        cmyk_colors             = true,
781        rgb_colors              = true,
782        spot_colors             = true,
783        calibrated_rgb_colors   = true, -- unknown
784        cielab_colors           = true, -- unknown
785        nchannel_colorspace     = true, -- unknown
786        internal_icc_profiles   = true, -- controls profile inclusion
787        external_icc_profiles   = true, -- controls profile inclusion
788        include_intents         = true,
789        open_prepress_interface = true, -- unknown
790        optional_content        = true, -- todo: block at lua level
791        transparency            = true, -- todo: block at lua level
792        jbig2_compression       = true, -- todo: block at lua level (dropped anyway)
793        jpeg2000_compression    = true, -- todo: block at lua level (dropped anyway)
794     -- include_cidsets         = true,
795     -- include_charsets        = true,
796     -- include_procsets        = true,
797        attachments             = true,
798        inject_metadata         = function()
799            -- nothing
800        end
801    },
802    data = pdfformats,
803}
804
805lpdf.formats = formats -- it does not hurt to have this one visible
806
807local filenames = {
808    "colorprofiles.xml",
809    "colorprofiles.lua",
810}
811
812local function locatefile(filename)
813    local fullname = resolvers.findfile(filename,"icc",1,true)
814    if not fullname or fullname == "" then
815        fullname = resolvers.finders.byscheme("loc",filename) -- could be specific to the project
816    end
817    return fullname or ""
818end
819
820local function loadprofile(name,filename)
821    local profile = false
822    local databases = filename and filename ~= "" and settings_to_array(filename) or filenames
823    for i=1,#databases do
824        local filename = locatefile(databases[i])
825        if filename and filename ~= "" then
826            local suffix = file.suffix(filename)
827            local lname = lower(name)
828            if suffix == "xml" then
829                local xmldata = xml.load(filename) -- no need for caching it
830                if xmldata then
831                    profile = xml.filter(xmldata,format('xml://profiles/profile/(info|filename)[lower(text())=="%s"]/../table()',lname))
832                end
833            elseif suffix == "lua" then
834                local luadata = loadfile(filename)
835                luadata = ludata and luadata()
836                if luadata then
837                    profile = luadata[name] or luadata[lname] -- hashed
838                    if not profile then
839                        for i=1,#luadata do
840                            local li = luadata[i]
841                            if lower(li.info) == lname then -- indexed
842                                profile = li
843                                break
844                            end
845                        end
846                    end
847                end
848            end
849            if profile then
850                if next(profile) then
851                    report_backend("profile specification %a loaded from %a",name,filename)
852                    return profile
853                elseif trace_format then
854                    report_backend("profile specification %a loaded from %a but empty",name,filename)
855                end
856                return false
857            end
858        end
859    end
860    report_backend("profile specification %a not found in %a",name,concat(filenames, ", "))
861end
862
863local function urls(url)
864    if not url or url == "" then
865        return nil
866    else
867        local u = pdfarray()
868        for url in gmatch(url,"([^, ]+)") do
869            if find(url,"^http") then
870                u[#u+1] = pdfdictionary {
871                    FS = pdfconstant("URL"),
872                    F  = pdfstring(url),
873                }
874            end
875        end
876        return u
877    end
878end
879
880local function profilename(filename)
881    return lower(file.basename(filename))
882end
883
884local internalprofiles = { }
885local defaultprofiles  = { }
886
887local function handleinternalprofile(s,include)
888    local filename, colorspace = s.filename or "", s.colorspace or ""
889    if filename == "" or colorspace == "" then
890        report_backend("error in internal profile specification: %s",serialize(s,false))
891    else
892        local tag = profilename(filename)
893        local profile = internalprofiles[tag]
894        if not profile then
895            local colorspace = lower(colorspace)
896            if include then
897             -- local fullname = resolvers.findctxfile(filename) or ""
898                local fullname = locatefile(filename)
899                if fullname == "" then
900                    fullname = locatefile("colo-imp-" .. filename)
901                end
902                local channel = channels[colorspace] or nil
903                if fullname == "" then
904                    report_backend("error, couldn't locate profile %a",filename)
905                elseif not channel then
906                    report_backend("error, couldn't resolve channel entry for colorspace %a",colorspace)
907                else
908                    profile = pdfflushstreamfileobject(fullname,pdfdictionary{ N = channel },false) -- uncompressed
909                    internalprofiles[tag] = profile
910                    defaultprofiles[channel] = profile
911                    if trace_format then
912                        report_backend("including %a color profile from %a",colorspace,fullname)
913                    end
914                end
915            else
916                internalprofiles[tag] = true
917                if trace_format then
918                    report_backend("not including %a color profile %a",colorspace,filename)
919                end
920            end
921        end
922        return profile
923    end
924end
925
926function codeinjections.defaultprofile(channel)
927 -- print("get profile",channel,defaultprofiles[channel])
928    return defaultprofiles[channel]
929end
930
931local externalprofiles = { }
932
933local function handleexternalprofile(s,include) -- specification (include ignored here)
934    local name, url, filename, checksum, version, colorspace =
935        s.info or s.filename or "", s.url or "", s.filename or "", s.checksum or "", s.version or "", s.colorspace or ""
936    if false then -- somehow leads to invalid pdf
937        local iccprofile = colors.iccprofile(filename)
938        if iccprofile then
939            name       = name       ~= "" and name       or iccprofile.tags.desc.cleaned       or ""
940            url        = url        ~= "" and url        or iccprofile.tags.dmnd.cleaned       or ""
941            checksum   = checksum   ~= "" and checksum   or file.checksum(iccprofile.fullname) or ""
942            version    = version    ~= "" and version    or iccprofile.header.version          or ""
943            colorspace = colorspace ~= "" and colorspace or iccprofile.header.colorspace       or ""
944        end
945     -- table.print(iccprofile)
946    end
947    if name == "" or url == "" or checksum == "" or version == "" or colorspace == "" or filename == "" then
948        local profile = handleinternalprofile(s)
949        if profile then
950            report_backend("incomplete external profile specification, falling back to internal")
951        else
952            report_backend("error in external profile specification: %s",serialize(s,false))
953        end
954    else
955        local tag = profilename(filename)
956        local profile = externalprofiles[tag]
957        if not profile then
958            local d = pdfdictionary {
959                ProfileName = name,                                     -- not file name!
960                ProfileCS   = colorspace,
961                URLs        = urls(url),                                -- array containing at least one URL
962                CheckSum    = pdfverbose { "<", lower(checksum), ">" }, -- 16byte MD5 hash
963                ICCVersion  = pdfverbose { "<", version, ">"  },        -- bytes 8..11 from the header of the ICC profile, as a hex string
964            }
965            profile = pdfflushobject(d)
966            externalprofiles[tag] = profile
967        end
968        return profile
969    end
970end
971
972local loadeddefaults = { }
973
974local function handledefaultprofile(s,spec) -- specification
975    local filename, colorspace = s.filename or "", lower(s.colorspace or "")
976    if filename == "" or colorspace == "" then
977        report_backend("error in default profile specification: %s",serialize(s,false))
978    elseif not loadeddefaults[colorspace] then
979        local tag = profilename(filename)
980        local n = internalprofiles[tag] -- or externalprofiles[tag]
981        if n == true then -- not internalized
982            report_backend("no default profile %a for colorspace %a",filename,colorspace)
983        elseif n then
984            local a = pdfarray {
985                pdfconstant("ICCBased"),
986                pdfreference(n),
987            }
988             -- used in page /Resources, so this must be inserted at runtime
989            lpdf.adddocumentcolorspace(prefixes[colorspace],pdfreference(pdfflushobject(a)))
990            loadeddefaults[colorspace] = true
991            report_backend("setting %a as default %a color space",filename,colorspace)
992        else
993            report_backend("no default profile %a for colorspace %a",filename,colorspace)
994        end
995    elseif trace_format then
996        report_backend("a default %a colorspace is already in use",colorspace)
997    end
998end
999
1000local loadedintents = { }
1001local intents       = pdfarray()
1002
1003local function handleoutputintent(s,spec)
1004    local url             = s.url or ""
1005    local filename        = s.filename or ""
1006    local name            = s.info or filename
1007    local id              = s.id or ""
1008    local outputcondition = s.outputcondition or ""
1009    local info            = s.info or ""
1010    if name == "" or id == "" then
1011        report_backend("error in output intent specification: %s",serialize(s,false))
1012    elseif not loadedintents[name] then
1013        local tag = profilename(filename)
1014        local internal, external = internalprofiles[tag], externalprofiles[tag]
1015        if internal or external then
1016            local d = {
1017                  Type                      = pdfconstant("OutputIntent"),
1018                  S                         = pdfconstant(spec.gts_flag or "GTS_PDFX"),
1019                  OutputConditionIdentifier = id,
1020                  RegistryName              = url,
1021                  OutputCondition           = outputcondition,
1022                  Info                      = info,
1023            }
1024            if internal and internal ~= true then
1025                d.DestOutputProfile    = pdfreference(internal)
1026            elseif external and external ~= true then
1027                d.DestOutputProfileRef = pdfreference(external)
1028            else
1029                report_backend("omitting reference to profile for intent %a",name)
1030            end
1031            intents[#intents+1] = pdfreference(pdfflushobject(pdfdictionary(d)))
1032            if trace_format then
1033                report_backend("setting output intent to %a with id %a for entry %a",name,id,#intents)
1034            end
1035        else
1036            report_backend("invalid output intent %a",name)
1037        end
1038        loadedintents[name] = true
1039    elseif trace_format then
1040        report_backend("an output intent with name %a is already in use",name)
1041    end
1042end
1043
1044local function handleiccprofile(message,spec,name,filename,how,options,alwaysinclude,gts_flag)
1045    if name and name ~= "" then
1046        local list = settings_to_array(name)
1047        for i=1,#list do
1048            local name = list[i]
1049            local profile = loadprofile(name,filename)
1050            if trace_format then
1051                report_backend("handling %s %a",message,name)
1052            end
1053            if profile then
1054                if formatspecification.cmyk_colors then
1055                    profile.colorspace = profile.colorspace or "CMYK"
1056                else
1057                    profile.colorspace = profile.colorspace or "RGB"
1058                end
1059                local external = formatspecification.external_icc_profiles
1060                local internal = formatspecification.internal_icc_profiles
1061                local include  = formatspecification.include_intents
1062                local always, never = options[variables.always], options[variables.never]
1063                if always or alwaysinclude then
1064                    if trace_format then
1065                        report_backend("forcing internal profiles") -- can make preflight unhappy
1066                    end
1067                 -- internal, external = true, false
1068                    internal, external = not never, false
1069                elseif never then
1070                    if trace_format then
1071                        report_backend("forcing external profiles") -- can make preflight unhappy
1072                    end
1073                    internal, external = false, true
1074                end
1075                if external then
1076                    if trace_format then
1077                        report_backend("handling external profiles cf. %a",name)
1078                    end
1079                    handleexternalprofile(profile,false)
1080                else
1081                    if trace_format then
1082                        report_backend("handling internal profiles cf. %a",name)
1083                    end
1084                    if internal then
1085                        handleinternalprofile(profile,always or include)
1086                    else
1087                        report_backend("no profile inclusion for %a",formatname)
1088                    end
1089                end
1090                how(profile,spec)
1091            elseif trace_format then
1092                report_backend("unknown profile %a",name)
1093            end
1094        end
1095    end
1096end
1097
1098local function flushoutputintents()
1099    if #intents > 0 then
1100        lpdf.addtocatalog("OutputIntents",pdfreference(pdfflushobject(intents)))
1101    end
1102end
1103
1104lpdf.registerdocumentfinalizer(flushoutputintents,2,"output intents")
1105
1106function codeinjections.setformat(s)
1107    local format   = s.format or ""
1108    local level    = tonumber(s.level)
1109    local intent   = s.intent or ""
1110    local profile  = s.profile or ""
1111    local option   = s.option or ""
1112    local filename = s.file or ""
1113    if format ~= "" then
1114        local form = lower(format)
1115              form = gsub(form,"pdf/","")
1116              form = gsub(form,"[^a-z0-9]","")
1117        local spec = pdfformats[form]
1118        if spec then
1119            formatspecification = spec
1120            formatname = spec.format_name
1121            report_backend("setting format %a to %a",format,formatname)
1122            local xmp_file = formatspecification.xmp_file or ""
1123            if xmp_file == "" then
1124                -- weird error
1125            else
1126                codeinjections.setxmpfile(xmp_file)
1127            end
1128            if not level then
1129                level = 3 -- good compromise, default anyway
1130            end
1131            local pdf_version         = spec.pdf_version * 10
1132            local inject_metadata     = spec.inject_metadata
1133            local inject_conformance  = spec.inject_conformance
1134            local majorversion        = math.floor(math.div(pdf_version,10))
1135            local minorversion        = math.floor(math.mod(pdf_version,10))
1136            local objectcompression   = spec.object_compression and pdf_version >= 15
1137            local compresslevel       = level or lpdf.compresslevel() -- keep default
1138            local objectcompresslevel = (objectcompression and (level or lpdf.objectcompresslevel())) or 0
1139            lpdf.setcompression(compresslevel,objectcompresslevel)
1140            lpdf.setversion(majorversion,minorversion)
1141            if objectcompression then
1142                report_backend("forcing pdf version %s.%s, compression level %s, object compression level %s",
1143                    majorversion,minorversion,compresslevel,objectcompresslevel)
1144            elseif compresslevel > 0 then
1145                report_backend("forcing pdf version %s.%s, compression level %s, object compression disabled",
1146                    majorversion,minorversion,compresslevel)
1147            else
1148                report_backend("forcing pdf version %s.%s, compression disabled",
1149                    majorversion,minorversion)
1150            end
1151            --
1152            -- maybe block by pdf version
1153            --
1154            codeinjections.settaggingsupport(formatspecification.tagging)
1155            codeinjections.setattachmentsupport(formatspecification.attachments)
1156            --
1157            -- context.setupcolors { -- not this way
1158            --     cmyk = spec.cmyk_colors and variables.yes or variables.no,
1159            --     rgb  = spec.rgb_colors  and variables.yes or variables.no,
1160            -- }
1161            --
1162            local rgb = spec.rgb_colors  and variables.yes or variables.no
1163            local cmy = spec.cmyk_colors and variables.yes or variables.no
1164            report_backend("permitted colorspaces: rgb %a, cmyk %a",rgb,cmy)
1165         -- token.expandmacro ("colo_force_colormodel",true,rgb,true,cmy)
1166            --
1167            colors.forcesupport(
1168                spec.gray_scale          or false,
1169                spec.rgb_colors          or false,
1170                spec.cmyk_colors         or false,
1171                spec.spot_colors         or false,
1172                spec.nchannel_colorspace or false
1173            )
1174            transparencies.forcesupport(
1175                spec.transparency        or false
1176            )
1177            viewerlayers.forcesupport(
1178                spec.optional_content    or false
1179            )
1180            viewerlayers.setfeatures(
1181                spec.has_order           or false -- new
1182            )
1183            --
1184            -- spec.jbig2_compression    : todo, block in image inclusion
1185            -- spec.jpeg2000_compression : todo, block in image inclusion
1186            --
1187            if type(inject_metadata) == "function" then
1188                inject_metadata()
1189            end
1190            if type(inject_conformance) == "function" then
1191                inject_conformance(environment.arguments.wtpdf)
1192            end
1193            local options = settings_to_hash(option)
1194            handleiccprofile("color profile",spec,profile,filename,handledefaultprofile,options,true)
1195            handleiccprofile("output intent",spec,intent,filename,handleoutputintent,options,false)
1196            if trace_variables then
1197                for k, v in sortedhash(formats.default) do
1198                    local v = formatspecification[k]
1199                    if type(v) ~= "function" then
1200                        report_backend("%a = %a",k,v or false)
1201                    end
1202                end
1203            end
1204            function codeinjections.setformat(noname)
1205                if trace_format then
1206                    report_backend("error, format is already set to %a, ignoring %a",formatname,noname.format)
1207                end
1208            end
1209            -- we could just pass the spec to the backend
1210            lpdf.setincludecidset(spec.include_cidsets)
1211            lpdf.setincludeprocset(spec.include_procsets)
1212        else
1213            report_backend("error, format %a is not supported, valid formats: % | t",
1214                format,table.sortedkeys(formats.data))
1215        end
1216    elseif level then
1217        lpdf.setcompression(level,level)
1218    else
1219        -- we ignore this as we hook it in \everysetupbackend
1220    end
1221end
1222
1223directives.register("backend.format", function(v) -- table !
1224    local tv = type(v)
1225    if tv == "table" then
1226        codeinjections.setformat(v)
1227    elseif tv == "string" then
1228        codeinjections.setformat { format = v }
1229    end
1230end)
1231
1232interfaces.implement {
1233    name      = "setformat",
1234    actions   = codeinjections.setformat,
1235    arguments = { { "*" } }
1236}
1237
1238function codeinjections.getformatoption(key)
1239    return formatspecification and formatspecification[key]
1240end
1241
1242-- function codeinjections.getformatspecification()
1243--     return formatspecification
1244-- end
1245
1246function codeinjections.supportedformats()
1247    local t = { }
1248    for k, v in sortedhash(formats.data) do
1249        t[#t+1] = k
1250    end
1251    return t
1252end
1253
1254-- The following is somewhat cleaner but then we need to flag that there are
1255-- color spaces set so that the page flusher does not optimize the (at that
1256-- moment) still empty array away. So, next(d_colorspaces) should then become
1257-- a different test, i.e. also on flag. I'll add that when we need more forward
1258-- referencing.
1259--
1260-- local function embedprofile = handledefaultprofile
1261--
1262-- local function flushembeddedprofiles()
1263--     for colorspace, filename in next, defaults do
1264--         embedprofile(colorspace,filename)
1265--     end
1266-- end
1267--
1268-- local function handledefaultprofile(s)
1269--     defaults[lower(s.colorspace)] = s.filename
1270-- end
1271--
1272-- lpdf.registerdocumentfinalizer(flushembeddedprofiles,1,"embedded color profiles")
1273