Carry the reading order a screen reader follows - #12
Merged
Conversation
A tagged document says which run of marks on which page is a heading, a paragraph, a table cell, the label of a form field. That is the reading order a screen reader follows, and for a government form it is often what the law requires: RGAA in France, Section 508 in the United States, EN 301 549 in the EU. #11 rebuilt the catalogue around the pages that survived and deliberately left /StructTreeRoot behind, because carrying it wrong sends a reader through the document in the wrong order rather than letting it fall back on the text. It is now rebuilt. Of 1 633 real forms from eighteen issuers, 1 021 carry a structure tree and 1 012 of those carry the number tree that indexes it; 6 869 of their 7 114 pages say under what number their marks are filed. The tree holds 853 108 elements, 470 269 marks named by number, 735 named by reference and 104 521 references to annotations. WHERE THE KEYS LIVE. A mark is filed under a key the object holding it carries, and it is not always the page. Of the 1 021 tagged forms, 991 file every mark under a page; 14 file some under a page and some under a form XObject drawn on it; and 3 — the UK lasting-power-of-attorney forms — carry no key on any page at all and file everything under the XObjects, whose stream dictionaries say /StructParents 0, 1, 2 and 3 while every page of the file says nothing. 6 869 keys sit on pages and 150 on drawn streams. That is read off the files, not off a specification, and it settles two things: a mark inside a drawn stream cannot be filed under the page's number, and the numbers handed to pages must start above every key a drawn stream already carries — because a copied stream keeps its key, a stream being written before the structure above it is rebuilt. WHAT IS CARRIED. The tree of elements, each element's page, the marks inside those pages, marked-content references, references to annotations, /RoleMap, /ClassMap, /IDTree, and the number tree, rebuilt in both directions. The numbers are handed out afresh: a page that kept the number it had in a file it is no longer part of is a page a reader would look up and be told, with every confidence, about somebody else's. An element whose every page has gone is removed and its parent pruned; one with some pages gone keeps the children that are left; an empty table cell is kept, because the shape of a table is part of what it says, and the corpus has 30 345 of them. A page written twice carries the structure once, on the first copy, since an element names one page. MEASURED. Rotating all 1 633: 1 021 trees in, 1 014 out. The seven that lose theirs are the seven whose /StructTreeRoot has no /K in the file we were given — a root with a role map and nothing under it. 991 of the 1 021 come out identical down to the last element, mark, reference and annotation reference. The thirty that differ are all of them: - 23 files drop 142 references to annotations that are on no page of the source either — the same 142 the OBJR survey found; 104 379 of 104 521 point at an annotation that is on a page. Ten of the 23 lose 236 elements with them, the elements whose only child was such a reference. - 4 files drop 109 marks inside a stream that no surviving page draws. 109 of the corpus's 215 such marks named a stream no page of the source drew either; the other 106 are carried, with the stream, under the key the stream carries. Two UK power-of-attorney forms go from 158 elements to 91 this way and cerfa_12496 from 16 to 10. - 3 files drop 33 marks that have no page anywhere above them. Nothing else moves: /RoleMap 984 in and 977 out, /ClassMap 356 and 349, /ParentTree 1 012 and 1 004 — in each case the missing ones are the seven empty trees, plus one file whose tree holds no mark and no annotation, so there is nothing to file. /IDTree goes from 173 to 153: the 20 files that lose it have no element carrying an /ID at all, so their identifier tree named nothing. READING ORDER, NOT JUST PRESENCE. The check walks both trees in step. At every level the children left must be the children that were there, in the order they were there, with nothing added and nothing moved; elements are told apart by what they contain, and the pairing backtracks, because two sibling table cells of the same kind are indistinguishable until their contents are compared and a checker that commits to the first plausible pairing reports a defect of its own. Then, on the output alone: every element's /Pg names a page that exists (0 failures), every mark the tree names is a mark that page's content stream really draws, and the number tree agrees with the tree in both directions — every mark the tree claims is filed under the page's key, and every non-null place in the page's array is a mark the tree claims (0 failures), and the same for the annotations (0 failures). The output names 1 387 marks that the page does not draw. The source names the same 1 387, in the same 17 files, mark for mark: not one wrong pointer is introduced. On the number tree we come out ahead of the source — 4 stream marks are misfiled in the file we were given and 1 in ours, the one that remains being two form XObjects that share a key and both claim mark 0, which a number tree can only answer once. The same check over five other verbs. Keeping the odd pages: 1 009 trees out of 1 021 — the five extra losses are files whose entire marked content is on even pages, so keeping the odd ones leaves nothing to describe — 210 191 marks and 49 302 annotation references pruned with their pages, 358 261 elements removed, 0 unexplained. Writing a page twice: identical to the rotate, and the second copy carries no /StructParents. Flatten and remove-annotations: all 104 521 annotation references go and 179 435 elements with them, and not one dangling reference is left. Watermarking: identical to the rotate, so a stamp does not disturb the marks underneath it. Merging two files carries no tree, as the catalogue and the form already did: two files have two /RoleMap and /ClassMap dictionaries in which the same name may stand for two different things, and there is no honest way to choose. The 6 897 pages that said where their structure was filed now say nothing, rather than pointing into a tree that is not there. TWO DEFECTS THIS FOUND. A page written twice used to leave anything pointing at its annotations pointing at the last copy rather than the first, which for a structure tree means describing page one and sending a reader to a widget on page two; the annotations a rebuild keeps now answer with the first copy, the same choice the destination map already made for the page itself. And an element whose own page was dropped but which survived through a child on another page was left with no /Pg, and so silently inherited the page of whatever it sat under — a page it is not on, stated as confidently as the right one would have been. It now names the page its remaining content is actually on. NOT CARRIED, with the reason in catalogue.go: a mark inside a stream no surviving page draws, an element's /Ref, and the structure of pages from more than one file. #11's figures do not move. Over the whole corpus of 2 268: 1 338 of 1 340 forms survive a rotate, 1 335 with every field, the same three files losing the same fields; language 866 of 866, marked-up flag 1 057 of 1 057. Checked by running the same harness against v0.6.0, which carries 0 trees of the 1 105 the corpus offers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A tagged document says which run of marks on which page is a heading, a
paragraph, a table cell, the label of a form field. That is the reading order a
screen reader follows, and for a government form it is often what the law
requires — RGAA in France, Section 508 in the United States, EN 301 549 in the
EU. #11 rebuilt the catalogue around the pages that survived and deliberately
left
/StructTreeRootbehind, because a tree that points at the wrong pages isworse than none: a reader follows it into the wrong order instead of falling
back on the text. This rebuilds it.
Before: what is actually there
Over the 1 633 real forms in
/Users/Shared/pdfforms(the 635 vendorfixtures excluded):
/StructTreeRoot/ParentTree/RoleMap//ClassMap//IDTreeThe finding that shaped the design
A mark is filed under a key the object holding it carries, and it is not
always the page. Of the 1 021 tagged forms:
all and file everything under the XObjects, whose stream dictionaries say
/StructParents 0, 1, 2, 3while every page of the file says nothing.6 869 keys sit on pages, 150 on drawn streams. That is read off the files, not
off a specification, and it settles two things. A mark inside a drawn stream
cannot be filed under the page's number — the two are numbered in different
spaces. And the numbers handed to pages must start above every key a drawn
stream already carries, because a copied stream keeps its key: a stream is
written before the structure above it is rebuilt, so it cannot be given a new
one. Numbering pages from zero would eventually hand a page the number a form
XObject on it is filed under, and a reader looking up a mark in that XObject
would be told, with every confidence, about the page's own elements.
What is carried
The tree of elements, each element's page, the marks inside those pages,
marked-content references, references to annotations,
/RoleMap,/ClassMap,/IDTree, and the number tree rebuilt in both directions. Numbers are handedout afresh. An element whose every page has gone is removed and its parent
pruned; one with some pages gone keeps the children that are left; an empty
table cell is kept, because the shape of a table is part of what it says. A page
written twice carries the structure once, on the first copy, since an element
names one page.
After: rotating all 1 633 forms
1 021 trees in, 1 014 out. The seven that lose theirs are the seven whose
/StructTreeRoothas no/Kin the file we were given — a root with a role mapand nothing under it. 991 of the 1 021 come out identical down to the last
element, mark, reference and annotation reference. The thirty that differ are
all of them:
the source either; 104 379 of 104 521 point at an annotation on a page. Ten of
the 23 lose 236 elements with them — the ones whose only child was such a
reference.
the corpus's 215 such marks named a stream no page of the source drew either;
the other 106 are carried, with the stream, under the key the stream
carries. Two power-of-attorney forms go from 158 elements to 91 this way,
cerfa_12496from 16 to 10.Nothing else moves.
/RoleMap984 → 977,/ClassMap356 → 349,/ParentTree1 012 → 1 004: in each case the missing ones are the seven empty trees, plus one
file whose tree holds no mark and no annotation, so there is nothing to file.
/IDTree173 → 153: the 20 files that lose it carry no/IDon any element, sotheir identifier tree named nothing.
Reading order, not just presence
The check walks both trees in step. At every level the children left must be the
children that were there, in the order they were there, with nothing added
and nothing moved; elements are told apart by what they contain, and the pairing
backtracks — two sibling table cells of the same kind are indistinguishable
until their contents are compared, and a checker that commits to the first
plausible pairing reports a defect of its own.
Then on the output alone:
/Pgnames a page that exists/StructParentnames its owning elementThe output names 1 387 marks the page does not draw — the source names the
same 1 387, in the same 17 files, mark for mark. Not one wrong pointer is
introduced. On the number tree we come out ahead of the source: 4 stream marks
are misfiled in the file we were given and 1 in ours, the one remaining being
two form XObjects that share a key and both claim mark 0, which a number tree
can only answer once.
The same check over five other verbs
files whose entire marked content is on even pages, so keeping the odd ones
leaves nothing to describe. 210 191 marks and 49 302 annotation references
pruned with their pages, 358 261 elements removed, 0 unexplained.
no
/StructParents.and 179 435 elements with them; not one dangling reference is left.
underneath it.
was filed now say nothing rather than pointing into a tree that is not there.
Two defects this found
last copy rather than the first — for a structure tree, describing page
one and sending a reader to a widget on page two. The annotations a rebuild
keeps now answer with the first copy, the same choice the destination map
already made for the page itself.
another page was left with no
/Pg, and so silently inherited the page ofwhatever it sat under — a page it is not on, stated as confidently as the
right one would have been. It now names the page its remaining content is on.
Not carried, with the reason in
catalogue.goA mark inside a stream no surviving page draws; a structure element's
/Ref(PDF 2.0, no file in the corpus has one, and copying it would drag a second copy
of the source's tree and pages in behind it); and the structure of pages from
more than one file — two files have two
/RoleMapand/ClassMapdictionariesin which the same name may stand for two different things, and there is no
honest way to choose.
No regression
#11's figures reproduce exactly. Over the whole corpus of 2 268: 1 338 of
1 340 forms survive a rotate, 1 335 with every field, the same three files
losing the same fields; language 866 of 866, marked-up flag 1 057 of 1 057.
Checked by running the same harness against v0.6.0, which carries 0 trees of the
1 105 the corpus offers.
Exact 100% statement coverage,
go vetclean,-raceclean, cross-compiles forall nine targets. No new dependency;
readerstays at v0.4.1, which haseverything this needs.