310 lines
11 KiB
C++
310 lines
11 KiB
C++
#include "content_builder.hpp"
|
|
#include "../core/image_decoder.hpp"
|
|
#include <iostream>
|
|
|
|
namespace pdfengine {
|
|
|
|
ContentBuilder::ContentBuilder(ResourceResolver* resolver) : resolver_(resolver) {}
|
|
|
|
std::vector<std::unique_ptr<ContentObject>> ContentBuilder::build(const std::vector<Operation>& operations) {
|
|
std::vector<std::unique_ptr<ContentObject>> objects;
|
|
|
|
for (const auto& op : operations) {
|
|
processOperation(op, objects);
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
void ContentBuilder::processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects) {
|
|
if (op.op == "q") {
|
|
stateStack_.push_back(state_);
|
|
} else if (op.op == "Q") {
|
|
if (!stateStack_.empty()) {
|
|
state_ = stateStack_.back();
|
|
stateStack_.pop_back();
|
|
}
|
|
} else if (op.op == "cm") {
|
|
handleCm(op);
|
|
} else if (op.op == "BT") {
|
|
state_.tm[0] = 1.0; state_.tm[1] = 0.0; state_.tm[2] = 0.0;
|
|
state_.tm[3] = 1.0; state_.tm[4] = 0.0; state_.tm[5] = 0.0;
|
|
} else if (op.op == "Tf") {
|
|
handleTf(op);
|
|
} else if (op.op == "Td" || op.op == "TD" || op.op == "Tm") {
|
|
handleTd(op);
|
|
} else if (op.op == "Tj" || op.op == "'") { // ' is equivalent to T* Tj
|
|
handleTj(op, outObjects);
|
|
} else if (op.op == "TJ") {
|
|
handleTJ_Array(op, outObjects);
|
|
} else if (op.op == "Do") {
|
|
handleDo(op, outObjects);
|
|
} else if (op.op == "m" || op.op == "l" || op.op == "c" || op.op == "re" || op.op == "h") {
|
|
handlePathConstruction(op);
|
|
} else if (op.op == "S") {
|
|
handlePathPaint(PathPaintOp::Stroke, outObjects);
|
|
} else if (op.op == "s") {
|
|
handlePathPaint(PathPaintOp::Stroke, outObjects, true);
|
|
} else if (op.op == "f" || op.op == "F" || op.op == "f*") {
|
|
handlePathPaint(PathPaintOp::Fill, outObjects);
|
|
} else if (op.op == "B" || op.op == "B*") {
|
|
handlePathPaint(PathPaintOp::FillStroke, outObjects);
|
|
} else if (op.op == "b" || op.op == "b*") {
|
|
handlePathPaint(PathPaintOp::FillStroke, outObjects, true);
|
|
} else if (op.op == "n") {
|
|
currentPath_.clear();
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleTf(const Operation& op) {
|
|
if (op.operands.size() >= 2) {
|
|
auto it = op.operands.end();
|
|
auto numNode = *(--it);
|
|
auto nameNode = *(--it);
|
|
if (nameNode->type == AstNodeType::Name) {
|
|
state_.fontName = nameNode->stringValue;
|
|
}
|
|
if (numNode->type == AstNodeType::Number) {
|
|
state_.fontSize = numNode->numberValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleTd(const Operation& op) {
|
|
if (op.op == "Tm" && op.operands.size() >= 6) {
|
|
// Tm takes 6 operands: a b c d e f
|
|
auto it = op.operands.end();
|
|
auto fNode = *(--it);
|
|
auto eNode = *(--it);
|
|
auto dNode = *(--it);
|
|
auto cNode = *(--it);
|
|
auto bNode = *(--it);
|
|
auto aNode = *(--it);
|
|
if (aNode->type == AstNodeType::Number) state_.tm[0] = aNode->numberValue;
|
|
if (bNode->type == AstNodeType::Number) state_.tm[1] = bNode->numberValue;
|
|
if (cNode->type == AstNodeType::Number) state_.tm[2] = cNode->numberValue;
|
|
if (dNode->type == AstNodeType::Number) state_.tm[3] = dNode->numberValue;
|
|
if (eNode->type == AstNodeType::Number) state_.tm[4] = eNode->numberValue;
|
|
if (fNode->type == AstNodeType::Number) state_.tm[5] = fNode->numberValue;
|
|
} else if ((op.op == "Td" || op.op == "TD") && op.operands.size() >= 2) {
|
|
// Td simply offsets e and f in the matrix
|
|
auto it = op.operands.end();
|
|
auto yNode = *(--it);
|
|
auto xNode = *(--it);
|
|
if (xNode->type == AstNodeType::Number) state_.tm[4] += xNode->numberValue;
|
|
if (yNode->type == AstNodeType::Number) state_.tm[5] += yNode->numberValue;
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleTj(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects) {
|
|
if (op.operands.empty()) return;
|
|
|
|
auto strNode = op.operands.back();
|
|
if (strNode->type == AstNodeType::String || strNode->type == AstNodeType::HexString) {
|
|
auto textObj = std::make_unique<TextObject>();
|
|
if (strNode->type == AstNodeType::String) {
|
|
textObj->text = strNode->stringValue;
|
|
} else {
|
|
textObj->text = std::string(strNode->bytesValue.begin(), strNode->bytesValue.end());
|
|
}
|
|
|
|
textObj->fontName = state_.fontName;
|
|
textObj->fontSize = state_.fontSize;
|
|
for (int i=0; i<6; ++i) textObj->tm[i] = state_.tm[i];
|
|
|
|
outObjects.push_back(std::move(textObj));
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleTJ_Array(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects) {
|
|
if (op.operands.empty()) return;
|
|
|
|
auto arrNode = op.operands.back();
|
|
if (arrNode->type != AstNodeType::Array) return;
|
|
|
|
std::string combinedText;
|
|
|
|
const auto& arr = arrNode->arrayItems;
|
|
for (const auto& item : arr) {
|
|
if (item->type == AstNodeType::String) {
|
|
combinedText += item->stringValue;
|
|
} else if (item->type == AstNodeType::HexString) {
|
|
combinedText += std::string(item->bytesValue.begin(), item->bytesValue.end());
|
|
} else if (item->type == AstNodeType::Number) {
|
|
if (item->numberValue < -500.0) {
|
|
combinedText += " ";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!combinedText.empty()) {
|
|
auto textObj = std::make_unique<TextObject>();
|
|
textObj->text = combinedText;
|
|
textObj->fontName = state_.fontName;
|
|
textObj->fontSize = state_.fontSize;
|
|
for (int i=0; i<6; ++i) textObj->tm[i] = state_.tm[i];
|
|
|
|
outObjects.push_back(std::move(textObj));
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleCm(const Operation& op) {
|
|
if (op.operands.size() >= 6) {
|
|
auto it = op.operands.end();
|
|
auto fNode = *(--it);
|
|
auto eNode = *(--it);
|
|
auto dNode = *(--it);
|
|
auto cNode = *(--it);
|
|
auto bNode = *(--it);
|
|
auto aNode = *(--it);
|
|
Matrix m;
|
|
if (aNode->type == AstNodeType::Number) m.a = static_cast<float>(aNode->numberValue);
|
|
if (bNode->type == AstNodeType::Number) m.b = static_cast<float>(bNode->numberValue);
|
|
if (cNode->type == AstNodeType::Number) m.c = static_cast<float>(cNode->numberValue);
|
|
if (dNode->type == AstNodeType::Number) m.d = static_cast<float>(dNode->numberValue);
|
|
if (eNode->type == AstNodeType::Number) m.e = static_cast<float>(eNode->numberValue);
|
|
if (fNode->type == AstNodeType::Number) m.f = static_cast<float>(fNode->numberValue);
|
|
state_.ctm = state_.ctm.multiply(m);
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handleDo(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects) {
|
|
if (!resolver_ || op.operands.empty()) return;
|
|
|
|
auto nameNode = op.operands.back();
|
|
if (nameNode->type != AstNodeType::Name) return;
|
|
|
|
std::string name = nameNode->stringValue;
|
|
ResolvedXObject resolved = resolver_->resolveXObject(name);
|
|
|
|
if (resolved.type == XObjectType::Image) {
|
|
QPDFObjectHandle streamDict = resolved.object.getDict();
|
|
int width = streamDict.hasKey("/Width") ? static_cast<int>(streamDict.getKey("/Width").getNumericValue()) : 0;
|
|
int height = streamDict.hasKey("/Height") ? static_cast<int>(streamDict.getKey("/Height").getNumericValue()) : 0;
|
|
int bpc = streamDict.hasKey("/BitsPerComponent") ? static_cast<int>(streamDict.getKey("/BitsPerComponent").getNumericValue()) : 8;
|
|
|
|
std::string colorSpace;
|
|
if (streamDict.hasKey("/ColorSpace")) {
|
|
auto cs = streamDict.getKey("/ColorSpace");
|
|
if (cs.isName()) {
|
|
colorSpace = cs.getName();
|
|
} else if (cs.isArray() && cs.getArrayItem(0).isName()) {
|
|
colorSpace = cs.getArrayItem(0).getName();
|
|
}
|
|
}
|
|
|
|
std::string filter;
|
|
if (streamDict.hasKey("/Filter")) {
|
|
auto f = streamDict.getKey("/Filter");
|
|
if (f.isName()) {
|
|
filter = f.getName();
|
|
} else if (f.isArray() && f.getArrayItem(0).isName()) {
|
|
filter = f.getArrayItem(0).getName();
|
|
}
|
|
}
|
|
|
|
auto imageObj = std::make_unique<ImageObject>();
|
|
imageObj->name = name;
|
|
imageObj->width = width;
|
|
imageObj->height = height;
|
|
imageObj->bitsPerComponent = bpc;
|
|
imageObj->colorSpace = colorSpace;
|
|
imageObj->filter = filter;
|
|
imageObj->hasSoftMask = streamDict.hasKey("/SMask");
|
|
imageObj->transform = state_.ctm;
|
|
|
|
imageObj->pixelData = ImageDecoder::decode(resolved.object, colorSpace, width, height, bpc, filter);
|
|
|
|
outObjects.push_back(std::move(imageObj));
|
|
} else if (resolved.type == XObjectType::Form) {
|
|
std::cerr << "Form XObject not fully supported yet.\n";
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handlePathConstruction(const Operation& op) {
|
|
auto numberOperand = [&op](size_t index, float& value) {
|
|
if (index >= op.operands.size() || op.operands[index]->type != AstNodeType::Number) {
|
|
return false;
|
|
}
|
|
value = static_cast<float>(op.operands[index]->numberValue);
|
|
return true;
|
|
};
|
|
|
|
if (op.op == "h") {
|
|
if (!currentPath_.empty()) {
|
|
currentPath_.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (op.op == "m" || op.op == "l") {
|
|
if (op.operands.size() < 2) return;
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
if (!numberOperand(op.operands.size() - 2, x) || !numberOperand(op.operands.size() - 1, y)) {
|
|
return;
|
|
}
|
|
if (op.op == "m") {
|
|
currentPath_.moveTo(x, y);
|
|
} else {
|
|
currentPath_.lineTo(x, y);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (op.op == "c") {
|
|
if (op.operands.size() < 6) return;
|
|
float x1 = 0.0f;
|
|
float y1 = 0.0f;
|
|
float x2 = 0.0f;
|
|
float y2 = 0.0f;
|
|
float x3 = 0.0f;
|
|
float y3 = 0.0f;
|
|
const size_t start = op.operands.size() - 6;
|
|
if (!numberOperand(start + 0, x1) || !numberOperand(start + 1, y1) ||
|
|
!numberOperand(start + 2, x2) || !numberOperand(start + 3, y2) ||
|
|
!numberOperand(start + 4, x3) || !numberOperand(start + 5, y3)) {
|
|
return;
|
|
}
|
|
currentPath_.cubicTo(x1, y1, x2, y2, x3, y3);
|
|
return;
|
|
}
|
|
|
|
if (op.op == "re") {
|
|
if (op.operands.size() < 4) return;
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float width = 0.0f;
|
|
float height = 0.0f;
|
|
const size_t start = op.operands.size() - 4;
|
|
if (!numberOperand(start + 0, x) || !numberOperand(start + 1, y) ||
|
|
!numberOperand(start + 2, width) || !numberOperand(start + 3, height)) {
|
|
return;
|
|
}
|
|
currentPath_.addRect(x, y, width, height);
|
|
}
|
|
}
|
|
|
|
void ContentBuilder::handlePathPaint(PathPaintOp paintOp,
|
|
std::vector<std::unique_ptr<ContentObject>>& outObjects,
|
|
bool closePath) {
|
|
if (closePath) {
|
|
if (!currentPath_.empty()) {
|
|
currentPath_.close();
|
|
}
|
|
}
|
|
if (currentPath_.empty()) {
|
|
return;
|
|
}
|
|
|
|
auto pathObj = std::make_unique<PathObject>();
|
|
pathObj->path = currentPath_;
|
|
pathObj->paintOp = paintOp;
|
|
pathObj->transform = state_.ctm;
|
|
outObjects.push_back(std::move(pathObj));
|
|
|
|
currentPath_.clear();
|
|
}
|
|
|
|
} // namespace pdfengine
|