merged code with dev
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@ node_modules/
|
||||
/frontend/.vite/
|
||||
|
||||
# WASM artifacts
|
||||
*.wasm
|
||||
|
||||
*.wasm.map
|
||||
|
||||
# Logs / misc
|
||||
|
||||
@@ -10,6 +10,8 @@ configure_file(
|
||||
add_library(pdfengine STATIC
|
||||
src/core/engine_info.cpp
|
||||
src/parser/pdfium_loader.cpp
|
||||
src/fonts/font_face.cpp
|
||||
src/fonts/hb_shaper.cpp
|
||||
)
|
||||
add_library(pdfengine::pdfengine ALIAS pdfengine)
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "font_face.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
FontFace::FontFace()
|
||||
: ft_library_(nullptr),
|
||||
face_(nullptr) {
|
||||
|
||||
if (FT_Init_FreeType(&ft_library_)) {
|
||||
std::cerr << "Failed to initialize FreeType\n";
|
||||
}
|
||||
}
|
||||
|
||||
FontFace::~FontFace() {
|
||||
|
||||
if (face_) {
|
||||
FT_Done_Face(face_);
|
||||
}
|
||||
|
||||
if (ft_library_) {
|
||||
FT_Done_FreeType(ft_library_);
|
||||
}
|
||||
}
|
||||
|
||||
FontFace::FontFace(FontFace&& other) noexcept
|
||||
: ft_library_(other.ft_library_),
|
||||
face_(other.face_) {
|
||||
other.ft_library_ = nullptr;
|
||||
other.face_ = nullptr;
|
||||
}
|
||||
|
||||
FontFace& FontFace::operator=(FontFace&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (face_) {
|
||||
FT_Done_Face(face_);
|
||||
}
|
||||
if (ft_library_) {
|
||||
FT_Done_FreeType(ft_library_);
|
||||
}
|
||||
ft_library_ = other.ft_library_;
|
||||
face_ = other.face_;
|
||||
other.ft_library_ = nullptr;
|
||||
other.face_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool FontFace::loadFromFile(const std::string& path) {
|
||||
|
||||
if (FT_New_Face(
|
||||
ft_library_,
|
||||
path.c_str(),
|
||||
0,
|
||||
&face_)) {
|
||||
|
||||
std::cerr << "Failed to load font: "
|
||||
<< path << '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set a default pixel size of 16px so that font coordinates and shaping
|
||||
// advances are non-zero by default.
|
||||
FT_Set_Pixel_Sizes(face_, 0, 16);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FT_Face FontFace::getFace() const {
|
||||
return face_;
|
||||
}
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
class FontFace {
|
||||
public:
|
||||
FontFace();
|
||||
~FontFace();
|
||||
|
||||
// FontFace is move-only to avoid double freeing FreeType resources.
|
||||
FontFace(const FontFace&) = delete;
|
||||
FontFace& operator=(const FontFace&) = delete;
|
||||
FontFace(FontFace&& other) noexcept;
|
||||
FontFace& operator=(FontFace&& other) noexcept;
|
||||
|
||||
bool loadFromFile(const std::string& path);
|
||||
|
||||
FT_Face getFace() const;
|
||||
|
||||
private:
|
||||
FT_Library ft_library_;
|
||||
FT_Face face_;
|
||||
};
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "hb_shaper.hpp"
|
||||
|
||||
#include <hb.h>
|
||||
#include <hb-ft.h>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
HbShaper::HbShaper() = default;
|
||||
HbShaper::~HbShaper() = default;
|
||||
|
||||
std::vector<ShapedGlyph> HbShaper::shapeText(const FontFace& fontFace, const std::string& text) {
|
||||
std::vector<ShapedGlyph> result;
|
||||
|
||||
FT_Face ftFace = fontFace.getFace();
|
||||
if (!ftFace) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create a HarfBuzz font wrapper around the FreeType face.
|
||||
// hb_ft_font_create_referenced increments the reference count of the FT_Face,
|
||||
// making it safe even if the FontFace object changes or moves.
|
||||
hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace);
|
||||
if (!hbFont) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set the scale of HarfBuzz font to match the FreeType face size.
|
||||
// If not set, HarfBuzz will default to using the font's design units (upem).
|
||||
hb_ft_font_changed(hbFont);
|
||||
|
||||
// Create a text buffer.
|
||||
hb_buffer_t* hbBuffer = hb_buffer_create();
|
||||
if (!hbBuffer) {
|
||||
hb_font_destroy(hbFont);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add text to buffer as UTF-8.
|
||||
hb_buffer_add_utf8(hbBuffer, text.c_str(), static_cast<int>(text.length()), 0, -1);
|
||||
|
||||
// Let HarfBuzz guess direction, script, and language properties.
|
||||
hb_buffer_guess_segment_properties(hbBuffer);
|
||||
|
||||
// Shape the text inside the buffer using the font.
|
||||
hb_shape(hbFont, hbBuffer, nullptr, 0);
|
||||
|
||||
// Retrieve the results.
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(hbBuffer, &glyphCount);
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(hbBuffer, &glyphCount);
|
||||
|
||||
if (glyphInfos && glyphPositions && glyphCount > 0) {
|
||||
result.reserve(glyphCount);
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
ShapedGlyph sg;
|
||||
sg.glyphIndex = glyphInfos[i].codepoint;
|
||||
// HarfBuzz coordinates are fractional 26.6 pixels (1/64 of a pixel).
|
||||
// Convert to standard double-precision float values.
|
||||
sg.xAdvance = static_cast<double>(glyphPositions[i].x_advance) / 64.0;
|
||||
sg.yAdvance = static_cast<double>(glyphPositions[i].y_advance) / 64.0;
|
||||
sg.xOffset = static_cast<double>(glyphPositions[i].x_offset) / 64.0;
|
||||
sg.yOffset = static_cast<double>(glyphPositions[i].y_offset) / 64.0;
|
||||
result.push_back(sg);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up HarfBuzz resources.
|
||||
hb_buffer_destroy(hbBuffer);
|
||||
hb_font_destroy(hbFont);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "font_face.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
struct ShapedGlyph {
|
||||
unsigned int glyphIndex;
|
||||
double xAdvance;
|
||||
double yAdvance;
|
||||
double xOffset;
|
||||
double yOffset;
|
||||
};
|
||||
|
||||
class HbShaper {
|
||||
public:
|
||||
HbShaper();
|
||||
~HbShaper();
|
||||
|
||||
HbShaper(const HbShaper&) = delete;
|
||||
HbShaper& operator=(const HbShaper&) = delete;
|
||||
HbShaper(HbShaper&&) noexcept = default;
|
||||
HbShaper& operator=(HbShaper&&) noexcept = default;
|
||||
|
||||
// Shapes the input UTF-8 text using the given FontFace.
|
||||
// Returns a vector of shaped glyphs.
|
||||
std::vector<ShapedGlyph> shapeText(const FontFace& fontFace, const std::string& text);
|
||||
};
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
add_executable(pdfengine_smoke
|
||||
smoke_test.cpp
|
||||
fonts_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(pdfengine_smoke
|
||||
@@ -11,6 +12,11 @@ target_link_libraries(pdfengine_smoke
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(pdfengine_smoke
|
||||
PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
||||
)
|
||||
|
||||
pdfengine_set_warnings(pdfengine_smoke)
|
||||
pdfengine_enable_sanitizers(pdfengine_smoke)
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "fonts/font_face.hpp"
|
||||
#include "fonts/hb_shaper.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string getSystemFontPath() {
|
||||
#if defined(_WIN32)
|
||||
// Common Windows fonts
|
||||
std::vector<std::string> paths = {
|
||||
"C:\\Windows\\Fonts\\arial.ttf",
|
||||
"C:\\Windows\\Fonts\\consola.ttf",
|
||||
"C:\\Windows\\Fonts\\tahoma.ttf"
|
||||
};
|
||||
#elif defined(__APPLE__)
|
||||
// Common macOS fonts
|
||||
std::vector<std::string> paths = {
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
"/System/Library/Fonts/Geneva.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf"
|
||||
};
|
||||
#else
|
||||
// Common Linux fonts
|
||||
std::vector<std::string> paths = {
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/freefont/FreeSans.ttf"
|
||||
};
|
||||
#endif
|
||||
|
||||
for (const auto& path : paths) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
TEST(FontTest, FontFaceInitialization) {
|
||||
FontFace face;
|
||||
EXPECT_EQ(face.getFace(), nullptr);
|
||||
}
|
||||
|
||||
TEST(FontTest, FontFaceLoadNonExistentFile) {
|
||||
FontFace face;
|
||||
EXPECT_FALSE(face.loadFromFile("this_file_does_not_exist_12345.ttf"));
|
||||
EXPECT_EQ(face.getFace(), nullptr);
|
||||
}
|
||||
|
||||
TEST(FontTest, FontFaceMoveSemantics) {
|
||||
std::string fontPath = getSystemFontPath();
|
||||
if (fontPath.empty()) {
|
||||
GTEST_SKIP() << "No system font found to run move semantics test.";
|
||||
}
|
||||
|
||||
FontFace face1;
|
||||
ASSERT_TRUE(face1.loadFromFile(fontPath));
|
||||
FT_Face rawFace = face1.getFace();
|
||||
ASSERT_NE(rawFace, nullptr);
|
||||
|
||||
// Move construction
|
||||
FontFace face2(std::move(face1));
|
||||
EXPECT_EQ(face1.getFace(), nullptr);
|
||||
EXPECT_EQ(face2.getFace(), rawFace);
|
||||
|
||||
// Move assignment
|
||||
FontFace face3;
|
||||
face3 = std::move(face2);
|
||||
EXPECT_EQ(face2.getFace(), nullptr);
|
||||
EXPECT_EQ(face3.getFace(), rawFace);
|
||||
}
|
||||
|
||||
TEST(FontTest, HbShaperEmptyInput) {
|
||||
std::string fontPath = getSystemFontPath();
|
||||
if (fontPath.empty()) {
|
||||
GTEST_SKIP() << "No system font found to run empty input shaper test.";
|
||||
}
|
||||
|
||||
FontFace face;
|
||||
ASSERT_TRUE(face.loadFromFile(fontPath));
|
||||
|
||||
HbShaper shaper;
|
||||
auto glyphs = shaper.shapeText(face, "");
|
||||
EXPECT_TRUE(glyphs.empty());
|
||||
}
|
||||
|
||||
TEST(FontTest, HbShaperNullFace) {
|
||||
FontFace face; // Null face
|
||||
HbShaper shaper;
|
||||
auto glyphs = shaper.shapeText(face, "Hello");
|
||||
EXPECT_TRUE(glyphs.empty());
|
||||
}
|
||||
|
||||
TEST(FontTest, HbShaperShapeTextSuccess) {
|
||||
std::string fontPath = getSystemFontPath();
|
||||
if (fontPath.empty()) {
|
||||
std::cout << "[ WARNING ] Skipping shape success test: no system font found." << std::endl;
|
||||
GTEST_SKIP() << "No system font found to run text shaping test.";
|
||||
}
|
||||
|
||||
FontFace face;
|
||||
ASSERT_TRUE(face.loadFromFile(fontPath));
|
||||
ASSERT_NE(face.getFace(), nullptr);
|
||||
|
||||
HbShaper shaper;
|
||||
std::string testText = "Hello World!";
|
||||
auto glyphs = shaper.shapeText(face, testText);
|
||||
|
||||
// Validate that some glyphs were shaped.
|
||||
// Note that the number of glyphs doesn't strictly have to match testText.length() (e.g. ligatures),
|
||||
// but for simple English it's usually 1:1.
|
||||
EXPECT_FALSE(glyphs.empty());
|
||||
|
||||
for (const auto& g : glyphs) {
|
||||
// Glyph index should be non-zero for valid glyphs (0 is usually .notdef)
|
||||
// Note: some fonts might not map all characters, but Arial/DejaVu/Consolas should map ASCII.
|
||||
EXPECT_GT(g.xAdvance, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,18 @@
|
||||
#include <iostream>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Exported function: add two numbers
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Exported function: print hello message
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void hello() {
|
||||
std::cout << "Hello from C++ WASM!" << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+12
-23
@@ -57,6 +57,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -266,29 +267,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
@@ -875,6 +853,7 @@
|
||||
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -885,6 +864,7 @@
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -944,6 +924,7 @@
|
||||
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
@@ -1174,6 +1155,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1264,6 +1246,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -1399,6 +1382,7 @@
|
||||
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
@@ -2287,6 +2271,7 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2348,6 +2333,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2504,6 +2490,7 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -2590,6 +2577,7 @@
|
||||
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
@@ -2714,6 +2702,7 @@
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function App() {
|
||||
try {
|
||||
const health = await gatewayService.getHealth();
|
||||
setBackendHealthy(health.engine_available || true);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setBackendHealthy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface RenderParams {
|
||||
export interface EditOperation {
|
||||
type: 'add_text' | 'delete_text' | 'draw_shape' | 'add_annotation';
|
||||
pageIndex: number;
|
||||
data: any;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
class GatewayService {
|
||||
|
||||
@@ -24,10 +24,6 @@ class WasmLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
// In a real production setup, we load the wasm glue script:
|
||||
// @ts-ignore
|
||||
// const Module = await import('@/wasm/pdfengine.js');
|
||||
// For Phase 0 / Gate G0a, we stub/mock the WASM loading process gracefully:
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.isLoaded = true;
|
||||
|
||||
@@ -20,7 +20,6 @@ interface AnnotationLayerProps {
|
||||
}
|
||||
|
||||
export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
||||
pageIndex: _pageIndex,
|
||||
width,
|
||||
height,
|
||||
zoom,
|
||||
|
||||
@@ -25,6 +25,8 @@ interface PageLayout {
|
||||
top: number;
|
||||
}
|
||||
|
||||
const generateUniqueId = () => `anno_${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
export const PDFViewer: React.FC<PDFViewerProps> = ({
|
||||
documentId,
|
||||
totalPages,
|
||||
@@ -136,32 +138,44 @@ export const PDFViewer: React.FC<PDFViewerProps> = ({
|
||||
|
||||
// Load the rendered SVG/Image URL for each visible page
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const fetchPageImages = async () => {
|
||||
const newUrls = [...renderedPages];
|
||||
|
||||
const renderPromises = visiblePages.map(async (page) => {
|
||||
if (!newUrls[page.index]) {
|
||||
const missingPages = visiblePages.filter((page) => !renderedPages[page.index]);
|
||||
if (missingPages.length === 0) return;
|
||||
|
||||
const renders = await Promise.all(
|
||||
missingPages.map(async (page) => {
|
||||
const url = await gatewayService.renderPage({
|
||||
documentId,
|
||||
pageIndex: page.index,
|
||||
zoom,
|
||||
rotation,
|
||||
});
|
||||
newUrls[page.index] = url;
|
||||
}
|
||||
});
|
||||
return { index: page.index, url };
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(renderPromises);
|
||||
setRenderedPages(newUrls);
|
||||
if (!active) return;
|
||||
|
||||
setRenderedPages((prev) => {
|
||||
const next = [...prev];
|
||||
renders.forEach(({ index, url }) => {
|
||||
next[index] = url;
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
fetchPageImages();
|
||||
}, [visiblePages, documentId, zoom, rotation]);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [visiblePages, documentId, zoom, rotation, renderedPages]);
|
||||
|
||||
const handleTextSelection = (text: string, bbox: Rect, _pageIndex: number) => {
|
||||
const handleTextSelection = (text: string, bbox: Rect) => {
|
||||
if (activeTool === 'highlight') {
|
||||
const newAnno: Annotation = {
|
||||
id: `anno_${Math.random().toString(36).substr(2, 9)}`,
|
||||
id: generateUniqueId(),
|
||||
type: 'highlight',
|
||||
bbox: {
|
||||
x: bbox.x / zoom,
|
||||
@@ -226,7 +240,7 @@ export const PDFViewer: React.FC<PDFViewerProps> = ({
|
||||
width={page.width}
|
||||
height={page.height}
|
||||
zoom={zoom}
|
||||
onTextSelected={(text, bbox) => handleTextSelection(text, bbox, page.index)}
|
||||
onTextSelected={(text, bbox) => handleTextSelection(text, bbox)}
|
||||
/>
|
||||
|
||||
{/* signature/ink overlay tool layer */}
|
||||
|
||||
@@ -13,7 +13,6 @@ export const SelectionLayer: React.FC<SelectionLayerProps> = ({
|
||||
pageIndex,
|
||||
width,
|
||||
height,
|
||||
zoom: _zoom,
|
||||
onTextSelected,
|
||||
}) => {
|
||||
const [dragStart, setDragStart] = useState<Point | null>(null);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WASM Hello World</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>WASM Hello World</h1>
|
||||
|
||||
<p>Open browser console (F12) to see output.</p>
|
||||
|
||||
<!-- Load generated Emscripten JS -->
|
||||
<script src="wasm_hello.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
// Wait until WASM runtime is ready
|
||||
Module.onRuntimeInitialized = () => {
|
||||
|
||||
console.log("WASM Runtime Initialized");
|
||||
|
||||
// Call C++ hello() function
|
||||
Module.ccall(
|
||||
'hello', // C++ function name
|
||||
null, // return type
|
||||
[], // argument types
|
||||
[] // arguments
|
||||
);
|
||||
|
||||
// Call C++ add() function
|
||||
const result = Module.ccall(
|
||||
'add', // function name
|
||||
'number', // return type
|
||||
['number', 'number'], // argument types
|
||||
[5, 7] // arguments
|
||||
);
|
||||
|
||||
console.log("Result =", result);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+4583
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Reference in New Issue
Block a user