summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/minim-pdf
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/minim-pdf')
-rw-r--r--macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt287
-rw-r--r--macros/luatex/generic/minim-pdf/README36
-rw-r--r--macros/luatex/generic/minim-pdf/minim-languagecodes.lua146
-rw-r--r--macros/luatex/generic/minim-pdf/minim-pdf.doc425
-rw-r--r--macros/luatex/generic/minim-pdf/minim-pdf.lua1114
-rw-r--r--macros/luatex/generic/minim-pdf/minim-pdf.pdfbin0 -> 108362 bytes
-rw-r--r--macros/luatex/generic/minim-pdf/minim-pdf.tex351
7 files changed, 2359 insertions, 0 deletions
diff --git a/macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt b/macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt
new file mode 100644
index 0000000000..4153cd3775
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt
@@ -0,0 +1,287 @@
+ EUROPEAN UNION PUBLIC LICENCE v. 1.2
+ EUPL © the European Union 2007, 2016
+
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
+below) which is provided under the terms of this Licence. Any use of the Work,
+other than as authorised under this Licence is prohibited (to the extent such
+use is covered by a right of the copyright holder of the Work).
+
+The Work is provided under the terms of this Licence when the Licensor (as
+defined below) has placed the following notice immediately following the
+copyright notice for the Work:
+
+ Licensed under the EUPL
+
+or has expressed by any other means his willingness to license under the EUPL.
+
+1. Definitions
+
+In this Licence, the following terms have the following meaning:
+
+- ‘The Licence’: this Licence.
+
+- ‘The Original Work’: the work or software distributed or communicated by the
+ Licensor under this Licence, available as Source Code and also as Executable
+ Code as the case may be.
+
+- ‘Derivative Works’: the works or software that could be created by the
+ Licensee, based upon the Original Work or modifications thereof. This Licence
+ does not define the extent of modification or dependence on the Original Work
+ required in order to classify a work as a Derivative Work; this extent is
+ determined by copyright law applicable in the country mentioned in Article 15.
+
+- ‘The Work’: the Original Work or its Derivative Works.
+
+- ‘The Source Code’: the human-readable form of the Work which is the most
+ convenient for people to study and modify.
+
+- ‘The Executable Code’: any code which has generally been compiled and which is
+ meant to be interpreted by a computer as a program.
+
+- ‘The Licensor’: the natural or legal person that distributes or communicates
+ the Work under the Licence.
+
+- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
+ Licence, or otherwise contributes to the creation of a Derivative Work.
+
+- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
+ the Work under the terms of the Licence.
+
+- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
+ renting, distributing, communicating, transmitting, or otherwise making
+ available, online or offline, copies of the Work or providing access to its
+ essential functionalities at the disposal of any other natural or legal
+ person.
+
+2. Scope of the rights granted by the Licence
+
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+sublicensable licence to do the following, for the duration of copyright vested
+in the Original Work:
+
+- use the Work in any circumstance and for all usage,
+- reproduce the Work,
+- modify the Work, and make Derivative Works based upon the Work,
+- communicate to the public, including the right to make available or display
+ the Work or copies thereof to the public and perform publicly, as the case may
+ be, the Work,
+- distribute the Work or copies thereof,
+- lend and rent the Work or copies thereof,
+- sublicense rights in the Work or copies thereof.
+
+Those rights can be exercised on any media, supports and formats, whether now
+known or later invented, as far as the applicable law permits so.
+
+In the countries where moral rights apply, the Licensor waives his right to
+exercise his moral right to the extent allowed by law in order to make effective
+the licence of the economic rights here above listed.
+
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
+any patents held by the Licensor, to the extent necessary to make use of the
+rights granted on the Work under this Licence.
+
+3. Communication of the Source Code
+
+The Licensor may provide the Work either in its Source Code form, or as
+Executable Code. If the Work is provided as Executable Code, the Licensor
+provides in addition a machine-readable copy of the Source Code of the Work
+along with each copy of the Work that the Licensor distributes or indicates, in
+a notice following the copyright notice attached to the Work, a repository where
+the Source Code is easily and freely accessible for as long as the Licensor
+continues to distribute or communicate the Work.
+
+4. Limitations on copyright
+
+Nothing in this Licence is intended to deprive the Licensee of the benefits from
+any exception or limitation to the exclusive rights of the rights owners in the
+Work, of the exhaustion of those rights or of other applicable limitations
+thereto.
+
+5. Obligations of the Licensee
+
+The grant of the rights mentioned above is subject to some restrictions and
+obligations imposed on the Licensee. Those obligations are the following:
+
+Attribution right: The Licensee shall keep intact all copyright, patent or
+trademarks notices and all notices that refer to the Licence and to the
+disclaimer of warranties. The Licensee must include a copy of such notices and a
+copy of the Licence with every copy of the Work he/she distributes or
+communicates. The Licensee must cause any Derivative Work to carry prominent
+notices stating that the Work has been modified and the date of modification.
+
+Copyleft clause: If the Licensee distributes or communicates copies of the
+Original Works or Derivative Works, this Distribution or Communication will be
+done under the terms of this Licence or of a later version of this Licence
+unless the Original Work is expressly distributed only under this version of the
+Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
+(becoming Licensor) cannot offer or impose any additional terms or conditions on
+the Work or Derivative Work that alter or restrict the terms of the Licence.
+
+Compatibility clause: If the Licensee Distributes or Communicates Derivative
+Works or copies thereof based upon both the Work and another work licensed under
+a Compatible Licence, this Distribution or Communication can be done under the
+terms of this Compatible Licence. For the sake of this clause, ‘Compatible
+Licence’ refers to the licences listed in the appendix attached to this Licence.
+Should the Licensee's obligations under the Compatible Licence conflict with
+his/her obligations under this Licence, the obligations of the Compatible
+Licence shall prevail.
+
+Provision of Source Code: When distributing or communicating copies of the Work,
+the Licensee will provide a machine-readable copy of the Source Code or indicate
+a repository where this Source will be easily and freely available for as long
+as the Licensee continues to distribute or communicate the Work.
+
+Legal Protection: This Licence does not grant permission to use the trade names,
+trademarks, service marks, or names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the copyright notice.
+
+6. Chain of Authorship
+
+The original Licensor warrants that the copyright in the Original Work granted
+hereunder is owned by him/her or licensed to him/her and that he/she has the
+power and authority to grant the Licence.
+
+Each Contributor warrants that the copyright in the modifications he/she brings
+to the Work are owned by him/her or licensed to him/her and that he/she has the
+power and authority to grant the Licence.
+
+Each time You accept the Licence, the original Licensor and subsequent
+Contributors grant You a licence to their contributions to the Work, under the
+terms of this Licence.
+
+7. Disclaimer of Warranty
+
+The Work is a work in progress, which is continuously improved by numerous
+Contributors. It is not a finished work and may therefore contain defects or
+‘bugs’ inherent to this type of development.
+
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis
+and without warranties of any kind concerning the Work, including without
+limitation merchantability, fitness for a particular purpose, absence of defects
+or errors, accuracy, non-infringement of intellectual property rights other than
+copyright as stated in Article 6 of this Licence.
+
+This disclaimer of warranty is an essential part of the Licence and a condition
+for the grant of any rights to the Work.
+
+8. Disclaimer of Liability
+
+Except in the cases of wilful misconduct or damages directly caused to natural
+persons, the Licensor will in no event be liable for any direct or indirect,
+material or moral, damages of any kind, arising out of the Licence or of the use
+of the Work, including without limitation, damages for loss of goodwill, work
+stoppage, computer failure or malfunction, loss of data or any commercial
+damage, even if the Licensor has been advised of the possibility of such damage.
+However, the Licensor will be liable under statutory product liability laws as
+far such laws apply to the Work.
+
+9. Additional agreements
+
+While distributing the Work, You may choose to conclude an additional agreement,
+defining obligations or services consistent with this Licence. However, if
+accepting obligations, You may act only on your own behalf and on your sole
+responsibility, not on behalf of the original Licensor or any other Contributor,
+and only if You agree to indemnify, defend, and hold each Contributor harmless
+for any liability incurred by, or claims asserted against such Contributor by
+the fact You have accepted any warranty or additional liability.
+
+10. Acceptance of the Licence
+
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
+placed under the bottom of a window displaying the text of this Licence or by
+affirming consent in any other similar way, in accordance with the rules of
+applicable law. Clicking on that icon indicates your clear and irrevocable
+acceptance of this Licence and all of its terms and conditions.
+
+Similarly, you irrevocably accept this Licence and all of its terms and
+conditions by exercising any rights granted to You by Article 2 of this Licence,
+such as the use of the Work, the creation by You of a Derivative Work or the
+Distribution or Communication by You of the Work or copies thereof.
+
+11. Information to the public
+
+In case of any Distribution or Communication of the Work by means of electronic
+communication by You (for example, by offering to download the Work from a
+remote location) the distribution channel or media (for example, a website) must
+at least provide to the public the information requested by the applicable law
+regarding the Licensor, the Licence and the way it may be accessible, concluded,
+stored and reproduced by the Licensee.
+
+12. Termination of the Licence
+
+The Licence and the rights granted hereunder will terminate automatically upon
+any breach by the Licensee of the terms of the Licence.
+
+Such a termination will not terminate the licences of any person who has
+received the Work from the Licensee under the Licence, provided such persons
+remain in full compliance with the Licence.
+
+13. Miscellaneous
+
+Without prejudice of Article 9 above, the Licence represents the complete
+agreement between the Parties as to the Work.
+
+If any provision of the Licence is invalid or unenforceable under applicable
+law, this will not affect the validity or enforceability of the Licence as a
+whole. Such provision will be construed or reformed so as necessary to make it
+valid and enforceable.
+
+The European Commission may publish other linguistic versions or new versions of
+this Licence or updated versions of the Appendix, so far this is required and
+reasonable, without reducing the scope of the rights granted by the Licence. New
+versions of the Licence will be published with a unique version number.
+
+All linguistic versions of this Licence, approved by the European Commission,
+have identical value. Parties can take advantage of the linguistic version of
+their choice.
+
+14. Jurisdiction
+
+Without prejudice to specific agreement between parties,
+
+- any litigation resulting from the interpretation of this License, arising
+ between the European Union institutions, bodies, offices or agencies, as a
+ Licensor, and any Licensee, will be subject to the jurisdiction of the Court
+ of Justice of the European Union, as laid down in article 272 of the Treaty on
+ the Functioning of the European Union,
+
+- any litigation arising between other parties and resulting from the
+ interpretation of this License, will be subject to the exclusive jurisdiction
+ of the competent court where the Licensor resides or conducts its primary
+ business.
+
+15. Applicable Law
+
+Without prejudice to specific agreement between parties,
+
+- this Licence shall be governed by the law of the European Union Member State
+ where the Licensor has his seat, resides or has his registered office,
+
+- this licence shall be governed by Belgian law if the Licensor has no seat,
+ residence or registered office inside a European Union Member State.
+
+Appendix
+
+‘Compatible Licences’ according to Article 5 EUPL are:
+
+- GNU General Public License (GPL) v. 2, v. 3
+- GNU Affero General Public License (AGPL) v. 3
+- Open Software License (OSL) v. 2.1, v. 3.0
+- Eclipse Public License (EPL) v. 1.0
+- CeCILL v. 2.0, v. 2.1
+- Mozilla Public Licence (MPL) v. 2
+- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
+ works other than software
+- European Union Public Licence (EUPL) v. 1.1, v. 1.2
+- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
+ Reciprocity (LiLiQ-R+).
+
+The European Commission may update this Appendix to later versions of the above
+licences without producing a new version of the EUPL, as long as they provide
+the rights granted in Article 2 of this Licence and protect the covered Source
+Code from exclusive appropriation.
+
+All other changes or additions to this Appendix require the production of a new
+EUPL version.
diff --git a/macros/luatex/generic/minim-pdf/README b/macros/luatex/generic/minim-pdf/README
new file mode 100644
index 0000000000..1d1f02a913
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/README
@@ -0,0 +1,36 @@
+Version: 2021/1.0
+
+SUMMARY
+
+This package adds low-level support to plain LuaTeX for marking up the
+structure of a PDF document. The implementation is rather basic, but should
+allow you to make your PDFs fully PDF/A-compliant.
+
+This encompasses the following features:
+
+ - Creating Tagged PDF
+ - Marking hyperlinks, bookmarks and page labels
+ - Marking spaces and (implicit) hyphens
+ - Embedding associated files
+ - Declaring PDF/A conformance
+
+Support for XMP data inclusion (another requirement of PDF/A) is provided by
+a separate package, minim-xmp.
+
+
+DOCUMENTATION
+
+In order to obtain the typeset manual of this package, simply say:
+
+ luatex minim-pdf.doc
+
+
+COPYING
+
+(c) 2021 Esger Renkema
+
+These files may be distributed under the terms of the European Union Public
+Licence (EUPL) version 1.2 or later. A copy can be obtained at:
+
+https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+
diff --git a/macros/luatex/generic/minim-pdf/minim-languagecodes.lua b/macros/luatex/generic/minim-pdf/minim-languagecodes.lua
new file mode 100644
index 0000000000..df3e95a65e
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/minim-languagecodes.lua
@@ -0,0 +1,146 @@
+
+-- Associating language codes with language names used with \uselanguage.
+
+return {
+ afrikaans = 'af',
+ akkadian = 'akk',
+ american = 'en-US',
+ amharic = 'am',
+ ancientgreek = 'grc',
+ arabic = 'ar',
+ aramaic = 'arc',
+ armenian = 'hy',
+ assamese = 'as',
+ avestan = 'ae',
+ basque = 'eu',
+ belarusian = 'be',
+ bengali = 'bn',
+ biblicalhebrew = 'hbo',
+ bohairic = 'cop-x-bohairic',
+ bokmal = 'nb',
+ bosnian = 'bs',
+ brazilian = 'pt-BR',
+ breton = 'br',
+ british = 'en-UK',
+ bulgarian = 'bg',
+ burmese = 'my',
+ byzantine = 'gkm',
+ cantonese = 'yue',
+ catalan = 'ca',
+ chechen = 'ce',
+ cherokee = 'chr',
+ chinese = 'zh',
+ churchlatin = 'la-x-liturgic',
+ churchslavonic = 'cu',
+ classiclatin = 'la-x-classic',
+ classictibetan = 'xtc',
+ coptic = 'cop',
+ croatian = 'hr',
+ czech = 'cs',
+ danish = 'da',
+ dari = 'prs',
+ dutch = 'nl',
+ english = 'en',
+ espanol = 'es',
+ esperanto = 'eo',
+ estonian = 'et',
+ ethiopic = 'mul-ethi',
+ etruscan = 'ett',
+ farsi = 'fa',
+ finnish = 'fi',
+ flemish = 'nl-BE',
+ francais = 'fr',
+ french = 'fr',
+ frisian = 'fy',
+ friulan = 'fur',
+ galician = 'gl',
+ geez = 'gez',
+ georgian = 'ka',
+ german = 'de',
+ ['german-x-latest'] = 'de',
+ gothic = 'got',
+ greek = 'el',
+ gujarati = 'gu',
+ hebrew = 'he',
+ hindi = 'hi',
+ hittite = 'hit',
+ hungarian = 'hu',
+ icelandic = 'is',
+ indonesian = 'id',
+ interlingua = 'ia',
+ irish = 'ga',
+ italian = 'it',
+ japanese = 'ja',
+ kannada = 'kn',
+ koine = 'grc-x-koine',
+ kurmanji = 'kmr',
+ latin = 'la',
+ latvian = 'lv',
+ lithuanian = 'lt',
+ liturgicallatin = 'la-x-liturgic',
+ malayalam = 'ml',
+ marathi = 'mr',
+ medievalgreek = 'gkm',
+ medievallatin = 'la-x-medieval',
+ mongolian = 'mn-cyrl',
+ mongolianlmc = 'mn-cyrl-x-lmc',
+ monogreek = 'el-monoton',
+ multiple = 'mul',
+ ngerman = 'de-1996',
+ ['ngerman-x-latest'] = 'de-1996',
+ nohyph = 'und',
+ nolang = 'zxx',
+ norsk = 'nb',
+ norwegian = 'nb',
+ nynorsk = 'nn',
+ occitan = 'oc',
+ oriya = 'or',
+ pali = 'pi',
+ patois = 'fr',
+ persian = 'fa',
+ piedmontese = 'pms',
+ pinyin = 'zh-latn-pinyin',
+ polish = 'pl',
+ polygreek = 'el-polyton',
+ portuges = 'pt',
+ portuguese = 'pt',
+ punjabi = 'pa',
+ romanian = 'ro',
+ romansh = 'rm',
+ russian = 'ru',
+ sahidic = 'cop-x-sahidic',
+ samaritan = 'smp',
+ sanskrit = 'sa',
+ serbian = 'sr-latn',
+ serbianc = 'sr-cyrl',
+ slovak = 'sk',
+ slovene = 'sl',
+ slovenian = 'sl',
+ spanish = 'es',
+ sumerian = 'sux',
+ swedish = 'sv',
+ swissgerman = 'de-CH',
+ syriac = 'syr',
+ talmudic = 'tmr',
+ tamil = 'ta',
+ telugu = 'te',
+ thai = 'th',
+ tibetan = 'bo',
+ turkish = 'tr',
+ turkmen = 'tk',
+ ugaritic = 'uga',
+ uncoded = 'mis',
+ undetermined = 'und',
+ ukenglish = 'en-UK',
+ UKenglish = 'en-UK',
+ ukrainian = 'uk',
+ uppersorbian = 'hsb',
+ usenglish = 'en-US',
+ USenglish = 'en-US',
+ usenglishmax = 'en-US',
+ vedic = 'sa-vaidika',
+ walloon = 'wa',
+ welsh = 'cy',
+ yiddish = 'yi',
+}
+
diff --git a/macros/luatex/generic/minim-pdf/minim-pdf.doc b/macros/luatex/generic/minim-pdf/minim-pdf.doc
new file mode 100644
index 0000000000..2c06dd25b0
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/minim-pdf.doc
@@ -0,0 +1,425 @@
+
+\input minim-doc.sty
+
+\manual{minim-pdf}
+
+\ifchapter \chapter Hypertext
+
+This chapter and the next document the support of the modern pdf features
+provided by the minim-pdf package.
+Load it by saying ⟦\input minim-pdf⟧.
+The next chapter concerns the creation of tagged pdf;
+all other features of the package are described here.
+
+\else \startmetadata
+ author {Esger Renkema}
+ title {minim-pdf}
+ date {2021-06-01}
+ version {2021/1.0}
+ keywords {LuaTeX; Plain TeX; PDF/A; Tagged PDF; accessibility; a11y;
+ hypertext; bookmarks; document outline; associated files}
+ stopmetadata
+\maketitle
+
+This package adds low-level support to plain luatex for marking up the
+structure of a pdf document. The implementation is rather basic, but should
+allow you to make your pdfs fully pdf/a-compliant.
+Load the package by saying ⟦\input minim-pdf⟧.
+
+The creation of tagged pdf will be described in the second half of this manual;
+all other features will be covered first.
+
+\fi
+
+\section Hyperlinks % 1
+
+For most simple cases, you can use
+⟦*\hyperlink [name {...} | url {...}] ... \endlink⟧ for linking to a named
+destination in your own document or to an external hyperlink respectively.
+There is no support for nonsimple cases.
+
+A named destination can be created with ⟦*\nameddestination {...}⟧ (also in
+horizontal mode, unlike the backend primitive) and if you cannot think of
+a name, ⟦*\newdestinationname⟧ should generate a unique one.
+If you need the latter twice, ⟦*\lastdestinationname⟧ gives the last
+generated name.
+
+
+\section Bookmarks % 1
+
+Bookmarks can be added with ⟦*\outline [open] [dest {name}] {title}⟧.
+Add ⟦open⟧ to have the bookmark appear initially open and
+say ⟦dest {name}⟧ for having it refer to a specific named destination
+(otherwise, a new one will be created where the ⟦\outline⟧ command appears).
+
+A bookmark is automatically associated with the current structure element and
+the hierarchy of structure elements determines the nesting of bookmarks.
+Therefore, if you want nested bookmarks, you \emph{must} precede the ⟦\outline⟧
+command with a declaration of the current structure element, even if you have
+otherwise disabled tagging. See the next chapter on how to do this.
+
+
+\section Page labels % 1
+
+If the page numbers of your document are not a simple sequence starting with~1,
+you can use ⟦*\setpagelabels [pre {prefix}] style nr⟧ for communicating
+this to the pdf viewer.
+This command affects the page labels from the next page on: ⟦nr⟧ should be the
+numerical page number of that page.
+The ⟦prefix⟧ is prepended to each number and the
+⟦style⟧ must be one of ⟦decimal⟧, ⟦roman⟧, ⟦Roman⟧, ⟦alphabetic⟧, ⟦Alphabetic⟧
+or ⟦none⟧. In the last case, only the prefix is used.
+
+
+\section PDF/A % 1
+
+You can declare pdf/a conformance with ⟦*\pdfalevel xy⟧, with version $\tt
+x ∈ \{1,2,3\}$ and conformance level $\tt y ∈ \{a,b,u\}$. This will set the
+correct pdf version and ⟦pdfaid⟧ metadata. If the conformance level is ‘⟦a⟧’,
+tagging will be enabled (see the next chapter). Finally, a default RGB colour
+profile will be included.
+The conformance level can be queried from the ⟦*\pdfaconformancelevel⟧ register.
+
+Note that merely declaring conformance will not make your document pdf/a
+compliant, and that minim will not warn you if it is not.
+However, the features described in this chapter and the next should be enough
+to make pdf/a compliance possible.
+
+Also note that there currently is no documented way of choosing a different
+colour profile from the default (i.e. the default rgb profile provided by the
+colorprofiles package).
+Should you need do that, you will have to do so manually, after
+disabling the automatic inclusion by saying
+⟦\expandafter\let \csname minim:default:rgb:profile\endcsname = \relax⟧.
+
+Finally, note that pdf/a requires that spaces are represented by actual space
+characters and that discretionary hyphens are marked as soft hyphens
+(⟦U+00AD⟧).
+Since both features benefit accessibility and text extraction in general, they
+are enabled by default.
+You can disable them by setting ⟦*\writehyphensandspaces⟧ to a nonpositive value.
+
+
+\section Embedded files % 1
+
+You can attach (associate) files with ⟦*\embedfile <options>⟧.
+The file will be attached to the current structure element (see the next
+chapter) unless the ⟦global⟧ option is given: then it will be added to the
+document catalog.
+Arguments consisting of a single word can be given without braces and
+exactly one of the options ⟦file⟧ or ⟦string⟧ must be present.
+
+\smallskip\marktableaslist \halign{#\quad\hfil&#\hfil\cr
+⟦file ⟧\hfill⟦{...}⟧ & The file to embed.\cr
+⟦string ⟧\hfill⟦{...}⟧ & The string to embed.\cr
+⟦global⟧ & Attach to the document catalog.\cr
+⟦uncompressed⟧ & Do not compress the file stream.\cr
+⟦mimetype ⟧\hfill⟦{...}⟧ & The file’s mime type.\cr
+⟦moddate ⟧\hfill⟦{...}⟧\rlap{*} & The modification date (see * below).\cr
+⟦desc ⟧\hfill⟦{...}⟧ & A description (the ⟦/Desc⟧ key).\cr
+⟦relation ⟧\hfill⟦{...}⟧ & The ⟦/AFRelationship⟧ value as defined in pdf/a-3.\cr
+⟦name ⟧\hfill⟦{...}⟧ & The file name (only required when writing a ⟦string⟧).\cr }
+
+* The modification date must be of the form ⟦yyyy[-m[m][-d[d]]]⟧. A default
+moddate can be set with ⟦*\setembeddedfilesmoddate {default}⟧.
+The ⟦default⟧ date will be expanded fully at the time of embedding.
+With the minim-xmp package, a useful setting is
+⟦\setembeddedfilesmoddate {\getmetadata date}⟧.
+
+\section Lua module % 1
+
+The interface of the lua module (available via
+⟦local M = require('minim-pdf')⟧) is not stable yet, and may change.
+One function of interest, however, is
+⟦M.pdf_string(...)⟧, wich converts a lua string to a pdf string. The
+surrounding ⟦<>⟧ or ⟦()⟧ characters are included in the return value.
+
+
+% 
+
+\chapter Tagged PDF
+
+This chapter is a continuation of the previous and describes the parts of
+minim-pdf that concern the creation of tagged pdf. All features in this chapter
+must be explicitly enabled by setting ⟦*\writedocumentstructure⟧ to a positive
+value. This will be done automatically if you declare pdf/a conformance (see
+above).
+
+This part of the package is rather low-level and this chapter rather technical.
+For a more general introduction to and discussion of tagged pdf, please read
+the (excellent) manual of latex’s tagpdf package.
+
+
+\section Purpose, limitations and pitfalls % 1
+
+The main purpose of this package is semi-automatically marking up the
+(hierarchical) structure of your document, thereby creating so-called tagged pdf.
+The mechanism presented here is not quite as versatile as the pdf format
+allows. The most important restriction is that all content of the document must
+be seen by tex’s stomach in the \emph{logical} order.
+
+Furthermore, while the macros in this package are sophisticated enough that
+tagging can be done without any manual intervantion, it is quite possible and
+rather easy to generate the wrong document structore, or even cause syntax
+errors in the resulting pdf code.
+You should always check the result in an external application.
+
+This is the full list of limitations, pitfalls and shortcomings:
+\startlist
+\item1. Document content must be seen by tex in its logical order (although you
+can mark out-of-order content explicitly if you know what you are doing; see
+below).
+\item2. Artifacts cannot be split across pages. A pagebreak inmidst an artifact
+will cause incorrect pdf without error or warning.
+\item3. The contents of ⟦\localleftbox⟧ and ⟦\localrightbox⟧ must be marked
+manually, probably as artifact.
+\item4. You must mark page header, page footer and footnote rule yourself; no
+default is set.
+\item5. There currently is no way of marking xforms or other pdf objects as
+content items of themselves.
+\item6. The content of xforms (i.e. pdf objects created by ⟦\useboxresource⟧)
+should not contain tagging commands.
+\item7. Likewise, you should be careful with box reuse: it might work, but you
+should check.
+\item8. The use of structure element attributes is currently not supported
+except in a limited number of cases.
+\item9. This package currently only supports pdf~1.7 tagging and is not yet
+ready for use with pdf~2.0.
+\stoplist
+
+In order to help you debugging, some errors will refer you to the resulting pdf
+file. If you get such errors, decompress the pdf and search for the string
+‘Warning:’. It will appear in the pdf stream at the exact spot the problem
+occurs.
+
+
+\section General overview % 1
+
+When speaking about tagging, we have to do with two (or perhaps three) separate
+and orthogonal tagging processes.
+The first is the creation of a hierarchical \emph{document structure}, made up
+of \emph{structure elements} (SEs). The document structure describes the
+logical structure of a document, made up of chapters, paragraphs, references
+etc.
+The second tagging process is the tagging of \emph{marked content items}
+(MCIs): this is the partition of the actual page contents into (disjoint)
+blocks that can be assigned to the proper structure element.
+Finally, as a separate process, some parts of the page can be marked as
+\emph{artifacts}, excluding their content from both content and structure
+tagging.
+
+When using this package, artifacts and structure elements (excluding
+paragraphs; see below) must be marked explicitly, while marked content items
+will be created, marked and assigned automatically.
+There is some (partial and optional) logic for automatically arranging
+structure elements in their correct hierarchical relation.
+
+The mechanism through which this is achieved uses attributes and whatsits for
+marking the contents and borders of SEs, MCIs and artifacts. At the end of the
+output routine, just before the pdf page is assembled, this information will be
+converted into markers inserted in the pdf stream.
+
+
+\section Marked content items % 1
+
+Content items are automatically delineated at page, artifact and structure
+element boundaries and terminated at paragraph or display skips.
+This should relieve you from any manual intervention. However, if you run into
+problems, the commands below might be helpful.
+
+Use of ⟦ActualText⟧, ⟦Alt⟧ or ⟦Lang⟧ attribute on MCIs, while allowed by the
+pdf standard, is not supported by this package. You should set these on the
+structure element instead.
+
+The begining and ending of a content item can be forced with
+⟦*\startcontentitem⟧ and ⟦*\stopcontentitem⟧, while ⟦*\ensurecontentitem⟧ will
+only open a new content item if you are currently outside any.
+If you need some part to be a single content item, that can use
+⟦*\startsinglecontentitem ... \stopsinglecontentitem⟧.
+This will disable all SE and MCI tagging inside.
+
+Tagging (both of MCIs and SEs) can be disabled and re-enabled locally with
+⟦*\stoptagging⟧ and ⟦*\starttagging⟧.
+
+
+\section Artifacts % 1
+
+Artifacts can be marked in two ways: with ⟦*\markartifact {type} {...}⟧ or with
+⟦*\startartifact {type} ... \stopartifact⟧.
+The ⟦type⟧ is written to the pdf attribute dictionary directly, so that if you
+need a subtype, you can write e.g.
+⟦\startartifact {Pagination /Subtype/Header} etc⟧.
+Do make sure your artifact does not contain a page break, as this will result
+in invalid output.
+
+Inside artifacts, other structure content markers will be ignored. Furthermore,
+this package makes sure artifacts are never part of marked content items,
+automatically closing and re-opening content items before and after the
+artifact.
+While the pdf standard does not require the latter, not enforcing this seems to
+confuse some pdf software.
+
+
+\section Document structure % 1
+
+Like artifacts, structure elements can be given as ⟦*\markelement {Tag} {...}⟧
+or ⟦*\startelement {Tag} ... \stopelement {Tag}⟧.
+Here, in many cases the ⟦\stopelement⟧ is optional: whenever opening an element
+would cause a nesting of incompatible ⟦Tag⟧s, the current element will be
+closed until such a nesting is possible.
+Thus, opening a ⟦TR⟧ will close the previous ⟦TR⟧, opening an ⟦H1⟧ will
+automatically close any open inline or block structure elements, opening
+a ⟦TOCI⟧ will close all elements up until the current ⟦TOC⟧ etc. etc.
+
+As a special case, the tags ⟦Document⟧, ⟦Part⟧, ⟦Art⟧, ⟦Sect⟧ and ⟦Div⟧ (and
+their aliases) will try and close all open structure elements up to and
+including the last structure element with the same tag.
+(An alias will of course only match the same alias.)
+
+While the above can greatly reduce the effort of tagging, the logic is neither
+perfect nor complete. You should always check the results in an external
+application.
+Particular care should be taken when ‘skipping’ structure levels: the sequence
+chapter – subsection – section will result in the section beneath the subsection.
+If you are in doubt about an element being closed already, you can use
+⟦*\ensurestopelement {Tag}⟧ instead of ⟦\stopelement⟧
+to prevent an error being raised.
+
+All these helpful features can also be disabled by setting
+⟦*\strictstructuretagging⟧ to a positive value. Then, every structure element
+will have to be closed by an explicit closing tag, as in xml.
+In this case, ⟦\stopelement⟧ and ⟦\ensurestopelement⟧ will be equivalent.
+
+By default, ⟦P⟧ structure elements are inserted automatically at the start of
+every paragraph. The tag can be changed with ⟦*\nextpartag {Tag}⟧; leaving the
+argument empty will prevent marking the next paragraph.
+Auto-marking paragraphs can be (locally) disabled or enabled by saying
+⟦*\markparagraphsfalse⟧ or ⟦*\markparagraphstrue⟧.
+
+
+\section Structure element aliases % 1
+
+New structure element tags can be created with
+⟦*\addstructuretype [options] Existing Alias⟧.
+This will create a new structure tag named ⟦Alias⟧ with the same properties as
+⟦Existing⟧.
+The properties can be modified by specifying ⟦options⟧: these will set values
+of the corresponding entry in the ⟦structure_types⟧ table (see the lua source
+file for this package).
+Any aliases you declare will be written to the pdf’s ⟦RoleMap⟧ only if they
+have actually been used.
+
+
+\section Manipulating the logical order % 1
+
+With the process outlined above, the logical order of structure elements has to
+coincide with the order in which the SEs are ‘digested’ by tex.
+This, together with the marked content items being assigned to structure
+elements in their order of appearance, lies behind the restriction that logical
+and processing orders should match.
+
+With manual intervention, this restriction can be relaxed somewhat.
+Issuing the pair ⟦*\savecurrentelement ... \continueelement⟧ will append the
+MCIs following ⟦\continueelement⟧ to the SE containing ⟦\savecurrentelement⟧.
+Since the assignments made here are global, this process cannot be nested;
+in more complicated situations you should therefore use
+⟦*\savecurrentelementto\name ... \continueelementfrom\name⟧ which restores the
+current SE from a named identifier ⟦\name⟧.
+
+
+\section Structure element options % 1
+
+The ⟦\startcontentitem⟧ command allows a few options that are not mentioned
+above: its full syntax is ⟦*\startcontentitem <options> {Tag}⟧.
+The three most useful options are ⟦alt⟧ for setting an alt-text (the ⟦/Alt⟧
+entry in the structure element dicionary), ⟦actual⟧ for a text replacement
+(⟦/ActualText⟧) and ⟦lang⟧ for the language (⟦/Lang⟧; see the next section).
+The alternative and actual texts can also be given after the fact with
+⟦*\setalttext{...}⟧ and ⟦*\setactualtext{...}⟧. These apply to the current
+structure element.
+
+Setting structure element attributes is not supported at this moment, except
+the placement attributes ⟦block⟧ and ⟦inline⟧, which can be given as options.
+
+
+\section Languages % 1
+
+If you do not specify a language code for a structure element, its language
+will be determined automatically.
+In order for this to work, you must associate a language code to every used
+language; you can do so with ⟦*\setlanguagecode name code⟧, where ⟦name⟧ must
+be an identifier used with ⟦\uselanguage {name}⟧ and ⟦code⟧ must be a two or
+three-letter language code, optionally followed by a dialect specification,
+a country code, and/or some other tag.
+Note that the language code is associated to a language \emph{name}, not to the
+numerical value of the ⟦\language⟧ parameter. This allows you to assign
+separate codes to dialects.
+
+There is a small set of default language code associations, which can be found
+in the file ⟦minim-languagecodes.lua⟧.
+It covers most languages defined by the hyph-utf8 package, as well as (due to
+their ubiquitous use) many ancient languages.
+
+An actual language change introduced by ⟦\uselanguage⟧ will not otherwise be
+acted upon by this package. Therefore, you will probably want to add
+⟦\startelement{Span}⟧ after every in-line invocation of ⟦\uselanguage⟧.
+
+You can set the document language with ⟦*\setdocumentlanguage language-code⟧.
+If unset, the language code associated with the first ⟦\uselanguage⟧ statement
+will be used, or else ⟦und⟧ (undetermined). The only function of the document
+language is that it is mentioned in the pdf catalog: it has no other influence.
+
+New languages can be declared with ⟦*\newnamedlanguage {name} {lhm} {rhm}⟧ and
+new dialects with ⟦*\newnameddialect {language name} {dialect name}⟧.
+Dialects will use the same hyphenation patterns (and will indeed have the same
+⟦\language⟧ value) as their parent languages; newly declared languages will
+start with no hyphenation patterns.
+Do note that you will probably also have to specify language codes for new
+languages or dialects.
+
+This package ensures the existence of the ⟦nohyph⟧, ⟦nolang⟧, ⟦uncoded⟧ and
+⟦undetermined⟧ dummy languages, all without hyphenation.
+
+
+\section Helper macros
+
+For marking up an entry in a table of contents, you can use the macro
+⟦*\marktocentry {dest} {lbl} {title} {filler} {pageno}⟧, which should insert all
+tags in the correct way. (The ⟦dest⟧ is a link destination and can be empty;
+the ⟦lbl⟧ is a section number and can also be empty.)
+
+For marking up tables, a whole array of helper macros is available. First,
+⟦*\marktable⟧ should be given \emph{before} the ⟦\halign⟧.
+Then, in the template, the first cell should start with ⟦*\marktablerow⟧ and
+each subsequent cell with ⟦*\marktablecell⟧.
+If your table starts with a header, insert ⟦*\marktableheader⟧ before it and
+⟦*\marktablebody⟧ after.
+Before a table footer, insert ⟦*\marktablefooter⟧.
+
+For greater convenience, insert just ⟦*\automarktable⟧ before the ⟦\halign⟧.
+Then you can leave out all the above commands (unless you ⟦\omit⟧ a template of
+course).
+This assumes the table has a single header row and more than one column.
+If you use a table for typesetting a list, you can use ⟦*\marktableaslist⟧
+instead, which marks the first column as list label and the second column as
+list item. Of course, this only works with two-column tables.
+
+Finally, you can auto-tag equations as formulas by specifing
+⟦*\autotagformulas⟧. This is especially dangerous, because sometimes equations
+are used for lay-out and should not be marked as such.
+After the latter command, auto-tagging can be switched off and on with
+⟦*\stopformulatagging⟧ and ⟦*\startformulatagging⟧.
+
+Both alt and actualtext of the ⟦Formula⟧ structure element will be set to the
+(unexpanded) source code of the equation, surrounded by the appropriate number
+of dollar signs.
+Furthermore, if ⟦\pdfaconformancelevel⟧ equals three, the source of the formula
+will be attached in an embedded file with the ⟦/AFRelation⟧ set to ⟦Source⟧.
+The name of this file can be changed by redefining ⟦*\formulafilename⟧ inside
+the equation.
+
+
+% 
+
+\endmanual
+
diff --git a/macros/luatex/generic/minim-pdf/minim-pdf.lua b/macros/luatex/generic/minim-pdf/minim-pdf.lua
new file mode 100644
index 0000000000..ffe845061c
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/minim-pdf.lua
@@ -0,0 +1,1114 @@
+
+local M = { }
+local alloc = require('minim-alloc')
+local cb = require('minim-callbacks')
+alloc.remember('minim-pdf')
+
+-- 1 helper functions
+
+local function insert_formatted(t, ...)
+ table.insert(t, string.format(...))
+end
+
+local function add_to_catalog(...)
+ pdf.setcatalog((pdf.getcatalog() or '') .. string.format(...) .. ' ')
+end
+
+local function add_to_names(...)
+ pdf.setnames((pdf.getnames() or '') .. string.format(...) .. ' ')
+end
+
+-- in-depth node list traversal
+-- returns current and parent node
+-- only dives into hbox and vbox nodes
+local function full_traverse(head)
+ return function(stack, last)
+ local next = last.next
+ if not next then
+ next, stack.box = table.unpack(table.remove(stack))
+ else
+ while next.list do
+ table.insert(stack, { next, stack.box })
+ next, stack.box = next.list, next
+ end
+ end
+ return next, stack.box
+ end, { { } }, { next = head }
+end
+
+-- re-encode to utf-16 and surround by <>
+local function pdf_hex_string(text)
+ local str = { [1] = '<feff' }
+ for i in text:utfvalues() do
+ if i < 0xFFFF then
+ insert_formatted(str, '%04x', i)
+ else
+ i = i - 0x10000
+ m = math.floor(i/0x400) + 0xD800
+ n = ( i % 0x400 ) + 0xDC00
+ insert_formatted(str, '%04x%04x' ,m, n)
+ end
+ end
+ table.insert(str, '>')
+ return table.concat(str,'')
+end
+
+-- try and produce a () string first, then fall back to <>
+function M.pdf_string(text)
+ local str = { '(' }
+ for i in text:utfvalues() do
+ if i > 0x7E then
+ return pdf_hex_string(text)
+ elseif i < 0x20 then
+ insert_formatted(str, '\\%03o', i)
+ elseif i == 0x28 or i == 0x29 or i == 0x5c then
+ insert_formatted(str, '\\%c', i)
+ else
+ insert_formatted(str, '%c', i)
+ end
+ end
+ table.insert(str, ')')
+ return table.concat(str,'')
+ end
+
+-- make available as \pdfstring{...}
+alloc.luadef('pdfstring', function() M.pdf_string(token.scan_string()) end)
+
+-- try and produce a date of the format (D:YYYYMMDD)
+function M.pdf_date(text)
+ local y, m, d = string.match(text, '^([12][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$')
+ if not y then d, m, y = string.match(text, '^([0-9][0-9]?)-([0-9][0-9]?)-([12][0-9][0-9][0-9])$') end
+ if y then
+ return string.format('(D:%4d%02d%02d)', y, m, d)
+ else
+ return string.format('(D:%s)', text)
+ end
+end
+
+function M.options_scanner()
+ return { scanners = { },
+ add = function(this, name, scanner)
+ this.scanners[name] = scanner
+ return this
+ end,
+ keyword = function(this, name)
+ return this:add(name, function() return true end)
+ end,
+ int = function(this, name)
+ return this:add(name, token.scan_int)
+ end,
+ glue = function(this, name)
+ return this:add(name, token.scan_glue)
+ end,
+ dimen = function(this, name)
+ return this:add(name, token.scan_dimen)
+ end,
+ string = function(this, name)
+ return this:add(name, token.scan_string)
+ end,
+ argument = function(this, name)
+ return this:add(name, token.scan_argument)
+ end,
+ word = function(this, name)
+ return this:add(name, token.scan_word)
+ end,
+ list = function(this, name)
+ return this:add(name, token.scan_list)
+ end,
+ scan = function(this, defaults)
+ local res = defaults or { }
+ repeat
+ local matched = false
+ for name, scanner in pairs(this.scanners) do
+ if token.scan_keyword(name) then
+ matched, this.scanners[name] = true, nil
+ res[name] = scanner()
+ end
+ end
+ until not matched
+ return res
+ end
+ }
+end
+
+local function pdf_err(n, msg)
+ local m = node.new(8,16)
+ m.mode, m.token = 2, '%% Warning: '..msg
+ node.insert_after(n, n, m)
+end
+
+-- 1 infrastructure and bookkeeping
+
+local function tagging_enabled()
+ return tex.count['writedocumentstructure'] > 0
+end
+
+local function spaces_enabled()
+ return tex.count['writehyphensandspaces'] > 0
+end
+
+-- we start with the document at the top level
+local structure = { {
+ index = 1,
+ struct = 'Document',
+ children = { },
+} }
+
+-- the parent tree will be filled automatically and separately
+local parent_tree = { } -- list of list of structure elements
+
+-- Our four helper attributes
+local current_struct = alloc.new_attribute('tagging:current:se')
+local current_order = alloc.new_attribute('tagging:element:order')
+local current_status = alloc.new_attribute('tagging:current:status')
+local current_lang = alloc.new_attribute('tagging:current:language')
+
+local function current_structure_element()
+ return structure[tex.attribute[current_struct]]
+end
+
+-- Marker whatsits
+local marker_whatsit = alloc.new_whatsit('tagged pdf marker')
+
+local function make_whatsit(t, se, order)
+ local n = node.new(8, 8); -- user_defined
+ n.user_id, n.type = marker_whatsit, 108
+ n[current_struct] = se or tex.attribute['tagging:current:se']
+ n[current_order] = order or tex.attribute['tagging:element:order']
+ n.value = t
+ return n
+end
+
+alloc.luadef('tagging:mci:content', function()
+ node.write(make_whatsit{ what = 'content' })
+end, 'protected')
+alloc.luadef('tagging:mci:markstart', function()
+ node.write(make_whatsit{ what = 'mci_start' })
+end, 'protected')
+alloc.luadef('tagging:mci:markstop', function()
+ node.write(make_whatsit{ what = 'mci_stop' })
+end, 'protected')
+alloc.luadef('tagging:art:markstart', function()
+ node.write(make_whatsit{ what = 'art_start', it = token.scan_string() })
+end, 'protected')
+alloc.luadef('tagging:art:markstop', function()
+ node.write(make_whatsit{ what = 'art_stop' })
+end, 'protected')
+
+-- 1 structure type table and type aliases
+
+local structure_types = alloc.saved_table('structure types', {
+ -- grouping types that may carry a bookmark
+ Document = { type = 'section' },
+ Part = { type = 'section' },
+ Art = { type = 'section' },
+ Sect = { type = 'section' },
+ Div = { type = 'group' },
+ -- grouping elements
+ BlockQuote = { type = 'group' },
+ Caption = { type = 'group' },
+ TOC = { type = 'group', contains = { TOC=1, TOCI=1, Caption=1 } },
+ TOCI = { contains = { Lbl=1, Reference=1, P=1, NonStruct=1, TOC=1 }, contained = { TOC=1 } },
+ Index = { type = 'group' },
+ NonStruct = { type = 'none' }, -- meaningless grouping
+ -- illustration elements
+ Figure = { type = 'inline', needsalt=1 },
+ Formula = { type = 'inline', needsalt=1 },
+ Form = { type = 'inline', needsalt=1 },
+ -- block level structure elements (BLSE)
+ P = { type = 'block' },
+ H = { type = 'block' },
+ H1 = { type = 'block' }, -- for nonhierarchical tagging (do not use?)
+ H2 = { type = 'block' }, -- __________ „ __________
+ H3 = { type = 'block' }, -- __________ „ __________
+ H4 = { type = 'block' }, -- __________ „ __________
+ H5 = { type = 'block' }, -- __________ „ __________
+ H6 = { type = 'block' }, -- __________ „ __________
+ L = { type = 'block', contains = { LI=1, Caption=1 } },
+ LI = { contains = { Lbl=1, LBody=1 }, contained = { L=1 } },
+ LBody = { type = 'contain', contained = { LI=1 } },
+ Lbl = { type = 'block', contained = { Note=1, Bibentry=1, LI=1, TOCI=1 } },
+ -- tables (neither BLSE nor ILSE)
+ Table = { type = 'block', contains = { TR=1, THead=1, TBody=1, TFoot=1, Caption=1 } },
+ THead = { contains = { TR=1 }, contained = { Table=1 } },
+ TBody = { contains = { TR=1 }, contained = { Table=1 } },
+ TFoot = { contains = { TR=1 }, contained = { Table=1 } },
+ TR = { contains = { TH=1, TD=1 }, contained = { THead=1, TBody=1, TFoot=1 } },
+ TH = { type = 'contain', contained = { TR=1 } }, -- header cell
+ TD = { type = 'contain', contained = { TR=1 } }, -- data cell
+ -- inline structure elements ILSE
+ Span = { type = 'inline' },
+ Quote = { type = 'inline' },
+ Note = { type = 'inline' },
+ Reference = { type = 'inline' },
+ BibEntry = { type = 'inline' },
+ Code = { type = 'inline' },
+ Link = { type = 'inline' }, -- contains link objects
+ Annot = { type = 'inline' }, -- contains other annotations
+ -- ruby/warichu
+ Ruby = { type = 'inline', contains = { RB=1, RT=1, RP=1 } },
+ RB = { contains = { }, contained = { Ruby=1 } },
+ RT = { contains = { }, contained = { Ruby=1 } },
+ RP = { contains = { }, contained = { Ruby=1 } },
+ Warichu = { type = 'inline', contains = { WT=1, WP=1 } },
+ WT = { contains = { }, contained = { Warichu=1 } },
+ WP = { contains = { }, contained = { Warichu=1 } },
+})
+
+local structure_type_compatibility = {
+ none = { none=1, section=1, group=1, block=1, inline=1, contain=1 },
+ section = { none=1, section=1, group=1, block=1 },
+ group = { none=1, group=1, block=1 },
+ block = { none=1, inline=1 },
+ inline = { none=1, inline=1 },
+ contain = { none=1, inline=1, block=1, group=1 },
+}
+
+local function check_structure_compatibility(parent, child, childtype)
+ local p, c = structure_types[parent], structure_types[child]
+ childtype = childtype or c.type
+ if p.contains then
+ return p.contains[child]
+ elseif c.contained then
+ return c.contained[parent]
+ elseif childtype == 'section' and parent == child then
+ return false
+ else
+ return structure_type_compatibility[p.type][childtype]
+ end
+end
+
+local function determine_parent_node(current, se)
+ local c = current
+ if tex.count['strictstructuretagging'] > 0 then
+ c = check_structure_compatibility(c.struct, se.struct, c.type) and c
+ else
+ while c and not check_structure_compatibility(c.struct, se.struct, c.type) do
+ c = c.parent
+ end
+ end
+ if not c then
+ alloc.err('Structure type mismatch: %s in %s', se.struct, current.struct)
+ end
+ return c or current
+end
+
+function M.add_structure_alias(stype, alias, settings)
+ local t, attr = { }, { }
+ local info = structure_types[stype] or
+ alloc.err('Unknown structure type ‘%s’', stype) and {}
+ for k, v in pairs(info) do t[k] = v end
+ for k,v in pairs(t.attributes or {}) do attr[k] = v end -- copy to new table
+ t.attributes = nil
+ for k, v in pairs(settings or {}) do t[k] = v end -- may re-set attributes
+ for k,v in pairs(t.attributes or {}) do attr[k] = v end
+ if #attr > 0 then t.attributes = attr end
+ for _, s in pairs(structure_types) do
+ if s.contains and s.contains[stype] then s.contains[alias] = 1 end
+ if s.contained and s.contained[stype] then s.contained[alias] = 1 end
+ end
+ t.aliasof, t.inuse = t.aliasof or stype, false
+ structure_types[alias] = t
+end
+
+alloc.luadef('struct:addalias', function()
+ local settings = load(token.scan_string())()
+ local stype = token.scan_string()
+ local alias = token.scan_string()
+ M.add_structure_alias(stype, alias, settings)
+end, 'protected')
+
+
+-- 1 writing the document structure
+
+local function stable_sort_on_order_field(unsorted)
+ -- n.b. ‘unsorted’ is likely to closely resemble two concatenated
+ -- monotonously increasing lists.
+ local sorted, oldsorted = { unsorted[1] }, nil
+ local i, next, c = 2, unsorted[2], nil
+ while next do
+ oldsorted, sorted = sorted, { }
+ for _, e in ipairs(oldsorted) do
+ c = e.order
+ while next and next.order < c do
+ table.insert(sorted, next);
+ i = i+1; next = unsorted[i]
+ end
+ table.insert(sorted, e);
+ end
+ while next and c <= next.order do
+ c = next.order
+ table.insert(sorted, next);
+ i = i+1; next = unsorted[i]
+ end
+ end
+ return sorted
+end
+
+local function format_K_array(se)
+ local res = { '[' }
+ for _, k in ipairs(stable_sort_on_order_field(se.children)) do
+ se.mainpage = se.mainpage or k.pageobj
+ if k.mcid then
+ if se.mainpage == k.pageobj then
+ insert_formatted(res, '%d', k.mcid)
+ else
+ insert_formatted(res, '<< /Type/MCR /Pg %d 0 R /MCID %d >>', k.pageobj, k.mcid)
+ end
+ elseif k.children then
+ insert_formatted(res, '%s 0 R', k.objnum)
+ else
+ if se.mainpage == k.pageobj then
+ insert_formatted(res, '<< /Type/OBJR /Obj %d 0 R >>', k.objnum)
+ else
+ insert_formatted(res, '<< /Type/OBJR /Obj %d 0 R /Pg %s 0 R >>', k.objnum, k.pageobj)
+ end
+ end
+ end
+ table.insert(res, ']')
+ return table.concat(res, ' ')
+end
+
+local function make_rolemap()
+ local aliases = { }
+ for k, v in pairs(structure_types) do
+ if v.aliasof and v.inuse and v.aliasof ~= k then
+ insert_formatted(aliases, '/%s/%s', k, v.aliasof)
+ end
+ end
+ if #aliases > 0 then
+ return '\n/RoleMap << ' .. table.concat(aliases,' ') .. ' >>'
+ end
+ return ''
+end
+
+local function write_structure()
+ if #structure == 1 then return end
+ -- reserve object numbers, prepare for writing
+ local root_obj, parent_tree_obj = pdf.reserveobj(), pdf.reserveobj()
+ structure[1].parent = { objnum = root_obj }
+ for _, se in ipairs(structure) do
+ if not se.hidden then se.objnum = pdf.reserveobj() end
+ end
+ -- update the document catalog
+ add_to_catalog('/MarkInfo << /Marked true >>')
+ add_to_catalog('/StructTreeRoot %s 0 R', root_obj)
+ -- write the structure tree root
+ pdf.immediateobj(root_obj, string.format(
+ '<< /Type/StructTreeRoot /K %d 0 R /ParentTree %d 0 R%s >>',
+ structure[1].objnum, parent_tree_obj, make_rolemap()))
+ -- write structure elements
+ for _, se in ipairs(structure) do
+ if not se.hidden then
+ local res = { '<<' }
+ insert_formatted(res, '/Type/StructElem /S/%s /P %d 0 R', se.struct, se.parent.objnum)
+ if se.lang and se.lang ~= se.parent.lang then insert_formatted(res, '/Lang (%s)', se.lang) end
+ if se.alt then insert_formatted(res, '/Alt %s', M.pdf_string(se.alt)) end
+ if se.actual then insert_formatted(res, '/ActualText %s', M.pdf_string(se.actual)) end
+ if #se.children > 0 then insert_formatted(res, '\n/K %s', format_K_array(se)) end
+ if se.mainpage then insert_formatted(res, '/Pg %d 0 R', se.mainpage) end
+ if se.attributes then
+ table.insert(res, '/A <<')
+ for k,v in pairs(se.attributes) do insert_formatted(res, '/%s %s', k, v) end
+ table.insert(res, '>>')
+ end
+ if se.files then
+ table.insert(res, '/AF [')
+ for _, fs in ipairs(se.files) do
+ insert_formatted(res, '%d 0 R', fs)
+ end
+ table.insert(res, ']')
+ end
+ table.insert(res, '>>')
+ pdf.immediateobj(se.objnum, table.concat(res, ' '))
+ end
+ end
+ -- write the parent tree
+ local res = { '<< /Nums [' }
+ for i, parents in ipairs(parent_tree) do
+ -- i, parents = StructParents index + 1, list of structure elems
+ if type(parents) == 'number' then
+ table.insert(res, string.format('%d %d 0 R', i-1, structure[parents].objnum))
+ else
+ local entry = { }
+ insert_formatted(entry, '%d [', i-1)
+ for _, p in ipairs(parents) do
+ insert_formatted(entry, '%d 0 R', p.objnum)
+ end
+ table.insert(entry, ']')
+ table.insert(res, table.concat(entry, ' '))
+ end
+ end
+ table.insert(res, '] >>')
+ pdf.immediateobj(parent_tree_obj, table.concat(res, '\n'))
+end
+
+-- 1 languages
+
+-- name|code ↦ nr & nr ↦ code
+local language_codes = alloc.saved_table('language codes')
+
+function M.set_language_code(name, code)
+ if not language_codes[code] then
+ table.insert(language_codes, code)
+ language_codes[code] = #language_codes
+ end
+ language_codes[name] = language_codes[code]
+end
+
+if not language_codes.sumerian then
+ local defaults = require('minim-languagecodes')
+ for name, code in pairs(defaults) do
+ M.set_language_code(name, code)
+ end
+end
+
+-- \setlanguagecode dutch nl_BE
+alloc.luadef('setlanguagecode', function()
+ M.set_language_code(token.scan_string(), token.scan_string())
+end, 'protected')
+
+function M.get_language_code(name_or_code)
+ local nr = language_codes[name_or_code]
+ return nr and language_codes[nr] or name_or_code
+end
+
+function M.get_current_language_code()
+ return language_codes[tex.attribute['tagging:current:language']]
+end
+
+function M.set_document_language(code)
+ structure[1].lang = code
+end
+
+alloc.luadef('setdocumentlanguage', function()
+ M.set_document_language(M.get_language_code(token.scan_string()))
+end, 'protected')
+
+-- \uselanguage patch; provide default document language
+-- and associate names with numbers
+alloc.luadef('minim:apply:language:code', function()
+ local name = token.scan_string()
+ local nr = language_codes[name] or alloc.err('No language code known for ‘%s’', name) and 1
+ if not structure[1].lang then M.set_document_language(language_codes[nr]) end
+ tex.sprint(nr)
+end)
+
+local function write_language()
+ add_to_catalog('/Lang (%s)', structure[1].lang or 'und')
+end
+
+-- 1 marking structure elements
+
+local function get_ancestor_of_type(se, stype)
+ while se and se.struct ~= stype do
+ if se.level == -1000 then return end
+ se = se.parent
+ end
+ return se and se.parent
+end
+
+function M.close_structure_node(stype, raiseerror)
+ local strict = tex.count['strictstructuretagging'] > 0
+ local current, open = current_structure_element(), false
+ if strict then
+ open = current.struct == stype and current.parent
+ else
+ open = get_ancestor_of_type(current, stype)
+ end
+ if open then
+ tex.setattribute(current_struct, open.index)
+ elseif strict then
+ alloc.err('Cannot close structure element %s: %s still open', stype, current.struct)
+ elseif raiseerror then
+ alloc.err('Cannot close structure element %s: no open tag found', stype)
+ end
+end
+
+alloc.luadef('tagging:stopelement', function()
+ M.close_structure_node(token.scan_string(), true)
+end, 'protected')
+
+alloc.luadef('tagging:ensurestopelement', function()
+ M.close_structure_node(token.scan_string(), false)
+end, 'protected')
+
+function M.open_structure_node(n)
+ local info = structure_types[n.struct]
+ if not info then
+ alloc.err('Unknown structure type %s replaced by NonStruct', n.struct)
+ n.struct, info = 'NonStruct', structure_types.NonStruct
+ end
+ info.inuse = true
+ if info.attributes then
+ n.attributes = n.attributes or { }
+ for k,v in pairs(info.attributes) do
+ n.attributes[k] = n.attributes[k] or v
+ end
+ end
+ if n.block then
+ n.type = 'block'
+ n.attributes = n.attributes or { }
+ n.attributes.Placement = '/Block'
+ elseif n.inline then
+ n.type = 'inline'
+ n.attributes = n.attributes or { }
+ n.attributes.Placement = '/Inline'
+ end
+ n.index = #structure + 1
+ n.children = { }
+ -- order and parent can be forced (needed for asynchronous elements)
+ n.order = n.order or tex.getattribute(current_order)
+ n.parent = (n.parent and structure[n.parent])
+ or determine_parent_node(current_structure_element(), n)
+ table.insert(structure, n)
+ if n.lang then
+ n.lang = get_language_code(n.lang)
+ else
+ local lang = tex.getattribute(current_lang)
+ n.lang = language_codes[lang]
+ end
+ if not n.hidden then table.insert(n.parent.children, n) end
+ if not n.async then tex.setattribute(current_struct, #structure) end
+end
+
+alloc.luadef('tagging:startelement', function()
+ local s = M.options_scanner()
+ :string('type') -- 'section', 'group', 'block' etc.
+ :argument('alt')
+ :argument('actual')
+ :string('lang')
+ :keyword('block')
+ :keyword('inline')
+ :scan()
+ s.struct = token.scan_string()
+ M.open_structure_node(s)
+end, 'protected')
+
+alloc.luadef('tagging:alt', function()
+ current_structure_element().alt = token.scan_string()
+end)
+
+alloc.luadef('tagging:actual', function()
+ current_structure_element().actual = token.scan_string()
+end)
+
+
+-- 1 marking content items
+
+-- All content items should be explicitly opened and closed, per page, by the
+-- late_lua nodes constructed beneath. After the page is finished, the MCIDs
+-- will be stored in the parent tree entry associated to the page.
+
+-- For the insertion of these late_lua nodes, see the next section; this one
+-- only deals with the bookkeeping mechanics.
+
+-- Note that we do not nest artifacts within marked content items. This would
+-- be allowed by the spec, but confuses some pdf readers (e.g. poppler).
+
+local mcid_list, pageobj
+
+local function clear_page_tagging_parameters()
+ mcid_list, pageobj = { }, nil
+ _open_mci_node_ = function (se_num, order)
+ pageobj = pageobj or pdf.getpageref(status.total_pages + 1)
+ local se = structure[se_num]
+ pdf.print(string.format('/%s <</MCID %d>> BDC ', se.struct, #mcid_list))
+ table.insert(se.children, { mcid = #mcid_list, order = order, pageobj = pageobj })
+ table.insert(mcid_list, se)
+ -- unhide hidden parents (done here to preserve the correct order)
+ if se.hidden then
+ table.insert(se.parent.children, se)
+ se.hidden = false
+ end
+ end
+end
+clear_page_tagging_parameters()
+
+local function new_open_mci_node(se, order)
+ local n = node.new(8,7) -- late_lua
+ n.token = string.format('_open_mci_node_(%d, %d)', se, order)
+ return n
+end
+
+local function new_open_art_node(atype)
+ local n = node.new(8,16) -- pdf_literal
+ n.mode, n.token = 1, string.format('/Artifact << /Type/%s >> BDC', atype)
+ return n
+end
+
+local function new_emc_node()
+ local n = node.new(8,16) -- pdf_literal
+ n.mode, n.token = 1, 'EMC'
+ return n
+end
+
+cb.register('finish_pdfpage', function(shippingout)
+ if shippingout then
+ local pageattr = string.gsub(pdf.getpageattributes() or '', ' */StructParents %d+', '' )
+ if #mcid_list > 0 then
+ pageattr = pageattr..string.format('/StructParents %d', #parent_tree)
+ table.insert(parent_tree, mcid_list)
+ end
+ pdf.setpageattributes(pageattr)
+ clear_page_tagging_parameters()
+ end
+end)
+
+-- 1 content item boundaries and linking
+
+function M.mark_content_items(box)
+ local se, order, open
+ local start_node, end_node, parent_node
+ local pageobj = pdf.getpageref(status.total_pages + 1)
+ -- inserting mci markers
+ local insert_tags = function(end_parent)
+ if not open then return end
+ parent_node.list = node.insert_before(parent_node.list, start_node, open)
+ node.insert_after(end_parent.list, end_node, new_emc_node())
+ start_node, end_node, parent_node, open = nil, nil, nil, nil
+ end
+ local start_content = function(n, b)
+ se, order = n[current_struct], n[current_order]
+ start_node, end_node, parent_node = n, n, b
+ end
+ -- traversing all nodes
+ for n, b in full_traverse(box) do
+ local marker = n.id == 8 and n.subtype == 8
+ and n.user_id == marker_whatsit and n.value
+ if marker and marker.what == 'art_start' then
+ -- first we start with marking artifacts
+ insert_tags(b);
+ start_content(n, b)
+ open = new_open_art_node(marker.it)
+ elseif marker and marker.what == 'art_stop' then
+ end_node = n
+ insert_tags(b);
+ elseif n[current_status] then
+ -- inside artifact, do nothing
+ elseif n.id == 8 and n.subtype == 19 then -- pdf_start_link
+ -- then attach links to Link elements
+ local _se, _order = n[current_struct], n[current_order]
+ local link = structure[_se]
+ if link.struct == 'Link' then
+ table.insert(link.children, { objnum = n.objnum, order = _order, pageobj = pageobj })
+ n.link_attr = string.format('%s /StructParent %d', n.link_attr, #parent_tree)
+ table.insert(parent_tree, _se)
+ else
+ alloc.err('Link found outside Link structure element (see pdf)')
+ pdf_err(n, 'unmarked link')
+ end
+ elseif marker and marker.what == 'mci_start' then
+ -- explicit start and stop nodes
+ insert_tags(b);
+ start_content(n, b)
+ open = new_open_mci_node(se, order)
+ elseif marker and marker.what == 'mci_stop' then
+ end_node = end_node and n; insert_tags(b)
+ elseif n.id == 2 or n.id == 29 -- rule, glyph or content marker
+ or marker and marker.what == 'content' then
+ -- now see if we need to intervene between content nodes
+ local _se, _order = n[current_struct], n[current_order]
+ if n.id == 2 and (n.width == 0 or n.height == 0 and n.depth == 0) then
+ -- ignore invisible rules
+ elseif not _se or not _order then
+ -- unmarkable content should not be possible (but is)
+ alloc.err('Unmarkable content on page %d (see pdf)', status.total_pages + 1)
+ pdf_err(n, 'possible unmarked content')
+ elseif se ~= _se or order ~= _order or not end_node then
+ -- new content item on changing attributes
+ insert_tags(b);
+ start_content(n, b)
+ open = new_open_mci_node(se, order)
+ else
+ -- nothing changed: continue current mci
+ end_node = n
+ end
+ elseif n.id == 12 and n.subtype > 2 and n.subtype < 8 then
+ -- parskip or displayskip always closes mci
+ insert_tags(b)
+ end
+ end
+ -- close the last mci
+ if start_node and start_node.user_id and start_node.value
+ and start_node.value.what == 'art_start' then
+ alloc.err('Page %d ends with a broken artifact', status.total_pages+1)
+ end
+ insert_tags(box)
+end
+
+cb.register('pre_shipout', function(nr)
+ if tagging_enabled() then
+ M.mark_content_items(tex.box[nr])
+ end
+end)
+
+-- 1 destinations
+
+local dest_count = alloc.new_count('link dest count')
+local function new_dest_name()
+ local dest_nr = tex.count[dest_count] + 1
+ tex.setcount('global', dest_count, dest_nr)
+ return 'hyper:dest:'..dest_nr
+end
+
+local function last_dest_name()
+ return 'hyper:dest:'..tex.count[dest_count]
+end
+
+local function write_new_dest()
+ local name = new_dest_name()
+ tex.sprint(string.format('\\nameddestination{%s}', name))
+ return name
+end
+
+-- make available as \newdestinationname, \lastdestinationname
+alloc.luadef('newdestinationname', function() tex.sprint(new_dest_name()) end)
+alloc.luadef('lastdestinationname', function() tex.sprint(last_dest_name()) end)
+
+-- 1 bookmarks
+
+-- start with a dummy top-level bookmark
+local bookmarks = { { count = 0 } }
+structure[1].bookmark = bookmarks[1]
+
+local function write_bookmarks()
+ if #bookmarks == 1 then return end
+ -- reserve objects in a row
+ for i=1, #bookmarks do
+ bookmarks[i].outline_obj = pdf.reserveobj()
+ end
+ -- write outlines object
+ add_to_catalog('/Outlines %s 0 R', bookmarks[1].outline_obj)
+ pdf.immediateobj(bookmarks[1].outline_obj, string.format(
+ ' <</Type /Outlines /First %s 0 R /Last %s 0 R /Count %s >> ',
+ bookmarks[2].outline_obj, bookmarks[1].last.outline_obj, #bookmarks - 1))
+ -- write bookmark objects
+ for i=2, #bookmarks do
+ local bm, res = bookmarks[i], { }
+ insert_formatted(res, '<< /Title %s\n/Parent %s 0 R /Dest (%s)', M.pdf_string(bm.title),
+ bm.parent and bm.parent.outline_obj or bookmarks[1].outline_obj, bm.dest)
+ if bm.next then insert_formatted(res, '/Next %s 0 R', bm.next.outline_obj) end
+ if bm.prev then insert_formatted(res, '/Prev %s 0 R', bm.prev.outline_obj) end
+ if bm.struct and bm.struct.objnum then
+ insert_formatted(res, '/SE %s 0 R', bm.struct.objnum)
+ end
+ if bm.count > 0 then
+ insert_formatted(res, '/First %s 0 R /Last %s 0 R /Count %s%s',
+ bm.first.outline_obj, bm.last.outline_obj, bm.open and '' or '-', bm.count)
+ end
+ pdf.immediateobj(bm.outline_obj, table.concat(res,'\n')..' >>')
+ end
+end
+
+function M.add_bookmark(bm)
+ local se = current_structure_element()
+ bm.parent, bm.count = false, 0
+ bm.dest = bm.dest or write_new_dest()
+ se.bookmark, bm.struct = bm, se
+ -- find and update the parent bookmarks
+ local p = se.parent
+ while p do
+ local pbm = p.bookmark
+ if pbm then
+ if not bm.parent then
+ if pbm.last then pbm.last.next = bm end
+ bm.parent, bm.prev = pbm, pbm.last
+ pbm.first, pbm.last = pbm.first or bm, bm
+ end
+ -- update the count until the first closed bookmark
+ pbm.count = pbm.count + 1
+ p = pbm.open and pbm.parent
+ else
+ p = p.parent
+ end
+ end
+ table.insert(bookmarks, bm)
+end
+
+alloc.luadef('outline', function()
+ local s = M.options_scanner()
+ :keyword('open')
+ :keyword('closed') -- default; ignored
+ :string('dest')
+ :scan()
+ s.title = token.scan_string()
+ M.add_bookmark(s)
+end, 'protected')
+
+
+-- 1 associated files
+
+local attached_files = { }
+local function write_fileattachments()
+ if #attached_files > 0 then
+ table.sort(attached_files, function(a,b) return a.name < b.name end)
+ local afs = { '/AF [' }
+ local nms = { '<< /Names [' }
+ for _, t in ipairs(attached_files) do
+ insert_formatted(afs, '%d 0 R', t.objnum)
+ insert_formatted(nms, '%s %d 0 R', t.name, t.objnum)
+ end
+ table.insert(afs, ']')
+ table.insert(nms, '] >>')
+ add_to_catalog(table.concat(afs, ' '))
+ local efs = pdf.immediateobj(table.concat(nms, ' '))
+ add_to_names('/EmbeddedFiles %d 0 R', efs)
+ end
+end
+
+function M.embed_file(t)
+ -- default moddate
+ if not t.moddate then
+ tex.runtoks(function() tex.print('\\csname minim:makedefaultmoddate\\endcsname') end)
+ local moddate = tex.toks['embeddedfiles:moddate']
+ if moddate ~= '' then t.moddate = moddate end
+ end
+ -- embed the file
+ local attr = { '/Type/EmbeddedFile' }
+ if t.mimetype then insert_formatted(attr, '/Subtype/%s', string.gsub(t.mimetype, '/', '#2F')) end
+ if t.moddate then insert_formatted(attr, '/Params << /ModDate %s >>', M.pdf_date(t.moddate)) end
+ local ef = pdf.obj {
+ immediate = true,
+ compresslevel = t.uncompressed and 0 or nil,
+ type = 'stream',
+ file = t.file and kpse.find_file(t.file),
+ string = t.string,
+ attr = table.concat(attr, ' ') }
+ -- write the filespec
+ local res = { '<< /Type/Filespec' }
+ insert_formatted(res, '/F %s /UF %s /EF << /F %d 0 R /UF %d 0 R >>', t.name, t.name, ef, ef)
+ if t.desc then insert_formatted(res, '/Desc %s', M.pdf_string(t.desc)) end
+ if t.relation then insert_formatted(res, '/AFRelationship /%s', t.relation) end
+ table.insert(res, '>>')
+ local fs = pdf.immediateobj(table.concat(res, ' '))
+ -- (globally) attach the file
+ return fs, ef
+end
+
+alloc.luadef('embedfile', function()
+ local t = M.options_scanner()
+ :string('desc')
+ :string('file')
+ :string('string')
+ :string('mimetype')
+ :string('moddate')
+ :string('relation')
+ :string('name')
+ :keyword('global')
+ :keyword('uncompressed')
+ :scan()
+ if not t.name then
+ t.name = t.file or aloc.err('No name specified for embedded file stream')
+ end
+ t.name = M.pdf_string(t.name or '(unnamed)')
+ local fs, ef = M.embed_file(t, t.global)
+ -- where to attach?
+ if t.global then
+ table.insert(attached_files, { name = t.name, objnum = fs } )
+ elseif tagging_enabled() then
+ local se = current_structure_element()
+ se.files = se.files or { }
+ table.insert(se.files, fs)
+ else
+ alloc.err('In untagged pdf, \\embedfile must be used with ‘global’ keyword')
+ table.insert(attached_files, { name = t.name, objnum = fs } )
+ end
+end, 'protected')
+
+
+-- 1 hyphenation
+
+function M.mark_discretionaries(head, gc)
+ if not spaces_enabled() then return end
+ for disc in node.traverse_id(7, head) do
+ if disc.subtype ~= 2 then -- ‘automatic’ (exclude explicit hyphens)
+ local pre, post, replace = disc.pre, disc.post, disc.replace
+ local se, order = disc[current_struct], disc[current_order]
+ -- get the replacement text
+ local actual = { }
+ for c in node.traverse_id(29, replace) do
+ table.insert(actual, c.char)
+ end
+ -- special case: a single hyphen
+ if #actual == 0 and pre.char and pre.char == 0x2D then
+ actual = '­' -- soft hyphen U+00AD
+ elseif #actual > 0 then
+ actual = string.utfcharacter(table.unpack(actual))
+ else
+ goto continue
+ end
+ -- reserve a structure element (but hide it)
+ M.open_structure_node { hidden = true, async = true,
+ struct = 'Span', parent = se, order = order, actual = actual }
+ -- apply the new se to pre and post
+ for n, _ in full_traverse(pre) do n[current_struct] = #structure end
+ for n, _ in full_traverse(past) do n[current_struct] = #structure end
+ end
+ ::continue::
+ end
+ return true
+end
+
+cb.register('pre_linebreak_filter', M.mark_discretionaries)
+
+-- 1 spaces
+
+local space_attr = alloc.new_attribute('space insertion marker')
+
+function M.mark_spaces(head, gc)
+ if not spaces_enabled() then return end
+ for g in node.traverse_id(12, head) do -- glue
+ if g.prev and g.subtype == 13 or g.subtype == 14 then -- (x)spaceskip
+ local p = g.prev
+ p[space_attr] = p.id == 29 and p.font
+ or g.next and g.next.id == 29 and g.next.font
+ or font.current()
+ end
+ end
+ return true
+end
+
+local function create_space(n)
+ local s = node.new(29) -- glyph
+ s.char, s.font, s.attr = 0x20, n[space_attr], n.attr
+ local b = node.hpack(s)
+ b.width, b.height, b.depth = 0, 0, 0
+ return b
+end
+
+local function insert_spaces(head)
+ for n in node.traverse(head) do
+ if n[space_attr] then
+ node.insert_after(head, n, create_space(n))
+ end
+ end
+end
+
+function M.insert_spaces(head, gc)
+ if not spaces_enabled() then return end
+ for line in node.traverse_id(0, head) do
+ insert_spaces(line.head)
+ end
+ return true
+end
+
+function M.mark_and_insert_spaces(head, gc, ...)
+ if not spaces_enabled() then return end
+ M.mark_spaces(head, gc)
+ insert_spaces(head)
+ return true
+end
+
+cb.register('pre_linebreak_filter', M.mark_spaces)
+cb.register('post_linebreak_filter', M.insert_spaces)
+cb.register('hpack_filter', M.mark_and_insert_spaces)
+
+-- 1 page labels
+
+local pagelabels = { }
+
+function M.page_labels(abs_nr, nr, style, prefix)
+ table.insert(pagelabels, { start = abs_nr, st = nr, s = style, p = prefix })
+end
+
+local pagelabel_styles = {
+ Decimal = 'D',
+ Roman = 'R',
+ roman = 'r',
+ Alphabetic = 'A',
+ alphabetic = 'a',
+ none = false }
+
+alloc.luadef('setpagelabels', function()
+ local prefix = token.scan_keyword('prefix') and token.scan_string()
+ local style = token.scan_word()
+ local nr = token.scan_int()
+ local st = pagelabel_styles[style]
+ if st == nil then alloc.err('Unknown page label style »%s«', style) end
+ M.page_labels(status.total_pages, nr, st, prefix)
+end, 'protected')
+
+local function write_pagelabels()
+ if #pagelabels == 0 then return end
+ if pagelabels[1].start ~= 0 then
+ table.insert(pagelabels, 1, { start = 0, st = 1, s = 'D' })
+ end
+ local res = { '/PageLabels << /Nums [' }
+ for _, l in ipairs(pagelabels) do
+ local s, p = l.s and '/S/'..l.s, l.p and ' /P '..M.pdf_string(l.p)
+ table.insert(res, string.format('%d << /St %d %s%s >>', l.start, l.st, s or '', p or ''))
+ end
+ table.insert(res, ']')
+ add_to_catalog(table.concat(res, ' '))
+end
+
+-- 1 output intents
+
+local output_intents = { }
+
+function M.add_output_intent(t)
+ table.insert(output_intents, t)
+end
+
+local function write_output_intents()
+ if #output_intents == 0 then return end
+ local res = { '[' }
+ for _, oi in ipairs(output_intents) do
+ local p = pdf.obj { type = 'stream',
+ file = kpse.find_file(oi.path),
+ compresslevel = 9,
+ attr = string.format('/N %d', oi.N),
+ immediate = true }
+ insert_formatted(res, '<< /Type/OutputIntent /DestOutputProfile %d 0 R', p)
+ insert_formatted(res, '/S/%s /OutputConditionIdentifier (%s)', oi.subtype, oi.id)
+ if oi.info then insert_formatted(res, '/Info (%s)', oi.info) end
+ if oi.condition then insert_formatted(res, '/OutputCondition (%s)', oi.condition) end
+ if oi.registry then insert_formatted(res, '/RegistryName (%s)', oi.registry) end
+ table.insert(res, '>>')
+ end
+ table.insert(res, ']')
+ local objnum = pdf.immediateobj(table.concat(res, ' '))
+ add_to_catalog('/OutputIntents %d 0 R', objnum)
+end
+
+function M.add_default_rgb_output_intent(t)
+ t = t or {}
+ M.add_output_intent {
+ subtype = t.subtype or 'GTS_PDFA1',
+ info = t.info or 'IEC 61966-2.1 Default RGB colour space - sRGB',
+ condition = t.condition,
+ registry = 'http://www.iev.ch',
+ id = 'IEC sRGB',
+ N = 3, path = 'sRGB.icc' }
+end
+
+function M.add_default_cmyk_output_intents(t)
+ t = t or {}
+ M.add_output_intent {
+ subtype = t.subtype or 'GTS_PDFA1',
+ info = t.info or 'FOGRA39L Coated',
+ condition = t.condition or 'FOGRA39L Coated',
+ registry = 'http://www.fogra.org',
+ id = 'Offset printing, according to ISO 12647-2:2004/Amd 1, OFCOM, paper type 1 or 2 = coated art, 115 g/m2, tone value increase curves A (CMY) and B (K)',
+ N = 4, path = 'FOGRA39L_coated.icc' }
+end
+
+alloc.luadef('minim:default:rgb:profile', function() M.add_default_rgb_output_intent() end)
+alloc.luadef('minim:default:cmyk:profile', function() M.add_default_cmyk_output_intent() end)
+
+--
+
+cb.register ('finish_pdffile', function()
+ if tagging_enabled() then
+ write_language()
+ write_structure()
+ end
+ write_bookmarks()
+ write_pagelabels()
+ write_output_intents()
+ write_fileattachments()
+end)
+
+return M
+
diff --git a/macros/luatex/generic/minim-pdf/minim-pdf.pdf b/macros/luatex/generic/minim-pdf/minim-pdf.pdf
new file mode 100644
index 0000000000..b460bdf4ed
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/minim-pdf.pdf
Binary files differ
diff --git a/macros/luatex/generic/minim-pdf/minim-pdf.tex b/macros/luatex/generic/minim-pdf/minim-pdf.tex
new file mode 100644
index 0000000000..f8075ae6e4
--- /dev/null
+++ b/macros/luatex/generic/minim-pdf/minim-pdf.tex
@@ -0,0 +1,351 @@
+
+\ifdefined\minimpdfloaded
+ \message{(skipped)}
+ \expandafter\endinput\fi
+\chardef\minimpdfloaded = \catcode`\:
+\catcode`\: = 11
+
+% Abbreviations used throughout this document:
+% se structure element
+% mci marked content item
+
+\newcount \writedocumentstructure
+\newcount \writehyphensandspaces
+\newcount \strictstructuretagging
+
+\writedocumentstructure = 0
+\writehyphensandspaces = 1
+\strictstructuretagging = 0
+
+% 1 the attributes
+
+\input minim-alloc
+\input minim-hooks
+
+% By three attributes do we determine the document structure.
+%
+% One for marking the current structure element:
+% - is the index of the lua-side se table
+% - assignments are always local
+\newattribute \tagging:current:se \tagging:current:se = 1
+%
+% One for ordering children of structure elements:
+% - assignments are always global
+% - should generally increase monotously
+% - should change (increase) at every se boundary
+\newattribute \tagging:element:order \tagging:element:order = 1
+%
+% One for marking the status:
+% - assignments are generally local
+% - if set, disables tagging and content marking
+\newattribute \tagging:current:status \tagging:current:status = \unset
+%
+% A fourth attribute keeps track of the current language
+% - assignments are local
+% - sets language of structure elements
+\newattribute \tagging:current:language \tagging:current:language = \language
+
+\directlua { require('minim-pdf') }
+
+% 1 artifacts and content items
+
+% \stoptagging ... \starttagging
+% disables marking structure elements
+\newif\iftagging:enabled \tagging:enabledtrue
+\protected\def\starttagging{\tagging:enabledtrue
+ \ifnum1=\tagging:current:status
+ \tagging:current:status\unset\fi}
+\protected\def\stoptagging{\tagging:enabledfalse
+ \ifnum\unset=\tagging:current:status
+ \tagging:current:status1\relax\fi}
+
+% \markartifact {Layout} {...}
+% \startartifact {Pagination /Subtype/Header} ... \stopartifact
+\long\def\markartifact#1#2{\startartifact{#1}#2\stopartifact}
+\protected\def\startartifact{\begingroup\stoptagging
+ \tagging:mci:incr\tagging:art:markstart}
+\protected\def\stopartifact{\tagging:art:markstop\tagging:mci:incr\endgroup}
+
+% \startcontentitem ... \stopcontentitem
+\protected\def\startcontentitem{\iftagging:enabled\tagging:mci:incr\tagging:mci:markstart\fi}
+\protected\def\stopcontentitem{\iftagging:enabled\tagging:mci:markstop\tagging:mci:incr\fi}
+
+% \startsinglecontentitem ... \stopsinglecontentitem
+\protected\def\startsinglecontentitem{\begingroup \startcontentitem\stoptagging}
+\protected\def\stopsinglecontentitem{\endgroup\stopcontentitem}
+
+% \ensurecontentitem
+\protected\def\ensurecontentitem{\iftagging:enabled\tagging:mci:content\fi}
+\protected\def\tagging:mci:incr{\global\advance\tagging:element:order1\relax}
+
+% 1 structure elements
+
+% \savecurrentelement ... \continueelement
+% \savecurrentelementto\name ... \continueelementfrom\name
+\protected\def\savecurrentelementto#1{\global\chardef#1\tagging:current:se}
+\protected\def\continueelementfrom#1{\tagging:current:se#1\tagging:mci:incr}
+\def\savecurrentelement{\savecurrentelementto\tagging:saved:se}
+\def\continueelement{\continueelementfrom\tagging:saved:se}
+
+% \markelement {Tag} {...}
+% \startelement {Tag} ... \stopelement {Tag}
+% \ensurestopelement {Tag}
+\long\protected\def\markelement#1#2{\iftagging:enabled
+ \startelement{#1}#2\stopelement{#1}\else#2\fi}
+\protected\def\startelement{\iftagging:enabled \tagging:mci:incr
+ \expandafter\tagging:startelement\else
+ \expandafter\tagging:ignore\fi}
+\protected\def\stopelement{\iftagging:enabled \tagging:mci:incr
+ \expandafter\tagging:stopelement\else
+ \expandafter\tagging:ignore\fi}
+\protected\def\ensurestopelement{\iftagging:enabled \tagging:mci:incr
+ % n.b. this can cause problems if you skip levels
+ \expandafter\tagging:ensurestopelement\else
+ \expandafter\tagging:ignore\fi}
+\def\tagging:ignore#1#{\ignore}
+
+% \addstructuretype [options] Existing Alias
+\protected\def\addstructuretype{\withoptions[]\addstructure:type}
+\def\addstructure:type[#1]{\struct:addalias{return {#1}}}
+
+% \setalttext {...} and \setactualtext {...}
+\protected\def\setalttext{\iftagging:enabled
+ \expandafter\tagging:alt\fi}
+\protected\def\setactualtext{\iftagging:enabled
+ \expandafter\tagging:actual\fi}
+
+
+% 1 auto-marking paragraphs
+
+% \nextpartag {H}
+% \nextpartag {} % no tag inserted for next paragraph
+% \markparagraphs(true|false)
+\newif\ifmarkparagraphs \markparagraphstrue
+\newtoks\nextpartag \nextpartag{P}
+\toksapp\minim:ateverypar{\iftagging:enabled \ifmarkparagraphs
+ \expandafter\ifx\expandafter\relax\the\nextpartag\relax\else
+ \startelement{\the\nextpartag}\fi
+ \nextpartag{P}\fi \fi}
+
+% 1 tagging helper macros
+
+% \marktocentry {destination}{label}{title}{filler}{page}
+\def\marktocentry#1#2#3#4#5{%
+ \ifx\marktocentry#1\marktocentry
+ \def\marktocentry:link##1{##1}\else
+ \def\marktocentry:link##1{\hyperlink dest{#1}##1\endlink}\fi
+ \markelement{TOCI}{\nextpartag{}\quitvmode
+ \ifx\marktocentry#2\marktocentry\else
+ \markelement{Lbl}{\marktocentry:link{#2}}\fi
+ \markelement{Reference}{\marktocentry:link{#3%
+ \ifx\marktocentry#4\marktocentry\else
+ \markartifact{Layout}{#4}\fi#5}}}}
+
+% \marktable \halign {\marktablerow#&\marktablecell#\cr
+% \marktableheader
+% Header & cells \cr
+% \marktablebody
+% ... & ... \cr ...
+% \marktablefooter
+% ... & ... \cr ... }
+%
+\def\marktable{\startelement{Table}%
+ \def\tagging:cell{TD}}
+\def\marktableheader{%
+ \noalign{\gdef\tagging:cell{TH}%
+ \startelement{THead}%
+ \savecurrentelementto\tagging:tpart}}
+\def\marktablebody{%
+ \noalign{\gdef\tagging:cell{TD}%
+ \startelement{TBody}%
+ \savecurrentelementto\tagging:tpart}}
+\def\marktablefooter{%
+ \noalign{\startelement{TFoot}%
+ \savecurrentelementto\tagging:tpart}}
+\def\marktablerow{
+ \unless\ifdefined\tagging:tpart
+ \startelement{TBody}%
+ \savecurrentelementto\tagging:tpart\fi
+ \continueelementfrom\tagging:tpart
+ \startelement{TR}%
+ \savecurrentelementto\tagging:row
+ \startelement{\tagging:cell}}
+\def\marktablecell{%
+ \continueelementfrom\tagging:row
+ \startelement{\tagging:cell}}
+
+% \automarktable \halign ... { ... }
+\def\automarktable#1#{\marktable
+ \def\tagging:table{\tagging:mktrow{#1}}%
+ \afterassignment\tagging:table
+ \let\tagging:tmp= }
+\def\tagging:mktrow#1#2&#3\cr{\iftrue
+ \tagging:mktcell{#1\bgroup\marktablerow#2}\fi
+ #3&\tagging:mktrow&\cr}
+\def\tagging:mktcell#1#2\fi#3&{\fi
+ \ifx\tagging:mktrow#3\tagging:mkthdr{#1}\fi
+ \ifx\tagging:mktrow#3\tagging:mktrow
+ \tagging:mktcell{#1&}\else
+ \tagging:mktcell{#1&\marktablecell#3}\fi}
+\def\tagging:mkthdr#1#2\cr#3\cr{\fi#1\cr
+ \marktableheader#3\cr\marktablebody}
+\def\tagging:fi{\fi}
+
+% \marktableaslist \halign ... { ... }
+\def\marktableaslist#1#{\startelement{L}%
+ \def\tagging:table{\tagging:mktlist{#1}}%
+ \afterassignment\tagging:table
+ \let\tagging:tmp= }
+\def\tagging:mktlist#1#2&{#1\bgroup
+ \startelement{LI}\savecurrentelement
+ \startelement{Lbl}#2&\continueelement
+ \startelement{LBody}}
+
+% \autotagformulas
+% \stopformulatagging ... \startformulatagging
+% \formulafilename
+\newif \iftagging:indisplay
+\newif \iftagging:toplevelmath \tagging:toplevelmathtrue
+\newif \iftagging:formulaenabled
+\let\startformulatagging = \tagging:formulaenabledtrue
+\let\stopformulatagging = \tagging:formulaenabledfalse
+\newcount \tagging:formulanr
+\def\formulafilename{unnumbered-equation-\the\tagging:formulanr}
+\def\autotagformulas{\startformulatagging
+ \everymath\expandafter{\the\everymath
+ \tagging:formula\tagging:startformula}%
+ \everydisplay\expandafter{\the\everydisplay \tagging:indisplaytrue
+ \tagging:formula\tagging:startdisplay}}
+\def\tagging:formula#1{%
+ \iftagging:enabled \iftagging:formulaenabled \iftagging:toplevelmath
+ \tagging:toplevelmathfalse #1\fi\fi\fi}
+\def\tagging:startformula\fi\fi\fi#1${\tagging:makeformula{#1}{$}}
+\def\tagging:startdisplay\fi\fi\fi#1$${\tagging:makeformula{#1}{$$}}
+\def\tagging:makeformula#1#2{\fi\fi\fi
+ \global\advance\tagging:formulanr1\relax
+ \startelement\iftagging:indisplay block\else inline\fi{Formula}%
+ \setalttext{\tagging:formulasource{#1}}%
+ \setactualtext{\tagging:formulasource{#1}}%
+ \ifnum3=\pdfaconformancelevel
+ \embedfile mimetype text/x-tex
+ relation Source desc {Equation source}
+ name {\formulafilename.tex}
+ string {\tagging:formulasource{#1}}\fi
+ \scantextokens{#1}#2}
+\def\tagging:formulasource#1{%
+ $\iftagging:indisplay$\fi
+ \unexpanded{#1}%
+ $\iftagging:indisplay$\fi}
+
+% 1 hyperlinks
+
+% provided by the lua module:
+% \newdestinationname
+% \lastdestinationname
+
+% \nameddestination {name}
+\def\nameddestination{\ifhmode\expandafter\linkdest:h\else\expandafter\linkdest:v\fi}
+\def\linkdest:h#1{\vadjust pre{\linkdest:v{#1}}}
+\def\linkdest:v#1{\pdfextension dest name {#1} xyz\nobreak}
+
+% \hyperlink dest {name} ... \endlink
+% \hyperlink url {url} ... \endlink
+\protected\def\endlink{\pdfextension endlink\stopelement{Link}\relax}
+\protected\def\startlink{\startelement{Link}\pdfextension startlink}
+\def\hyperlink#1#{\quitvmode\hyperlink:rmspace#1 \hyperlink:rmspace}
+\def\hyperlink:rmspace#1 #2\hyperlink:rmspace{%
+ \startlink attr {\minim:linkattr}%
+ \csname hyperlink:#1\endcsname}
+\def\hyperlink:dest#1{user {/Subtype/Link /F 4 /A <</S/GoTo /D (#1)>>}}
+\def\hyperlink:url#1{user {/Subtype/Link /F 4 /A <</S/URI /URI (#1)>>}}
+\def\minim:linkattr{/Border [0 0 0]}
+
+% 1 languages
+
+% provided by the lua module:
+% \setdocumentlanguage {name or code}
+% \setlanguagecode {name} {code}
+% \minim:apply:language:code{name} → index
+
+% patch in language codes to \uselanguage
+\ifcsname uselanguage@hook\endcsname
+ \expandafter\let
+ \expandafter\minim:uselanguagehook
+ \lastnamedcs \fi
+\expandafter\edef\csname uselanguage@hook\endcsname#1{%
+ % tagging support
+ \noexpand\tagging:current:language
+ \noexpand\minim:apply:language:code{#1}%
+ % previous definitions
+ \ifdefined\minim:uselanguagehook
+ \noexpand\minim:uselanguagehook{#1}\fi}
+
+% \newnamedlanguage {name} {lhm} {rhm}
+\def\newnamedlanguage#1#2#3{%
+ \expandafter\newlanguage\csname lang@#1\endcsname
+ \expandafter\chardef\csname lhm@#1\endcsname=#2\relax
+ \expandafter\chardef\csname rhm@#1\endcsname=#3\relax
+ \csname lu@texhyphen@loaded@\the\csname lang@#1\endcsname\endcsname}
+
+% \newnameddialect {language} {dialect}
+\def\newnameddialect#1#2{%
+ \expandafter\chardef\csname lang@#2\endcsname\csname lang@#1\endcsname
+ \expandafter\chardef\csname lhm@#2\endcsname\csname lhm@#1\endcsname
+ \expandafter\chardef\csname rhm@#2\endcsname\csname rhm@#1\endcsname}
+
+% and provide several dummy languages
+\ifcsname lang@nohyph\endcsname \else
+ \newnamedlanguage {nohyph} 1 1 \fi
+\ifcsname lang@nolang\endcsname \else
+ \newnameddialect {nohyph} {nolang} \fi
+\ifcsname lang@uncoded\endcsname \else
+ \newnameddialect {nohyph} {uncoded} \fi
+\ifcsname lang@undetermined\endcsname \else
+ \newnameddialect {nohyph} {undetermined} \fi
+
+% 1 embedded files
+
+% provided by the lua module
+% \embedfile <options>
+
+% \setembeddedfilesmodate { yyyy-mm-dd }
+\newtoks\setembeddedfilesmoddate
+ \setembeddedfilesmoddate{}
+\newtoks\embeddedfiles:moddate
+\def\minim:makedefaultmoddate{%
+ \expandafter\edef\expandafter
+ \minim:tmp\expandafter{\the\setembeddedfilesmoddate}%
+ \embeddedfiles:moddate\expandafter{\minim:tmp}}
+
+% 1 declarations of pdf/a conformance
+
+\newcount \pdfaconformancelevel
+\pdfaconformancelevel = 0
+
+% \pdfalevel 2b
+\def\pdfalevel#1#2{%
+ \global\pdfaconformancelevel=#1\relax
+ \ifcsname minim:pdfa:#1#2\endcsname \lastnamedcs\else
+ \errmessage{Unknown pdf/a standard pdf/a-#1}\fi}
+
+\def\minim:pdfasettings#1#2#3{%
+ \pdfvariable minorversion #1\relax
+ \minim:default:rgb:profile
+ \if#2A\writedocumentstructure1\fi
+ \input minim-xmp
+ \setmetadata pdfaid:conformance{#2}%
+ \setmetadata pdfaid:part{#3}}
+
+\expandafter\def\csname minim:pdfa:1a\endcsname{\minim:pdfasettings 4A1}
+\expandafter\def\csname minim:pdfa:1b\endcsname{\minim:pdfasettings 4B1}
+\expandafter\def\csname minim:pdfa:2a\endcsname{\minim:pdfasettings 7A2}
+\expandafter\def\csname minim:pdfa:2b\endcsname{\minim:pdfasettings 7B2}
+\expandafter\def\csname minim:pdfa:2u\endcsname{\minim:pdfasettings 7U2}
+\expandafter\def\csname minim:pdfa:3a\endcsname{\minim:pdfasettings 7A3}
+\expandafter\def\csname minim:pdfa:3b\endcsname{\minim:pdfasettings 7B3}
+\expandafter\def\csname minim:pdfa:3u\endcsname{\minim:pdfasettings 7U3}
+
+% 
+
+\catcode`\: = \minimpdfloaded
+