// Evidence exporter using the same sandbox, font broker and tile IPC as the GUI. // This executable deliberately does not link PDFium. #include "broker/font_broker.h" #include "common/worker_process.h" #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char **argv) { QCoreApplication app(argc, argv); if (app.arguments().size() != 3 && app.arguments().size() != 4) return 2; bool validScale = true; const double scale = app.arguments().size() == 4 ? app.arguments()[3].toDouble(&validScale) : 2.0; if (!validScale || !std::isfinite(scale) || scale < .25 || scale > 4) return 2; docview::FontBroker broker; docview::WorkerProcess worker("pdf-render-evidence", 1); QVariantList fontSelections; qint64 transferred = 0; worker.setFontRequestHandler([&](const QString &op, const QCborMap &payload, std::function done) { broker.request(op, payload, [&, op, payload, done = std::move(done)](QCborMap result) { if (op == "font.map") { auto item = result.toVariantMap(); item.remove("fontId"); item.remove("sha256"); item["requestedFace"] = QString::fromLocal8Bit(payload.value("faceLocal").toByteArray()); item["requestedWeight"] = payload.value("weight").toInteger(); item["requestedItalic"] = payload.value("italic").toBool(); item["actualFace"] = QString::fromLocal8Bit(result.value("actualFaceLocal").toByteArray()); item.remove("actualFaceLocal"); fontSelections.append(item); } else if (op == "font.read") transferred += result.value("data").toByteArray().size(); done(std::move(result)); }); }); bool ready = false, failed = false; QString failure; QEventLoop bootstrap; QObject::connect(&worker, &docview::WorkerProcess::ready, &bootstrap, [&] { ready = true; bootstrap.quit(); }); QObject::connect(&worker, &docview::WorkerProcess::failed, &bootstrap, [&](QString code, QString message) { failed = true; failure = code + ": " + message; bootstrap.quit(); }); QTimer::singleShot(10000, &bootstrap, &QEventLoop::quit); if (!worker.start(QCoreApplication::applicationDirPath() + "/docview-pdf-worker", {"--source", app.arguments()[1]})) return 3; bootstrap.exec(); if (!ready || failed) { QTextStream(stderr) << failure; return 3; } auto call = [&](const QString &op, const QCborMap &payload = QCborMap{}) { QEventLoop wait; QCborMap response; bool complete = false; QTimer::singleShot(125000, &wait, &QEventLoop::quit); worker.request(op, payload, [&](const QCborMap &reply) { response = reply; complete = true; wait.quit(); }); if (!complete) wait.exec(); if (!complete || failed || response.contains(QStringLiteral("error"))) { QTextStream(stderr) << op << ": " << failure << " " << QJsonDocument::fromVariant(response.toVariantMap()).toJson(); if (!complete) worker.stop(); // Do not retain a callback to this stack frame. return QCborMap{}; } return response.value("result").toMap(); }; const auto metadata = call("open"); const auto pages = metadata.value("pages").toArray(); if (pages.isEmpty() || pages.size() != metadata.value("pageCount").toInteger()) return 4; if (!QDir().mkpath(app.arguments()[2])) return 5; for (int page = 0; page < pages.size(); ++page) { const auto info = pages[page].toMap(); const int width = int(std::ceil(info.value("width").toDouble() * scale)); const int height = int(std::ceil(info.value("height").toDouble() * scale)); if (width <= 0 || height <= 0 || width > 8192 || height > 8192) return 6; QImage image(width, height, QImage::Format_ARGB32); image.fill(Qt::white); QPainter painter(&image); for (int y = 0; y < height; y += 512) for (int x = 0; x < width; x += 512) { const int tw = std::min(512, width - x), th = std::min(512, height - y); const auto tile = call("render", {{"page", page}, {"scale", scale}, {"rotation", 0}, {"x", x}, {"y", y}, {"width", tw}, {"height", th}, {"renderRevision", 1}}); const auto bytes = tile.value("data").toByteArray(); if (bytes.size() != tw * th * 4) return 7; const QImage source(reinterpret_cast(bytes.constData()), tw, th, tw * 4, QImage::Format_ARGB32); painter.drawImage(x, y, source); } painter.end(); if (!image.save(app.arguments()[2] + QString("/page-%1.png").arg(page + 1))) return 8; } auto report = metadata.toVariantMap(); report["japaneseSearch"] = call("search", {{"query", QString::fromUtf8("日本語")}, {"page", 0}, {"limit", 100}}).toVariantMap(); report["fontSelections"] = fontSelections; report["fontBytesTransferred"] = transferred; report["renderScale"] = scale; report["transport"] = "production WorkerProcess and FontBroker; sandbox enabled"; QFile output(app.arguments()[2] + "/metadata.json"); if (!output.open(QIODevice::WriteOnly)) return 9; output.write(QJsonDocument::fromVariant(report).toJson()); worker.stop(); broker.revoke(); return failed ? 10 : 0; }