// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors #include "Roundtrip.h" #include #include #include #include #include #include #include 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 { q.processInputFile(input.string()); } 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.getArrayAsArray(); 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); w.setOutputFile(output.string()); // 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.setQDF(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