Commit e51eee74071546f15be46f73a4ff93f89bd8d191

Authored by m-holger
Committed by GitHub
2 parents c290a82f fcc7f702

Merge pull request #1585 from m-holger/dict

Refactor private-API dictionary methods
include/qpdf/ObjectHandle.hh
@@ -83,8 +83,11 @@ namespace qpdf @@ -83,8 +83,11 @@ namespace qpdf
83 QPDFObjectHandle operator[](size_t n) const; 83 QPDFObjectHandle operator[](size_t n) const;
84 QPDFObjectHandle operator[](int n) const; 84 QPDFObjectHandle operator[](int n) const;
85 85
  86 + QPDFObjectHandle& at(std::string const& key) const;
86 bool contains(std::string const& key) const; 87 bool contains(std::string const& key) const;
87 size_t erase(std::string const& key); 88 size_t erase(std::string const& key);
  89 + QPDFObjectHandle& find(std::string const& key) const;
  90 + bool replace(std::string const& key, QPDFObjectHandle value);
88 QPDFObjectHandle const& operator[](std::string const& key) const; 91 QPDFObjectHandle const& operator[](std::string const& key) const;
89 92
90 std::shared_ptr<QPDFObject> copy(bool shallow = false) const; 93 std::shared_ptr<QPDFObject> copy(bool shallow = false) const;
@@ -134,6 +137,8 @@ namespace qpdf @@ -134,6 +137,8 @@ namespace qpdf
134 137
135 std::string description() const; 138 std::string description() const;
136 139
  140 + inline QPDFObjectHandle const& get(std::string const& key) const;
  141 +
137 void no_ci_warn_if(bool condition, std::string const& warning) const; 142 void no_ci_warn_if(bool condition, std::string const& warning) const;
138 void no_ci_stop_if(bool condition, std::string const& warning) const; 143 void no_ci_stop_if(bool condition, std::string const& warning) const;
139 void no_ci_stop_damaged_if(bool condition, std::string const& warning) const; 144 void no_ci_stop_damaged_if(bool condition, std::string const& warning) const;
libqpdf/NNTree.cc
@@ -168,7 +168,7 @@ NNTreeIterator::resetLimits(Dictionary a_node, std::list&lt;PathElement&gt;::iterator @@ -168,7 +168,7 @@ NNTreeIterator::resetLimits(Dictionary a_node, std::list&lt;PathElement&gt;::iterator
168 } 168 }
169 } 169 }
170 if (a_node != path.begin()->node) { 170 if (a_node != path.begin()->node) {
171 - a_node.replaceKey("/Limits", Array({first, last})); 171 + a_node.replace("/Limits", Array({first, last}));
172 } 172 }
173 } 173 }
174 174
@@ -266,7 +266,7 @@ NNTreeIterator::split(Dictionary to_split, std::list&lt;PathElement&gt;::iterator pare @@ -266,7 +266,7 @@ NNTreeIterator::split(Dictionary to_split, std::list&lt;PathElement&gt;::iterator pare
266 new_kids.push_back(first_node); 266 new_kids.push_back(first_node);
267 to_split.erase("/Limits"); // already shouldn't be there for root 267 to_split.erase("/Limits"); // already shouldn't be there for root
268 to_split.erase(impl.itemsKey()); 268 to_split.erase(impl.itemsKey());
269 - to_split.replaceKey("/Kids", new_kids); 269 + to_split.replace("/Kids", new_kids);
270 if (is_leaf) { 270 if (is_leaf) {
271 node = first_node; 271 node = first_node;
272 } else { 272 } else {
@@ -446,7 +446,7 @@ NNTreeIterator::remove() @@ -446,7 +446,7 @@ NNTreeIterator::remove()
446 if (parent == path.end()) { 446 if (parent == path.end()) {
447 // We erased the very last item. Convert the root to an empty items array. 447 // We erased the very last item. Convert the root to an empty items array.
448 element->node.erase("/Kids"); 448 element->node.erase("/Kids");
449 - element->node.replaceKey(impl.itemsKey(), Array::empty()); 449 + element->node.replace(impl.itemsKey(), Array::empty());
450 path.clear(); 450 path.clear();
451 setItemNumber(impl.tree_root, -1); 451 setItemNumber(impl.tree_root, -1);
452 return; 452 return;
@@ -673,8 +673,8 @@ NNTreeImpl::repair() @@ -673,8 +673,8 @@ NNTreeImpl::repair()
673 for (auto const& [key, value]: items) { 673 for (auto const& [key, value]: items) {
674 repl.insert(key, value); 674 repl.insert(key, value);
675 } 675 }
676 - tree_root.replaceKey("/Kids", new_node["/Kids"]);  
677 - tree_root.replaceKey(itemsKey(), new_node[itemsKey()]); 676 + tree_root.replace("/Kids", new_node["/Kids"]);
  677 + tree_root.replace(itemsKey(), new_node[itemsKey()]);
678 } 678 }
679 679
680 NNTreeImpl::iterator 680 NNTreeImpl::iterator
libqpdf/QPDF.cc
@@ -572,7 +572,7 @@ Objects::Foreign::Copier::replace_indirect_object(QPDFObjectHandle const&amp; foreig @@ -572,7 +572,7 @@ Objects::Foreign::Copier::replace_indirect_object(QPDFObjectHandle const&amp; foreig
572 auto result = Dictionary::empty(); 572 auto result = Dictionary::empty();
573 for (auto const& [key, value]: Dictionary(foreign)) { 573 for (auto const& [key, value]: Dictionary(foreign)) {
574 if (!value.null()) { 574 if (!value.null()) {
575 - result.replaceKey(key, replace_indirect_object(value)); 575 + result.replace(key, replace_indirect_object(value));
576 } 576 }
577 } 577 }
578 return result; 578 return result;
@@ -584,7 +584,7 @@ Objects::Foreign::Copier::replace_indirect_object(QPDFObjectHandle const&amp; foreig @@ -584,7 +584,7 @@ Objects::Foreign::Copier::replace_indirect_object(QPDFObjectHandle const&amp; foreig
584 auto dict = result.getDict(); 584 auto dict = result.getDict();
585 for (auto const& [key, value]: stream.getDict()) { 585 for (auto const& [key, value]: stream.getDict()) {
586 if (!value.null()) { 586 if (!value.null()) {
587 - dict.replaceKey(key, replace_indirect_object(value)); 587 + dict.replace(key, replace_indirect_object(value));
588 } 588 }
589 } 589 }
590 stream.copy_data_to(result); 590 stream.copy_data_to(result);
@@ -658,7 +658,7 @@ QPDF::getRoot() @@ -658,7 +658,7 @@ QPDF::getRoot()
658 // approach to more extensive checks and warning levels. 658 // approach to more extensive checks and warning levels.
659 if (m->cf.check_mode() && Name(Root["/Type"]) != "/Catalog") { 659 if (m->cf.check_mode() && Name(Root["/Type"]) != "/Catalog") {
660 warn(m->c.damagedPDF("", -1, "catalog /Type entry missing or invalid")); 660 warn(m->c.damagedPDF("", -1, "catalog /Type entry missing or invalid"));
661 - Root.replaceKey("/Type", Name("/Catalog")); 661 + Root.replace("/Type", Name("/Catalog"));
662 } 662 }
663 return Root.oh(); 663 return Root.oh();
664 } 664 }
libqpdf/QPDFAcroFormDocumentHelper.cc
@@ -799,7 +799,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -799,7 +799,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
799 } 799 }
800 QPDFObjectHandle(dr).makeResourcesIndirect(qpdf); 800 QPDFObjectHandle(dr).makeResourcesIndirect(qpdf);
801 if (!dr.indirect()) { 801 if (!dr.indirect()) {
802 - acroform.replaceKey("/DR", qpdf.makeIndirectObject(dr)); 802 + acroform.replace("/DR", qpdf.makeIndirectObject(dr));
803 dr = acroform["/DR"]; 803 dr = acroform["/DR"];
804 } 804 }
805 // Merge the other document's /DR, creating a conflict map. mergeResources checks to 805 // Merge the other document's /DR, creating a conflict map. mergeResources checks to
@@ -839,7 +839,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -839,7 +839,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
839 if (parent.indirect()) { 839 if (parent.indirect()) {
840 auto parent_og = parent.id_gen(); 840 auto parent_og = parent.id_gen();
841 if (orig_to_copy.contains(parent_og)) { 841 if (orig_to_copy.contains(parent_og)) {
842 - obj.replaceKey("/Parent", orig_to_copy[parent_og]); 842 + obj.replace("/Parent", orig_to_copy[parent_og]);
843 } else { 843 } else {
844 parent.warn( 844 parent.warn(
845 "while traversing field " + obj.id_gen().unparse(',') + 845 "while traversing field " + obj.id_gen().unparse(',') +
@@ -869,7 +869,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -869,7 +869,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
869 // chrome, firefox, the mac Preview application, and several of the free 869 // chrome, firefox, the mac Preview application, and several of the free
870 // readers on Linux all ignore /DR at the field level. 870 // readers on Linux all ignore /DR at the field level.
871 if (obj.contains("/DR")) { 871 if (obj.contains("/DR")) {
872 - obj.replaceKey("/DR", dr); 872 + obj.replace("/DR", dr);
873 } 873 }
874 if (obj["/DA"].isString() && !dr_map.empty()) { 874 if (obj["/DA"].isString() && !dr_map.empty()) {
875 adjustDefaultAppearances(obj, dr_map); 875 adjustDefaultAppearances(obj, dr_map);
@@ -988,7 +988,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -988,7 +988,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
988 Dictionary apdict = ah.getAppearanceDictionary(); 988 Dictionary apdict = ah.getAppearanceDictionary();
989 std::vector<QPDFObjectHandle> streams; 989 std::vector<QPDFObjectHandle> streams;
990 auto replace_stream = [](auto& dict, auto& key, auto& old) { 990 auto replace_stream = [](auto& dict, auto& key, auto& old) {
991 - dict.replaceKey(key, old.copyStream()); 991 + dict.replace(key, old.copyStream());
992 return dict[key]; 992 return dict[key];
993 }; 993 };
994 994
@@ -1015,7 +1015,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations( @@ -1015,7 +1015,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
1015 } 1015 }
1016 apcm.concat(cm); 1016 apcm.concat(cm);
1017 if (omatrix || apcm != QPDFMatrix()) { 1017 if (omatrix || apcm != QPDFMatrix()) {
1018 - dict.replaceKey("/Matrix", QPDFObjectHandle::newFromMatrix(apcm)); 1018 + dict.replace("/Matrix", QPDFObjectHandle::newFromMatrix(apcm));
1019 } 1019 }
1020 Dictionary resources = dict["/Resources"]; 1020 Dictionary resources = dict["/Resources"];
1021 if (!dr_map.empty() && resources) { 1021 if (!dr_map.empty() && resources) {
libqpdf/QPDFAnnotationObjectHelper.cc
@@ -33,14 +33,14 @@ QPDFAnnotationObjectHelper::getAppearanceDictionary() @@ -33,14 +33,14 @@ QPDFAnnotationObjectHelper::getAppearanceDictionary()
33 std::string 33 std::string
34 QPDFAnnotationObjectHelper::getAppearanceState() 34 QPDFAnnotationObjectHelper::getAppearanceState()
35 { 35 {
36 - Name AS = (*this)["/AS"]; 36 + Name AS = get("/AS");
37 return AS ? AS.value() : ""; 37 return AS ? AS.value() : "";
38 } 38 }
39 39
40 int 40 int
41 QPDFAnnotationObjectHelper::getFlags() 41 QPDFAnnotationObjectHelper::getFlags()
42 { 42 {
43 - Integer flags_obj = (*this)["/F"]; 43 + Integer flags_obj = get("/F");
44 return flags_obj ? flags_obj : 0; 44 return flags_obj ? flags_obj : 0;
45 } 45 }
46 46
libqpdf/QPDFEFStreamObjectHelper.cc
@@ -31,7 +31,7 @@ void @@ -31,7 +31,7 @@ void
31 QPDFEFStreamObjectHelper::setParam(std::string const& pkey, QPDFObjectHandle const& pval) 31 QPDFEFStreamObjectHelper::setParam(std::string const& pkey, QPDFObjectHandle const& pval)
32 { 32 {
33 if (Dictionary Params = oh().getDict()["/Params"]) { 33 if (Dictionary Params = oh().getDict()["/Params"]) {
34 - Params.replaceKey(pkey, pval); 34 + Params.replace(pkey, pval);
35 return; 35 return;
36 } 36 }
37 oh().getDict().replaceKey("/Params", Dictionary({{pkey, pval}})); 37 oh().getDict().replaceKey("/Params", Dictionary({{pkey, pval}}));
libqpdf/QPDFFileSpecObjectHelper.cc
@@ -42,7 +42,7 @@ std::string @@ -42,7 +42,7 @@ std::string
42 QPDFFileSpecObjectHelper::getFilename() 42 QPDFFileSpecObjectHelper::getFilename()
43 { 43 {
44 for (auto const& i: name_keys) { 44 for (auto const& i: name_keys) {
45 - if (String k = oh()[i]) { 45 + if (String k = get(i)) {
46 return k.utf8_value(); 46 return k.utf8_value();
47 } 47 }
48 } 48 }
@@ -54,7 +54,7 @@ QPDFFileSpecObjectHelper::getFilenames() @@ -54,7 +54,7 @@ QPDFFileSpecObjectHelper::getFilenames()
54 { 54 {
55 std::map<std::string, std::string> result; 55 std::map<std::string, std::string> result;
56 for (auto const& i: name_keys) { 56 for (auto const& i: name_keys) {
57 - if (String k = oh()[i]) { 57 + if (String k = get(i)) {
58 result[i] = k.utf8_value(); 58 result[i] = k.utf8_value();
59 } 59 }
60 } 60 }
@@ -64,7 +64,7 @@ QPDFFileSpecObjectHelper::getFilenames() @@ -64,7 +64,7 @@ QPDFFileSpecObjectHelper::getFilenames()
64 QPDFObjectHandle 64 QPDFObjectHandle
65 QPDFFileSpecObjectHelper::getEmbeddedFileStream(std::string const& key) 65 QPDFFileSpecObjectHelper::getEmbeddedFileStream(std::string const& key)
66 { 66 {
67 - if (Dictionary EF = oh()["/EF"]) { 67 + if (Dictionary EF = get("/EF")) {
68 if (!key.empty() && EF.contains(key)) { 68 if (!key.empty() && EF.contains(key)) {
69 if (auto result = EF[key]) { 69 if (auto result = EF[key]) {
70 return result; 70 return result;
libqpdf/QPDFFormFieldObjectHelper.cc
@@ -956,7 +956,7 @@ FormField::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh) @@ -956,7 +956,7 @@ FormField::generateTextAppearance(QPDFAnnotationObjectHelper&amp; aoh)
956 aoh.getObjectHandle().replaceKey("/AP", Dictionary::empty()); 956 aoh.getObjectHandle().replaceKey("/AP", Dictionary::empty());
957 AP = aoh.getAppearanceDictionary(); 957 AP = aoh.getAppearanceDictionary();
958 } 958 }
959 - AP.replaceKey("/N", AS); 959 + AP.replace("/N", AS);
960 } 960 }
961 if (!AS.isStream()) { 961 if (!AS.isStream()) {
962 aoh.warn("unable to get normal appearance stream for update"); 962 aoh.warn("unable to get normal appearance stream for update");
libqpdf/QPDFOutlineObjectHelper.cc
@@ -53,9 +53,9 @@ QPDFOutlineObjectHelper::getKids() @@ -53,9 +53,9 @@ QPDFOutlineObjectHelper::getKids()
53 QPDFObjectHandle 53 QPDFObjectHandle
54 QPDFOutlineObjectHelper::getDest() 54 QPDFOutlineObjectHelper::getDest()
55 { 55 {
56 - auto dest = (*this)["/Dest"]; 56 + auto dest = get("/Dest");
57 if (dest.null()) { 57 if (dest.null()) {
58 - auto const& A = (*this)["/A"]; 58 + auto const& A = get("/A");
59 if (Name(A["/S"]) == "/GoTo") { 59 if (Name(A["/S"]) == "/GoTo") {
60 dest = A["/D"]; 60 dest = A["/D"];
61 } 61 }
libqpdf/QPDF_Dictionary.cc
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 2
3 #include <qpdf/QPDFObject_private.hh> 3 #include <qpdf/QPDFObject_private.hh>
4 #include <qpdf/QTC.hh> 4 #include <qpdf/QTC.hh>
  5 +#include <qpdf/Util.hh>
5 6
6 using namespace std::literals; 7 using namespace std::literals;
7 using namespace qpdf; 8 using namespace qpdf;
@@ -29,12 +30,70 @@ BaseHandle::operator[](std::string const&amp; key) const @@ -29,12 +30,70 @@ BaseHandle::operator[](std::string const&amp; key) const
29 return null_obj; 30 return null_obj;
30 } 31 }
31 32
  33 +/// Retrieves a reference to the QPDFObjectHandle associated with the given key in the
  34 +/// dictionary object contained within this instance.
  35 +///
  36 +/// If the current object is not of dictionary type, a `std::runtime_error` is thrown.
  37 +/// According to the PDF specification, missing keys in the dictionary are treated as
  38 +/// keys with a `null` value. This behavior is reflected in this function's implementation,
  39 +/// where a missing key will still return a reference to a newly inserted null value entry.
  40 +///
  41 +/// @param key The key for which the corresponding value in the dictionary is retrieved.
  42 +/// @return A reference to the QPDFObjectHandle associated with the specified key.
  43 +/// @throws std::runtime_error if the current object is not a dictionary.
  44 +QPDFObjectHandle&
  45 +BaseHandle::at(std::string const& key) const
  46 +{
  47 + auto d = as<QPDF_Dictionary>();
  48 + if (!d) {
  49 + throw std::runtime_error("Expected a dictionary but found a non-dictionary object");
  50 + }
  51 + return d->items[key];
  52 +}
  53 +
  54 +/// @brief Checks if the specified key exists in the object.
  55 +///
  56 +/// This method determines whether the given key is present in the object by verifying if the
  57 +/// associated value is non-null.
  58 +///
  59 +/// @param key The key to look for in the object.
  60 +/// @return True if the key exists and its associated value is non-null. Otherwise, returns false.
32 bool 61 bool
33 BaseHandle::contains(std::string const& key) const 62 BaseHandle::contains(std::string const& key) const
34 { 63 {
35 return !(*this)[key].null(); 64 return !(*this)[key].null();
36 } 65 }
37 66
  67 +/// @brief Retrieves the value associated with the given key from dictionary.
  68 +///
  69 +/// This method attempts to find the value corresponding to the specified key for objects that can
  70 +/// be interpreted as dictionaries.
  71 +///
  72 +/// - If the object is a dictionary and the specified key exists, it returns a reference
  73 +/// to the associated value.
  74 +/// - If the object is not a dictionary or the specified key does not exist, it returns
  75 +/// a reference to a static uninitialized object handle.
  76 +///
  77 +/// @note Modifying the uninitialized object returned when the key is not found is strictly
  78 +/// prohibited.
  79 +///
  80 +/// @param key The key whose associated value should be retrieved.
  81 +/// @return A reference to the associated value if the key is found or a reference to a static
  82 +/// uninitialized object if the key is not found.
  83 +QPDFObjectHandle&
  84 +BaseHandle::find(std::string const& key) const
  85 +{
  86 + static const QPDFObjectHandle null_obj;
  87 + qpdf_invariant(!null_obj);
  88 + if (auto d = as<QPDF_Dictionary>()) {
  89 + auto it = d->items.find(key);
  90 + if (it != d->items.end()) {
  91 + return it->second;
  92 + }
  93 + }
  94 + return const_cast<QPDFObjectHandle&>(null_obj);
  95 +}
  96 +
38 std::set<std::string> 97 std::set<std::string>
39 BaseDictionary::getKeys() 98 BaseDictionary::getKeys()
40 { 99 {
@@ -63,18 +122,29 @@ BaseHandle::erase(const std::string&amp; key) @@ -63,18 +122,29 @@ BaseHandle::erase(const std::string&amp; key)
63 return 0; 122 return 0;
64 } 123 }
65 124
  125 +bool
  126 +BaseHandle::replace(std::string const& key, QPDFObjectHandle value)
  127 +{
  128 + if (auto d = as<QPDF_Dictionary>()) {
  129 + if (value.null() && !value.indirect()) {
  130 + // The PDF spec doesn't distinguish between keys with null values and missing keys.
  131 + // Allow indirect nulls which are equivalent to a dangling reference, which is permitted
  132 + // by the spec.
  133 + d->items.erase(key);
  134 + } else {
  135 + // add or replace value
  136 + d->items[key] = value;
  137 + }
  138 + return true;
  139 + }
  140 + return false;
  141 +}
  142 +
66 void 143 void
67 -BaseDictionary::replaceKey(std::string const& key, QPDFObjectHandle value)  
68 -{  
69 - auto d = dict();  
70 - if (value.null() && !value.indirect()) {  
71 - // The PDF spec doesn't distinguish between keys with null values and missing keys.  
72 - // Allow indirect nulls which are equivalent to a dangling reference, which is  
73 - // permitted by the spec.  
74 - d->items.erase(key);  
75 - } else {  
76 - // add or replace value  
77 - d->items[key] = value; 144 +BaseDictionary::replace(std::string const& key, QPDFObjectHandle value)
  145 +{
  146 + if (!BaseHandle::replace(key, value)) {
  147 + (void)dict();
78 } 148 }
79 } 149 }
80 150
@@ -121,7 +191,7 @@ QPDFObjectHandle::hasKey(std::string const&amp; key) const @@ -121,7 +191,7 @@ QPDFObjectHandle::hasKey(std::string const&amp; key) const
121 QPDFObjectHandle 191 QPDFObjectHandle
122 QPDFObjectHandle::getKey(std::string const& key) const 192 QPDFObjectHandle::getKey(std::string const& key) const
123 { 193 {
124 - if (auto result = (*this)[key]) { 194 + if (auto result = get(key)) {
125 return result; 195 return result;
126 } 196 }
127 if (isDictionary()) { 197 if (isDictionary()) {
@@ -166,11 +236,10 @@ QPDFObjectHandle::replaceKey(std::string const&amp; key, QPDFObjectHandle const&amp; val @@ -166,11 +236,10 @@ QPDFObjectHandle::replaceKey(std::string const&amp; key, QPDFObjectHandle const&amp; val
166 { 236 {
167 if (auto dict = as_dictionary(strict)) { 237 if (auto dict = as_dictionary(strict)) {
168 checkOwnership(value); 238 checkOwnership(value);
169 - dict.replaceKey(key, value); 239 + dict.replace(key, value);
170 return; 240 return;
171 } 241 }
172 typeWarning("dictionary", "ignoring key replacement request"); 242 typeWarning("dictionary", "ignoring key replacement request");
173 - QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring replaceKey");  
174 } 243 }
175 244
176 QPDFObjectHandle 245 QPDFObjectHandle
@@ -200,7 +269,7 @@ QPDFObjectHandle::removeKey(std::string const&amp; key) @@ -200,7 +269,7 @@ QPDFObjectHandle::removeKey(std::string const&amp; key)
200 QPDFObjectHandle 269 QPDFObjectHandle
201 QPDFObjectHandle::removeKeyAndGetOld(std::string const& key) 270 QPDFObjectHandle::removeKeyAndGetOld(std::string const& key)
202 { 271 {
203 - auto result = (*this)[key]; 272 + auto result = get(key);
204 erase(key); 273 erase(key);
205 return result ? result : newNull(); 274 return result ? result : newNull();
206 } 275 }
libqpdf/qpdf/QPDFObjectHandle_private.hh
@@ -127,7 +127,7 @@ namespace qpdf @@ -127,7 +127,7 @@ namespace qpdf
127 // The following methods are not part of the public API. 127 // The following methods are not part of the public API.
128 std::set<std::string> getKeys(); 128 std::set<std::string> getKeys();
129 std::map<std::string, QPDFObjectHandle> const& getAsMap() const; 129 std::map<std::string, QPDFObjectHandle> const& getAsMap() const;
130 - void replaceKey(std::string const& key, QPDFObjectHandle value); 130 + void replace(std::string const& key, QPDFObjectHandle value);
131 131
132 using iterator = std::map<std::string, QPDFObjectHandle>::iterator; 132 using iterator = std::map<std::string, QPDFObjectHandle>::iterator;
133 using const_iterator = std::map<std::string, QPDFObjectHandle>::const_iterator; 133 using const_iterator = std::map<std::string, QPDFObjectHandle>::const_iterator;
@@ -806,6 +806,20 @@ namespace qpdf @@ -806,6 +806,20 @@ namespace qpdf
806 } 806 }
807 } 807 }
808 808
  809 + /// @brief Retrieves the QPDFObjectHandle const& associated with the given key.
  810 + ///
  811 + /// This method provides a convenience alternative to the direct use of the subscript operator
  812 + /// "(*this)[key]" or "oh()[key]" in derived classes, enabling a simplified and readable way to
  813 + /// access object handles by key.
  814 + ///
  815 + /// @param key The string key used to look up the corresponding QPDFObjectHandle.
  816 + /// @return A constant reference to the QPDFObjectHandle associated with the specified key.
  817 + inline QPDFObjectHandle const&
  818 + BaseHandle::get(std::string const& key) const
  819 + {
  820 + return (*this)[key];
  821 + }
  822 +
809 inline bool 823 inline bool
810 BaseHandle::null() const 824 BaseHandle::null() const
811 { 825 {
libtests/objects.cc
@@ -54,35 +54,30 @@ test_0(QPDF&amp; pdf, char const* arg2) @@ -54,35 +54,30 @@ test_0(QPDF&amp; pdf, char const* arg2)
54 assert_compare_numbers(INT_MAX, t.getKey("/Q1").getIntValueAsInt()); 54 assert_compare_numbers(INT_MAX, t.getKey("/Q1").getIntValueAsInt());
55 try { 55 try {
56 assert_compare_numbers(0u, QPDFObjectHandle::newNull().getUIntValueAsUInt()); 56 assert_compare_numbers(0u, QPDFObjectHandle::newNull().getUIntValueAsUInt());
57 - std::cerr << "convert null to uint did not throw\n";  
58 } catch (QPDFExc const&) { 57 } catch (QPDFExc const&) {
59 std::cerr << "caught expected type error\n"; 58 std::cerr << "caught expected type error\n";
60 } 59 }
61 assert_compare_numbers(std::numeric_limits<int8_t>::max(), Integer(q1).value<int8_t>()); 60 assert_compare_numbers(std::numeric_limits<int8_t>::max(), Integer(q1).value<int8_t>());
62 assert_compare_numbers(std::numeric_limits<int8_t>::min(), Integer(-q1).value<int8_t>()); 61 assert_compare_numbers(std::numeric_limits<int8_t>::min(), Integer(-q1).value<int8_t>());
63 try { 62 try {
64 - int8_t q1_8 = Integer(q1);  
65 - std::cerr << "q1_8: " << std::to_string(q1_8) << '\n'; 63 + [[maybe_unused]] int8_t q1_8 = Integer(q1);
66 } catch (std::overflow_error const&) { 64 } catch (std::overflow_error const&) {
67 std::cerr << "caught expected int8_t overflow error\n"; 65 std::cerr << "caught expected int8_t overflow error\n";
68 } 66 }
69 try { 67 try {
70 - int8_t q1_8 = Integer(-q1);  
71 - std::cerr << "q1_8: " << std::to_string(q1_8) << '\n'; 68 + [[maybe_unused]] int8_t q1_8 = Integer(-q1);
72 } catch (std::underflow_error const&) { 69 } catch (std::underflow_error const&) {
73 std::cerr << "caught expected int8_t underflow error\n"; 70 std::cerr << "caught expected int8_t underflow error\n";
74 } 71 }
75 assert_compare_numbers(std::numeric_limits<uint8_t>::max(), Integer(q1).value<uint8_t>()); 72 assert_compare_numbers(std::numeric_limits<uint8_t>::max(), Integer(q1).value<uint8_t>());
76 assert_compare_numbers(0, Integer(-q1).value<uint8_t>()); 73 assert_compare_numbers(0, Integer(-q1).value<uint8_t>());
77 try { 74 try {
78 - uint8_t q1_u8 = Integer(q1);  
79 - std::cerr << "q1_u8: " << std::to_string(q1_u8) << '\n'; 75 + [[maybe_unused]] uint8_t q1_u8 = Integer(q1);
80 } catch (std::overflow_error const&) { 76 } catch (std::overflow_error const&) {
81 std::cerr << "caught expected uint8_t overflow error\n"; 77 std::cerr << "caught expected uint8_t overflow error\n";
82 } 78 }
83 try { 79 try {
84 - uint8_t q1_u8 = Integer(-q1);  
85 - std::cerr << "q1_u8: " << std::to_string(q1_u8) << '\n'; 80 + [[maybe_unused]] uint8_t q1_u8 = Integer(-q1);
86 } catch (std::underflow_error const&) { 81 } catch (std::underflow_error const&) {
87 std::cerr << "caught expected uint8_t underflow error\n"; 82 std::cerr << "caught expected uint8_t underflow error\n";
88 } 83 }
@@ -95,6 +90,70 @@ test_0(QPDF&amp; pdf, char const* arg2) @@ -95,6 +90,70 @@ test_0(QPDF&amp; pdf, char const* arg2)
95 assert_compare_numbers(UINT_MAX, t.getKey("/Q3").getUIntValueAsUInt()); 90 assert_compare_numbers(UINT_MAX, t.getKey("/Q3").getUIntValueAsUInt());
96 } 91 }
97 92
  93 +
  94 +static void
  95 +test_1(QPDF& pdf, char const* arg2)
  96 +{
  97 + // Test new dictionary methods.
  98 + using namespace qpdf;
  99 + auto d = Dictionary({{"/A", {}}, {"/B", Null()}, {"/C", Dictionary::empty()}});
  100 +
  101 + // contains
  102 + assert(!d.contains("/A"));
  103 + assert(!d.contains("/B"));
  104 + assert(d.contains("/C"));
  105 +
  106 + auto i = Integer(42);
  107 + assert(!i.contains("/A"));
  108 +
  109 + // at
  110 + assert(!d.at("/A"));
  111 + assert(d.at("/B"));
  112 + assert(d.at("/B").null());
  113 + assert(d.at("/C"));
  114 + assert(!d.at("/C").null());
  115 + d.at("/C") = Integer(42);
  116 + assert(d.at("/C") == 42);
  117 + assert(!d.at("/D"));
  118 + assert(d.at("/D").null());
  119 + assert(QPDFObjectHandle(d).getDictAsMap().contains("/D"));
  120 + assert(QPDFObjectHandle(d).getDictAsMap().size() == 4);
  121 +
  122 + bool thrown = false;
  123 + try {
  124 + i.at("/A");
  125 + } catch (std::runtime_error const&) {
  126 + thrown = true;
  127 + }
  128 + assert(thrown);
  129 +
  130 + // find
  131 + assert(!d.find("/A"));
  132 + assert(d.find("/B"));
  133 + assert(d.find("/B").null());
  134 + assert(d.find("/C"));
  135 + assert(Integer(d.find("/C")) == 42);
  136 + d.find("/C") = Name("/DontPanic");
  137 + assert(Name(d.find("/C")) == "/DontPanic");
  138 + assert(!d.find("/E"));
  139 + assert(!QPDFObjectHandle(d).getDictAsMap().contains("/E"));
  140 + assert(QPDFObjectHandle(d).getDictAsMap().size() == 4);
  141 +
  142 + // replace
  143 + assert(!i.replace("/A", Name("/DontPanic")));
  144 + Dictionary di = i.oh();
  145 + thrown = false;
  146 + try {
  147 + di.replace("/A", Name("/DontPanic"));
  148 + } catch (std::runtime_error const&) {
  149 + thrown = true;
  150 + }
  151 + assert(thrown);
  152 + d.replace("/C", Integer(42));
  153 + assert(Integer(d["/C"]) == 42);
  154 + assert(QPDFObjectHandle(d).getDictAsMap().size() == 4);
  155 +}
  156 +
98 void 157 void
99 runtest(int n, char const* filename1, char const* arg2) 158 runtest(int n, char const* filename1, char const* arg2)
100 { 159 {
@@ -102,7 +161,7 @@ runtest(int n, char const* filename1, char const* arg2) @@ -102,7 +161,7 @@ runtest(int n, char const* filename1, char const* arg2)
102 // the test suite to see how the test is invoked to find the file 161 // the test suite to see how the test is invoked to find the file
103 // that the test is supposed to operate on. 162 // that the test is supposed to operate on.
104 163
105 - std::set<int> ignore_filename = {}; 164 + std::set<int> ignore_filename = {1,};
106 165
107 QPDF pdf; 166 QPDF pdf;
108 std::shared_ptr<char> file_buf; 167 std::shared_ptr<char> file_buf;
@@ -116,7 +175,7 @@ runtest(int n, char const* filename1, char const* arg2) @@ -116,7 +175,7 @@ runtest(int n, char const* filename1, char const* arg2)
116 } 175 }
117 176
118 std::map<int, void (*)(QPDF&, char const*)> test_functions = { 177 std::map<int, void (*)(QPDF&, char const*)> test_functions = {
119 - {0, test_0}, 178 + {0, test_0}, {1, test_1},
120 }; 179 };
121 180
122 auto fn = test_functions.find(n); 181 auto fn = test_functions.find(n);
libtests/qtest/objects.test
@@ -11,12 +11,16 @@ require TestDriver; @@ -11,12 +11,16 @@ require TestDriver;
11 11
12 my $td = new TestDriver('objects'); 12 my $td = new TestDriver('objects');
13 13
14 -my $n_tests = 1;  
15 - 14 +my $n_tests = 2;
16 15
17 $td->runtest("integer type checks", 16 $td->runtest("integer type checks",
18 {$td->COMMAND => "objects 0 minimal.pdf"}, 17 {$td->COMMAND => "objects 0 minimal.pdf"},
19 {$td->FILE => "test0.out", $td->EXIT_STATUS => 0}, 18 {$td->FILE => "test0.out", $td->EXIT_STATUS => 0},
20 $td->NORMALIZE_NEWLINES); 19 $td->NORMALIZE_NEWLINES);
21 20
  21 +$td->runtest("dictionary checks",
  22 + {$td->COMMAND => "objects 1 -"},
  23 + {$td->STRING => => "test 1 done\n", $td->EXIT_STATUS => 0},
  24 + $td->NORMALIZE_NEWLINES);
  25 +
22 $td->report($n_tests); 26 $td->report($n_tests);
qpdf/qpdf.testcov
@@ -171,7 +171,6 @@ QPDFObjectHandle array ignoring erase item 0 @@ -171,7 +171,6 @@ QPDFObjectHandle array ignoring erase item 0
171 QPDFObjectHandle dictionary false for hasKey 0 171 QPDFObjectHandle dictionary false for hasKey 0
172 QPDFObjectHandle dictionary empty set for getKeys 0 172 QPDFObjectHandle dictionary empty set for getKeys 0
173 QPDFObjectHandle dictionary empty map for asMap 0 173 QPDFObjectHandle dictionary empty map for asMap 0
174 -QPDFObjectHandle dictionary ignoring replaceKey 0  
175 QPDFObjectHandle numeric non-numeric 0 174 QPDFObjectHandle numeric non-numeric 0
176 QPDFObjectHandle erase array bounds 0 175 QPDFObjectHandle erase array bounds 0
177 qpdf-c called qpdf_check_pdf 0 176 qpdf-c called qpdf_check_pdf 0