Clarify and improve QPDFPageObjectHelper::get*Box methods

Add copy_if_fallback and explain how it differs from copy_if_shared.
This commit is contained in:
Jay Berkenbilt 2022-09-06 19:00:40 -04:00
parent c1def4ead4
commit 76cd7ea67a
8 changed files with 825 additions and 42 deletions

View File

@ -1,5 +1,13 @@
2022-09-06 Jay Berkenbilt <ejb@ql.org>
* For all bounding box methods in QPDFPageObjectHelper other than
MediaBox, add a parameter `copy_if_fallback`, and add comments
explaining in depth exactly what copy_if_shared and
copy_if_fallback mean. Fixes #664.
* Add new methods getArtBox and getBleedBox to
QPDFPageObjectHelper, completing the set of bounding box methods.
* Add == equality for QPDFObjectHandle. Two QPDFObjectHandle
objects are equal if they point to the same underlying object,
meaning changes to one will be reflected in the other.

View File

@ -45,30 +45,152 @@ class QPDFPageObjectHelper: public QPDFObjectHelper
QPDF_DLL
virtual ~QPDFPageObjectHelper() = default;
// Works with pages and form XObjects. Return the effective value
// of this attribute for the page/form XObject. For pages, if the
// requested attribute is not present on the page but is
// inheritable, look up through the page's ancestors in the page
// tree. If copy_if_shared is true, then this method will replace
// the attribute with a shallow copy if it is in indirect or
// inherited and return the copy. You should do this if you are
// going to modify the returned object and want the modifications
// to apply to the current page/form XObject only.
// PAGE ATTRIBUTES
// The getAttribute method works with pages and form XObjects. It
// return the value of the requested attribute from the page/form
// XObject's dictionary, taking inheritance from the pages tree
// into consideration. For pages, the attributes /MediaBox,
// /CropBox, /Resources, and /Rotate are inheritable, meaning that
// if they are not present directly on the page node, they may be
// inherited from ancestor nodes in the pages tree.
//
// There are two ways that an attribute can be "shared":
//
// * For inheritable attributes on pages, it may appear in a
// higher level node of the pages tree
//
// * For any attribute, the attribute may be an indirect object
// which may be referenced by more than one page/form XObject.
//
// If copy_if_shared is true, then this method will replace the
// attribute with a shallow copy if it is indirect or inherited
// and return the copy. You should do this if you are going to
// modify the returned object and want the modifications to apply
// to the current page/form XObject only.
QPDF_DLL
QPDFObjectHandle getAttribute(std::string const& name, bool copy_if_shared);
// Return the TrimBox. If not defined, fall back to CropBox
QPDF_DLL
QPDFObjectHandle getTrimBox(bool copy_if_shared = false);
// PAGE BOXES
//
// Pages have various types of boundary boxes. These are described
// in detail in the PDF specification (section 14.11.2 Page
// boundaries). They are, by key in the page dictionary:
//
// * /MediaBox -- boundaries of physical page
// * /CropBox -- clipping region of what is displayed
// * /BleedBox -- clipping region for production environments
// * /TrimBox -- dimensions of final printed page after trimming
// * /ArtBox -- extent of meaningful content including margins
//
// Of these, only /MediaBox is required. If any are absent, the
// fallback value for /CropBox is /MediaBox, and the fallback
// values for the other boxes are /CropBox.
//
// As noted above (PAGE ATTRIBUTES), /MediaBox and /CropBox can be
// inherited from parent nodes in the pages tree. The other boxes
// can't be inherited.
//
// When the comments below refer to the "effective value" of an
// box, this takes into consideration both inheritance through the
// pages tree (in the case of /MediaBox and /CropBox) and fallback
// values for missing attributes (for all except /MediaBox).
//
// For the methods below, copy_if_shared is passed to getAttribute
// and therefore refers only to indirect objects and values that
// are inherited through the pages tree.
//
// If copy_if_fallback is true, a copy is made if the object's
// value was obtained by falling back to a different box.
//
// The copy_if_shared and copy_if_fallback parameters carry across
// multiple layers. This is explained below.
//
// You should set copy_if_shared to true if you want to modify a
// bounding box for the current page without affecting other pages
// but you don't want to change the fallback behavior. For
// example, if you want to modify the /TrimBox for the current
// page only but have it continue to fall back to the value of
// /CropBox or /MediaBox if they are not defined, you could set
// copy_if_shared to true.
//
// You should set copy_if_fallback to true if you want to modify a
// specific box as distinct from any other box. For example, if
// you want to make /TrimBox differ from /CropBox, then you should
// set copy_if_fallback to true.
//
// The copy_if_fallback flags were added in qpdf 11.
//
// For example, suppose that neither /CropBox nor /TrimBox is
// present on a page but /CropBox is present in the page's parent
// node in the page tree.
//
// * getTrimBox(false, false) would return the /CropBox from the
// parent node.
//
// * getTrimBox(true, false) would make a shallow copy of the
// /CropBox from the parent node into the current node and
// return it.
//
// * getTrimBox(false, true) would make a shallow copy of the
// /CropBox from the parent node into /TrimBox of the current
// node and return it.
//
// * getTrimBox(true, true) would make a shallow copy of the
// /CropBox from the parent node into the current node, then
// make a shallow copy of the resulting copy to /TrimBox of the
// current node, and then return that.
//
// To illustrate how these parameters carry across multiple
// layers, suppose that neither /MediaBox, /CropBox, nor /TrimBox
// is present on a page but /MediaBox is present on the parent. In
// this case:
//
// * getTrimBox(false, false) would return the value of /MediaBox
// from the parent node.
//
// * getTrimBox(true, false) would copy /MediaBox to the current
// node and return it.
//
// * getTrimBox(false, true) would first copy /MediaBox from the
// parent to /CropBox, then copy /CropBox to /TrimBox, and then
// return the result.
//
// * getTrimBox(true, true) would first copy /MediaBox from the
// parent to the current page, then copy it to /CropBox, then
// copy /CropBox to /TrimBox, and then return the result.
//
// If you need different behavior, call getAttribute directly and
// take care of your own copying.
// Return the CropBox. If not defined, fall back to MediaBox
QPDF_DLL
QPDFObjectHandle getCropBox(bool copy_if_shared = false);
// Return the MediaBox
// Return the effective MediaBox
QPDF_DLL
QPDFObjectHandle getMediaBox(bool copy_if_shared = false);
// Return the effective CropBox. If not defined, fall back to
// MediaBox
QPDF_DLL
QPDFObjectHandle
getCropBox(bool copy_if_shared = false, bool copy_if_fallback = false);
// Return the effective BleedBox. If not defined, fall back to
// CropBox.
QPDF_DLL
QPDFObjectHandle
getBleedBox(bool copy_if_shared = false, bool copy_if_fallback = false);
// Return the effective TrimBox. If not defined, fall back to
// CropBox.
QPDF_DLL
QPDFObjectHandle
getTrimBox(bool copy_if_shared = false, bool copy_if_fallback = false);
// Return the effective ArtBox. If not defined, fall back to
// CropBox.
QPDF_DLL
QPDFObjectHandle
getArtBox(bool copy_if_shared = false, bool copy_if_fallback = false);
// Iterate through XObjects, possibly recursing into form
// XObjects. This works with pages or form XObjects. Call action
// on each XObject for which selector, if specified, returns true.
@ -373,6 +495,11 @@ class QPDFPageObjectHelper: public QPDFObjectHelper
QPDFAcroFormDocumentHelper* from_afdh = nullptr);
private:
QPDFObjectHandle getAttribute(
std::string const& name,
bool copy_if_shared,
std::function<QPDFObjectHandle()> get_fallback,
bool copy_if_fallback);
static bool removeUnreferencedResourcesHelper(
QPDFPageObjectHelper ph, std::set<std::string>& unresolved);

View File

@ -237,6 +237,16 @@ QPDFPageObjectHelper::QPDFPageObjectHelper(QPDFObjectHandle oh) :
QPDFObjectHandle
QPDFPageObjectHelper::getAttribute(std::string const& name, bool copy_if_shared)
{
return getAttribute(name, copy_if_shared, nullptr, false);
}
QPDFObjectHandle
QPDFPageObjectHelper::getAttribute(
std::string const& name,
bool copy_if_shared,
std::function<QPDFObjectHandle()> get_fallback,
bool copy_if_fallback)
{
QPDFObjectHandle result;
QPDFObjectHandle dict;
@ -272,28 +282,17 @@ QPDFPageObjectHelper::getAttribute(std::string const& name, bool copy_if_shared)
"qpdf",
"QPDFPageObjectHelper copy shared attribute",
is_form_xobject ? 0 : 1);
result = result.shallowCopy();
dict.replaceKey(name, result);
result = dict.replaceKeyAndGetNew(name, result.shallowCopy());
}
return result;
}
QPDFObjectHandle
QPDFPageObjectHelper::getTrimBox(bool copy_if_shared)
{
QPDFObjectHandle result = getAttribute("/TrimBox", copy_if_shared);
if (result.isNull()) {
result = getCropBox(copy_if_shared);
}
return result;
}
QPDFObjectHandle
QPDFPageObjectHelper::getCropBox(bool copy_if_shared)
{
QPDFObjectHandle result = getAttribute("/CropBox", copy_if_shared);
if (result.isNull()) {
result = getMediaBox();
if (result.isNull() && get_fallback) {
result = get_fallback();
if (copy_if_fallback && !result.isNull()) {
QTC::TC("qpdf", "QPDFPageObjectHelper copied fallback");
result = dict.replaceKeyAndGetNew(name, result.shallowCopy());
} else {
QTC::TC(
"qpdf", "QPDFPageObjectHelper used fallback without copying");
}
}
return result;
}
@ -304,6 +303,52 @@ QPDFPageObjectHelper::getMediaBox(bool copy_if_shared)
return getAttribute("/MediaBox", copy_if_shared);
}
QPDFObjectHandle
QPDFPageObjectHelper::getCropBox(bool copy_if_shared, bool copy_if_fallback)
{
return getAttribute(
"/CropBox",
copy_if_shared,
[this, copy_if_shared]() { return this->getMediaBox(copy_if_shared); },
copy_if_fallback);
}
QPDFObjectHandle
QPDFPageObjectHelper::getTrimBox(bool copy_if_shared, bool copy_if_fallback)
{
return getAttribute(
"/TrimBox",
copy_if_shared,
[this, copy_if_shared, copy_if_fallback]() {
return this->getCropBox(copy_if_shared, copy_if_fallback);
},
copy_if_fallback);
}
QPDFObjectHandle
QPDFPageObjectHelper::getArtBox(bool copy_if_shared, bool copy_if_fallback)
{
return getAttribute(
"/ArtBox",
copy_if_shared,
[this, copy_if_shared, copy_if_fallback]() {
return this->getCropBox(copy_if_shared, copy_if_fallback);
},
copy_if_fallback);
}
QPDFObjectHandle
QPDFPageObjectHelper::getBleedBox(bool copy_if_shared, bool copy_if_fallback)
{
return getAttribute(
"/BleedBox",
copy_if_shared,
[this, copy_if_shared, copy_if_fallback]() {
return this->getCropBox(copy_if_shared, copy_if_fallback);
},
copy_if_fallback);
}
void
QPDFPageObjectHelper::forEachXObject(
bool recursive,

View File

@ -261,6 +261,11 @@ For a detailed list of changes, please see the file
generation parameters. The old versions will continue to be
supported and are not deprecated.
- In ``QPDFPageObjectHelper``, add a ``copy_if_fallback``
parameter to most of the page bounding box methods, and clarify
in the comments about the difference between ``copy_if_shared``
and ``copy_if_fallback``.
- Add a move constructor to the ``Buffer`` class.
- Other changes

View File

@ -676,3 +676,5 @@ QPDF_json missing json version 0
QPDF_json bad json version 0
QPDF_json bad calledgetallpages 0
QPDF_json bad pushedinheritedpageresources 0
QPDFPageObjectHelper copied fallback 0
QPDFPageObjectHelper used fallback without copying 0

View File

@ -14,8 +14,6 @@ cleanup();
my $td = new TestDriver('page-api');
my $n_tests = 11;
$td->runtest("basic page API",
{$td->COMMAND => "test_driver 15 page_api_1.pdf"},
{$td->STRING => "test 15 done\n", $td->EXIT_STATUS => 0},
@ -58,5 +56,10 @@ $td->runtest("flatten rotation",
$td->runtest("check output",
{$td->FILE => "a.pdf"},
{$td->FILE => "boxes-flattened.pdf"});
$td->runtest("get box methods",
{$td->COMMAND => "test_driver 94 boxes2.pdf"},
{$td->STRING => "test 94 done\n", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
cleanup();
$td->report($n_tests);
$td->report(12);

491
qpdf/qtest/qpdf/boxes2.pdf Normal file
View File

@ -0,0 +1,491 @@
%PDF-1.3
%¿÷¢þ
%QDF-1.0
1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 5
/Kids [
3 0 R
4 0 R
5 0 R
6 0 R
7 0 R
]
/MediaBox [
0
0
612
792
]
/Type /Pages
>>
endobj
%% Page 1
3 0 obj
<<
/Contents 8 0 R
/Parent 2 0 R
/Resources <<
/Font <<
/F1 10 0 R
>>
/ProcSet 11 0 R
/XObject <<
/Fx1 12 0 R
>>
>>
/Type /Page
>>
endobj
%% Page 2
4 0 obj
<<
/Contents 14 0 R
/CropBox [
10
20
582
752
]
/Parent 2 0 R
/Resources <<
/Font <<
/F1 16 0 R
>>
/ProcSet 17 0 R
/XObject <<
/Fx1 12 0 R
>>
>>
/Type /Page
>>
endobj
%% Page 3
5 0 obj
<<
/Contents 18 0 R
/CropBox [
10
20
582
752
]
/MediaBox [
0
0
612
792
]
/Parent 2 0 R
/Resources <<
/Font <<
/F1 20 0 R
>>
/ProcSet 21 0 R
/XObject <<
/Fx1 12 0 R
>>
>>
/Type /Page
>>
endobj
%% Page 4
6 0 obj
<<
/BleedBox [
20
40
552
712
]
/Contents 22 0 R
/CropBox 24 0 R
/MediaBox [
0
0
612
792
]
/Parent 2 0 R
/Resources <<
/Font <<
/F1 25 0 R
>>
/ProcSet 26 0 R
/XObject <<
/Fx1 12 0 R
>>
>>
/TrimBox [
30
60
522
672
]
/Type /Page
>>
endobj
%% Page 5
7 0 obj
<<
/ArtBox [
25
50
527
722
]
/Contents 27 0 R
/Parent 2 0 R
/Resources <<
/Font <<
/F1 29 0 R
>>
/ProcSet 30 0 R
/XObject <<
/Fx1 12 0 R
>>
>>
/TrimBox [
30
60
522
672
]
/Type /Page
>>
endobj
%% Contents for page 1
8 0 obj
<<
/Length 9 0 R
>>
stream
q
BT
/F1 12 Tf
144 470 Td
(Media inherited) Tj
ET
Q
q
1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
/Fx1 Do
Q
endstream
endobj
9 0 obj
121
endobj
10 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
11 0 obj
[
/PDF
/Text
]
endobj
12 0 obj
<<
/BBox [
0
0
612
792
]
/Resources <<
/Font <<
/F1 10 0 R
>>
/ProcSet 31 0 R
>>
/Subtype /Form
/Type /XObject
/Length 13 0 R
>>
stream
BT
/F1 12 Tf
144 600 Td
1 0 0 rg
(red rectangle at media [0 0 612 792]) Tj
0 -15 Td
0 1 0 rg
(green at crop [10 20 582 752]) Tj
0 -15 Td
0 0 1 rg
(blue at bleed [20 40 552 712]) Tj
0 -15 Td
1 .5 0 rg
(orange at trim [30 60 522 672]) Tj
0 -15 Td
1 0 1 rg
(purple at art [40 80 452 552]) Tj
0 -15 Td
0 0 0 rg
(if crop is present, page is cropped) Tj
ET
5 w
1 0 0 RG
0 0 612 792 re s
0 1 0 RG
10 20 572 732 re s
0 0 1 RG
20 40 532 672 re s
1 .5 0 RG
30 60 492 612 re s
1 0 1 RG
40 80 452 552 re s
endstream
endobj
13 0 obj
532
endobj
%% Contents for page 2
14 0 obj
<<
/Length 15 0 R
>>
stream
q
BT
/F1 12 Tf
144 470 Td
(Media inherited, Crop present) Tj
ET
Q
q
1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
/Fx1 Do
Q
endstream
endobj
15 0 obj
135
endobj
16 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
17 0 obj
[
/PDF
/Text
]
endobj
%% Contents for page 3
18 0 obj
<<
/Length 19 0 R
>>
stream
q
BT
/F1 12 Tf
144 470 Td
(Media, Crop present) Tj
ET
Q
q
1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
/Fx1 Do
Q
endstream
endobj
19 0 obj
125
endobj
20 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
21 0 obj
[
/PDF
/Text
]
endobj
%% Contents for page 4
22 0 obj
<<
/Length 23 0 R
>>
stream
q
BT
/F1 12 Tf
144 470 Td
(Media, Trim, Bleed present, Crop indirect) Tj
ET
Q
q
1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
/Fx1 Do
Q
endstream
endobj
23 0 obj
147
endobj
24 0 obj
[
10
20
582
752
]
endobj
25 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
26 0 obj
[
/PDF
/Text
]
endobj
%% Contents for page 5
27 0 obj
<<
/Length 28 0 R
>>
stream
q
BT
/F1 12 Tf
144 470 Td
(Media inherited, Trim, Art present) Tj
ET
Q
q
1.00000 0.00000 0.00000 1.00000 0.00000 0.00000 cm
/Fx1 Do
Q
endstream
endobj
28 0 obj
140
endobj
29 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
30 0 obj
[
/PDF
/Text
]
endobj
31 0 obj
[
/PDF
/Text
]
endobj
xref
0 32
0000000000 65535 f
0000000025 00000 n
0000000079 00000 n
0000000247 00000 n
0000000446 00000 n
0000000693 00000 n
0000000986 00000 n
0000001345 00000 n
0000001651 00000 n
0000001827 00000 n
0000001847 00000 n
0000001966 00000 n
0000002002 00000 n
0000002745 00000 n
0000002789 00000 n
0000002981 00000 n
0000003002 00000 n
0000003121 00000 n
0000003180 00000 n
0000003362 00000 n
0000003383 00000 n
0000003502 00000 n
0000003561 00000 n
0000003765 00000 n
0000003786 00000 n
0000003829 00000 n
0000003948 00000 n
0000004007 00000 n
0000004204 00000 n
0000004225 00000 n
0000004344 00000 n
0000004380 00000 n
trailer <<
/Root 1 0 R
/Size 32
/ID [<42ed290ee4e4c51171853f92a1a7642d><4529bd7e2686f4deaa59ab2fa8e0338d>]
>>
startxref
4416
%%EOF

View File

@ -3302,6 +3302,108 @@ test_93(QPDF& pdf, char const* arg2)
assert(trailer.getKey("/Potato") == oh2);
}
static void
test_94(QPDF& pdf, char const* arg2)
{
// Exercise methods to get page boxes. This test is built for
// boxes2.pdf.
// /MediaBox is present in the pages tree root.
// Each page has the following boxes present directly:
// 1. none
// 2. crop
// 3. media, crop
// 4. media, crop, trim, bleed; crop is indirect
// 5. trim, art
auto pages_root = pdf.getRoot().getKey("/Pages");
auto root_media = pages_root.getKey("/MediaBox");
auto root_media_unparse = root_media.unparse();
auto pages = QPDFPageDocumentHelper(pdf).getAllPages();
assert(pages.size() == 5);
auto& p1 = pages[0];
auto& p2 = pages[1];
auto& p3 = pages[2];
auto& p4 = pages[3];
auto& p5 = pages[4];
assert(p1.getObjectHandle().getKey("/MediaBox").isNull());
// MediaBox not present, so get inherited one
assert(p1.getMediaBox(false) == root_media);
// Other boxesBox not present, so fall back to MediaBox
assert(p1.getCropBox(false, false) == root_media);
assert(p1.getBleedBox(false, false) == root_media);
assert(p1.getTrimBox(false, false) == root_media);
assert(p1.getArtBox(false, false) == root_media);
// Make copy of artbox
auto p1_new_art = p1.getArtBox(false, true);
assert(p1_new_art.unparse() == root_media_unparse);
assert(p1_new_art != root_media);
// This also copied cropbox
auto p1_new_crop = p1.getCropBox(false, false);
assert(p1_new_crop != root_media);
assert(p1_new_crop != p1_new_art);
assert(p1_new_crop.unparse() == root_media_unparse);
// But it didn't copy Media
assert(p1.getMediaBox(false) == root_media);
// Now fall back to new crop
assert(p1.getTrimBox(false, false) == p1_new_crop);
// Request copy. The value returned has the same structure but is
// a different object.
auto p1_effective_media = p1.getMediaBox(true);
assert(p1_effective_media.unparse() == root_media_unparse);
assert(p1_effective_media != root_media);
// copy_on_fallback didn't have to copy media to crop
assert(p2.getMediaBox(false) == root_media);
auto p2_crop = p2.getCropBox(false, false);
auto p2_new_trim = p2.getTrimBox(false, true);
assert(p2_new_trim.unparse() == p2_crop.unparse());
assert(p2_new_trim != p2_crop);
assert(p2.getMediaBox(false) == root_media);
// We didn't need to copy anything
auto p3_media = p3.getMediaBox(false);
auto p3_crop = p3.getCropBox(false, false);
assert(p3.getMediaBox(true) == p3_media);
assert(p3.getCropBox(true, true) == p3_crop);
// We didn't have to copy for bleed but we did for art
auto p4_orig_crop = p4.getObjectHandle().getKey("/CropBox");
auto p4_crop = p4.getCropBox(false, false);
assert(p4_orig_crop == p4_crop);
auto p4_bleed1 = p4.getBleedBox(false, false);
auto p4_bleed2 = p4.getBleedBox(false, true);
assert(p4_bleed1 != p4_crop);
assert(p4_bleed1 == p4_bleed2);
auto p4_art1 = p4.getArtBox(false, false);
assert(p4_art1 == p4_crop);
auto p4_art2 = p4.getArtBox(false, true);
assert(p4_art2 != p4_crop);
auto p4_new_crop = p4.getCropBox(true, false);
assert(p4_new_crop != p4_orig_crop);
assert(p4_orig_crop.isIndirect());
assert(!p4_new_crop.isIndirect());
assert(p4_new_crop.unparse() == p4_orig_crop.unparseResolved());
// Exercise copying for inheritence and fallback
assert(p5.getMediaBox(false) == root_media);
assert(p5.getCropBox(false, false) == root_media);
assert(p5.getBleedBox(false, false) == root_media);
auto p5_new_bleed = p5.getBleedBox(true, true);
auto p5_new_media = p5.getMediaBox(false);
auto p5_new_crop = p5.getCropBox(false, false);
assert(p5_new_media != root_media);
assert(p5_new_crop != root_media);
assert(p5_new_crop != p5_new_media);
assert(p5_new_bleed != root_media);
assert(p5_new_bleed != p5_new_media);
assert(p5_new_bleed != p5_new_crop);
assert(p5_new_media.unparse() == root_media_unparse);
assert(p5_new_crop.unparse() == root_media_unparse);
assert(p5_new_bleed.unparse() == root_media_unparse);
}
void
runtest(int n, char const* filename1, char const* arg2)
{
@ -3411,7 +3513,7 @@ runtest(int n, char const* filename1, char const* arg2)
{80, test_80}, {81, test_81}, {82, test_82}, {83, test_83},
{84, test_84}, {85, test_85}, {86, test_86}, {87, test_87},
{88, test_88}, {89, test_89}, {90, test_90}, {91, test_91},
{92, test_92}, {93, test_93}};
{92, test_92}, {93, test_93}, {94, test_94}};
auto fn = test_functions.find(n);
if (fn == test_functions.end()) {