Commit e80fad86e95af978ada2a6cc5c4b209a1f65f7c0

Authored by Jay Berkenbilt
1 parent ff73d71e

Add new QPDFObjectHandle methods for more fluent programming

ChangeLog
  1 +2022-04-29 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * QPDFObjectHandle: for the methods insertItem, appendItem,
  4 + eraseItem, replaceKey, and removeKey, have the methods return a
  5 + reference to the original object, making a fluent interface to
  6 + initializing or modifying QPDFObjectHandle possible. Also, for
  7 + each one, add a corresponding "AndGet" method (insertItemAndGet,
  8 + appendItemAndGet, eraseItemAndGet, replaceKeyAndGet, and
  9 + removeKeyAndGet) that returns the newly inserted, replaced, or
  10 + removed item. This makes it possible to create a new object, add
  11 + it to an array or dictionary, and get a handle to it all in one
  12 + line.
  13 +
1 2022-04-24 Jay Berkenbilt <ejb@ql.org> 14 2022-04-24 Jay Berkenbilt <ejb@ql.org>
2 15
3 * Bug fix: "removeAttachment" in the job JSON now takes an array 16 * Bug fix: "removeAttachment" in the job JSON now takes an array
@@ -474,13 +474,6 @@ This is a list of changes to make next time there is an ABI change. @@ -474,13 +474,6 @@ This is a list of changes to make next time there is an ABI change.
474 Comments appear in the code prefixed by "ABI". Always Search for ABI 474 Comments appear in the code prefixed by "ABI". Always Search for ABI
475 in source and header files to find items not listed here. 475 in source and header files to find items not listed here.
476 476
477 -* Having QPDFObjectHandle setters return Class& to allow for  
478 - use of fluent interfaces. This includes array and dictionary  
479 - mutators.  
480 - newDictionary().replaceKey("/X", "1"_qpdf).replaceKey("/Y", "(asdf)"_qpdf);  
481 -* Add replaceKeyAndGet, appendItemAndGet, setArrayItemAndGet,  
482 - insertItemAndGet that return the new item so you can say  
483 - auto oh = dict.replaceKeyAndGet("/Key", QPDFObjectHandle::newSomething());  
484 * Add getKeyOrInsert("/X", oh) that returns the existing value or adds 477 * Add getKeyOrInsert("/X", oh) that returns the existing value or adds
485 oh as the new value and returns it. 478 oh as the new value and returns it.
486 * Add default values to the getters, like getIntValue(default_value). 479 * Add default values to the getters, like getIntValue(default_value).
include/qpdf/QPDFObjectHandle.hh
@@ -990,7 +990,20 @@ class QPDFObjectHandle @@ -990,7 +990,20 @@ class QPDFObjectHandle
990 QPDF_DLL 990 QPDF_DLL
991 QPDFObjectHandle copyStream(); 991 QPDFObjectHandle copyStream();
992 992
993 - // Mutator methods. Use with caution. 993 + // Mutator methods.
  994 +
  995 + // Since qpdf 11: when a mutator object returns QPDFObjectHandle&,
  996 + // it is a reference to the object itself. This makes it possible
  997 + // to use a fluent style. For example:
  998 + //
  999 + // array.appendItem(i1).appendItem(i2);
  1000 + //
  1001 + // would append i1 and then i2 to the array. There are also items
  1002 + // that end with AndGet and return a QPDFObjectHandle. These
  1003 + // return the newly added object. For example:
  1004 + //
  1005 + // auto new_dict = dict.replaceKeyAndGet(
  1006 + // "/New", QPDFObjectHandle::newDictionary());
994 1007
995 // Recursively copy this object, making it direct. An exception is 1008 // Recursively copy this object, making it direct. An exception is
996 // thrown if a loop is detected. With allow_streams true, keep 1009 // thrown if a loop is detected. With allow_streams true, keep
@@ -1010,31 +1023,54 @@ class QPDFObjectHandle @@ -1010,31 +1023,54 @@ class QPDFObjectHandle
1010 QPDF_DLL 1023 QPDF_DLL
1011 void setArrayFromVector(std::vector<QPDFObjectHandle> const& items); 1024 void setArrayFromVector(std::vector<QPDFObjectHandle> const& items);
1012 // Insert an item before the item at the given position ("at") so 1025 // Insert an item before the item at the given position ("at") so
1013 - // that it has that position after insertion. If "at" is equal to  
1014 - // the size of the array, insert the item at the end. 1026 + // that it has that position after insertion. If "at" is equal to
  1027 + // the size of the array, insert the item at the end. Return a
  1028 + // reference to the array (not the new item).
  1029 + QPDF_DLL
  1030 + QPDFObjectHandle& insertItem(int at, QPDFObjectHandle const& item);
  1031 + // Like insertItem but return the item that was inserted.
1015 QPDF_DLL 1032 QPDF_DLL
1016 - void insertItem(int at, QPDFObjectHandle const& item); 1033 + QPDFObjectHandle insertItemAndGet(int at, QPDFObjectHandle const& item);
  1034 + // Append an item, and return a reference to the original array
  1035 + // (not the new item).
1017 QPDF_DLL 1036 QPDF_DLL
1018 - void appendItem(QPDFObjectHandle const& item); 1037 + QPDFObjectHandle& appendItem(QPDFObjectHandle const& item);
  1038 + // Append an item, and return the newly added item.
  1039 + QPDF_DLL
  1040 + QPDFObjectHandle appendItemAndGet(QPDFObjectHandle const& item);
1019 // Remove the item at that position, reducing the size of the 1041 // Remove the item at that position, reducing the size of the
1020 - // array by one. 1042 + // array by one. Return a reference the original array (not the
  1043 + // item that was removed).
  1044 + QPDF_DLL
  1045 + QPDFObjectHandle& eraseItem(int at);
  1046 + // Erase and item and return the item that was removed.
1021 QPDF_DLL 1047 QPDF_DLL
1022 - void eraseItem(int at); 1048 + QPDFObjectHandle eraseItemAndGet(int at);
1023 1049
1024 // Mutator methods for dictionary objects 1050 // Mutator methods for dictionary objects
1025 1051
1026 // Replace value of key, adding it if it does not exist. If value 1052 // Replace value of key, adding it if it does not exist. If value
1027 - // is null, remove the key. 1053 + // is null, remove the key. Return a reference to the original
  1054 + // dictionary (not the new item).
  1055 + QPDF_DLL
  1056 + QPDFObjectHandle&
  1057 + replaceKey(std::string const& key, QPDFObjectHandle const& value);
  1058 + // Replace value of key and return the value.
1028 QPDF_DLL 1059 QPDF_DLL
1029 - void replaceKey(std::string const& key, QPDFObjectHandle const& value);  
1030 - // Remove key, doing nothing if key does not exist 1060 + QPDFObjectHandle
  1061 + replaceKeyAndGet(std::string const& key, QPDFObjectHandle const& value);
  1062 + // Remove key, doing nothing if key does not exist. Return the
  1063 + // original dictionary (not the removed item).
1031 QPDF_DLL 1064 QPDF_DLL
1032 - void removeKey(std::string const& key); 1065 + QPDFObjectHandle& removeKey(std::string const& key);
  1066 + // Remove key and return the old value. If the old value didn't
  1067 + // exist, return a null object.
  1068 + QPDF_DLL
  1069 + QPDFObjectHandle removeKeyAndGet(std::string const& key);
1033 1070
1034 // ABI: Remove in qpdf 12 1071 // ABI: Remove in qpdf 12
1035 - [[deprecated("use replaceKey -- it does the same thing")]]  
1036 - QPDF_DLL  
1037 - void replaceOrRemoveKey(std::string const& key, QPDFObjectHandle const&); 1072 + [[deprecated("use replaceKey -- it does the same thing")]] QPDF_DLL void
  1073 + replaceOrRemoveKey(std::string const& key, QPDFObjectHandle const&);
1038 1074
1039 // Methods for stream objects 1075 // Methods for stream objects
1040 QPDF_DLL 1076 QPDF_DLL
libqpdf/QPDFObjectHandle.cc
@@ -959,7 +959,7 @@ QPDFObjectHandle::setArrayFromVector(std::vector&lt;QPDFObjectHandle&gt; const&amp; items) @@ -959,7 +959,7 @@ QPDFObjectHandle::setArrayFromVector(std::vector&lt;QPDFObjectHandle&gt; const&amp; items)
959 } 959 }
960 } 960 }
961 961
962 -void 962 +QPDFObjectHandle&
963 QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const& item) 963 QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const& item)
964 { 964 {
965 if (isArray()) { 965 if (isArray()) {
@@ -968,9 +968,17 @@ QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const&amp; item) @@ -968,9 +968,17 @@ QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const&amp; item)
968 typeWarning("array", "ignoring attempt to insert item"); 968 typeWarning("array", "ignoring attempt to insert item");
969 QTC::TC("qpdf", "QPDFObjectHandle array ignoring insert item"); 969 QTC::TC("qpdf", "QPDFObjectHandle array ignoring insert item");
970 } 970 }
  971 + return *this;
971 } 972 }
972 973
973 -void 974 +QPDFObjectHandle
  975 +QPDFObjectHandle::insertItemAndGet(int at, QPDFObjectHandle const& item)
  976 +{
  977 + insertItem(at, item);
  978 + return item;
  979 +}
  980 +
  981 +QPDFObjectHandle&
974 QPDFObjectHandle::appendItem(QPDFObjectHandle const& item) 982 QPDFObjectHandle::appendItem(QPDFObjectHandle const& item)
975 { 983 {
976 if (isArray()) { 984 if (isArray()) {
@@ -980,9 +988,17 @@ QPDFObjectHandle::appendItem(QPDFObjectHandle const&amp; item) @@ -980,9 +988,17 @@ QPDFObjectHandle::appendItem(QPDFObjectHandle const&amp; item)
980 typeWarning("array", "ignoring attempt to append item"); 988 typeWarning("array", "ignoring attempt to append item");
981 QTC::TC("qpdf", "QPDFObjectHandle array ignoring append item"); 989 QTC::TC("qpdf", "QPDFObjectHandle array ignoring append item");
982 } 990 }
  991 + return *this;
983 } 992 }
984 993
985 -void 994 +QPDFObjectHandle
  995 +QPDFObjectHandle::appendItemAndGet(QPDFObjectHandle const& item)
  996 +{
  997 + appendItem(item);
  998 + return item;
  999 +}
  1000 +
  1001 +QPDFObjectHandle&
986 QPDFObjectHandle::eraseItem(int at) 1002 QPDFObjectHandle::eraseItem(int at)
987 { 1003 {
988 if (isArray() && (at < getArrayNItems()) && (at >= 0)) { 1004 if (isArray() && (at < getArrayNItems()) && (at >= 0)) {
@@ -996,6 +1012,18 @@ QPDFObjectHandle::eraseItem(int at) @@ -996,6 +1012,18 @@ QPDFObjectHandle::eraseItem(int at)
996 QTC::TC("qpdf", "QPDFObjectHandle array ignoring erase item"); 1012 QTC::TC("qpdf", "QPDFObjectHandle array ignoring erase item");
997 } 1013 }
998 } 1014 }
  1015 + return *this;
  1016 +}
  1017 +
  1018 +QPDFObjectHandle
  1019 +QPDFObjectHandle::eraseItemAndGet(int at)
  1020 +{
  1021 + auto result = QPDFObjectHandle::newNull();
  1022 + if (isArray() && (at < getArrayNItems()) && (at >= 0)) {
  1023 + result = getArrayItem(at);
  1024 + }
  1025 + eraseItem(at);
  1026 + return result;
999 } 1027 }
1000 1028
1001 // Dictionary accessors 1029 // Dictionary accessors
@@ -1267,7 +1295,7 @@ QPDFObjectHandle::getOwningQPDF() @@ -1267,7 +1295,7 @@ QPDFObjectHandle::getOwningQPDF()
1267 1295
1268 // Dictionary mutators 1296 // Dictionary mutators
1269 1297
1270 -void 1298 +QPDFObjectHandle&
1271 QPDFObjectHandle::replaceKey( 1299 QPDFObjectHandle::replaceKey(
1272 std::string const& key, QPDFObjectHandle const& value) 1300 std::string const& key, QPDFObjectHandle const& value)
1273 { 1301 {
@@ -1278,9 +1306,18 @@ QPDFObjectHandle::replaceKey( @@ -1278,9 +1306,18 @@ QPDFObjectHandle::replaceKey(
1278 typeWarning("dictionary", "ignoring key replacement request"); 1306 typeWarning("dictionary", "ignoring key replacement request");
1279 QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring replaceKey"); 1307 QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring replaceKey");
1280 } 1308 }
  1309 + return *this;
1281 } 1310 }
1282 1311
1283 -void 1312 +QPDFObjectHandle
  1313 +QPDFObjectHandle::replaceKeyAndGet(
  1314 + std::string const& key, QPDFObjectHandle const& value)
  1315 +{
  1316 + replaceKey(key, value);
  1317 + return value;
  1318 +}
  1319 +
  1320 +QPDFObjectHandle&
1284 QPDFObjectHandle::removeKey(std::string const& key) 1321 QPDFObjectHandle::removeKey(std::string const& key)
1285 { 1322 {
1286 if (isDictionary()) { 1323 if (isDictionary()) {
@@ -1289,6 +1326,18 @@ QPDFObjectHandle::removeKey(std::string const&amp; key) @@ -1289,6 +1326,18 @@ QPDFObjectHandle::removeKey(std::string const&amp; key)
1289 typeWarning("dictionary", "ignoring key removal request"); 1326 typeWarning("dictionary", "ignoring key removal request");
1290 QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring removeKey"); 1327 QTC::TC("qpdf", "QPDFObjectHandle dictionary ignoring removeKey");
1291 } 1328 }
  1329 + return *this;
  1330 +}
  1331 +
  1332 +QPDFObjectHandle
  1333 +QPDFObjectHandle::removeKeyAndGet(std::string const& key)
  1334 +{
  1335 + auto result = QPDFObjectHandle::newNull();
  1336 + if (isDictionary()) {
  1337 + result = getKey(key);
  1338 + }
  1339 + removeKey(key);
  1340 + return result;
1292 } 1341 }
1293 1342
1294 void 1343 void
manual/release-notes.rst
@@ -73,6 +73,15 @@ For a detailed list of changes, please see the file @@ -73,6 +73,15 @@ For a detailed list of changes, please see the file
73 ``QPDFNumberTreeObjectHelper`` constructors that don't take a 73 ``QPDFNumberTreeObjectHelper`` constructors that don't take a
74 ``QPDF&`` argument. 74 ``QPDF&`` argument.
75 75
  76 + - Library Enhancements
  77 +
  78 + - Support for more fluent programming with ``QPDFObjectHandle``.
  79 + The methods ``insertItem``, ``appendItem``, ``eraseItem``,
  80 + ``replaceKey``, and ``removeKey`` all return a reference to the
  81 + object being modified. New methods ``insertItemAndGet``,
  82 + ``appendItemAndGet``, ``eraseItemAndGet``, ``replaceKeyAndGet``,
  83 + and ``removeKeyAndGet`` return the newly added or removed object.
  84 +
76 - Other changes 85 - Other changes
77 86
78 - A new chapter on contributing to qpdf has been added to the 87 - A new chapter on contributing to qpdf has been added to the
qpdf/qtest/qpdf.test
@@ -1440,13 +1440,16 @@ foreach (my $i = 1; $i &lt;= 3; ++$i) @@ -1440,13 +1440,16 @@ foreach (my $i = 1; $i &lt;= 3; ++$i)
1440 1440
1441 show_ntests(); 1441 show_ntests();
1442 # ---------- 1442 # ----------
1443 -$td->notify("--- Dictionary keys ---");  
1444 -$n_tests += 1; 1443 +$td->notify("--- Miscellaneous QPDFObjectHandle API ---");
  1444 +$n_tests += 2;
1445 1445
1446 $td->runtest("dictionary keys", 1446 $td->runtest("dictionary keys",
1447 {$td->COMMAND => "test_driver 87 - -"}, 1447 {$td->COMMAND => "test_driver 87 - -"},
1448 - {$td->STRING => "test 87 done\n",  
1449 - $td->EXIT_STATUS => 0}, 1448 + {$td->STRING => "test 87 done\n", $td->EXIT_STATUS => 0},
  1449 + $td->NORMALIZE_NEWLINES);
  1450 +$td->runtest("fluent interfaces",
  1451 + {$td->COMMAND => "test_driver 88 minimal.pdf -"},
  1452 + {$td->FILE => "test88.out", $td->EXIT_STATUS => 0},
1450 $td->NORMALIZE_NEWLINES); 1453 $td->NORMALIZE_NEWLINES);
1451 1454
1452 show_ntests(); 1455 show_ntests();
qpdf/qtest/qpdf/test88.out 0 โ†’ 100644
  1 +WARNING: test array: ignoring attempt to erase out of bounds array item
  2 +test 88 done
qpdf/test_driver.cc
@@ -3178,6 +3178,63 @@ test_87(QPDF&amp; pdf, char const* arg2) @@ -3178,6 +3178,63 @@ test_87(QPDF&amp; pdf, char const* arg2)
3178 assert(dict.getJSON().unparse() == "{\n \"/A\": 2\n}"); 3178 assert(dict.getJSON().unparse() == "{\n \"/A\": 2\n}");
3179 } 3179 }
3180 3180
  3181 +static void
  3182 +test_88(QPDF& pdf, char const* arg2)
  3183 +{
  3184 + // Exercise fluent QPDFObjectHandle mutators and similar methods
  3185 + // added for qpdf 11.
  3186 + auto dict = QPDFObjectHandle::newDictionary()
  3187 + .replaceKey("/One", QPDFObjectHandle::newInteger(1))
  3188 + .replaceKey("/Two", QPDFObjectHandle::newInteger(2));
  3189 + dict.replaceKeyAndGet("/Three", QPDFObjectHandle::newArray())
  3190 + .appendItem("(a)"_qpdf)
  3191 + .appendItem("(b)"_qpdf)
  3192 + .appendItemAndGet(QPDFObjectHandle::newDictionary())
  3193 + .replaceKey("/Z", "/Y"_qpdf)
  3194 + .replaceKey("/X", "/W"_qpdf);
  3195 + assert(dict.unparse() == R"(
  3196 + <<
  3197 + /One 1
  3198 + /Two 2
  3199 + /Three [ (a) (b) << /Z /Y /X /W >> ]
  3200 + >>
  3201 + )"_qpdf.unparse());
  3202 + auto arr = dict.getKey("/Three")
  3203 + .insertItem(0, QPDFObjectHandle::newString("0"))
  3204 + .insertItem(0, QPDFObjectHandle::newString("00"));
  3205 + assert(
  3206 + arr.unparse() ==
  3207 + "[ (00) (0) (a) (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
  3208 + auto new_dict = arr.insertItemAndGet(1, "<< /P /Q /R /S >>"_qpdf);
  3209 + arr.eraseItem(2).eraseItem(0);
  3210 + assert(
  3211 + arr.unparse() ==
  3212 + "[ << /P /Q /R /S >> (a) (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
  3213 +
  3214 + // new_dict shares internals with the one in the array. It has
  3215 + // always been this way, and there is code that relies on this
  3216 + // behavior. Maybe it would be different if I could start over
  3217 + // again...
  3218 + new_dict.removeKey("/R").replaceKey("/T", "/U"_qpdf);
  3219 + assert(
  3220 + arr.unparse() ==
  3221 + "[ << /P /Q /T /U >> (a) (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
  3222 + auto s = arr.eraseItemAndGet(1);
  3223 + assert(s.unparse() == "(a)");
  3224 + assert(
  3225 + arr.unparse() ==
  3226 + "[ << /P /Q /T /U >> (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
  3227 +
  3228 + assert(new_dict.removeKeyAndGet("/M").isNull());
  3229 + assert(new_dict.removeKeyAndGet("/P").unparse() == "/Q");
  3230 + assert(new_dict.unparse() == "<< /T /U >>"_qpdf.unparse());
  3231 +
  3232 + // Test errors
  3233 + auto arr2 = pdf.getRoot().replaceKeyAndGet("/QTest", "[1 2]"_qpdf);
  3234 + arr2.setObjectDescription(&pdf, "test array");
  3235 + assert(arr2.eraseItemAndGet(50).isNull());
  3236 +}
  3237 +
3181 void 3238 void
3182 runtest(int n, char const* filename1, char const* arg2) 3239 runtest(int n, char const* filename1, char const* arg2)
3183 { 3240 {
@@ -3280,7 +3337,7 @@ runtest(int n, char const* filename1, char const* arg2) @@ -3280,7 +3337,7 @@ runtest(int n, char const* filename1, char const* arg2)
3280 {76, test_76}, {77, test_77}, {78, test_78}, {79, test_79}, 3337 {76, test_76}, {77, test_77}, {78, test_78}, {79, test_79},
3281 {80, test_80}, {81, test_81}, {82, test_82}, {83, test_83}, 3338 {80, test_80}, {81, test_81}, {82, test_82}, {83, test_83},
3282 {84, test_84}, {85, test_85}, {86, test_86}, {87, test_87}, 3339 {84, test_84}, {85, test_85}, {86, test_86}, {87, test_87},
3283 - }; 3340 + {88, test_88}};
3284 3341
3285 auto fn = test_functions.find(n); 3342 auto fn = test_functions.find(n);
3286 if (fn == test_functions.end()) { 3343 if (fn == test_functions.end()) {