diff --git a/ChangeLog b/ChangeLog index 97e8506c..abf10459 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2022-06-18 Jay Berkenbilt + * Add examples that show how to capture QPDFJob's output by + configuring the default logger (qpdfjob-save-attachment.cc, + qpdfjob-c-save-attachment.c). Fixes #691. + + * Add C API for QPDFLogger -- see qpdflogger-c.h + * Add additional qpdfjob C API functions take a handle. * Add qpdf_exit_code_e to Constants.h so that exit codes from diff --git a/include/qpdf/QPDFLogger.hh b/include/qpdf/QPDFLogger.hh index cd439caa..54ab7efe 100644 --- a/include/qpdf/QPDFLogger.hh +++ b/include/qpdf/QPDFLogger.hh @@ -130,9 +130,9 @@ class QPDFLogger void setError(std::shared_ptr); // See notes above about the save pipeline QPDF_DLL - void setSave(std::shared_ptr); + void setSave(std::shared_ptr, bool only_if_not_set); QPDF_DLL - void saveToStandardOutput(); + void saveToStandardOutput(bool only_if_not_set); // Shortcut for logic to reset output to new output/error streams. // out_stream is used for info, err_stream is used for error, and diff --git a/include/qpdf/qpdflogger-c.h b/include/qpdf/qpdflogger-c.h new file mode 100644 index 00000000..9d31f79c --- /dev/null +++ b/include/qpdf/qpdflogger-c.h @@ -0,0 +1,100 @@ +/* Copyright (c) 2005-2022 Jay Berkenbilt + * + * This file is part of qpdf. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Versions of qpdf prior to version 7 were released under the terms + * of version 2.0 of the Artistic License. At your option, you may + * continue to consider qpdf to be licensed under those terms. Please + * see the manual for additional information. + */ + +#ifndef QPDFLOGGER_H +#define QPDFLOGGER_H + +/* + * This file provides a C API for QPDFLogger. See QPDFLogger.hh for + * information about the logger. + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + /* To operate on a logger, you need a handle to it. call + * qpdflogger_default_logger to get a handle for the default + * logger. The qpdf and qpdfjob functions may offer ways to get + * other logger handles. When you're done with the logger handler, + * call qpdflogger_cleanup. This does not destroy the underlying + * log object. It just cleans up the handle to it. + */ + + typedef struct _qpdflogger_handle* qpdflogger_handle; + QPDF_DLL + qpdflogger_handle qpdflogger_default_logger(); + + QPDF_DLL + void qpdflogger_cleanup(qpdflogger_handle* l); + + enum qpdf_log_dest_e { + qpdf_log_dest_default = 0, + qpdf_log_dest_stdout = 1, + qpdf_log_dest_stderr = 2, + qpdf_log_dest_discard = 3, + qpdf_log_dest_custom = 4, + }; + + typedef void (*qpdf_log_fn_t)(char const* data, size_t len, void* udata); + + QPDF_DLL + void qpdflogger_set_info( + qpdflogger_handle l, + enum qpdf_log_dest_e dest, + qpdf_log_fn_t fn, + void* udata); + QPDF_DLL + void qpdflogger_set_warn( + qpdflogger_handle l, + enum qpdf_log_dest_e dest, + qpdf_log_fn_t fn, + void* udata); + QPDF_DLL + void qpdflogger_set_error( + qpdflogger_handle l, + enum qpdf_log_dest_e dest, + qpdf_log_fn_t fn, + void* udata); + + /* A non-zero value for only_if_not_set means that the save + * pipeline will only be changed if it is not already set. + */ + QPDF_DLL + void qpdflogger_set_save( + qpdflogger_handle l, + enum qpdf_log_dest_e dest, + qpdf_log_fn_t fn, + void* udata, + int only_if_not_set); + QPDF_DLL + void qpdflogger_save_to_standard_output( + qpdflogger_handle l, int only_if_not_set); + +#ifdef __cplusplus +} +#endif + +#endif // QPDFLOGGER_H diff --git a/libqpdf/CMakeLists.txt b/libqpdf/CMakeLists.txt index a6b036a7..3c14115b 100644 --- a/libqpdf/CMakeLists.txt +++ b/libqpdf/CMakeLists.txt @@ -110,7 +110,8 @@ set(libqpdf_SOURCES SF_FlateLzwDecode.cc SparseOHArray.cc qpdf-c.cc - qpdfjob-c.cc) + qpdfjob-c.cc + qpdflogger-c.cc) include(FindPkgConfig) include(CheckTypeSize) diff --git a/libqpdf/QPDFJob.cc b/libqpdf/QPDFJob.cc index b25257a1..606a612a 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -710,7 +710,7 @@ QPDFJob::checkConfiguration() save_to_stdout = true; } if (save_to_stdout) { - this->m->log->saveToStandardOutput(); + this->m->log->saveToStandardOutput(true); } if ((!m->split_pages) && QUtil::same_file(m->infilename.get(), m->outfilename.get())) { @@ -925,7 +925,7 @@ QPDFJob::doShowObj(QPDF& pdf) } else { // If anything has been written to standard output, // this will fail. - this->m->log->saveToStandardOutput(); + this->m->log->saveToStandardOutput(true); obj.pipeStreamData( this->m->log->getSave().get(), (filter && m->normalize) ? qpdf_ef_normalize : 0, @@ -1031,7 +1031,7 @@ QPDFJob::doShowAttachment(QPDF& pdf) auto efs = fs->getEmbeddedFileStream(); // saveToStandardOutput has already been called, but it's harmless // to call it again, so do as defensive coding. - this->m->log->saveToStandardOutput(); + this->m->log->saveToStandardOutput(true); efs.pipeStreamData(this->m->log->getSave().get(), 0, qpdf_dl_all); } @@ -3289,7 +3289,7 @@ QPDFJob::writeOutfile(QPDF& pdf) } else { // saveToStandardOutput has already been called, but // calling it again is defensive and harmless. - this->m->log->saveToStandardOutput(); + this->m->log->saveToStandardOutput(true); w.setOutputPipeline(this->m->log->getSave().get()); } setWriterOptions(pdf, w); diff --git a/libqpdf/QPDFLogger.cc b/libqpdf/QPDFLogger.cc index 3b25c050..78cb2a32 100644 --- a/libqpdf/QPDFLogger.cc +++ b/libqpdf/QPDFLogger.cc @@ -181,8 +181,11 @@ QPDFLogger::setError(std::shared_ptr p) } void -QPDFLogger::setSave(std::shared_ptr p) +QPDFLogger::setSave(std::shared_ptr p, bool only_if_not_set) { + if (only_if_not_set && (this->m->p_save != nullptr)) { + return; + } if (this->m->p_save == p) { return; } @@ -202,9 +205,9 @@ QPDFLogger::setSave(std::shared_ptr p) } void -QPDFLogger::saveToStandardOutput() +QPDFLogger::saveToStandardOutput(bool only_if_not_set) { - setSave(standardOutput()); + setSave(standardOutput(), only_if_not_set); } void diff --git a/libqpdf/qpdflogger-c.cc b/libqpdf/qpdflogger-c.cc new file mode 100644 index 00000000..46fd9fba --- /dev/null +++ b/libqpdf/qpdflogger-c.cc @@ -0,0 +1,163 @@ +#include + +#include +#include +#include +#include +#include + +struct _qpdflogger_handle +{ + _qpdflogger_handle(std::shared_ptr l); + ~_qpdflogger_handle() = default; + + std::shared_ptr l; +}; + +namespace +{ + class FunctionPipeline: public Pipeline + { + public: + FunctionPipeline(char const* identifier, qpdf_log_fn_t fn, void* udata); + virtual ~FunctionPipeline() = default; + + virtual void write(unsigned char const* buf, size_t len) override; + virtual void finish() override; + + private: + qpdf_log_fn_t fn; + void* udata; + }; +}; // namespace + +FunctionPipeline::FunctionPipeline( + char const* identifier, qpdf_log_fn_t fn, void* udata) : + Pipeline(identifier, nullptr), + fn(fn), + udata(udata) +{ +} + +void +FunctionPipeline::write(unsigned char const* buf, size_t len) +{ + fn(reinterpret_cast(buf), QIntC::to_ulong(len), udata); +} + +void +FunctionPipeline::finish() +{ + // Nothing needed +} + +_qpdflogger_handle::_qpdflogger_handle(std::shared_ptr l) : + l(l) +{ +} + +qpdflogger_handle +qpdflogger_default_logger() +{ + return new _qpdflogger_handle(QPDFLogger::defaultLogger()); +} + +void +qpdflogger_cleanup(qpdflogger_handle* l) +{ + delete *l; + *l = nullptr; +} + +static void +set_log_dest( + QPDFLogger* l, + std::function)> method, + qpdf_log_dest_e dest, + char const* identifier, + qpdf_log_fn_t fn, + void* udata) +{ + switch (dest) { + case qpdf_log_dest_default: + method(nullptr); + break; + case qpdf_log_dest_stdout: + method(l->standardOutput()); + break; + case qpdf_log_dest_stderr: + method(l->standardError()); + break; + case qpdf_log_dest_discard: + method(l->discard()); + break; + case qpdf_log_dest_custom: + method(std::make_shared(identifier, fn, udata)); + break; + } +} + +static void +set_log_dest( + QPDFLogger* l, + void (QPDFLogger::*method)(std::shared_ptr), + qpdf_log_dest_e dest, + char const* identifier, + qpdf_log_fn_t fn, + void* udata) +{ + set_log_dest( + l, + std::bind(std::mem_fn(method), l, std::placeholders::_1), + dest, + identifier, + fn, + udata); +} + +void +qpdflogger_set_info( + qpdflogger_handle l, qpdf_log_dest_e dest, qpdf_log_fn_t fn, void* udata) +{ + set_log_dest( + l->l.get(), &QPDFLogger::setInfo, dest, "info logger", fn, udata); +} + +void +qpdflogger_set_warn( + qpdflogger_handle l, qpdf_log_dest_e dest, qpdf_log_fn_t fn, void* udata) +{ + set_log_dest( + l->l.get(), &QPDFLogger::setWarn, dest, "warn logger", fn, udata); +} + +void +qpdflogger_set_error( + qpdflogger_handle l, qpdf_log_dest_e dest, qpdf_log_fn_t fn, void* udata) +{ + set_log_dest( + l->l.get(), &QPDFLogger::setError, dest, "error logger", fn, udata); +} + +void +qpdflogger_set_save( + qpdflogger_handle l, + qpdf_log_dest_e dest, + qpdf_log_fn_t fn, + void* udata, + int only_if_not_set) +{ + auto method = std::bind( + std::mem_fn(&QPDFLogger::setSave), + l->l.get(), + std::placeholders::_1, + only_if_not_set); + set_log_dest(l->l.get(), method, dest, "save logger", fn, udata); +} + +void +qpdflogger_save_to_standard_output(qpdflogger_handle l, int only_if_not_set) +{ + qpdflogger_set_save( + l, qpdf_log_dest_stdout, nullptr, nullptr, only_if_not_set); +} diff --git a/libtests/CMakeLists.txt b/libtests/CMakeLists.txt index 196cd3eb..62ba390e 100644 --- a/libtests/CMakeLists.txt +++ b/libtests/CMakeLists.txt @@ -33,10 +33,18 @@ set(TEST_PROGRAMS runlength sha2 sparse_array) +set(TEST_C_PROGRAMS + logger_c) + foreach(PROG ${TEST_PROGRAMS}) add_executable(${PROG} ${PROG}.cc) target_link_libraries(${PROG} libqpdf_object) endforeach() +foreach(PROG ${TEST_C_PROGRAMS}) + add_executable(${PROG} ${PROG}.c) + target_link_libraries(${PROG} libqpdf_object) + set_property(TARGET ${PROG} PROPERTY LINKER_LANGUAGE CXX) +endforeach() # Since libtests link with the object library and don't use the DLL, # we don't need to (and shouldn't) add the libqpdf target directory to diff --git a/libtests/logger.cc b/libtests/logger.cc index f10319f6..b968422e 100644 --- a/libtests/logger.cc +++ b/libtests/logger.cc @@ -22,7 +22,7 @@ test1() *(logger->getInfo()) << "getSave exception: " << e.what() << "\n"; } try { - logger->saveToStandardOutput(); + logger->saveToStandardOutput(true); assert(false); } catch (std::logic_error& e) { *(logger->getInfo()) @@ -40,12 +40,12 @@ test2() // First call saveToStandardOutput. Then use info, which then to // go stderr. QPDFLogger l; - l.saveToStandardOutput(); + l.saveToStandardOutput(true); l.info(std::string("info to stderr\n")); *(l.getSave()) << "save to stdout\n"; l.setInfo(nullptr); l.info("info still to stderr\n"); - l.setSave(nullptr); + l.setSave(nullptr, false); l.setInfo(nullptr); l.info("info back to stdout\n"); } diff --git a/libtests/logger_c.c b/libtests/logger_c.c new file mode 100644 index 00000000..94815601 --- /dev/null +++ b/libtests/logger_c.c @@ -0,0 +1,108 @@ +#include + +#include + +#include +#include + +#include +#include + +static void +fn(char const* data, size_t len, void* udata) +{ + FILE* f = (FILE*)udata; + fwrite(data, 1, len, f); +} + +static void +do_run(char const* json, int exp_status) +{ + int status = qpdfjob_run_from_json(json); + assert(status == exp_status); +} + +static FILE* +do_fopen(char const* filename) +{ + FILE* f = NULL; +#ifdef _MSC_VER + if (fopen_s(&f, filename, "wb") != 0) { + f = NULL; + } +#else + f = fopen(filename, "wb"); +#endif + if (f == NULL) { + fprintf(stderr, "unable to open %s\n", filename); + exit(2); + } + return f; +} + +int +main() +{ + FILE* info = do_fopen("info"); + FILE* warn = do_fopen("warn"); + FILE* error = do_fopen("error"); + FILE* save = do_fopen("save"); + FILE* save2 = do_fopen("save2"); + qpdflogger_handle l = qpdflogger_default_logger(); + + qpdflogger_set_info(l, qpdf_log_dest_custom, fn, (void*)info); + qpdflogger_set_warn(l, qpdf_log_dest_custom, fn, (void*)warn); + qpdflogger_set_error(l, qpdf_log_dest_custom, fn, (void*)error); + qpdflogger_set_save(l, qpdf_log_dest_custom, fn, (void*)save, 0); + + do_run( + "{\"inputFile\": \"normal.pdf\", \"showNpages\": \"\"}", + qpdf_exit_success); + do_run( + "{\"inputFile\": \"warning.pdf\", \"showNpages\": \"\"}", + qpdf_exit_warning); + do_run( + "{\"inputFile\": \"missing.pdf\", \"showNpages\": \"\"}", + qpdf_exit_error); + do_run( + "{\"inputFile\": \"normal.pdf\"," + " \"staticId\": \"\"," + " \"outputFile\": \"-\"}", + qpdf_exit_success); + + fclose(info); + fclose(warn); + fclose(error); + fclose(save); + + qpdflogger_set_info(l, qpdf_log_dest_stderr, NULL, NULL); + qpdflogger_set_warn(l, qpdf_log_dest_stdout, NULL, NULL); + qpdflogger_set_error(l, qpdf_log_dest_default, NULL, NULL); + qpdflogger_set_save(l, qpdf_log_dest_custom, fn, (void*)save2, 0); + + do_run( + "{\"inputFile\": \"2pages.pdf\", \"showNpages\": \"\"}", + qpdf_exit_success); + do_run( + "{\"inputFile\": \"warning.pdf\", \"showNpages\": \"\"}", + qpdf_exit_warning); + do_run( + + "{\"inputFile\": \"missing.pdf\", \"showNpages\": \"\"}", + qpdf_exit_error); + do_run( + "{\"inputFile\": \"attach.pdf\"," + " \"showAttachment\": \"a\"}", + qpdf_exit_success); + + /* This won't change save since it's already set */ + qpdflogger_save_to_standard_output(l, 1); + do_run( + "{\"inputFile\": \"attach.pdf\"," + " \"showAttachment\": \"a\"}", + qpdf_exit_success); + + qpdflogger_cleanup(&l); + + return 0; +} diff --git a/libtests/qtest/logger.test b/libtests/qtest/logger.test index 86767eff..786af4fb 100644 --- a/libtests/qtest/logger.test +++ b/libtests/qtest/logger.test @@ -24,10 +24,22 @@ $td->runtest("check stderr", {$td->FILE => "exp-stderr"}, $td->NORMALIZE_NEWLINES); +$td->runtest("logger C API", + {$td->COMMAND => "logger_c >stdout 2>stderr"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +foreach my $f (qw(stdout stderr info warn error save save2)) +{ + $td->runtest("check $f (C)", + {$td->FILE => "$f"}, + {$td->FILE => "c-exp-$f"}, + $td->NORMALIZE_NEWLINES); +} + cleanup(); -$td->report(3); +$td->report(11); sub cleanup { - unlink "stdout", "stderr"; + unlink "stdout", "stderr", "info", "warn", "error", "save", "save2"; } diff --git a/libtests/qtest/logger/2pages.pdf b/libtests/qtest/logger/2pages.pdf new file mode 100644 index 00000000..f3e1700d Binary files /dev/null and b/libtests/qtest/logger/2pages.pdf differ diff --git a/libtests/qtest/logger/attach.pdf b/libtests/qtest/logger/attach.pdf new file mode 100644 index 00000000..bbc09588 Binary files /dev/null and b/libtests/qtest/logger/attach.pdf differ diff --git a/libtests/qtest/logger/c-exp-error b/libtests/qtest/logger/c-exp-error new file mode 100644 index 00000000..8e33a38a --- /dev/null +++ b/libtests/qtest/logger/c-exp-error @@ -0,0 +1 @@ +qpdfjob json: open missing.pdf: No such file or directory diff --git a/libtests/qtest/logger/c-exp-info b/libtests/qtest/logger/c-exp-info new file mode 100644 index 00000000..6ed281c7 --- /dev/null +++ b/libtests/qtest/logger/c-exp-info @@ -0,0 +1,2 @@ +1 +1 diff --git a/libtests/qtest/logger/c-exp-save b/libtests/qtest/logger/c-exp-save new file mode 100644 index 00000000..b8c692ed Binary files /dev/null and b/libtests/qtest/logger/c-exp-save differ diff --git a/libtests/qtest/logger/c-exp-save2 b/libtests/qtest/logger/c-exp-save2 new file mode 100644 index 00000000..3d1fe696 --- /dev/null +++ b/libtests/qtest/logger/c-exp-save2 @@ -0,0 +1,2 @@ +quack +quack diff --git a/libtests/qtest/logger/c-exp-stderr b/libtests/qtest/logger/c-exp-stderr new file mode 100644 index 00000000..bfbb2cff --- /dev/null +++ b/libtests/qtest/logger/c-exp-stderr @@ -0,0 +1,3 @@ +2 +1 +qpdfjob json: open missing.pdf: No such file or directory diff --git a/libtests/qtest/logger/c-exp-stdout b/libtests/qtest/logger/c-exp-stdout new file mode 100644 index 00000000..e7566de7 --- /dev/null +++ b/libtests/qtest/logger/c-exp-stdout @@ -0,0 +1,4 @@ +WARNING: warning.pdf: file is damaged +WARNING: warning.pdf (offset 1556): xref not found +WARNING: warning.pdf: Attempting to reconstruct cross-reference table +qpdfjob json: operation succeeded with warnings diff --git a/libtests/qtest/logger/c-exp-warn b/libtests/qtest/logger/c-exp-warn new file mode 100644 index 00000000..e7566de7 --- /dev/null +++ b/libtests/qtest/logger/c-exp-warn @@ -0,0 +1,4 @@ +WARNING: warning.pdf: file is damaged +WARNING: warning.pdf (offset 1556): xref not found +WARNING: warning.pdf: Attempting to reconstruct cross-reference table +qpdfjob json: operation succeeded with warnings diff --git a/libtests/qtest/logger/normal.pdf b/libtests/qtest/logger/normal.pdf new file mode 100644 index 00000000..a7e01f91 --- /dev/null +++ b/libtests/qtest/logger/normal.pdf @@ -0,0 +1,79 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R +>> +startxref +556 +%%EOF diff --git a/libtests/qtest/logger/warning.pdf b/libtests/qtest/logger/warning.pdf new file mode 100644 index 00000000..e8a7042c --- /dev/null +++ b/libtests/qtest/logger/warning.pdf @@ -0,0 +1,79 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R +>> +startxref +1556 +%%EOF diff --git a/manual/release-notes.rst b/manual/release-notes.rst index f7d673e7..f0e54db9 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -158,6 +158,8 @@ For a detailed list of changes, please see the file output and errors that slipped through the cracks with ``setOutputStreams``. + - A C API is available in :file:`include/qpdf/qpdflogger-c.h`. + - New methods ``insertItemAndGet``, ``appendItemAndGet``, ``eraseItemAndGet``, ``replaceKeyAndGet``, and ``removeKeyAndGet`` return the newly added or removed object.