125 lines
4.3 KiB
C++
125 lines
4.3 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
|
|
|
#include "Roundtrip.h"
|
|
|
|
#include <qpdf/QPDF.hh>
|
|
#include <qpdf/QPDFPageDocumentHelper.hh>
|
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
#include <qpdf/QPDFWriter.hh>
|
|
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
namespace freepdfeditor::spike::a {
|
|
|
|
namespace {
|
|
|
|
// Read a whole file into a string. QPDF can read from disk directly, but for
|
|
// the round-trip we want to control the bytes that go in (and out) so the
|
|
// byte-equality check is against the original input, not a re-read.
|
|
bool read_file(const std::filesystem::path& p, std::string& out)
|
|
{
|
|
std::ifstream in(p, std::ios::binary);
|
|
if (!in) return false;
|
|
std::ostringstream ss;
|
|
ss << in.rdbuf();
|
|
out = ss.str();
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FileResult roundtrip(const std::filesystem::path& input,
|
|
const std::filesystem::path& output)
|
|
{
|
|
FileResult r{Outcome::Failed, 0, {}};
|
|
|
|
std::string original;
|
|
if (!read_file(input, original)) {
|
|
r.error = "could not read input";
|
|
return r;
|
|
}
|
|
|
|
QPDF q;
|
|
try {
|
|
// Keep the input filename alive across processFile: QPDF opens the
|
|
// file lazily and may dereference the pointer later.
|
|
const std::string input_str = input.string();
|
|
q.processFile(input_str.c_str());
|
|
} catch (const std::exception& e) {
|
|
r.error = std::string("QPDF open failed: ") + e.what();
|
|
return r;
|
|
}
|
|
|
|
// Exercise the content-stream parse path: walk every page and force-decode
|
|
// its content stream(s). This is what proves "parse content streams" rather
|
|
// than just "QPDF rewrites the file". A page whose content we can't decode
|
|
// is reported as a structural success anyway — the gate is byte-identity,
|
|
// and QPDF preserves even streams it can't decode by copying them verbatim.
|
|
try {
|
|
QPDFPageDocumentHelper helper(q);
|
|
auto pages = helper.getAllPages();
|
|
r.pages = pages.size();
|
|
for (auto& page : pages) {
|
|
QPDFObjectHandle content = page.getAttribute("/Contents", true);
|
|
if (content.isArray()) {
|
|
auto items = content.getArrayAsVector();
|
|
for (auto& item : items) {
|
|
if (item.isStream()) {
|
|
(void)item.getStreamData(); // force decode
|
|
}
|
|
}
|
|
} else if (content.isStream()) {
|
|
(void)content.getStreamData();
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
// A page-walk failure is a structural defect worth surfacing, but the
|
|
// round-trip gate is still meaningful — record it in the error field
|
|
// without failing the whole round-trip.
|
|
r.error = std::string("page walk warning: ") + e.what();
|
|
}
|
|
|
|
try {
|
|
QPDFWriter w(q);
|
|
// Keep the output filename string alive across the write() call:
|
|
// QPDFWriter retains the const char* and dereferences it during
|
|
// write(), so a temporary from output.string().c_str() would be a
|
|
// use-after-free (caught by ASan during the M0 spike run).
|
|
const std::string output_str = output.string();
|
|
w.setOutputFilename(output_str.c_str());
|
|
// Preserve as much as possible: write with the same object stream
|
|
// strategy and linearisation state as the input so byte-identity is a
|
|
// realistic target. The defaults preserve the structure verbatim where
|
|
// QPDF can.
|
|
w.setPreserveEncryption(true);
|
|
w.setQDFMode(false);
|
|
w.setLinearization(false);
|
|
w.write();
|
|
} catch (const std::exception& e) {
|
|
r.error = std::string("QPDF write failed: ") + e.what();
|
|
r.outcome = Outcome::Failed;
|
|
return r;
|
|
}
|
|
|
|
// Compare bytes.
|
|
std::string written;
|
|
if (!read_file(output, written)) {
|
|
r.error = "could not read back output";
|
|
r.outcome = Outcome::Failed;
|
|
return r;
|
|
}
|
|
|
|
if (original.size() == written.size() &&
|
|
std::memcmp(original.data(), written.data(), original.size()) == 0) {
|
|
r.outcome = Outcome::ByteIdentical;
|
|
r.error.clear();
|
|
} else {
|
|
r.outcome = Outcome::StructurallySame;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
} // namespace freepdfeditor::spike::a
|