Commit bf0e6eb3022bf2fde5623a0a3d151c07f5e82945
1 parent
bfbeec54
Add QUtil methods for dealing with PDF timestamp strings
Showing
6 changed files
with
189 additions
and
1 deletions
ChangeLog
| 1 | +2021-02-09 Jay Berkenbilt <ejb@ql.org> | |
| 2 | + | |
| 3 | + * Add methods to QUtil for working with PDF timestamp strings: | |
| 4 | + pdf_time_to_qpdf_time, qpdf_time_to_pdf_time, | |
| 5 | + get_current_qpdf_time. | |
| 6 | + | |
| 1 | 7 | 2021-02-07 Jay Berkenbilt <ejb@ql.org> |
| 2 | 8 | |
| 3 | 9 | * Add new functions QUtil::pipe_file and QUtil::file_provider for | ... | ... |
include/qpdf/QUtil.hh
| ... | ... | @@ -158,7 +158,6 @@ namespace QUtil |
| 158 | 158 | QPDF_DLL |
| 159 | 159 | void setLineBuf(FILE*); |
| 160 | 160 | |
| 161 | - | |
| 162 | 161 | // May modify argv0 |
| 163 | 162 | QPDF_DLL |
| 164 | 163 | char* getWhoami(char* argv0); |
| ... | ... | @@ -172,6 +171,51 @@ namespace QUtil |
| 172 | 171 | QPDF_DLL |
| 173 | 172 | time_t get_current_time(); |
| 174 | 173 | |
| 174 | + // Portable structure representing a point in time with second | |
| 175 | + // granularity and time zone offset | |
| 176 | + struct QPDFTime | |
| 177 | + { | |
| 178 | + QPDFTime() = default; | |
| 179 | + QPDFTime(QPDFTime const&) = default; | |
| 180 | + QPDFTime& operator=(QPDFTime const&) = default; | |
| 181 | + QPDFTime(int year, int month, int day, int hour, | |
| 182 | + int minute, int second, int tz_delta) : | |
| 183 | + year(year), | |
| 184 | + month(month), | |
| 185 | + day(day), | |
| 186 | + hour(hour), | |
| 187 | + minute(minute), | |
| 188 | + second(second), | |
| 189 | + tz_delta(tz_delta) | |
| 190 | + { | |
| 191 | + } | |
| 192 | + int year; // actual year, no 1900 stuff | |
| 193 | + int month; // 1--12 | |
| 194 | + int day; // 1--31 | |
| 195 | + int hour; | |
| 196 | + int minute; | |
| 197 | + int second; | |
| 198 | + int tz_delta; // minutes before UTC | |
| 199 | + }; | |
| 200 | + | |
| 201 | + QPDF_DLL | |
| 202 | + QPDFTime get_current_qpdf_time(); | |
| 203 | + | |
| 204 | + // Convert a QPDFTime structure to a PDF timestamp string, which | |
| 205 | + // is "D:yyyymmddhhmmss<z>" where <z> is either "Z" for UTC or | |
| 206 | + // "-hh'mm'" or "+hh'mm'" for timezone offset. Examples: | |
| 207 | + // "D:20210207161528-05'00'", "D:20210207211528Z". See | |
| 208 | + // get_current_qpdf_time and the QPDFTime structure above. | |
| 209 | + QPDF_DLL | |
| 210 | + std::string qpdf_time_to_pdf_time(QPDFTime const&); | |
| 211 | + | |
| 212 | + // Convert a PDF timestamp string to a QPDFTime. If syntactically | |
| 213 | + // valid, return true and fill in qtm. If not valid, return false, | |
| 214 | + // and do not modify qtm. If qtm is null, just check the validity | |
| 215 | + // of the string. | |
| 216 | + QPDF_DLL | |
| 217 | + bool pdf_time_to_qpdf_time(std::string const&, QPDFTime* qtm = nullptr); | |
| 218 | + | |
| 175 | 219 | // Return a string containing the byte representation of the UTF-8 |
| 176 | 220 | // encoding for the unicode value passed in. |
| 177 | 221 | QPDF_DLL | ... | ... |
libqpdf/QUtil.cc
| ... | ... | @@ -23,6 +23,7 @@ |
| 23 | 23 | #include <fcntl.h> |
| 24 | 24 | #include <memory> |
| 25 | 25 | #include <locale> |
| 26 | +#include <regex> | |
| 26 | 27 | #ifndef QPDF_NO_WCHAR_T |
| 27 | 28 | # include <cwchar> |
| 28 | 29 | #endif |
| ... | ... | @@ -823,6 +824,108 @@ QUtil::get_current_time() |
| 823 | 824 | #endif |
| 824 | 825 | } |
| 825 | 826 | |
| 827 | +QUtil::QPDFTime | |
| 828 | +QUtil::get_current_qpdf_time() | |
| 829 | +{ | |
| 830 | +#ifdef _WIN32 | |
| 831 | + SYSTEMTIME ltime; | |
| 832 | + GetLocalTime(<ime); | |
| 833 | + TIME_ZONE_INFORMATION tzinfo; | |
| 834 | + GetTimeZoneInformation(&tzinfo); | |
| 835 | + return QPDFTime(static_cast<int>(ltime.wYear), | |
| 836 | + static_cast<int>(ltime.wMonth), | |
| 837 | + static_cast<int>(ltime.wDay), | |
| 838 | + static_cast<int>(ltime.wHour), | |
| 839 | + static_cast<int>(ltime.wMinute), | |
| 840 | + static_cast<int>(ltime.wSecond), | |
| 841 | + static_cast<int>(tzinfo.Bias)); | |
| 842 | +#else | |
| 843 | + struct tm ltime; | |
| 844 | + time_t now = time(0); | |
| 845 | + tzset(); | |
| 846 | + localtime_r(&now, <ime); | |
| 847 | + return QPDFTime(static_cast<int>(ltime.tm_year + 1900), | |
| 848 | + static_cast<int>(ltime.tm_mon + 1), | |
| 849 | + static_cast<int>(ltime.tm_mday), | |
| 850 | + static_cast<int>(ltime.tm_hour), | |
| 851 | + static_cast<int>(ltime.tm_min), | |
| 852 | + static_cast<int>(ltime.tm_sec), | |
| 853 | + static_cast<int>(timezone / 60)); | |
| 854 | +#endif | |
| 855 | +} | |
| 856 | + | |
| 857 | +std::string | |
| 858 | +QUtil::qpdf_time_to_pdf_time(QPDFTime const& qtm) | |
| 859 | +{ | |
| 860 | + std::string tz_offset; | |
| 861 | + int t = qtm.tz_delta; | |
| 862 | + if (t == 0) | |
| 863 | + { | |
| 864 | + tz_offset = "Z"; | |
| 865 | + } | |
| 866 | + else | |
| 867 | + { | |
| 868 | + if (t < 0) | |
| 869 | + { | |
| 870 | + t = -t; | |
| 871 | + tz_offset += "+"; | |
| 872 | + } | |
| 873 | + else | |
| 874 | + { | |
| 875 | + tz_offset += "-"; | |
| 876 | + } | |
| 877 | + tz_offset += | |
| 878 | + QUtil::int_to_string(t / 60, 2) + "'" + | |
| 879 | + QUtil::int_to_string(t % 60, 2) + "'"; | |
| 880 | + } | |
| 881 | + return ("D:" + | |
| 882 | + QUtil::int_to_string(qtm.year, 4) + | |
| 883 | + QUtil::int_to_string(qtm.month, 2) + | |
| 884 | + QUtil::int_to_string(qtm.day, 2) + | |
| 885 | + QUtil::int_to_string(qtm.hour, 2) + | |
| 886 | + QUtil::int_to_string(qtm.minute, 2) + | |
| 887 | + QUtil::int_to_string(qtm.second, 2) + | |
| 888 | + tz_offset); | |
| 889 | +} | |
| 890 | + | |
| 891 | +bool | |
| 892 | +QUtil::pdf_time_to_qpdf_time(std::string const& str, QPDFTime* qtm) | |
| 893 | +{ | |
| 894 | + static std::regex pdf_date("^D:([0-9]{4})([0-9]{2})([0-9]{2})" | |
| 895 | + "([0-9]{2})([0-9]{2})([0-9]{2})" | |
| 896 | + "(?:(Z)|([\\+\\-])([0-9]{2})'([0-9]{2})')$"); | |
| 897 | + std::smatch m; | |
| 898 | + if (! std::regex_match(str, m, pdf_date)) | |
| 899 | + { | |
| 900 | + return false; | |
| 901 | + } | |
| 902 | + int tz_delta = 0; | |
| 903 | + auto to_i = [](std::string const& s) { | |
| 904 | + return QUtil::string_to_int(s.c_str()); | |
| 905 | + }; | |
| 906 | + | |
| 907 | + if (m[7] == "") | |
| 908 | + { | |
| 909 | + tz_delta = ((to_i(m[9]) * 60) + | |
| 910 | + to_i(m[10])); | |
| 911 | + if (m[8] == "+") | |
| 912 | + { | |
| 913 | + tz_delta = -tz_delta; | |
| 914 | + } | |
| 915 | + } | |
| 916 | + if (qtm) | |
| 917 | + { | |
| 918 | + *qtm = QPDFTime(to_i(m[1]), | |
| 919 | + to_i(m[2]), | |
| 920 | + to_i(m[3]), | |
| 921 | + to_i(m[4]), | |
| 922 | + to_i(m[5]), | |
| 923 | + to_i(m[6]), | |
| 924 | + tz_delta); | |
| 925 | + } | |
| 926 | + return true; | |
| 927 | +} | |
| 928 | + | |
| 826 | 929 | std::string |
| 827 | 930 | QUtil::toUTF8(unsigned long uval) |
| 828 | 931 | { | ... | ... |
libtests/qtest/qutil/qutil.out
libtests/qutil.cc
| ... | ... | @@ -581,6 +581,27 @@ void rename_delete_test() |
| 581 | 581 | assert_no_file("old\xcf\x80.~tmp"); |
| 582 | 582 | } |
| 583 | 583 | |
| 584 | +void timestamp_test() | |
| 585 | +{ | |
| 586 | + auto check = [](QUtil::QPDFTime const& t) { | |
| 587 | + std::string pdf = QUtil::qpdf_time_to_pdf_time(t); | |
| 588 | + std::cout << pdf << std::endl; | |
| 589 | + QUtil::QPDFTime t2; | |
| 590 | + assert(QUtil::pdf_time_to_qpdf_time(pdf, &t2)); | |
| 591 | + assert(QUtil::qpdf_time_to_pdf_time(t2) == pdf); | |
| 592 | + }; | |
| 593 | + check(QUtil::QPDFTime(2021, 2, 9, 14, 49, 25, 300)); | |
| 594 | + check(QUtil::QPDFTime(2021, 2, 10, 1, 19, 25, -330)); | |
| 595 | + check(QUtil::QPDFTime(2021, 2, 9, 19, 19, 25, 0)); | |
| 596 | + assert(! QUtil::pdf_time_to_qpdf_time("potato")); | |
| 597 | + // Round trip on the current time without actually printing it. | |
| 598 | + // Manual testing was done to ensure that we are actually getting | |
| 599 | + // back the current time in various timezones. | |
| 600 | + assert(QUtil::pdf_time_to_qpdf_time( | |
| 601 | + QUtil::qpdf_time_to_pdf_time( | |
| 602 | + QUtil::get_current_qpdf_time()))); | |
| 603 | +} | |
| 604 | + | |
| 584 | 605 | int main(int argc, char* argv[]) |
| 585 | 606 | { |
| 586 | 607 | try |
| ... | ... | @@ -611,6 +632,8 @@ int main(int argc, char* argv[]) |
| 611 | 632 | hex_encode_decode_test(); |
| 612 | 633 | std::cout << "---- rename/delete" << std::endl; |
| 613 | 634 | rename_delete_test(); |
| 635 | + std::cout << "---- timestamp" << std::endl; | |
| 636 | + timestamp_test(); | |
| 614 | 637 | } |
| 615 | 638 | catch (std::exception& e) |
| 616 | 639 | { | ... | ... |
manual/qpdf-manual.xml
| ... | ... | @@ -4943,6 +4943,14 @@ print "\n"; |
| 4943 | 4943 | </listitem> |
| 4944 | 4944 | <listitem> |
| 4945 | 4945 | <para> |
| 4946 | + Add <function>QUtil::get_current_qpdf_time</function>, | |
| 4947 | + <function>QUtil::pdf_time_to_qpdf_time</function>, and | |
| 4948 | + <function>QUtil::qpdf_time_to_pdf_time</function> for | |
| 4949 | + working with PDF timestamp strings. | |
| 4950 | + </para> | |
| 4951 | + </listitem> | |
| 4952 | + <listitem> | |
| 4953 | + <para> | |
| 4946 | 4954 | Add <function>warn</function> to |
| 4947 | 4955 | <classname>QPDF</classname>'s public API. |
| 4948 | 4956 | </para> | ... | ... |