% \iffalse meta-comment % %% File: l3pdfoutline.dtx % % Copyright (C) 2026 The LaTeX Project % % It may be distributed and/or modified under the conditions of the % LaTeX Project Public License (LPPL), either version 1.3c of this % license or (at your option) any later version. The latest version % of this license is in the file % % http://www.latex-project.org/lppl.txt % % This file is part of the "LaTeX PDF management testphase bundle" (The Work in LPPL) % and all files in that bundle must be distributed together. % % ----------------------------------------------------------------------- % % The development version of the bundle can be found at % % https://github.com/latex3/pdfresources % % for those people who are interested. % %<*driver> \DocumentMetadata{tagging=on,pdfstandard=ua-2,testphase=bookmark} \documentclass[kernel]{l3in2edoc} \usepackage{booktabs} \hypersetup{pdfauthor=The LaTeX Project, pdftitle=l3pdfoutline (LaTeX PDF management bundle)} \begin{document} \DocInput{\jobname.dtx} \end{document} % % \fi % \providecommand\hook[1]{\texttt{#1}} % \title{^^A % The \pkg{l3pdfoutline} module\\ Commands for PDF bookmarks ^^A % \\ \LaTeX{} PDF management bundle % } % % \author{^^A % The \LaTeX{} Project\thanks % {^^A % E-mail: % \href{mailto:latex-team@latex-project.org} % {latex-team@latex-project.org}^^A % }^^A % } % % \date{Version 0.96z, released 2026-04-15} % % \maketitle % \begin{documentation} % % \section{\pkg{l3pdfoutline} documentation} % \subsection{Introduction} % This module contains a number of commands to create the PDF outline, also % called bookmarks. % % \emph{Attention:} There can be only one outline in a PDF and % the commands of this module should not be mixed % with other code that creates outline items, this would mess up the tree! % % A reference implementation that shows the use of the command is in \texttt{latex-lab-bookmark.dtx}, % a package that can replace the package \pkg{bookmark}. % % The outline is a tree build with a set of objects. % The root is the \emph{outline dictionary} % which is referenced from the catalog with the \texttt{/Outline} key. The other objects % are called \emph{outline items}. % % The dictionaries of the \emph{outline items} contain % keys which can be divided into three classes: % \begin{description} % \item[\enquote{Management}] % The connection between the root, the items and their children % are described with keys \texttt{/Parent}, \texttt{/Next}, \texttt{/Prev}, \texttt{/First}, % \texttt{/Last}. The \texttt{/Count} key describes how many subitems are visible (open). % % \item[\enquote{Attributes}] % The \texttt{/Title} key contains the text shown in the bookmarks, % \texttt{/C} sets its color, the flag \texttt{/F} sets the font type (italic and/or bold). % There is a \texttt{/SE} key which can reference a structure, but % its function is unclear (the note in the reference says, that it is not meant for % navigation, but does not say what it is for). % % % \item[\enquote{Action}] % The action that should be executed can be set either with an \texttt{/A} or a % \texttt{/Dest} key. \LaTeX{} typically uses only the first option and adds an action, % e.g., for a simple link to a destination something like % |<< /S /GoTo /D (section.1) /SD 19 0 R >>| % (the \texttt{SD} key is used when there are structures). % % While in most cases the outline is used with a \texttt{GoTo} action and more or less mirrors % the table of contents, other actions are possible too, an outline item % can, e.g., open an external url or another file. The PDF reference lists in total % 17 different, theoretically usable actions\footnote{Sound and Movie are deprecated in PDF~2.0}: % GoTo, GoToR, GoToE, GoToDp (PDF~2.0), Launch, Thread, URI, Sound, Movie, % Hide, Named, SetOCGState, Rendition, Trans, GoTo3DView, JavaScript, RichMediaExecute. % % An action dictionary typically looks like this: % \begin{verbatim} % /A << /Type /Action %optional % /S ... % type name, e.g. /URI or /GoTo % /Next ... % optional, next action % ... ... % more keys % >> % \end{verbatim} % % The |/Next| key allows to chain actions. The value can be a reference to a % single action, or an array of actions. % It depends on the action type which other keys should be used. % % From all these action types the \pkg{bookmark} package supports |GoTo| (through the % |dest| and |page| keys), |GoToR| (through the |gotor| key together % with the |dest| and |page| keys), % |Named| (through the |named| key) and |URI| (through the |uri| key). % These standard types looks like this: % \begin{description} % \item[GoTo link to a named destination] e.g. % |/S /GoTo /D (section.1) /SD 19 0 R|, this can be handled with |\pdfoutline_goto:nnn|. % % \item[GoTo link to a page] |/S /GoTo /D [5 0 R /FitH 0]| % % \item[Named] e.g. |/S/Named/N/FirstPage|, values are \texttt{FirstPage}, % \texttt{NextPage}, \texttt{PrevPage}, \texttt{LastPage}. % % \item[URI] e.g.|/S/URI/URI(https://blub.de)| % % \item[GoToR] |/S/GoToR/F(blub.pdf)/D[0/Fit]|, |/S/GoToR/F(blub.pdf)/D[3/FitH 0]|, % |/S/GoToR/F(blub.pdf)/D(duck)| % \end{description} % All other actions (and also chained actions) are not % directly supported by \pkg{bookmark} but can be % implemented by using the |rawaction| key. % \end{description} % % Theoretically all needed objects described above could be created manually, % but getting all the management keys right wouldn't be trivial. % For \texttt{/Count} e.g. one would have to track recursively % the number of visible subitems. Luckily all backends have % dedicated primitives or \verb+\special+'s to create outline items which take % care of the management keys. Not quite so lucky is that the % backends use different methods to decide if % an outline item is a sibling or a child or some great-uncle of the previous item. % % The \texttt{(x)dvipdfmx} backend has the easiest implementation: % The general syntax is \verb+\special{pdf:out [-]number << attr>>}+ % where the parameter \emph{number} is an integer representing % the level of the outline entry. The first item % has to start with the level 1 and one can't skip levels, so one can't % simply use a fixed number for the various headings; instead one has to keep track of % which level has been used previously. This is a bit awkward, nevertheless with this backend % it is possible to build the outline directly while compiling the document and % one doesn't have to use the \texttt{aux} or to delay code until the end of the document. % % When using \pdfTeX{}, lua\TeX{} or the \texttt{dvips} backend, a count must be given % in the primitive which describes the number of direct children of the outline. % The backend then collects (recursively) following items until all children are found % and then goes up a level again. % To get this number the code has to obviously look ahead % to count all subsections below the current section. \pkg{hyperref} solved % this problem by writing everything into the \texttt{.out} file and to % process it at begin of the next compilation to get the correct count. % The \pkg{bookmark} package writes % the content and attributes (hex encoded) into the \texttt{.aux}-file. % Additionally it keeps track of the number of children during the compilation and % stores it for every bookmark in a command. When the % \texttt{.aux} file is read in again at the end of the document, it decodes % content and attributes again and writes out all the outline objects. In % this way it can produce the outline tree % for \pdfTeX{} and lua\TeX{} directly in the % first compilation. With the \texttt{dvips} backend it creates a \texttt{.ps} file % which is then read in at the next compilation. % % The new implementation in this module doesn't use external files but keeps all % bookmarks in memory and outputs them at the end of the document (for the % dvi based engines in the shipout/lastpage hook, this can require a second compilation if the % page numbers change). % % \subsection{The commands} % % \subsection{Creating an outline item} % \begin{function} % {\pdfoutline_action:nnn,\pdfoutline_action:neo,\pdfoutline_action:nee} % \begin{syntax} % \cs{pdfoutline_action:nnn}\Arg{level}\Arg{action}\Arg{title} % \end{syntax} % This creates an outline item. \meta{level} is an integer expression % and sets the (relative) level: A positive % number creates a child of the current outline item, 0 creates a sibling, % and negative numbers go back. % \meta{action} should be a list of dictionary keys and its values % that can be used within the |/A| key, e.g. the output from |\pdfdict_use:n|\footnote{It can % not be an object reference as this doesn't work with most backends.}\footnote{This is not completely % accurate: the dvips backend expects in some cases special names, e.g. a color must be given % with \texttt{/Color} and not \texttt{/C}. Complex actions must therefore be tested % and if needed one must add backend abstractions.}. % \meta{title} is the title, it should be % correctly escaped and encoded for use as a literal string % (but without the outer parentheses), such a representation can for example be created % with |\pdfstringdef| or with a combination of |\pdf_purify:nN| and % |\pdf_string_from_unicode:nnN| from the \texttt{l3pdftools} module. % The command can be used for general outline item, for the special case of % a GoTo link to a named destination see the following command. % \end{function} % % \begin{function} % {\pdfoutline_goto:nnn,\pdfoutline_goto:neo,\pdfoutline_goto:nee} % \begin{syntax} % \cs{pdfoutline_goto:nnn}\Arg{level}\Arg{destination}\Arg{title} % \end{syntax} % This creates an outline which creates a link to a named destination. % While such a GoTo link can also be created with \cs{pdfoutline_action:nnn} % the use of this command is recommended as it will also create a reference to a % structure destination if tagging is activated. % \meta{level} is an integer expression % and sets the (relative) level: A positive % number creates a child of the current outline, 0 creates a sibling, and negative numbers % go back. \meta{destination} is a destination name. % \meta{title} is the title, it should be % correctly escaped and encoded for a use as a literal string % (but without the outer parentheses), such a representation can for example be created % with |\pdfstringdef| with a combination of |\pdf_purify:nN| and % |\pdf_string_from_unicode:nnN| from the \texttt{l3pdftools} module. % \end{function} % % \begin{variable} % {\l_pdfoutline_active_bool} % This boolean decides if bookmarks are shown % at all. % \end{variable} % % \begin{variable} % {\l_pdfoutline_open_bool} % This boolean decides if bookmarks are shown % \enquote{open}, so show their children or not. % \end{variable} % % \begin{variable} % {\l_pdfoutline_open_int} % With this integer you can set the number of % visible levels. Bookmarks with a level above the value of this integer are % always closed. So if it is set to 1, everything is closed and so % only the top level bookmarks are shown % if its value is 2, the first level will show its children, % etc. By default this integer is set to |\c_max_int| and % so everything is open. % \end{variable} % % \begin{variable} % {\l_pdfoutline_F_bitset} % This bitset is used to set the flag for the font in bookmarks. % It knows the two values \texttt{italic} and \texttt{bold}, so after % \begin{verbatim} % \bitset_set_true:Nn \l_pdfoutline_F_bitset {italic} % \end{verbatim} % italic will be enabled. % \end{variable} % % \begin{variable} % {\l_pdfoutline_color_tl,\l_pdfoutline_color_model_tl} % \cs{l_pdfoutline_color_tl} can be empty or hold a color expression to set the color of % the outline(s). It then should have either the format |[model]{value}| % or be a color expression. For examples: |[rgb]{1,0,.5}| or |red!50!blue|. % \cs{l_pdfoutline_color_model_tl} should be |rgb| or |cmyk|. The first is % the normal one, the option to use also |cmyk| is only offered for the % case that some PDF/A validator complains. % \end{variable} % % \subsection{References} % A user package that uses these commands to create a tree must be able % to retrieve parents and level data. % \begin{function}[EXP] % {\pdfoutline_id_ref_last:,\pdfoutline_level_ref_last:} % \begin{syntax} % \cs{pdfoutline_id_ref_last:} \\ % \cs{pdfoutline_level_ref_last:} % \end{syntax} % These expandable functions give back the id and the level % of the last bookmark created. % \end{function} % % \begin{function}[EXP]{\pdfoutline_parent_ref:n} % \begin{syntax} % \cs{pdfoutline_parent_ref:n}\Arg{id} % \end{syntax} % This retrieves the parent of the bookmark with id \meta{id}. % If the bookmark is at the root level the value is 1. % \end{function} % \end{documentation} % % \begin{implementation} % % \section{\pkg{l3pdfoutline} implementation} % \subsection{Package declaration} % \begin{macrocode} %<@@=pdfoutline> %<*header> \ProvidesExplPackage{l3pdfoutline}{2026-04-15}{0.96z} {PDF outlines} % % \end{macrocode} % \begin{macrocode} %<*package> % \end{macrocode} % % \subsection{Public variables} % \begin{variable}{\l_pdfoutline_active_bool,\l_pdfoutline_open_bool} % The state of the first boolean decides % if a bookmark is created at all, this allows to disable bookmarks % fully or in parts. % The second boolean decides if a bookmark is open (so shows its children) % or not. % \begin{macrocode} \bool_new:N \l_pdfoutline_active_bool \bool_set_true:N \l_pdfoutline_active_bool \bool_new:N \l_pdfoutline_open_bool \bool_set_true:N \l_pdfoutline_open_bool % \end{macrocode} % \end{variable} % % \begin{variable}{\l_pdfoutline_open_int} % This is the level up to which a bookmark is opened. By default the maximum % integer value is used and everything is open. % % \begin{macrocode} \int_new:N \l_pdfoutline_open_int \int_set:Nn \l_pdfoutline_open_int { \c_max_int } % \end{macrocode} % \end{variable} % % \begin{variable}{\l_pdfoutline_F_bitset} % Outlines have a /F flag, we provide a public % bitset for it. % \begin{macrocode} \bitset_new:Nn \l_pdfoutline_F_bitset { Italic = 1, italic = 1, Bold = 2, bold = 2 } % \end{macrocode} % \end{variable} % % \begin{variable}{\l_pdfoutline_color_tl,\l_pdfoutline_color_model_tl} % The variable for the color expression. % \begin{macrocode} \tl_new:N \l_pdfoutline_color_tl \tl_new:N \l_pdfoutline_color_model_tl \tl_set:Nn \l_pdfoutline_color_model_tl {rgb} % \end{macrocode} % \end{variable} % % \subsection{Data structure for the Count} % % \begin{variable}{\l_@@_tmpa_tl} % \begin{macrocode} \tl_new:N\l_@@_tmpa_tl % \end{macrocode} % \end{variable} % \begin{variable}{\g_@@_id_int} % This integer is used to track the bookmarks. % id 1 is the root, the first \enquote{real} bookmark % has id 2. % \begin{macrocode} \int_new:N \g_@@_id_int \int_gincr:N \g_@@_id_int % \end{macrocode} % \end{variable} % % \begin{variable}{\g_@@_current_parent_id_tl} % This holds the id of the current parent % so that we do not have to retrieve it all the time. % \begin{macrocode} \tl_new:N \g_@@_current_parent_id_tl \tl_gset:Nn \g_@@_current_parent_id_tl {1} % \end{macrocode} % \end{variable} % % \begin{variable}{\g_@@_current_level_int} % This holds the (relative) level. It starts at 1 as dvipdfmx counts this way. % \begin{macrocode} \int_new:N \g_@@_current_level_int \int_gincr:N \g_@@_current_level_int % \end{macrocode} % \end{variable} % % We do not assume that we will ever have more than 2000 bookmarks, but we set a block % size so that we can make the range dynamic if needed like in the object code. % % \begin{variable}{\c_@@_block_size_int} % Sets the block size used for managing outlines. % \begin{macrocode} \int_const:Nn \c_@@_block_size_int { 2000 } % \end{macrocode} % \end{variable} % % For some backends we must know for every outline % the numbers of direct children, and % we must know the id of the parent to be able to go % up the tree again. For this we use two intarray variables. % \begin{variable}{\g_@@_kid_count_intarray,\g_@@_parent_id_intarray} % The first contains for every id the number of (direct) kids. % The second contains the id of the parent. % \begin{macrocode} \intarray_new:Nn \g_@@_kid_count_intarray { \c_@@_block_size_int } \intarray_new:Nn \g_@@_parent_id_intarray { \c_@@_block_size_int } \intarray_gset:Nnn \g_@@_parent_id_intarray {1}{1} % \end{macrocode} % \end{variable} % % Unlike \pkg{bookmark} we do not use an external file but keep all content in memory % in one large tl var. % \begin{variable}{\g_@@_collect_build_tl} % \begin{macrocode} \tl_new:N \g_@@_collect_build_tl % \end{macrocode} % \end{variable} % % The main commands % \begin{macro}{\pdfoutline_action:nnn} % \begin{macrocode} \cs_new_protected:Npn \pdfoutline_action:nnn #1 #2 #3 % #1 level, #2 action, #3 title { \bool_if:NT \l_pdfoutline_active_bool { \int_compare:nNnTF {\g_@@_id_int}={1} { \tl_build_gbegin:N \g_@@_collect_build_tl \@@_item:nnnn {0}{#2}{#3}{action} \tl_gput_right:Nn \g__kernel_pdfmanagement_end_run_code_tl { \tl_build_gend:N \g_@@_collect_build_tl \tl_use:N \g_@@_collect_build_tl } } { \@@_item:nnnn {#1}{#2}{#3}{action} } \cs_gset_protected:Npn \pdfoutline_action:nnn ##1 ##2 ##3 { \bool_if:NT \l_pdfoutline_active_bool {\@@_item:nnnn {##1}{##2}{##3}{action}} } } } \cs_generate_variant:Nn \pdfoutline_action:nnn {nee} % \end{macrocode} % \end{macro} % % \begin{macro}{\pdfoutline_goto:nnn} % \begin{macrocode} \cs_new_protected:Npn \pdfoutline_goto:nnn #1 #2 #3 % #1 level, #2 destination, #3 title { \bool_if:NT \l_pdfoutline_active_bool { \int_compare:nNnTF {\g_@@_id_int}={1} { \tl_build_gbegin:N \g_@@_collect_build_tl \@@_item:nnnn {0}{#2}{#3}{goto} \tl_gput_right:Nn \g__kernel_pdfmanagement_end_run_code_tl { \tl_build_gend:N \g_@@_collect_build_tl \tl_use:N \g_@@_collect_build_tl } } { \@@_item:nnnn {#1}{#2}{#3}{goto} } \cs_gset_protected:Npn \pdfoutline_goto:nnn ##1 ##2 ##3 { \bool_if:NT \l_pdfoutline_active_bool {\@@_item:nnnn {##1}{##2}{##3}{goto}} } } } \cs_generate_variant:Nn \pdfoutline_goto:nnn {nee} % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_item:nnnn} % \begin{macrocode} \cs_new_protected:Npn \@@_item:nnnn #1 #2 #3 #4 % #1 level, #2 action, #3 title, #4 keyword action or goto { \int_gincr:N \g_@@_id_int % \end{macrocode} % Follow up bookmarks. At first siblings. % The parent doesn't change. % \begin{macrocode} \int_compare:nNnTF {#1}={0} { \intarray_gset:Nnn \g_@@_parent_id_intarray { \g_@@_id_int } { \g_@@_current_parent_id_tl } \intarray_gset:Nnn \g_@@_kid_count_intarray { \g_@@_current_parent_id_tl } { \intarray_item:Nn \g_@@_kid_count_intarray { \g_@@_current_parent_id_tl } + 1 } } { % \end{macrocode} % The first child of the previous bookmark % \begin{macrocode} \int_compare:nNnTF {#1} > {0} { \int_gincr:N \g_@@_current_level_int \tl_gset:Ne \g_@@_current_parent_id_tl { \int_eval:n { \g_@@_id_int -1 } } \intarray_gset:Nnn \g_@@_parent_id_intarray { \g_@@_id_int } { \g_@@_current_parent_id_tl } \intarray_gset:Nnn \g_@@_kid_count_intarray { \g_@@_current_parent_id_tl } { 1 } } % \end{macrocode} % The most complicated case: going up the hierarchy. % The root level is 1, so we can go up at most current level minus 1 % steps. If we are already at the root level, the current parent is 1. % \begin{macrocode} { \int_compare:nNnTF { \g_@@_current_level_int } = {1} { \tl_gset:Ne \g_@@_current_parent_id_tl {1} } { \int_step_inline:nn { \int_min:nn { \int_abs:n {#1} } { \g_@@_current_level_int -1 } } { % \end{macrocode} % Get recursively the parent of the current parent and decrease the level. % \begin{macrocode} \tl_gset:Ne \g_@@_current_parent_id_tl { \intarray_item:Nn \g_@@_parent_id_intarray { \g_@@_current_parent_id_tl } } \int_gdecr:N \g_@@_current_level_int } } % \end{macrocode} % Store the parent and % increase the kid count of the finally found parent (if this is the root level, % the id is 1) % \begin{macrocode} \intarray_gset:Nnn \g_@@_parent_id_intarray { \g_@@_id_int } { \g_@@_current_parent_id_tl } \intarray_gset:Nnn \g_@@_kid_count_intarray { \g_@@_current_parent_id_tl } { \intarray_item:Nn \g_@@_kid_count_intarray { \g_@@_current_parent_id_tl } + 1 } } } % \end{macrocode} % Now we store the collected data into the tl var. % \begin{macrocode} \@@_store:nnn {#2}{#3}{#4} } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_color_export:nnN} % This exports space separated values from a color expression or a |[model]{values}| % expression. It is used in the next command. % \begin{macrocode} \cs_new_protected:Npn \@@_color_export_aux:wnnN [#1] #2 #3 #4 { \color_export:nnnN {#1}{#2}{ space-sep-#3 }#4 } \cs_new_protected:Npn \@@_color_export:nnN #1 #2 #3 %#1 color, #2 target model, #3 command { \tl_if_blank:nTF { #1 } { \tl_clear:N #3 } { \tl_if_head_eq_charcode:nNTF {#1} [ %] { \@@_color_export_aux:wnnN #1 { #2 } #3 } { \color_export:nnN { #1 } { space-sep-#2 } #3 } } } \cs_generate_variant:Nn \@@_color_export:nnN {ee} % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_store:nnn} % This command stores the data into the tl var. The backends have different requirements % regarding the handling, so we keep arguments separate and generic to stay flexible. % \begin{macrocode} \cs_new_protected:Npn \@@_store:nnn #1 #2 #3 % #1: action or destination, #2: title, #3: keyword action or goto { \int_compare:nNnF { \g_@@_current_level_int } < { \l_pdfoutline_open_int } { \bool_set_false:N \l_pdfoutline_open_bool } \@@_color_export:eeN {\l_pdfoutline_color_tl}{\l_pdfoutline_color_model_tl}\l_@@_tmpa_tl \tl_build_gput_right:Ne \g_@@_collect_build_tl { \exp_not:N \use:c { __pdf_backend_#3:nnnnnnn } % #1 level, #2 kid count, #3 open? #4 color, #5 flag, #6 action/destination, #7 title, { \int_use:N \g_@@_current_level_int } { \exp_not:N \intarray_item:Nn \exp_not:N \g_@@_kid_count_intarray { \int_use:N \g_@@_id_int } } { \bool_if:NTF\l_pdfoutline_open_bool {\exp_not:N \c_true_bool }{ \exp_not:N \c_false_bool} } { \l_@@_tmpa_tl } { \bitset_to_arabic:N \l_pdfoutline_F_bitset } { \exp_not:n {#1} % action/destination, or expand?? } { \exp_not:n {#2} % title, or expand?? } } } % \end{macrocode} % \end{macro} % % \subsection{References} % \begin{macro}{\pdfoutline_id_ref_last:,} % \begin{macrocode} \cs_new:Npn \pdfoutline_id_ref_last: {\int_use:N\g_@@_id_int} % \end{macrocode} % \end{macro} % \begin{macro}{\pdfoutline_level_ref_last:} % \begin{macrocode} \cs_new:Npn \pdfoutline_level_ref_last: {\int_use:N\g_@@_current_level_int} % \end{macrocode} % \end{macro} % \begin{macro}{\pdfoutline_parent_ref:n} % \begin{macrocode} \cs_new:Npn \pdfoutline_parent_ref:n #1 { \intarray_item:Nn\g_@@_parent_id_intarray{#1} } % % \end{macrocode} % \end{macro} % % \end{implementation} % % \PrintIndex