From 2bd77f5a4d02b0d1649589aa7a548e79b198f6af Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Thu, 10 Jun 2021 03:01:49 +0000 Subject: CTAN sync 202106100301 --- macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt | 287 +++++ macros/luatex/generic/minim-pdf/README | 36 + .../generic/minim-pdf/minim-languagecodes.lua | 146 +++ macros/luatex/generic/minim-pdf/minim-pdf.doc | 425 ++++++++ macros/luatex/generic/minim-pdf/minim-pdf.lua | 1114 ++++++++++++++++++++ macros/luatex/generic/minim-pdf/minim-pdf.pdf | Bin 0 -> 108362 bytes macros/luatex/generic/minim-pdf/minim-pdf.tex | 351 ++++++ 7 files changed, 2359 insertions(+) create mode 100644 macros/luatex/generic/minim-pdf/EUPL-1.2-EN.txt create mode 100644 macros/luatex/generic/minim-pdf/README create mode 100644 macros/luatex/generic/minim-pdf/minim-languagecodes.lua create mode 100644 macros/luatex/generic/minim-pdf/minim-pdf.doc create mode 100644 macros/luatex/generic/minim-pdf/minim-pdf.lua create mode 100644 macros/luatex/generic/minim-pdf/minim-pdf.pdf create mode 100644 macros/luatex/generic/minim-pdf/minim-pdf.tex (limited to 'macros/luatex/generic/minim-pdf') 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 ⟧. +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 {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] = '') + 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 <> 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( + ' <> ', + 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 Binary files /dev/null and b/macros/luatex/generic/minim-pdf/minim-pdf.pdf 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\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 <>}} +\def\hyperlink:url#1{user {/Subtype/Link /F 4 /A <>}} +\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 + +% \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 + -- cgit v1.2.3