fix the issue
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <pdfengine/graphics_state.hpp>
|
#include <pdfengine/graphics_state.hpp>
|
||||||
|
#include <pdfengine/path.hpp>
|
||||||
|
|
||||||
namespace pdfengine {
|
namespace pdfengine {
|
||||||
|
|
||||||
@@ -59,4 +60,19 @@ public:
|
|||||||
Matrix transform;
|
Matrix transform;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class PathPaintOp {
|
||||||
|
Stroke,
|
||||||
|
Fill,
|
||||||
|
FillStroke
|
||||||
|
};
|
||||||
|
|
||||||
|
class PathObject : public ContentObject {
|
||||||
|
public:
|
||||||
|
ContentObjectType getType() const override { return ContentObjectType::Path; }
|
||||||
|
|
||||||
|
Path path;
|
||||||
|
PathPaintOp paintOp = PathPaintOp::Stroke;
|
||||||
|
Matrix transform;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace pdfengine
|
} // namespace pdfengine
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ void ContentBuilder::processOperation(const Operation& op, std::vector<std::uniq
|
|||||||
handleTJ_Array(op, outObjects);
|
handleTJ_Array(op, outObjects);
|
||||||
} else if (op.op == "Do") {
|
} else if (op.op == "Do") {
|
||||||
handleDo(op, outObjects);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,4 +221,89 @@ void ContentBuilder::handleDo(const Operation& op, std::vector<std::unique_ptr<C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
} // namespace pdfengine
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ private:
|
|||||||
GraphicsState state_;
|
GraphicsState state_;
|
||||||
std::vector<GraphicsState> stateStack_;
|
std::vector<GraphicsState> stateStack_;
|
||||||
ResourceResolver* resolver_;
|
ResourceResolver* resolver_;
|
||||||
|
Path currentPath_;
|
||||||
|
|
||||||
void processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
void processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ private:
|
|||||||
void handleTJ_Array(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
void handleTJ_Array(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
||||||
void handleCm(const Operation& op);
|
void handleCm(const Operation& op);
|
||||||
void handleDo(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
void handleDo(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
|
||||||
|
void handlePathConstruction(const Operation& op);
|
||||||
|
void handlePathPaint(PathPaintOp paintOp, std::vector<std::unique_ptr<ContentObject>>& outObjects, bool closePath = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace pdfengine
|
} // namespace pdfengine
|
||||||
|
|||||||
@@ -14,6 +14,26 @@
|
|||||||
|
|
||||||
using namespace pdfengine;
|
using namespace pdfengine;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<ContentObject>> buildObjects(const std::string& content) {
|
||||||
|
Lexer lexer(content);
|
||||||
|
auto tokens = lexer.tokenize();
|
||||||
|
ContentParser parser(tokens);
|
||||||
|
ContentBuilder builder;
|
||||||
|
return builder.build(parser.parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
const PathObject* requirePathObject(const std::unique_ptr<ContentObject>& object) {
|
||||||
|
EXPECT_EQ(object->getType(), ContentObjectType::Path);
|
||||||
|
if (object->getType() != ContentObjectType::Path) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return static_cast<const PathObject*>(object.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TEST(ContentBuilderTest, SimpleTextState) {
|
TEST(ContentBuilderTest, SimpleTextState) {
|
||||||
Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj");
|
Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj");
|
||||||
auto tokens = lexer.tokenize();
|
auto tokens = lexer.tokenize();
|
||||||
@@ -73,6 +93,100 @@ TEST(ContentBuilderTest, RotatedTextMatrix) {
|
|||||||
EXPECT_DOUBLE_EQ(textObj->tm[5], 200.0);
|
EXPECT_DOUBLE_EQ(textObj->tm[5], 200.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ContentBuilderTest, StrokePathFromMoveAndLine) {
|
||||||
|
auto objects = buildObjects("10 20 m 30 40 l S");
|
||||||
|
ASSERT_EQ(objects.size(), 1);
|
||||||
|
|
||||||
|
const auto* pathObj = requirePathObject(objects[0]);
|
||||||
|
ASSERT_NE(pathObj, nullptr);
|
||||||
|
EXPECT_EQ(pathObj->paintOp, PathPaintOp::Stroke);
|
||||||
|
|
||||||
|
const auto& segments = pathObj->path.segments();
|
||||||
|
ASSERT_EQ(segments.size(), 2);
|
||||||
|
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[0].points[0].x, 10.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[0].points[0].y, 20.0f);
|
||||||
|
EXPECT_EQ(segments[1].verb, Path::Verb::LineTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].x, 30.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].y, 40.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ContentBuilderTest, FillRectanglePath) {
|
||||||
|
auto objects = buildObjects("5 6 7 8 re f");
|
||||||
|
ASSERT_EQ(objects.size(), 1);
|
||||||
|
|
||||||
|
const auto* pathObj = requirePathObject(objects[0]);
|
||||||
|
ASSERT_NE(pathObj, nullptr);
|
||||||
|
EXPECT_EQ(pathObj->paintOp, PathPaintOp::Fill);
|
||||||
|
|
||||||
|
const auto& segments = pathObj->path.segments();
|
||||||
|
ASSERT_EQ(segments.size(), 5);
|
||||||
|
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[0].points[0].x, 5.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[0].points[0].y, 6.0f);
|
||||||
|
EXPECT_EQ(segments[1].verb, Path::Verb::LineTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].x, 12.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].y, 6.0f);
|
||||||
|
EXPECT_EQ(segments[2].verb, Path::Verb::LineTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[2].points[0].x, 12.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[2].points[0].y, 14.0f);
|
||||||
|
EXPECT_EQ(segments[3].verb, Path::Verb::LineTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[3].points[0].x, 5.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[3].points[0].y, 14.0f);
|
||||||
|
EXPECT_EQ(segments[4].verb, Path::Verb::Close);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ContentBuilderTest, FillStrokeCubicPath) {
|
||||||
|
auto objects = buildObjects("1 2 m 3 4 5 6 7 8 c B");
|
||||||
|
ASSERT_EQ(objects.size(), 1);
|
||||||
|
|
||||||
|
const auto* pathObj = requirePathObject(objects[0]);
|
||||||
|
ASSERT_NE(pathObj, nullptr);
|
||||||
|
EXPECT_EQ(pathObj->paintOp, PathPaintOp::FillStroke);
|
||||||
|
|
||||||
|
const auto& segments = pathObj->path.segments();
|
||||||
|
ASSERT_EQ(segments.size(), 2);
|
||||||
|
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
|
||||||
|
EXPECT_EQ(segments[1].verb, Path::Verb::CubicBezierTo);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].x, 3.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[0].y, 4.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[1].x, 5.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[1].y, 6.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[2].x, 7.0f);
|
||||||
|
EXPECT_FLOAT_EQ(segments[1].points[2].y, 8.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ContentBuilderTest, PathPaintClearsCurrentPath) {
|
||||||
|
auto objects = buildObjects("0 0 m 10 10 l S 20 20 m 30 30 l f");
|
||||||
|
ASSERT_EQ(objects.size(), 2);
|
||||||
|
|
||||||
|
const auto* stroke = requirePathObject(objects[0]);
|
||||||
|
const auto* fill = requirePathObject(objects[1]);
|
||||||
|
ASSERT_NE(stroke, nullptr);
|
||||||
|
ASSERT_NE(fill, nullptr);
|
||||||
|
|
||||||
|
EXPECT_EQ(stroke->paintOp, PathPaintOp::Stroke);
|
||||||
|
EXPECT_EQ(fill->paintOp, PathPaintOp::Fill);
|
||||||
|
ASSERT_EQ(stroke->path.segments().size(), 2);
|
||||||
|
ASSERT_EQ(fill->path.segments().size(), 2);
|
||||||
|
EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].x, 20.0f);
|
||||||
|
EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].y, 20.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ContentBuilderTest, PathCapturesCurrentTransform) {
|
||||||
|
auto objects = buildObjects("q 2 0 0 3 10 20 cm 1 2 3 4 re f Q");
|
||||||
|
ASSERT_EQ(objects.size(), 1);
|
||||||
|
|
||||||
|
const auto* pathObj = requirePathObject(objects[0]);
|
||||||
|
ASSERT_NE(pathObj, nullptr);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.a, 2.0f);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.b, 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.c, 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.d, 3.0f);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.e, 10.0f);
|
||||||
|
EXPECT_FLOAT_EQ(pathObj->transform.f, 20.0f);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ContentBuilderTest, IntegrationHelloWorld) {
|
TEST(ContentBuilderTest, IntegrationHelloWorld) {
|
||||||
pdfengine::qpdf_layer::QpdfExtractor extractor;
|
pdfengine::qpdf_layer::QpdfExtractor extractor;
|
||||||
std::filesystem::path path = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf";
|
std::filesystem::path path = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf";
|
||||||
|
|||||||
@@ -721,6 +721,62 @@ TEST(ImageXObjectVerification, LoadRenderCloseStress100Pages1000ImageDraws) {
|
|||||||
|
|
||||||
const auto pdf = makePdf(std::move(objects));
|
const auto pdf = makePdf(std::move(objects));
|
||||||
|
|
||||||
|
auto verifyCustomDecode = [&](std::string_view phase) {
|
||||||
|
QPDF qpdf;
|
||||||
|
qpdf.processMemoryFile("image-xobject-stress", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
||||||
|
auto pages = qpdf.getAllPages();
|
||||||
|
ASSERT_EQ(pages.size(), static_cast<size_t>(kPages)) << phase;
|
||||||
|
|
||||||
|
size_t decodedImages = 0;
|
||||||
|
for (size_t pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
|
||||||
|
const auto& page = pages[pageIndex];
|
||||||
|
const std::string content = decodedStream(page.getKey("/Contents"));
|
||||||
|
Lexer lexer(content);
|
||||||
|
auto tokens = lexer.tokenize();
|
||||||
|
ContentParser parser(tokens);
|
||||||
|
auto operations = parser.parse();
|
||||||
|
size_t doOps = 0;
|
||||||
|
std::vector<std::string> doNames;
|
||||||
|
for (const auto& operation : operations) {
|
||||||
|
if (operation.op == "Do") {
|
||||||
|
++doOps;
|
||||||
|
if (!operation.operands.empty() && operation.operands.back()->type == AstNodeType::Name) {
|
||||||
|
doNames.push_back(operation.operands.back()->stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QpdfResourceResolver resolver(page.getKey("/Resources"));
|
||||||
|
std::vector<std::string> unresolved;
|
||||||
|
for (int imageIndex = 0; imageIndex < kImagesPerPage; ++imageIndex) {
|
||||||
|
const std::string name = "Im" + std::to_string(imageIndex);
|
||||||
|
if (resolver.resolveXObject(name).type != XObjectType::Image) {
|
||||||
|
unresolved.push_back(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto pageObjects = buildPageObjects(page);
|
||||||
|
auto parsedImages = imageObjects(pageObjects);
|
||||||
|
std::vector<std::string> emittedNames;
|
||||||
|
for (const auto* image : parsedImages) {
|
||||||
|
emittedNames.push_back(image->name);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage))
|
||||||
|
<< phase << " page=" << pageIndex
|
||||||
|
<< " doOps=" << doOps
|
||||||
|
<< " doNames=" << ::testing::PrintToString(doNames)
|
||||||
|
<< " unresolved=" << ::testing::PrintToString(unresolved)
|
||||||
|
<< " emitted=" << ::testing::PrintToString(emittedNames)
|
||||||
|
<< " decoded=[" << content << "]"
|
||||||
|
<< " resources=[" << page.getKey("/Resources").unparse() << "]";
|
||||||
|
for (const auto* image : parsedImages) {
|
||||||
|
EXPECT_EQ(image->pixelData.size(), 4u) << phase << " page=" << pageIndex;
|
||||||
|
}
|
||||||
|
decodedImages += parsedImages.size();
|
||||||
|
}
|
||||||
|
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage)) << phase;
|
||||||
|
};
|
||||||
|
|
||||||
|
verifyCustomDecode("before-pdfium-render");
|
||||||
|
|
||||||
auto document = PdfDocument::loadFromMemory(pdf);
|
auto document = PdfDocument::loadFromMemory(pdf);
|
||||||
if (!document) {
|
if (!document) {
|
||||||
GTEST_SKIP() << "PDFium load unavailable in this build";
|
GTEST_SKIP() << "PDFium load unavailable in this build";
|
||||||
@@ -734,20 +790,5 @@ TEST(ImageXObjectVerification, LoadRenderCloseStress100Pages1000ImageDraws) {
|
|||||||
}
|
}
|
||||||
document = {};
|
document = {};
|
||||||
|
|
||||||
QPDF qpdf;
|
verifyCustomDecode("after-pdfium-render");
|
||||||
qpdf.processMemoryFile("image-xobject-stress", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
|
||||||
auto pages = qpdf.getAllPages();
|
|
||||||
ASSERT_EQ(pages.size(), static_cast<size_t>(kPages));
|
|
||||||
|
|
||||||
size_t decodedImages = 0;
|
|
||||||
for (const auto& page : pages) {
|
|
||||||
auto pageObjects = buildPageObjects(page);
|
|
||||||
auto parsedImages = imageObjects(pageObjects);
|
|
||||||
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage));
|
|
||||||
for (const auto* image : parsedImages) {
|
|
||||||
EXPECT_EQ(image->pixelData.size(), 4u);
|
|
||||||
}
|
|
||||||
decodedImages += parsedImages.size();
|
|
||||||
}
|
|
||||||
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { gatewayService } from '../lib/gatewayService';
|
|||||||
import type { TextObjectResponse } from '../lib/gatewayService';
|
import type { TextObjectResponse } from '../lib/gatewayService';
|
||||||
import { toast } from '../lib/toast';
|
import { toast } from '../lib/toast';
|
||||||
|
|
||||||
|
import { loadPdfFont } from '../lib/fontFaceLoader';
|
||||||
|
|
||||||
// Measure a run's width + ascent/descent in its actual font (memoized). Sizes are returned in the
|
// Measure a run's width + ascent/descent in its actual font (memoized). Sizes are returned in the
|
||||||
// same units as `sizePx`, so callers scale by the text matrix + zoom. Before the embedded @font-face
|
// same units as `sizePx`, so callers scale by the text matrix + zoom. Before the embedded @font-face
|
||||||
// loads this measures the fallback chain (still far better than a char-count guess); clearMeasureCache
|
// loads this measures the fallback chain (still far better than a char-count guess); clearMeasureCache
|
||||||
@@ -53,6 +55,7 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [fontsReady, setFontsReady] = useState(false);
|
const [fontsReady, setFontsReady] = useState(false);
|
||||||
|
const [fontFamilyMap, setFontFamilyMap] = useState<Record<string, string>>({});
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -124,13 +127,30 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!uniqueFonts.length) return;
|
if (!uniqueFonts.length) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
const fonts = (document as unknown as { fonts?: { load: (f: string) => Promise<unknown> } }).fonts;
|
|
||||||
if (!fonts) return;
|
Promise.all(uniqueFonts.map(async (fn) => {
|
||||||
Promise.all(uniqueFonts.map(fn => fonts.load(`16px 'PDF_${fn}'`).catch(() => {})))
|
const cssFamily = await loadPdfFont(documentId, fn);
|
||||||
.then(() => { if (active) { clearMeasureCache(); setFontsReady(v => !v); } });
|
return { fn, cssFamily };
|
||||||
|
})).then((results) => {
|
||||||
|
if (!active) return;
|
||||||
|
let changed = false;
|
||||||
|
const newMap: Record<string, string> = { ...fontFamilyMap };
|
||||||
|
for (const res of results) {
|
||||||
|
if (res.cssFamily && newMap[res.fn] !== res.cssFamily) {
|
||||||
|
newMap[res.fn] = res.cssFamily;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
setFontFamilyMap(newMap);
|
||||||
|
clearMeasureCache();
|
||||||
|
setFontsReady(v => !v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => { active = false; };
|
return () => { active = false; };
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [uniqueFontsKey]);
|
}, [uniqueFontsKey, documentId]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return null;
|
return null;
|
||||||
@@ -138,14 +158,6 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 left-0 z-[40]" style={{ width: `${width}px`, height: `${height}px` }}>
|
<div className="absolute top-0 left-0 z-[40]" style={{ width: `${width}px`, height: `${height}px` }}>
|
||||||
<style>
|
|
||||||
{uniqueFonts.map(fontName => `
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PDF_${fontName}';
|
|
||||||
src: url('${gatewayService.baseUrl}/documents/${documentId}/font?internal_font_id=${encodeURIComponent(fontName)}');
|
|
||||||
}
|
|
||||||
`).join('\n')}
|
|
||||||
</style>
|
|
||||||
{objects.map((obj, i) => {
|
{objects.map((obj, i) => {
|
||||||
// tm[4] is X, tm[5] is Y (baseline, bottom-left origin)
|
// tm[4] is X, tm[5] is Y (baseline, bottom-left origin)
|
||||||
const pdfX = obj.tm[4];
|
const pdfX = obj.tm[4];
|
||||||
@@ -160,7 +172,8 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
const fontSizeScreen = obj.fontSize * scaleY * zoom;
|
const fontSizeScreen = obj.fontSize * scaleY * zoom;
|
||||||
|
|
||||||
const isEditing = editingIndex === i;
|
const isEditing = editingIndex === i;
|
||||||
const fontFamily = obj.fontName ? `'PDF_${obj.fontName}', sans-serif` : 'sans-serif';
|
const cssFam = obj.fontName ? fontFamilyMap[obj.fontName] : null;
|
||||||
|
const fontFamily = cssFam ? `'${cssFam}', sans-serif` : 'sans-serif';
|
||||||
|
|
||||||
// P2b — EXACT hit-box from real font metrics (measured in the actual embedded font once
|
// P2b — EXACT hit-box from real font metrics (measured in the actual embedded font once
|
||||||
// it loads; falls back to the sans-serif chain before then) instead of the old
|
// it loads; falls back to the sans-serif chain before then) instead of the old
|
||||||
|
|||||||
@@ -268,18 +268,18 @@ def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
|
|||||||
|
|
||||||
# Cap the id length; never echo it back into error bodies.
|
# Cap the id length; never echo it back into error bodies.
|
||||||
if not internal_font_id or len(internal_font_id) > 256:
|
if not internal_font_id or len(internal_font_id) > 256:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
doc_info = document_store.get_document(document_id)
|
doc_info = document_store.get_document(document_id)
|
||||||
if not doc_info:
|
if not doc_info:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
|
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
|
||||||
except Exception:
|
except Exception:
|
||||||
data = b""
|
data = b""
|
||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
magic = data[:4]
|
magic = data[:4]
|
||||||
if magic in _SFNT_TTF_MAGIC:
|
if magic in _SFNT_TTF_MAGIC:
|
||||||
@@ -288,7 +288,7 @@ def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
|
|||||||
media_type = "font/otf"
|
media_type = "font/otf"
|
||||||
else:
|
else:
|
||||||
# Type1 (\x80\x01 / "%!") or anything not sfnt-wrapped — not browser-loadable.
|
# Type1 (\x80\x01 / "%!") or anything not sfnt-wrapped — not browser-loadable.
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not loadable")
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
|
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
Reference in New Issue
Block a user