initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
// 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 <QCoreApplication>
#include <QCborArray>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QImage>
#include <QJsonDocument>
#include <QPainter>
#include <QTextStream>
#include <QTimer>
#include <cmath>
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<void(QCborMap)> 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<const uchar *>(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;
}