aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2022-06-18 19:38:36 +0200
committerJay Berkenbilt <ejb@ql.org>2022-06-19 14:46:58 +0200
commit8130d50e3b5aa0235a133c3c5a3018ac01afb5e1 (patch)
tree461c961df0299707b628f4fa6a41539fafa2d193
parentdaef4e8fb856e84e2a9151cd7715a941a0ae9c6c (diff)
downloadqpdf-8130d50e3b5aa0235a133c3c5a3018ac01afb5e1.tar.zst
Add C API to QPDFLogger
-rw-r--r--ChangeLog6
-rw-r--r--include/qpdf/QPDFLogger.hh4
-rw-r--r--include/qpdf/qpdflogger-c.h100
-rw-r--r--libqpdf/CMakeLists.txt3
-rw-r--r--libqpdf/QPDFJob.cc8
-rw-r--r--libqpdf/QPDFLogger.cc9
-rw-r--r--libqpdf/qpdflogger-c.cc163
-rw-r--r--libtests/CMakeLists.txt8
-rw-r--r--libtests/logger.cc6
-rw-r--r--libtests/logger_c.c108
-rw-r--r--libtests/qtest/logger.test16
-rw-r--r--libtests/qtest/logger/2pages.pdfbin0 -> 1287 bytes
-rw-r--r--libtests/qtest/logger/attach.pdfbin0 -> 1290 bytes
-rw-r--r--libtests/qtest/logger/c-exp-error1
-rw-r--r--libtests/qtest/logger/c-exp-info2
-rw-r--r--libtests/qtest/logger/c-exp-savebin0 -> 799 bytes
-rw-r--r--libtests/qtest/logger/c-exp-save22
-rw-r--r--libtests/qtest/logger/c-exp-stderr3
-rw-r--r--libtests/qtest/logger/c-exp-stdout4
-rw-r--r--libtests/qtest/logger/c-exp-warn4
-rw-r--r--libtests/qtest/logger/normal.pdf79
-rw-r--r--libtests/qtest/logger/warning.pdf79
-rw-r--r--manual/release-notes.rst2
23 files changed, 592 insertions, 15 deletions
diff --git a/ChangeLog b/ChangeLog
index 97e8506c..abf10459 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
2022-06-18 Jay Berkenbilt <ejb@ql.org>
+ * 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<Pipeline>);
// See notes above about the save pipeline
QPDF_DLL
- void setSave(std::shared_ptr<Pipeline>);
+ void setSave(std::shared_ptr<Pipeline>, 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 <qpdf/DLL.h>
+#include <stddef.h>
+
+#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<Pipeline> p)
}
void
-QPDFLogger::setSave(std::shared_ptr<Pipeline> p)
+QPDFLogger::setSave(std::shared_ptr<Pipeline> 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<Pipeline> 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 <qpdf/qpdflogger-c.h>
+
+#include <qpdf/Pipeline.hh>
+#include <qpdf/QIntC.hh>
+#include <qpdf/QPDFLogger.hh>
+#include <functional>
+#include <memory>
+
+struct _qpdflogger_handle
+{
+ _qpdflogger_handle(std::shared_ptr<QPDFLogger> l);
+ ~_qpdflogger_handle() = default;
+
+ std::shared_ptr<QPDFLogger> 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<char const*>(buf), QIntC::to_ulong(len), udata);
+}
+
+void
+FunctionPipeline::finish()
+{
+ // Nothing needed
+}
+
+_qpdflogger_handle::_qpdflogger_handle(std::shared_ptr<QPDFLogger> 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<void(std::shared_ptr<Pipeline>)> 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<FunctionPipeline>(identifier, fn, udata));
+ break;
+ }
+}
+
+static void
+set_log_dest(
+ QPDFLogger* l,
+ void (QPDFLogger::*method)(std::shared_ptr<Pipeline>),
+ 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 <qpdf/assert_test.h>
+
+#include <qpdf/qpdflogger-c.h>
+
+#include <qpdf/Constants.h>
+#include <qpdf/qpdfjob-c.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+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
--- /dev/null
+++ b/libtests/qtest/logger/2pages.pdf
Binary files differ
diff --git a/libtests/qtest/logger/attach.pdf b/libtests/qtest/logger/attach.pdf
new file mode 100644
index 00000000..bbc09588
--- /dev/null
+++ b/libtests/qtest/logger/attach.pdf
Binary files 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
--- /dev/null
+++ b/libtests/qtest/logger/c-exp-save
Binary files 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.