freepdfeditor/spike/A_verbatim_roundtrip/Roundtrip.cpp

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