Merge branch 'dev' of https://gitea.maskantech.in/gitea_admin/pdf into furqan
This commit is contained in:
@@ -428,7 +428,14 @@ PYBIND11_MODULE(pdfengine, m) {
|
||||
.def("page_to_device", &pdfengine::PdfPage::pageToDevice,
|
||||
py::arg("page_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
||||
.def("device_to_page", &pdfengine::PdfPage::deviceToPage,
|
||||
py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0);
|
||||
py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
||||
.def("extract_display_list", [](const pdfengine::PdfPage& self) {
|
||||
return get_or_throw(self.extractDisplayListJson());
|
||||
})
|
||||
.def("extract_image_xobject", [](const pdfengine::PdfPage& self, const std::string& name) {
|
||||
auto res = get_or_throw(self.extractImageXObject(name));
|
||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
||||
}, py::arg("name"));
|
||||
|
||||
py::class_<pdfengine::PdfDocument, std::shared_ptr<pdfengine::PdfDocument>>(m, "PdfDocument")
|
||||
.def_static("load_from_file", [](const std::string& path, const std::string& password) {
|
||||
|
||||
@@ -13,6 +13,7 @@ add_library(pdfengine STATIC
|
||||
src/core/engine_info.cpp
|
||||
src/core/graphics_state.cpp
|
||||
src/core/display_list.cpp
|
||||
src/core/path_interpreter.cpp
|
||||
src/core/skia_renderer.cpp
|
||||
src/parser/pdfium_loader.cpp
|
||||
src/parser/pdfium_document.cpp
|
||||
|
||||
@@ -68,6 +68,7 @@ public:
|
||||
|
||||
Path path;
|
||||
PathPaintOp paintOp = PathPaintOp::Stroke;
|
||||
FillRule fillRule = FillRule::NonZero;
|
||||
Matrix transform;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ struct DrawTextCommand : public Command {
|
||||
|
||||
struct FillPathCommand : public Command {
|
||||
Path path;
|
||||
explicit FillPathCommand(Path path) : path(std::move(path)) {}
|
||||
FillRule rule;
|
||||
explicit FillPathCommand(Path path, FillRule rule = FillRule::NonZero) : path(std::move(path)), rule(rule) {}
|
||||
void accept(CommandVisitor& visitor) const override;
|
||||
};
|
||||
|
||||
@@ -55,6 +56,13 @@ struct StrokePathCommand : public Command {
|
||||
void accept(CommandVisitor& visitor) const override;
|
||||
};
|
||||
|
||||
struct FillStrokePathCommand : public Command {
|
||||
Path path;
|
||||
FillRule rule;
|
||||
explicit FillStrokePathCommand(Path path, FillRule rule = FillRule::NonZero) : path(std::move(path)), rule(rule) {}
|
||||
void accept(CommandVisitor& visitor) const override;
|
||||
};
|
||||
|
||||
struct DrawImageCommand : public Command {
|
||||
ImageInfo image;
|
||||
Matrix matrix;
|
||||
@@ -77,6 +85,7 @@ public:
|
||||
virtual void visit(const DrawTextCommand& cmd) = 0;
|
||||
virtual void visit(const FillPathCommand& cmd) = 0;
|
||||
virtual void visit(const StrokePathCommand& cmd) = 0;
|
||||
virtual void visit(const FillStrokePathCommand& cmd) = 0;
|
||||
virtual void visit(const DrawImageCommand& cmd) = 0;
|
||||
};
|
||||
|
||||
@@ -94,8 +103,9 @@ public:
|
||||
void setTransform(const Matrix& m);
|
||||
void fillRect(float x, float y, float w, float h);
|
||||
void drawText(const std::string& text, float x, float y);
|
||||
void fillPath(const Path& path);
|
||||
void fillPath(const Path& path, FillRule rule = FillRule::NonZero);
|
||||
void strokePath(const Path& path);
|
||||
void fillStrokePath(const Path& path, FillRule rule = FillRule::NonZero);
|
||||
void drawImage(const ImageInfo& image, const Matrix& m, float opacity = 1.0f);
|
||||
|
||||
[[nodiscard]] size_t size() const noexcept { return m_commands.size(); }
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
enum class FillRule {
|
||||
NonZero,
|
||||
EvenOdd
|
||||
};
|
||||
|
||||
// Basic point structure
|
||||
struct Point {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <pdfengine/content_object.hpp>
|
||||
#include <pdfengine/display_list.hpp>
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
// PathObjectInterpreter converts a PathObject into DisplayList commands
|
||||
class PathObjectInterpreter {
|
||||
public:
|
||||
static void interpret(const PathObject& pathObj, DisplayList& displayList);
|
||||
};
|
||||
|
||||
} // namespace pdfengine
|
||||
@@ -210,6 +210,9 @@ public:
|
||||
|
||||
[[nodiscard]] virtual DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
|
||||
[[nodiscard]] virtual Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
|
||||
|
||||
[[nodiscard]] virtual std::expected<std::string, EngineError> extractDisplayListJson() const = 0;
|
||||
[[nodiscard]] virtual std::expected<std::vector<uint8_t>, EngineError> extractImageXObject(const std::string& name) const = 0;
|
||||
};
|
||||
|
||||
namespace fonts::pdf_fonts { class Font; }
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
void visit(const DrawTextCommand& cmd) override;
|
||||
void visit(const FillPathCommand& cmd) override;
|
||||
void visit(const StrokePathCommand& cmd) override;
|
||||
void visit(const FillStrokePathCommand& cmd) override;
|
||||
void visit(const DrawImageCommand& cmd) override;
|
||||
void render(const DisplayList& displayList);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ void FillRectCommand::accept(CommandVisitor& visitor) const { visitor.visit(*thi
|
||||
void DrawTextCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
|
||||
void FillPathCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
|
||||
void StrokePathCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
|
||||
void FillStrokePathCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
|
||||
void DrawImageCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
|
||||
|
||||
void DisplayList::addCommand(std::unique_ptr<Command> cmd) {
|
||||
@@ -43,14 +44,18 @@ void DisplayList::drawText(const std::string& text, float x, float y) {
|
||||
addCommand(std::make_unique<DrawTextCommand>(text, x, y));
|
||||
}
|
||||
|
||||
void DisplayList::fillPath(const Path& path) {
|
||||
addCommand(std::make_unique<FillPathCommand>(path));
|
||||
void DisplayList::fillPath(const Path& path, FillRule rule) {
|
||||
addCommand(std::make_unique<FillPathCommand>(path, rule));
|
||||
}
|
||||
|
||||
void DisplayList::strokePath(const Path& path) {
|
||||
addCommand(std::make_unique<StrokePathCommand>(path));
|
||||
}
|
||||
|
||||
void DisplayList::fillStrokePath(const Path& path, FillRule rule) {
|
||||
addCommand(std::make_unique<FillStrokePathCommand>(path, rule));
|
||||
}
|
||||
|
||||
void DisplayList::drawImage(const ImageInfo& image, const Matrix& m, float opacity) {
|
||||
addCommand(std::make_unique<DrawImageCommand>(image, m, opacity));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include <pdfengine/path_interpreter.hpp>
|
||||
|
||||
namespace pdfengine {
|
||||
|
||||
void PathObjectInterpreter::interpret(const PathObject& pathObj, DisplayList& displayList) {
|
||||
if (pathObj.path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayList.saveState();
|
||||
displayList.setTransform(pathObj.transform);
|
||||
|
||||
switch (pathObj.paintOp) {
|
||||
case PathPaintOp::Stroke:
|
||||
displayList.strokePath(pathObj.path);
|
||||
break;
|
||||
case PathPaintOp::Fill:
|
||||
displayList.fillPath(pathObj.path, pathObj.fillRule);
|
||||
break;
|
||||
case PathPaintOp::FillStroke:
|
||||
displayList.fillStrokePath(pathObj.path, pathObj.fillRule);
|
||||
break;
|
||||
}
|
||||
|
||||
displayList.restoreState();
|
||||
}
|
||||
|
||||
} // namespace pdfengine
|
||||
@@ -122,6 +122,12 @@ void SkiaRenderer::visit(const FillPathCommand& cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.rule == FillRule::EvenOdd) {
|
||||
skPath.setFillType(SkPathFillType::kEvenOdd);
|
||||
} else {
|
||||
skPath.setFillType(SkPathFillType::kWinding);
|
||||
}
|
||||
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(SkPaint::kFill_Style);
|
||||
@@ -178,6 +184,64 @@ void SkiaRenderer::visit(const StrokePathCommand& cmd) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void SkiaRenderer::visit(const FillStrokePathCommand& cmd) {
|
||||
(void)cmd;
|
||||
#ifdef PDFENGINE_WITH_SKIA
|
||||
if (!m_canvas || cmd.path.empty()) return;
|
||||
|
||||
SkPath skPath;
|
||||
for (const auto& segment : cmd.path.segments()) {
|
||||
switch (segment.verb) {
|
||||
case Path::Verb::MoveTo:
|
||||
skPath.moveTo(segment.points[0].x, segment.points[0].y);
|
||||
break;
|
||||
case Path::Verb::LineTo:
|
||||
skPath.lineTo(segment.points[0].x, segment.points[0].y);
|
||||
break;
|
||||
case Path::Verb::CubicBezierTo:
|
||||
skPath.cubicTo(
|
||||
segment.points[0].x, segment.points[0].y,
|
||||
segment.points[1].x, segment.points[1].y,
|
||||
segment.points[2].x, segment.points[2].y
|
||||
);
|
||||
break;
|
||||
case Path::Verb::Close:
|
||||
skPath.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.rule == FillRule::EvenOdd) {
|
||||
skPath.setFillType(SkPathFillType::kEvenOdd);
|
||||
} else {
|
||||
skPath.setFillType(SkPathFillType::kWinding);
|
||||
}
|
||||
|
||||
const auto& color = m_stateStack.current().fillColor;
|
||||
|
||||
// First fill
|
||||
SkPaint fillPaint;
|
||||
fillPaint.setAntiAlias(true);
|
||||
fillPaint.setStyle(SkPaint::kFill_Style);
|
||||
fillPaint.setColor(SkColorSetARGB(255,
|
||||
static_cast<uint8_t>(color.r * 255),
|
||||
static_cast<uint8_t>(color.g * 255),
|
||||
static_cast<uint8_t>(color.b * 255)));
|
||||
m_canvas->drawPath(skPath, fillPaint);
|
||||
|
||||
// Then stroke
|
||||
SkPaint strokePaint;
|
||||
strokePaint.setAntiAlias(true);
|
||||
strokePaint.setStyle(SkPaint::kStroke_Style);
|
||||
strokePaint.setColor(SkColorSetARGB(255,
|
||||
static_cast<uint8_t>(color.r * 255),
|
||||
static_cast<uint8_t>(color.g * 255),
|
||||
static_cast<uint8_t>(color.b * 255)));
|
||||
strokePaint.setStrokeWidth(1.0f);
|
||||
m_canvas->drawPath(skPath, strokePaint);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SkiaRenderer::visit(const DrawImageCommand& cmd) {
|
||||
(void)cmd;
|
||||
#ifdef PDFENGINE_WITH_SKIA
|
||||
|
||||
@@ -46,11 +46,14 @@ void ContentBuilder::processOperation(const Operation& op, std::vector<std::uniq
|
||||
} 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);
|
||||
FillRule rule = (op.op == "f*") ? FillRule::EvenOdd : FillRule::NonZero;
|
||||
handlePathPaint(PathPaintOp::Fill, outObjects, false, rule);
|
||||
} else if (op.op == "B" || op.op == "B*") {
|
||||
handlePathPaint(PathPaintOp::FillStroke, outObjects);
|
||||
FillRule rule = (op.op == "B*") ? FillRule::EvenOdd : FillRule::NonZero;
|
||||
handlePathPaint(PathPaintOp::FillStroke, outObjects, false, rule);
|
||||
} else if (op.op == "b" || op.op == "b*") {
|
||||
handlePathPaint(PathPaintOp::FillStroke, outObjects, true);
|
||||
FillRule rule = (op.op == "b*") ? FillRule::EvenOdd : FillRule::NonZero;
|
||||
handlePathPaint(PathPaintOp::FillStroke, outObjects, true, rule);
|
||||
} else if (op.op == "n") {
|
||||
currentPath_.clear();
|
||||
}
|
||||
@@ -285,7 +288,8 @@ void ContentBuilder::handlePathConstruction(const Operation& op) {
|
||||
|
||||
void ContentBuilder::handlePathPaint(PathPaintOp paintOp,
|
||||
std::vector<std::unique_ptr<ContentObject>>& outObjects,
|
||||
bool closePath) {
|
||||
bool closePath,
|
||||
FillRule fillRule) {
|
||||
if (closePath) {
|
||||
if (!currentPath_.empty()) {
|
||||
currentPath_.close();
|
||||
@@ -298,6 +302,7 @@ void ContentBuilder::handlePathPaint(PathPaintOp paintOp,
|
||||
auto pathObj = std::make_unique<PathObject>();
|
||||
pathObj->path = currentPath_;
|
||||
pathObj->paintOp = paintOp;
|
||||
pathObj->fillRule = fillRule;
|
||||
pathObj->transform = state_.ctm;
|
||||
outObjects.push_back(std::move(pathObj));
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ private:
|
||||
void handleCm(const Operation& op);
|
||||
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);
|
||||
void handlePathPaint(PathPaintOp paintOp, std::vector<std::unique_ptr<ContentObject>>& outObjects, bool closePath = false, FillRule fillRule = FillRule::NonZero);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include "fonts/shaping/hb_shaper.hpp"
|
||||
#include "fonts/face/font_face.hpp"
|
||||
#include "decoration_builder.hpp"
|
||||
#include "qpdf/qpdf_extractor.hpp"
|
||||
#include "parser/lexer.hpp"
|
||||
#include "parser/parser.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
@@ -951,6 +954,7 @@ std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWith
|
||||
}
|
||||
|
||||
namespace {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
inline void repagSetParaId(FPDF_DOCUMENT doc, FPDF_PAGEOBJECT obj, const std::string& paraId) {
|
||||
if (paraId.empty() || !obj) return;
|
||||
FPDF_PAGEOBJECTMARK mark = FPDFPageObj_AddMark(obj, "PDFPARA");
|
||||
@@ -982,6 +986,7 @@ inline std::string repagGetParaId(FPDF_PAGEOBJECT obj) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
||||
@@ -4437,6 +4442,51 @@ std::expected<std::shared_ptr<fonts::pdf_fonts::Font>, std::string> PdfiumDocume
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<std::string, EngineError> PdfiumPage::extractDisplayListJson() const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!ownerDoc_) return std::unexpected(EngineError::Unknown);
|
||||
const auto& buffer = ownerDoc_->getMemoryBuffer();
|
||||
if (buffer.empty()) return std::unexpected(EngineError::FileNotFound);
|
||||
|
||||
qpdf_layer::QpdfExtractor extractor;
|
||||
auto stream = extractor.extractPageStreamFromMemory(buffer, pageIndex_);
|
||||
if (!stream) return std::unexpected(EngineError::InvalidFormat);
|
||||
|
||||
Lexer lexer(stream->decodedContent);
|
||||
auto tokens = lexer.tokenize();
|
||||
ContentParser parser(tokens);
|
||||
auto operations = parser.parse();
|
||||
|
||||
nlohmann::json j_array = nlohmann::json::array();
|
||||
for (const auto& op : operations) {
|
||||
nlohmann::json j_op = nlohmann::json::object();
|
||||
j_op["op"] = op.op;
|
||||
if (!op.operands.empty()) {
|
||||
nlohmann::json j_args = nlohmann::json::array();
|
||||
for (const auto& arg : op.operands) {
|
||||
if (arg->type == AstNodeType::Number) {
|
||||
j_args.push_back(arg->numberValue);
|
||||
} else if (arg->type == AstNodeType::Name || arg->type == AstNodeType::String) {
|
||||
j_args.push_back(arg->stringValue);
|
||||
} else {
|
||||
j_args.push_back("<other>");
|
||||
}
|
||||
}
|
||||
j_op["args"] = j_args;
|
||||
}
|
||||
j_array.push_back(j_op);
|
||||
}
|
||||
return j_array.dump();
|
||||
#else
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<std::vector<uint8_t>, EngineError> PdfiumPage::extractImageXObject(const std::string& name) const {
|
||||
(void)name;
|
||||
return std::unexpected(EngineError::Unknown); // Stub for now
|
||||
}
|
||||
|
||||
void PdfiumDocument::invalidateCaches() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
||||
|
||||
@@ -55,6 +55,9 @@ public:
|
||||
|
||||
std::expected<double, EngineError> getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const override;
|
||||
|
||||
std::expected<std::string, EngineError> extractDisplayListJson() const override;
|
||||
std::expected<std::vector<uint8_t>, EngineError> extractImageXObject(const std::string& name) const override;
|
||||
|
||||
DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
|
||||
@@ -102,6 +105,8 @@ public:
|
||||
std::string lastReflowLayout() const { return lastReflowLayout_; }
|
||||
bool lastReflowOverflowed() const { return lastReflowOverflowed_; }
|
||||
|
||||
const std::vector<uint8_t>& getMemoryBuffer() const { return memoryBuffer_; }
|
||||
|
||||
private:
|
||||
mutable std::string lastReflowLayout_;
|
||||
mutable bool lastReflowOverflowed_ = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
void visit(const DrawTextCommand& cmd) override { calls.push_back("DrawText(" + cmd.text + ")"); }
|
||||
void visit(const FillPathCommand&) override { calls.push_back("FillPath"); }
|
||||
void visit(const StrokePathCommand&) override { calls.push_back("StrokePath"); }
|
||||
void visit(const FillStrokePathCommand&) override { calls.push_back("FillStrokePath"); }
|
||||
void visit(const DrawImageCommand&) override { calls.push_back("DrawImage"); }
|
||||
};
|
||||
|
||||
|
||||
@@ -530,6 +530,16 @@ class GatewayService {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getPageDisplayList(documentId: string, pageIndex: number): Promise<any[]> {
|
||||
const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/display_list`);
|
||||
if (!response.ok) throw new Error(`Failed to get display list: ${response.statusText}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
getImageXObjectUrl(documentId: string, pageIndex: number, name: string): string {
|
||||
return `${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/xobjects/${encodeURIComponent(name)}`;
|
||||
}
|
||||
|
||||
private getMockDocuments(): DocumentInfo[] {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
|
||||
interface CanvasLayerProps {
|
||||
documentId: string;
|
||||
pageIndex: number;
|
||||
imageUrl: string;
|
||||
zoom: number;
|
||||
@@ -11,6 +13,7 @@ interface CanvasLayerProps {
|
||||
}
|
||||
|
||||
export const CanvasLayer: React.FC<CanvasLayerProps> = ({
|
||||
documentId,
|
||||
pageIndex,
|
||||
imageUrl,
|
||||
zoom,
|
||||
@@ -20,6 +23,13 @@ export const CanvasLayer: React.FC<CanvasLayerProps> = ({
|
||||
onRenderComplete,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [vectorOps, setVectorOps] = useState<any[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
gatewayService.getPageDisplayList(documentId, pageIndex)
|
||||
.then(ops => setVectorOps(ops))
|
||||
.catch(console.error);
|
||||
}, [documentId, pageIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -50,11 +60,49 @@ export const CanvasLayer: React.FC<CanvasLayerProps> = ({
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
ctx.restore();
|
||||
|
||||
// Render vector ops on top
|
||||
if (vectorOps && vectorOps.length > 0) {
|
||||
ctx.save();
|
||||
ctx.translate(0, height);
|
||||
ctx.scale(1, -1);
|
||||
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
|
||||
ctx.fillStyle = 'rgba(0, 0, 255, 0.2)';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
ctx.beginPath();
|
||||
for (const op of vectorOps) {
|
||||
if (op.op === 'm' && op.args && op.args.length === 2) {
|
||||
ctx.moveTo(op.args[0], op.args[1]);
|
||||
} else if (op.op === 'l' && op.args && op.args.length === 2) {
|
||||
ctx.lineTo(op.args[0], op.args[1]);
|
||||
} else if (op.op === 'c' && op.args && op.args.length === 6) {
|
||||
ctx.bezierCurveTo(op.args[0], op.args[1], op.args[2], op.args[3], op.args[4], op.args[5]);
|
||||
} else if (op.op === 're' && op.args && op.args.length === 4) {
|
||||
ctx.rect(op.args[0], op.args[1], op.args[2], op.args[3]);
|
||||
} else if (op.op === 'h') {
|
||||
ctx.closePath();
|
||||
} else if (op.op === 'S' || op.op === 's') {
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
} else if (op.op === 'f' || op.op === 'F') {
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
} else if (op.op === 'B' || op.op === 'b') {
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
} else if (op.op === 'n') {
|
||||
ctx.beginPath();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
onRenderComplete?.(pageIndex);
|
||||
};
|
||||
img.src = imageUrl;
|
||||
|
||||
}, [imageUrl, zoom, rotation, width, height, onRenderComplete]);
|
||||
}, [imageUrl, zoom, rotation, width, height, onRenderComplete, vectorOps]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
||||
@@ -397,6 +397,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
{imageUrl ? (
|
||||
<>
|
||||
<CanvasLayer
|
||||
documentId={documentId}
|
||||
pageIndex={page.index}
|
||||
imageUrl={imageUrl}
|
||||
zoom={zoom}
|
||||
|
||||
@@ -835,3 +835,39 @@ def replace_text_object(document_id: str, page_index: int, object_index: int, re
|
||||
os.remove(tmp_path)
|
||||
if os.path.exists(out_path):
|
||||
os.remove(out_path)
|
||||
|
||||
@router.get("/{document_id}/pages/{page_index}/display_list")
|
||||
def get_display_list(document_id: str, page_index: int):
|
||||
if not engine.is_available():
|
||||
raise HTTPException(status_code=501, detail="Engine not available")
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
try:
|
||||
doc = doc_info["doc_instance"]
|
||||
page = doc.get_page(page_index)
|
||||
json_str = page.extract_display_list()
|
||||
return Response(content=json_str, media_type="application/json")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{document_id}/pages/{page_index}/xobjects/{name}")
|
||||
def get_image_xobject(document_id: str, page_index: int, name: str):
|
||||
if not engine.is_available():
|
||||
raise HTTPException(status_code=501, detail="Engine not available")
|
||||
|
||||
doc_info = document_store.get_document(document_id)
|
||||
if not doc_info:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
try:
|
||||
doc = doc_info["doc_instance"]
|
||||
page = doc.get_page(page_index)
|
||||
img_bytes = page.extract_image_xobject(name)
|
||||
if not img_bytes:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
return Response(content=img_bytes, media_type="image/png")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
Reference in New Issue
Block a user