Commit e7e20772ed29f3eb9756b31fe0bd9bc29a445891

Authored by Jay Berkenbilt
1 parent 5816fb44

name/number trees: remove

ChangeLog
  1 +2021-01-24 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Implement remove for name and number trees as well as exposing
  4 + remove and insertAfter methods for iterators. With this addition,
  5 + qpdf now has robust read/write support for name and number trees.
  6 +
1 2021-01-23 Jay Berkenbilt <ejb@ql.org> 7 2021-01-23 Jay Berkenbilt <ejb@ql.org>
2 8
3 * Add an insert method to QPDFNameTreeObjectHelper and 9 * Add an insert method to QPDFNameTreeObjectHelper and
include/qpdf/QPDFNameTreeObjectHelper.hh
@@ -125,6 +125,11 @@ class QPDFNameTreeObjectHelper: public QPDFObjectHelper @@ -125,6 +125,11 @@ class QPDFNameTreeObjectHelper: public QPDFObjectHelper
125 QPDF_DLL 125 QPDF_DLL
126 void insertAfter(std::string const& key, QPDFObjectHandle value); 126 void insertAfter(std::string const& key, QPDFObjectHandle value);
127 127
  128 + // Remove the current item and advance the iterator to the
  129 + // next item.
  130 + QPDF_DLL
  131 + void remove();
  132 +
128 private: 133 private:
129 iterator(std::shared_ptr<NNTreeIterator> const&); 134 iterator(std::shared_ptr<NNTreeIterator> const&);
130 std::shared_ptr<NNTreeIterator> impl; 135 std::shared_ptr<NNTreeIterator> impl;
@@ -152,6 +157,12 @@ class QPDFNameTreeObjectHelper: public QPDFObjectHelper @@ -152,6 +157,12 @@ class QPDFNameTreeObjectHelper: public QPDFObjectHelper
152 QPDF_DLL 157 QPDF_DLL
153 iterator insert(std::string const& key, QPDFObjectHandle value); 158 iterator insert(std::string const& key, QPDFObjectHandle value);
154 159
  160 + // Remove an item. Return true if the item was found and removed;
  161 + // otherwise return false. If value is not null, initialize it to
  162 + // the value that was removed.
  163 + QPDF_DLL
  164 + bool remove(std::string const& key, QPDFObjectHandle* value = nullptr);
  165 +
155 // Return the contents of the name tree as a map. Note that name 166 // Return the contents of the name tree as a map. Note that name
156 // trees may be very large, so this may use a lot of RAM. It is 167 // trees may be very large, so this may use a lot of RAM. It is
157 // more efficient to use QPDFNameTreeObjectHelper's iterator. 168 // more efficient to use QPDFNameTreeObjectHelper's iterator.
include/qpdf/QPDFNumberTreeObjectHelper.hh
@@ -144,6 +144,11 @@ class QPDFNumberTreeObjectHelper: public QPDFObjectHelper @@ -144,6 +144,11 @@ class QPDFNumberTreeObjectHelper: public QPDFObjectHelper
144 QPDF_DLL 144 QPDF_DLL
145 void insertAfter(numtree_number key, QPDFObjectHandle value); 145 void insertAfter(numtree_number key, QPDFObjectHandle value);
146 146
  147 + // Remove the current item and advance the iterator to the
  148 + // next item.
  149 + QPDF_DLL
  150 + void remove();
  151 +
147 private: 152 private:
148 iterator(std::shared_ptr<NNTreeIterator> const&); 153 iterator(std::shared_ptr<NNTreeIterator> const&);
149 std::shared_ptr<NNTreeIterator> impl; 154 std::shared_ptr<NNTreeIterator> impl;
@@ -170,6 +175,12 @@ class QPDFNumberTreeObjectHelper: public QPDFObjectHelper @@ -170,6 +175,12 @@ class QPDFNumberTreeObjectHelper: public QPDFObjectHelper
170 QPDF_DLL 175 QPDF_DLL
171 iterator insert(numtree_number key, QPDFObjectHandle value); 176 iterator insert(numtree_number key, QPDFObjectHandle value);
172 177
  178 + // Remove an item. Return true if the item was found and removed;
  179 + // otherwise return false. If value is not null, initialize it to
  180 + // the value that was removed.
  181 + QPDF_DLL
  182 + bool remove(numtree_number key, QPDFObjectHandle* value = nullptr);
  183 +
173 // Return the contents of the number tree as a map. Note that 184 // Return the contents of the number tree as a map. Note that
174 // number trees may be very large, so this may use a lot of RAM. 185 // number trees may be very large, so this may use a lot of RAM.
175 // It is more efficient to use QPDFNumberTreeObjectHelper's 186 // It is more efficient to use QPDFNumberTreeObjectHelper's
libqpdf/NNTree.cc
@@ -163,6 +163,13 @@ NNTreeIterator::resetLimits(QPDFObjectHandle node, @@ -163,6 +163,13 @@ NNTreeIterator::resetLimits(QPDFObjectHandle node,
163 bool done = false; 163 bool done = false;
164 while (! done) 164 while (! done)
165 { 165 {
  166 + if (parent == this->path.end())
  167 + {
  168 + QTC::TC("qpdf", "NNTree remove limits from root");
  169 + node.removeKey("/Limits");
  170 + done = true;
  171 + break;
  172 + }
166 auto kids = node.getKey("/Kids"); 173 auto kids = node.getKey("/Kids");
167 int nkids = kids.isArray() ? kids.getArrayNItems() : 0; 174 int nkids = kids.isArray() ? kids.getArrayNItems() : 0;
168 auto items = node.getKey(impl.details.itemsKey()); 175 auto items = node.getKey(impl.details.itemsKey());
@@ -459,7 +466,7 @@ NNTreeIterator::insertAfter(QPDFObjectHandle key, QPDFObjectHandle value) @@ -459,7 +466,7 @@ NNTreeIterator::insertAfter(QPDFObjectHandle key, QPDFObjectHandle value)
459 } 466 }
460 if (items.getArrayNItems() < this->item_number + 2) 467 if (items.getArrayNItems() < this->item_number + 2)
461 { 468 {
462 - error(impl.qpdf, node, "items array is too short"); 469 + error(impl.qpdf, node, "insert: items array is too short");
463 } 470 }
464 items.insertItem(this->item_number + 2, key); 471 items.insertItem(this->item_number + 2, key);
465 items.insertItem(this->item_number + 3, value); 472 items.insertItem(this->item_number + 3, value);
@@ -468,6 +475,144 @@ NNTreeIterator::insertAfter(QPDFObjectHandle key, QPDFObjectHandle value) @@ -468,6 +475,144 @@ NNTreeIterator::insertAfter(QPDFObjectHandle key, QPDFObjectHandle value)
468 increment(false); 475 increment(false);
469 } 476 }
470 477
  478 +void
  479 +NNTreeIterator::remove()
  480 +{
  481 + // Remove this item, leaving the tree valid and this iterator
  482 + // pointing to the next item.
  483 +
  484 + if (! valid())
  485 + {
  486 + throw std::logic_error("attempt made to remove an invalid iterator");
  487 + }
  488 + auto items = this->node.getKey(impl.details.itemsKey());
  489 + int nitems = items.getArrayNItems();
  490 + if (this->item_number + 2 > nitems)
  491 + {
  492 + error(impl.qpdf, this->node,
  493 + "found short items array while removing an item");
  494 + }
  495 +
  496 + items.eraseItem(this->item_number);
  497 + items.eraseItem(this->item_number);
  498 + nitems -= 2;
  499 +
  500 + if (nitems > 0)
  501 + {
  502 + // There are still items left
  503 +
  504 + if ((this->item_number == 0) || (this->item_number == nitems))
  505 + {
  506 + // We removed either the first or last item of an items array
  507 + // that remains non-empty, so we have to adjust limits.
  508 + QTC::TC("qpdf", "NNTree remove reset limits");
  509 + resetLimits(this->node, lastPathElement());
  510 + }
  511 +
  512 + if (this->item_number == nitems)
  513 + {
  514 + // We removed the last item of a non-empty items array, so
  515 + // advance to the successor of the previous item.
  516 + QTC::TC("qpdf", "NNTree erased last item");
  517 + this->item_number -= 2;
  518 + increment(false);
  519 + }
  520 + else if (this->item_number < nitems)
  521 + {
  522 + // We don't have to do anything since the removed item's
  523 + // successor now occupies its former location.
  524 + QTC::TC("qpdf", "NNTree erased non-last item");
  525 + }
  526 + else
  527 + {
  528 + // We already checked to ensure this condition would not
  529 + // happen.
  530 + throw std::logic_error(
  531 + "NNTreeIterator::remove: item_number > nitems after erase");
  532 + }
  533 + return;
  534 + }
  535 +
  536 + if (this->path.empty())
  537 + {
  538 + // Special case: if this is the root node, we can leave it
  539 + // empty.
  540 + QTC::TC("qpdf", "NNTree erased all items on leaf/root");
  541 + setItemNumber(impl.oh, -1);
  542 + return;
  543 + }
  544 +
  545 + QTC::TC("qpdf", "NNTree items is empty after remove");
  546 +
  547 + // We removed the last item from this items array, so we need to
  548 + // remove this node from the parent on up the tree. Then we need
  549 + // to position ourselves at the removed item's successor.
  550 + bool done = false;
  551 + while (! done)
  552 + {
  553 + auto element = lastPathElement();
  554 + auto parent = element;
  555 + --parent;
  556 + auto kids = element->node.getKey("/Kids");
  557 + kids.eraseItem(element->kid_number);
  558 + auto nkids = kids.getArrayNItems();
  559 + if (nkids > 0)
  560 + {
  561 + // The logic here is similar to the items case.
  562 + if ((element->kid_number == 0) || (element->kid_number == nkids))
  563 + {
  564 + QTC::TC("qpdf", "NNTree erased first or last kid");
  565 + resetLimits(element->node, parent);
  566 + }
  567 + if (element->kid_number == nkids)
  568 + {
  569 + // Move to the successor of the last child of the
  570 + // previous kid.
  571 + setItemNumber(QPDFObjectHandle(), -1);
  572 + --element->kid_number;
  573 + deepen(kids.getArrayItem(element->kid_number), false, true);
  574 + if (valid())
  575 + {
  576 + increment(false);
  577 + if (! valid())
  578 + {
  579 + QTC::TC("qpdf", "NNTree erased last item in tree");
  580 + }
  581 + else
  582 + {
  583 + QTC::TC("qpdf", "NNTree erased last kid");
  584 + }
  585 + }
  586 + }
  587 + else
  588 + {
  589 + // Next kid is in deleted kid's position
  590 + QTC::TC("qpdf", "NNTree erased non-last kid");
  591 + deepen(kids.getArrayItem(element->kid_number), true, true);
  592 + }
  593 + done = true;
  594 + }
  595 + else if (parent == this->path.end())
  596 + {
  597 + // We erased the very last item. Convert the root to an
  598 + // empty items array.
  599 + QTC::TC("qpdf", "NNTree non-flat tree is empty after remove");
  600 + element->node.removeKey("/Kids");
  601 + element->node.replaceKey(impl.details.itemsKey(),
  602 + QPDFObjectHandle::newArray());
  603 + this->path.clear();
  604 + setItemNumber(impl.oh, -1);
  605 + done = true;
  606 + }
  607 + else
  608 + {
  609 + // Walk up the tree and continue
  610 + QTC::TC("qpdf", "NNTree remove walking up tree");
  611 + this->path.pop_back();
  612 + }
  613 + }
  614 +}
  615 +
471 NNTreeIterator& 616 NNTreeIterator&
472 NNTreeIterator::operator++() 617 NNTreeIterator::operator++()
473 { 618 {
@@ -494,7 +639,7 @@ NNTreeIterator::operator*() @@ -494,7 +639,7 @@ NNTreeIterator::operator*()
494 auto items = this->node.getKey(impl.details.itemsKey()); 639 auto items = this->node.getKey(impl.details.itemsKey());
495 if (items.getArrayNItems() < this->item_number + 2) 640 if (items.getArrayNItems() < this->item_number + 2)
496 { 641 {
497 - error(impl.qpdf, node, "items array is too short"); 642 + error(impl.qpdf, node, "operator*: items array is too short");
498 } 643 }
499 return std::make_pair(items.getArrayItem(this->item_number), 644 return std::make_pair(items.getArrayItem(this->item_number),
500 items.getArrayItem(1+this->item_number)); 645 items.getArrayItem(1+this->item_number));
@@ -980,3 +1125,20 @@ NNTreeImpl::insert(QPDFObjectHandle key, QPDFObjectHandle value) @@ -980,3 +1125,20 @@ NNTreeImpl::insert(QPDFObjectHandle key, QPDFObjectHandle value)
980 } 1125 }
981 return iter; 1126 return iter;
982 } 1127 }
  1128 +
  1129 +bool
  1130 +NNTreeImpl::remove(QPDFObjectHandle key, QPDFObjectHandle* value)
  1131 +{
  1132 + auto iter = find(key, false);
  1133 + if (! iter.valid())
  1134 + {
  1135 + QTC::TC("qpdf", "NNTree remove not found");
  1136 + return false;
  1137 + }
  1138 + if (value)
  1139 + {
  1140 + *value = (*iter).second;
  1141 + }
  1142 + iter.remove();
  1143 + return true;
  1144 +}
libqpdf/QPDFNameTreeObjectHelper.cc
@@ -109,6 +109,12 @@ QPDFNameTreeObjectHelper::iterator::insertAfter( @@ -109,6 +109,12 @@ QPDFNameTreeObjectHelper::iterator::insertAfter(
109 impl->insertAfter(QPDFObjectHandle::newUnicodeString(key), value); 109 impl->insertAfter(QPDFObjectHandle::newUnicodeString(key), value);
110 } 110 }
111 111
  112 +void
  113 +QPDFNameTreeObjectHelper::iterator::remove()
  114 +{
  115 + impl->remove();
  116 +}
  117 +
112 QPDFNameTreeObjectHelper::iterator 118 QPDFNameTreeObjectHelper::iterator
113 QPDFNameTreeObjectHelper::begin() const 119 QPDFNameTreeObjectHelper::begin() const
114 { 120 {
@@ -146,6 +152,14 @@ QPDFNameTreeObjectHelper::insert(std::string const&amp; key, @@ -146,6 +152,14 @@ QPDFNameTreeObjectHelper::insert(std::string const&amp; key,
146 } 152 }
147 153
148 bool 154 bool
  155 +QPDFNameTreeObjectHelper::remove(std::string const& key,
  156 + QPDFObjectHandle* value)
  157 +{
  158 + return this->m->impl->remove(
  159 + QPDFObjectHandle::newUnicodeString(key), value);
  160 +}
  161 +
  162 +bool
149 QPDFNameTreeObjectHelper::hasName(std::string const& name) 163 QPDFNameTreeObjectHelper::hasName(std::string const& name)
150 { 164 {
151 auto i = find(name); 165 auto i = find(name);
libqpdf/QPDFNumberTreeObjectHelper.cc
@@ -105,6 +105,12 @@ QPDFNumberTreeObjectHelper::iterator::insertAfter( @@ -105,6 +105,12 @@ QPDFNumberTreeObjectHelper::iterator::insertAfter(
105 impl->insertAfter(QPDFObjectHandle::newInteger(key), value); 105 impl->insertAfter(QPDFObjectHandle::newInteger(key), value);
106 } 106 }
107 107
  108 +void
  109 +QPDFNumberTreeObjectHelper::iterator::remove()
  110 +{
  111 + impl->remove();
  112 +}
  113 +
108 QPDFNumberTreeObjectHelper::iterator 114 QPDFNumberTreeObjectHelper::iterator
109 QPDFNumberTreeObjectHelper::begin() const 115 QPDFNumberTreeObjectHelper::begin() const
110 { 116 {
@@ -140,6 +146,14 @@ QPDFNumberTreeObjectHelper::insert(numtree_number key, QPDFObjectHandle value) @@ -140,6 +146,14 @@ QPDFNumberTreeObjectHelper::insert(numtree_number key, QPDFObjectHandle value)
140 return iterator(std::make_shared<NNTreeIterator>(i)); 146 return iterator(std::make_shared<NNTreeIterator>(i));
141 } 147 }
142 148
  149 +bool
  150 +QPDFNumberTreeObjectHelper::remove(numtree_number key,
  151 + QPDFObjectHandle* value)
  152 +{
  153 + return this->m->impl->remove(
  154 + QPDFObjectHandle::newInteger(key), value);
  155 +}
  156 +
143 QPDFNumberTreeObjectHelper::numtree_number 157 QPDFNumberTreeObjectHelper::numtree_number
144 QPDFNumberTreeObjectHelper::getMin() 158 QPDFNumberTreeObjectHelper::getMin()
145 { 159 {
libqpdf/qpdf/NNTree.hh
@@ -49,6 +49,7 @@ class NNTreeIterator: public std::iterator&lt; @@ -49,6 +49,7 @@ class NNTreeIterator: public std::iterator&lt;
49 49
50 void insertAfter( 50 void insertAfter(
51 QPDFObjectHandle key, QPDFObjectHandle value); 51 QPDFObjectHandle key, QPDFObjectHandle value);
  52 + void remove();
52 53
53 private: 54 private:
54 class PathElement 55 class PathElement
@@ -94,6 +95,7 @@ class NNTreeImpl @@ -94,6 +95,7 @@ class NNTreeImpl
94 iterator find(QPDFObjectHandle key, bool return_prev_if_not_found = false); 95 iterator find(QPDFObjectHandle key, bool return_prev_if_not_found = false);
95 iterator insertFirst(QPDFObjectHandle key, QPDFObjectHandle value); 96 iterator insertFirst(QPDFObjectHandle key, QPDFObjectHandle value);
96 iterator insert(QPDFObjectHandle key, QPDFObjectHandle value); 97 iterator insert(QPDFObjectHandle key, QPDFObjectHandle value);
  98 + bool remove(QPDFObjectHandle key, QPDFObjectHandle* value = nullptr);
97 99
98 // Change the split threshold for easier testing. There's no real 100 // Change the split threshold for easier testing. There's no real
99 // reason to expose this to downstream tree helpers, but it has to 101 // reason to expose this to downstream tree helpers, but it has to
manual/qpdf-manual.xml
@@ -4859,7 +4859,8 @@ print &quot;\n&quot;; @@ -4859,7 +4859,8 @@ print &quot;\n&quot;;
4859 and <classname>QPDFNumberTreeObjectHelper</classname> to be 4859 and <classname>QPDFNumberTreeObjectHelper</classname> to be
4860 more efficient, add an iterator-based API, give them the 4860 more efficient, add an iterator-based API, give them the
4861 capability to repair broken trees, and create methods for 4861 capability to repair broken trees, and create methods for
4862 - modifying the trees. 4862 + modifying the trees. With this change, qpdf has a robust
  4863 + read/write implementation of name and number trees.
4863 </para> 4864 </para>
4864 </listitem> 4865 </listitem>
4865 </itemizedlist> 4866 </itemizedlist>
qpdf/qpdf.testcov
@@ -553,3 +553,16 @@ NNTree node is not a dictionary 0 @@ -553,3 +553,16 @@ NNTree node is not a dictionary 0
553 NNTree limits didn't change 0 553 NNTree limits didn't change 0
554 NNTree increment end() 0 554 NNTree increment end() 0
555 NNTree insertAfter inserts first 0 555 NNTree insertAfter inserts first 0
  556 +NNTree remove not found 0
  557 +NNTree remove reset limits 0
  558 +NNTree erased last item 0
  559 +NNTree erased non-last item 0
  560 +NNTree items is empty after remove 0
  561 +NNTree erased all items on leaf/root 0
  562 +NNTree erased first or last kid 0
  563 +NNTree erased last kid 0
  564 +NNTree erased non-last kid 0
  565 +NNTree non-flat tree is empty after remove 0
  566 +NNTree remove walking up tree 0
  567 +NNTree erased last item in tree 0
  568 +NNTree remove limits from root 0
qpdf/qtest/qpdf.test
@@ -583,7 +583,7 @@ foreach my $input (@ext_inputs) @@ -583,7 +583,7 @@ foreach my $input (@ext_inputs)
583 show_ntests(); 583 show_ntests();
584 # ---------- 584 # ----------
585 $td->notify("--- Number and Name Trees ---"); 585 $td->notify("--- Number and Name Trees ---");
586 -$n_tests += 4; 586 +$n_tests += 6;
587 587
588 $td->runtest("number trees", 588 $td->runtest("number trees",
589 {$td->COMMAND => "test_driver 46 number-tree.pdf"}, 589 {$td->COMMAND => "test_driver 46 number-tree.pdf"},
@@ -600,6 +600,13 @@ $td-&gt;runtest(&quot;nntree split&quot;, @@ -600,6 +600,13 @@ $td-&gt;runtest(&quot;nntree split&quot;,
600 $td->runtest("check file", 600 $td->runtest("check file",
601 {$td->FILE => "a.pdf"}, 601 {$td->FILE => "a.pdf"},
602 {$td->FILE => "split-nntree-out.pdf"}); 602 {$td->FILE => "split-nntree-out.pdf"});
  603 +$td->runtest("nntree erase",
  604 + {$td->COMMAND => "test_driver 75 erase-nntree.pdf"},
  605 + {$td->FILE => "erase-nntree.out", $td->EXIT_STATUS => 0},
  606 + $td->NORMALIZE_NEWLINES);
  607 +$td->runtest("check file",
  608 + {$td->FILE => "a.pdf"},
  609 + {$td->FILE => "erase-nntree-out.pdf"});
603 610
604 show_ntests(); 611 show_ntests();
605 # ---------- 612 # ----------
qpdf/qtest/qpdf/erase-nntree-out.pdf 0 โ†’ 100644
  1 +%PDF-1.3
  2 +%ยฟรทยขรพ
  3 +%QDF-1.0
  4 +
  5 +%% Original object ID: 1 0
  6 +1 0 obj
  7 +<<
  8 + /Pages 6 0 R
  9 + /Type /Catalog
  10 +>>
  11 +endobj
  12 +
  13 +%% Original object ID: 8 0
  14 +2 0 obj
  15 +<<
  16 + /Names [
  17 + ]
  18 +>>
  19 +endobj
  20 +
  21 +%% Original object ID: 9 0
  22 +3 0 obj
  23 +<<
  24 + /Kids [
  25 + 7 0 R
  26 + 8 0 R
  27 + ]
  28 +>>
  29 +endobj
  30 +
  31 +%% Original object ID: 14 0
  32 +4 0 obj
  33 +<<
  34 + /Nums [
  35 + ]
  36 +>>
  37 +endobj
  38 +
  39 +%% Original object ID: 18 0
  40 +5 0 obj
  41 +<<
  42 + /Kids [
  43 + 9 0 R
  44 + 10 0 R
  45 + ]
  46 +>>
  47 +endobj
  48 +
  49 +%% Original object ID: 2 0
  50 +6 0 obj
  51 +<<
  52 + /Count 1
  53 + /Kids [
  54 + 11 0 R
  55 + ]
  56 + /Type /Pages
  57 +>>
  58 +endobj
  59 +
  60 +%% Original object ID: 10 0
  61 +7 0 obj
  62 +<<
  63 + /Kids [
  64 + 12 0 R
  65 + ]
  66 + /Limits [
  67 + 220
  68 + 220
  69 + ]
  70 +>>
  71 +endobj
  72 +
  73 +%% Original object ID: 11 0
  74 +8 0 obj
  75 +<<
  76 + /Limits [
  77 + 230
  78 + 240
  79 + ]
  80 + /Nums [
  81 + 230
  82 + (230)
  83 + 240
  84 + (240)
  85 + ]
  86 +>>
  87 +endobj
  88 +
  89 +%% Original object ID: 19 0
  90 +9 0 obj
  91 +<<
  92 + /Kids [
  93 + 13 0 R
  94 + ]
  95 + /Limits [
  96 + 410
  97 + 410
  98 + ]
  99 +>>
  100 +endobj
  101 +
  102 +%% Original object ID: 20 0
  103 +10 0 obj
  104 +<<
  105 + /Limits [
  106 + 430
  107 + 430
  108 + ]
  109 + /Nums [
  110 + 430
  111 + (430)
  112 + ]
  113 +>>
  114 +endobj
  115 +
  116 +%% Page 1
  117 +%% Original object ID: 3 0
  118 +11 0 obj
  119 +<<
  120 + /Contents 14 0 R
  121 + /MediaBox [
  122 + 0
  123 + 0
  124 + 612
  125 + 792
  126 + ]
  127 + /Parent 6 0 R
  128 + /Resources <<
  129 + /Font <<
  130 + /F1 16 0 R
  131 + >>
  132 + /ProcSet 17 0 R
  133 + >>
  134 + /Type /Page
  135 +>>
  136 +endobj
  137 +
  138 +%% Original object ID: 13 0
  139 +12 0 obj
  140 +<<
  141 + /Limits [
  142 + 220
  143 + 220
  144 + ]
  145 + /Nums [
  146 + 220
  147 + (220)
  148 + ]
  149 +>>
  150 +endobj
  151 +
  152 +%% Original object ID: 21 0
  153 +13 0 obj
  154 +<<
  155 + /Limits [
  156 + 410
  157 + 410
  158 + ]
  159 + /Nums [
  160 + 410
  161 + (410)
  162 + ]
  163 +>>
  164 +endobj
  165 +
  166 +%% Contents for page 1
  167 +%% Original object ID: 4 0
  168 +14 0 obj
  169 +<<
  170 + /Length 15 0 R
  171 +>>
  172 +stream
  173 +BT
  174 + /F1 24 Tf
  175 + 72 720 Td
  176 + (Potato) Tj
  177 +ET
  178 +endstream
  179 +endobj
  180 +
  181 +15 0 obj
  182 +44
  183 +endobj
  184 +
  185 +%% Original object ID: 6 0
  186 +16 0 obj
  187 +<<
  188 + /BaseFont /Helvetica
  189 + /Encoding /WinAnsiEncoding
  190 + /Name /F1
  191 + /Subtype /Type1
  192 + /Type /Font
  193 +>>
  194 +endobj
  195 +
  196 +%% Original object ID: 7 0
  197 +17 0 obj
  198 +[
  199 + /PDF
  200 + /Text
  201 +]
  202 +endobj
  203 +
  204 +xref
  205 +0 18
  206 +0000000000 65535 f
  207 +0000000052 00000 n
  208 +0000000133 00000 n
  209 +0000000197 00000 n
  210 +0000000281 00000 n
  211 +0000000345 00000 n
  212 +0000000429 00000 n
  213 +0000000530 00000 n
  214 +0000000637 00000 n
  215 +0000000769 00000 n
  216 +0000000876 00000 n
  217 +0000001000 00000 n
  218 +0000001224 00000 n
  219 +0000001339 00000 n
  220 +0000001476 00000 n
  221 +0000001577 00000 n
  222 +0000001624 00000 n
  223 +0000001770 00000 n
  224 +trailer <<
  225 + /Erase1 2 0 R
  226 + /Erase2 3 0 R
  227 + /Erase3 4 0 R
  228 + /Erase4 5 0 R
  229 + /Root 1 0 R
  230 + /Size 18
  231 + /ID [<2c3b7a6ec7fc61db8a5db4eebf57f540><31415926535897932384626433832795>]
  232 +>>
  233 +startxref
  234 +1806
  235 +%%EOF
qpdf/qtest/qpdf/erase-nntree.out 0 โ†’ 100644
  1 +test 75 done
qpdf/qtest/qpdf/erase-nntree.pdf 0 โ†’ 100644
  1 +%PDF-1.3
  2 +%ยฟรทยขรพ
  3 +%QDF-1.0
  4 +
  5 +1 0 obj
  6 +<<
  7 + /Pages 2 0 R
  8 + /Type /Catalog
  9 +>>
  10 +endobj
  11 +
  12 +2 0 obj
  13 +<<
  14 + /Count 1
  15 + /Kids [
  16 + 3 0 R
  17 + ]
  18 + /Type /Pages
  19 +>>
  20 +endobj
  21 +
  22 +%% Page 1
  23 +3 0 obj
  24 +<<
  25 + /Contents 4 0 R
  26 + /MediaBox [
  27 + 0
  28 + 0
  29 + 612
  30 + 792
  31 + ]
  32 + /Parent 2 0 R
  33 + /Resources <<
  34 + /Font <<
  35 + /F1 6 0 R
  36 + >>
  37 + /ProcSet 7 0 R
  38 + >>
  39 + /Type /Page
  40 +>>
  41 +endobj
  42 +
  43 +%% Contents for page 1
  44 +4 0 obj
  45 +<<
  46 + /Length 5 0 R
  47 +>>
  48 +stream
  49 +BT
  50 + /F1 24 Tf
  51 + 72 720 Td
  52 + (Potato) Tj
  53 +ET
  54 +endstream
  55 +endobj
  56 +
  57 +5 0 obj
  58 +44
  59 +endobj
  60 +
  61 +6 0 obj
  62 +<<
  63 + /BaseFont /Helvetica
  64 + /Encoding /WinAnsiEncoding
  65 + /Name /F1
  66 + /Subtype /Type1
  67 + /Type /Font
  68 +>>
  69 +endobj
  70 +
  71 +7 0 obj
  72 +[
  73 + /PDF
  74 + /Text
  75 +]
  76 +endobj
  77 +
  78 +8 0 obj
  79 +<<
  80 + /Names [
  81 + (1A) (a)
  82 + (1B) (b)
  83 + (1C) (c)
  84 + (1D) (d)
  85 + ]
  86 +>>
  87 +endobj
  88 +
  89 +9 0 obj
  90 +<<
  91 + /Kids [
  92 + 10 0 R
  93 + 11 0 R
  94 + ]
  95 +>>
  96 +endobj
  97 +
  98 +10 0 obj
  99 +<<
  100 + /Limits [ 210 220 ]
  101 + /Kids [
  102 + 12 0 R
  103 + 13 0 R
  104 + ]
  105 +>>
  106 +endobj
  107 +
  108 +11 0 obj
  109 +<<
  110 + /Limits [ 230 250 ]
  111 + /Nums [
  112 + 230 (230)
  113 + 240 (240)
  114 + 250 (250)
  115 + ]
  116 +>>
  117 +endobj
  118 +
  119 +12 0 obj
  120 +<<
  121 + /Limits [ 210 210 ]
  122 + /Nums [
  123 + 210 (210)
  124 + ]
  125 +>>
  126 +endobj
  127 +
  128 +13 0 obj
  129 +<<
  130 + /Limits [ 220 220 ]
  131 + /Nums [
  132 + 220 (220)
  133 + ]
  134 +>>
  135 +endobj
  136 +
  137 +14 0 obj
  138 +<<
  139 + /Kids [
  140 + 15 0 R
  141 + ]
  142 +>>
  143 +endobj
  144 +
  145 +15 0 obj
  146 +<<
  147 + /Limits [ 310 320 ]
  148 + /Kids [
  149 + 16 0 R
  150 + 17 0 R
  151 + ]
  152 +>>
  153 +endobj
  154 +
  155 +16 0 obj
  156 +<<
  157 + /Limits [ 310 310 ]
  158 + /Nums [
  159 + 310 (310)
  160 + ]
  161 +>>
  162 +endobj
  163 +
  164 +17 0 obj
  165 +<<
  166 + /Limits [ 320 320 ]
  167 + /Nums [
  168 + 320 (320)
  169 + ]
  170 +>>
  171 +endobj
  172 +
  173 +18 0 obj
  174 +<<
  175 + /Kids [
  176 + 19 0 R
  177 + 20 0 R
  178 + ]
  179 +>>
  180 +endobj
  181 +
  182 +19 0 obj
  183 +<<
  184 + /Limits [ 410 420 ]
  185 + /Kids [
  186 + 21 0 R
  187 + 22 0 R
  188 + ]
  189 +>>
  190 +endobj
  191 +
  192 +20 0 obj
  193 +<<
  194 + /Limits [ 430 430 ]
  195 + /Nums [
  196 + 430 (430)
  197 + ]
  198 +>>
  199 +endobj
  200 +
  201 +21 0 obj
  202 +<<
  203 + /Limits [ 410 410 ]
  204 + /Nums [
  205 + 410 (410)
  206 + ]
  207 +>>
  208 +endobj
  209 +
  210 +22 0 obj
  211 +<<
  212 + /Limits [ 420 420 ]
  213 + /Nums [
  214 + 420 (420)
  215 + ]
  216 +>>
  217 +endobj
  218 +
  219 +xref
  220 +0 23
  221 +0000000000 65535 f
  222 +0000000025 00000 n
  223 +0000000079 00000 n
  224 +0000000161 00000 n
  225 +0000000376 00000 n
  226 +0000000475 00000 n
  227 +0000000494 00000 n
  228 +0000000612 00000 n
  229 +0000000647 00000 n
  230 +0000000736 00000 n
  231 +0000000794 00000 n
  232 +0000000875 00000 n
  233 +0000000976 00000 n
  234 +0000001049 00000 n
  235 +0000001122 00000 n
  236 +0000001170 00000 n
  237 +0000001251 00000 n
  238 +0000001324 00000 n
  239 +0000001397 00000 n
  240 +0000001456 00000 n
  241 +0000001537 00000 n
  242 +0000001610 00000 n
  243 +0000001683 00000 n
  244 +trailer <<
  245 + /Root 1 0 R
  246 + /Erase1 8 0 R
  247 + /Erase2 9 0 R
  248 + /Erase3 14 0 R
  249 + /Erase4 18 0 R
  250 + /Size 23
  251 + /ID [<2c3b7a6ec7fc61db8a5db4eebf57f540><2c3b7a6ec7fc61db8a5db4eebf57f540>]
  252 +>>
  253 +startxref
  254 +1756
  255 +%%EOF
qpdf/test_driver.cc
@@ -2615,6 +2615,65 @@ void runtest(int n, char const* filename1, char const* arg2) @@ -2615,6 +2615,65 @@ void runtest(int n, char const* filename1, char const* arg2)
2615 w.setQDFMode(true); 2615 w.setQDFMode(true);
2616 w.write(); 2616 w.write();
2617 } 2617 }
  2618 + else if (n == 75)
  2619 + {
  2620 + // This test is crafted to work with erase-nntree.pdf
  2621 + auto erase1 = QPDFNameTreeObjectHelper(
  2622 + pdf.getTrailer().getKey("/Erase1"), pdf);
  2623 + QPDFObjectHandle value;
  2624 + assert(! erase1.remove("1X"));
  2625 + assert(erase1.remove("1C", &value));
  2626 + assert(value.getUTF8Value() == "c");
  2627 + auto iter1 = erase1.find("1B");
  2628 + iter1.remove();
  2629 + assert((*iter1).first == "1D");
  2630 + iter1.remove();
  2631 + assert(iter1 == erase1.end());
  2632 + --iter1;
  2633 + assert((*iter1).first == "1A");
  2634 + iter1.remove();
  2635 + assert(iter1 == erase1.end());
  2636 +
  2637 + auto erase2_oh = pdf.getTrailer().getKey("/Erase2");
  2638 + auto erase2 = QPDFNumberTreeObjectHelper(erase2_oh, pdf);
  2639 + auto iter2 = erase2.find(250);
  2640 + iter2.remove();
  2641 + assert(iter2 == erase2.end());
  2642 + --iter2;
  2643 + assert((*iter2).first == 240);
  2644 + auto k1 = erase2_oh.getKey("/Kids").getArrayItem(1);
  2645 + auto l1 = k1.getKey("/Limits");
  2646 + assert(l1.getArrayItem(0).getIntValue() == 230);
  2647 + assert(l1.getArrayItem(1).getIntValue() == 240);
  2648 + iter2 = erase2.find(210);
  2649 + iter2.remove();
  2650 + assert((*iter2).first == 220);
  2651 + k1 = erase2_oh.getKey("/Kids").getArrayItem(0);
  2652 + l1 = k1.getKey("/Limits");
  2653 + assert(l1.getArrayItem(0).getIntValue() == 220);
  2654 + assert(l1.getArrayItem(1).getIntValue() == 220);
  2655 + k1 = k1.getKey("/Kids");
  2656 + assert(k1.getArrayNItems() == 1);
  2657 +
  2658 + auto erase3 = QPDFNumberTreeObjectHelper(
  2659 + pdf.getTrailer().getKey("/Erase3"), pdf);
  2660 + iter2 = erase3.find(320);
  2661 + iter2.remove();
  2662 + assert(iter2 == erase3.end());
  2663 + erase3.remove(310);
  2664 + assert(erase3.begin() == erase3.end());
  2665 +
  2666 + auto erase4 = QPDFNumberTreeObjectHelper(
  2667 + pdf.getTrailer().getKey("/Erase4"), pdf);
  2668 + iter2 = erase4.find(420);
  2669 + iter2.remove();
  2670 + assert((*iter2).first == 430);
  2671 +
  2672 + QPDFWriter w(pdf, "a.pdf");
  2673 + w.setStaticID(true);
  2674 + w.setQDFMode(true);
  2675 + w.write();
  2676 + }
2618 else 2677 else
2619 { 2678 {
2620 throw std::runtime_error(std::string("invalid test ") + 2679 throw std::runtime_error(std::string("invalid test ") +