101 lines
5.4 KiB
C++
101 lines
5.4 KiB
C++
// 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;
|
|
}
|