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 | 2021-02-07 Jay Berkenbilt <ejb@ql.org> | 7 | 2021-02-07 Jay Berkenbilt <ejb@ql.org> |
| 2 | 8 | ||
| 3 | * Add new functions QUtil::pipe_file and QUtil::file_provider for | 9 | * Add new functions QUtil::pipe_file and QUtil::file_provider for |
include/qpdf/QUtil.hh
| @@ -158,7 +158,6 @@ namespace QUtil | @@ -158,7 +158,6 @@ namespace QUtil | ||
| 158 | QPDF_DLL | 158 | QPDF_DLL |
| 159 | void setLineBuf(FILE*); | 159 | void setLineBuf(FILE*); |
| 160 | 160 | ||
| 161 | - | ||
| 162 | // May modify argv0 | 161 | // May modify argv0 |
| 163 | QPDF_DLL | 162 | QPDF_DLL |
| 164 | char* getWhoami(char* argv0); | 163 | char* getWhoami(char* argv0); |
| @@ -172,6 +171,51 @@ namespace QUtil | @@ -172,6 +171,51 @@ namespace QUtil | ||
| 172 | QPDF_DLL | 171 | QPDF_DLL |
| 173 | time_t get_current_time(); | 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 | // Return a string containing the byte representation of the UTF-8 | 219 | // Return a string containing the byte representation of the UTF-8 |
| 176 | // encoding for the unicode value passed in. | 220 | // encoding for the unicode value passed in. |
| 177 | QPDF_DLL | 221 | QPDF_DLL |
libqpdf/QUtil.cc
| @@ -23,6 +23,7 @@ | @@ -23,6 +23,7 @@ | ||
| 23 | #include <fcntl.h> | 23 | #include <fcntl.h> |
| 24 | #include <memory> | 24 | #include <memory> |
| 25 | #include <locale> | 25 | #include <locale> |
| 26 | +#include <regex> | ||
| 26 | #ifndef QPDF_NO_WCHAR_T | 27 | #ifndef QPDF_NO_WCHAR_T |
| 27 | # include <cwchar> | 28 | # include <cwchar> |
| 28 | #endif | 29 | #endif |
| @@ -823,6 +824,108 @@ QUtil::get_current_time() | @@ -823,6 +824,108 @@ QUtil::get_current_time() | ||
| 823 | #endif | 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 | std::string | 929 | std::string |
| 827 | QUtil::toUTF8(unsigned long uval) | 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,6 +581,27 @@ void rename_delete_test() | ||
| 581 | assert_no_file("old\xcf\x80.~tmp"); | 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 | int main(int argc, char* argv[]) | 605 | int main(int argc, char* argv[]) |
| 585 | { | 606 | { |
| 586 | try | 607 | try |
| @@ -611,6 +632,8 @@ int main(int argc, char* argv[]) | @@ -611,6 +632,8 @@ int main(int argc, char* argv[]) | ||
| 611 | hex_encode_decode_test(); | 632 | hex_encode_decode_test(); |
| 612 | std::cout << "---- rename/delete" << std::endl; | 633 | std::cout << "---- rename/delete" << std::endl; |
| 613 | rename_delete_test(); | 634 | rename_delete_test(); |
| 635 | + std::cout << "---- timestamp" << std::endl; | ||
| 636 | + timestamp_test(); | ||
| 614 | } | 637 | } |
| 615 | catch (std::exception& e) | 638 | catch (std::exception& e) |
| 616 | { | 639 | { |
manual/qpdf-manual.xml
| @@ -4943,6 +4943,14 @@ print "\n"; | @@ -4943,6 +4943,14 @@ print "\n"; | ||
| 4943 | </listitem> | 4943 | </listitem> |
| 4944 | <listitem> | 4944 | <listitem> |
| 4945 | <para> | 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 | Add <function>warn</function> to | 4954 | Add <function>warn</function> to |
| 4947 | <classname>QPDF</classname>'s public API. | 4955 | <classname>QPDF</classname>'s public API. |
| 4948 | </para> | 4956 | </para> |