initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user