feat: wasm build along with c++ facade
This commit is contained in:
+42
-30
@@ -35,8 +35,42 @@ if(EMSCRIPTEN)
|
||||
set(PDFENGINE_WASM ON CACHE INTERNAL "Building for WebAssembly")
|
||||
endif()
|
||||
|
||||
option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON)
|
||||
option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF)
|
||||
option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
|
||||
option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF)
|
||||
option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF)
|
||||
|
||||
if(PDFENGINE_WASM)
|
||||
set(PDFENGINE_BUILD_TESTS OFF CACHE BOOL "Build engine unit/smoke tests" FORCE)
|
||||
set(PDFENGINE_WITH_PDFIUM OFF CACHE BOOL "Link the PDFium static lib" FORCE)
|
||||
set(PDFENGINE_ENABLE_SANITIZERS OFF CACHE BOOL "ASan/UBSan" FORCE)
|
||||
endif()
|
||||
|
||||
include(CompilerWarnings)
|
||||
include(Sanitizers)
|
||||
|
||||
if(PDFENGINE_WITH_PDFIUM)
|
||||
include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON
|
||||
endif()
|
||||
if(PDFENGINE_WITH_SKIA)
|
||||
include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON
|
||||
endif()
|
||||
|
||||
find_package(freetype CONFIG REQUIRED)
|
||||
find_package(harfbuzz CONFIG REQUIRED)
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
if(PDFENGINE_BUILD_TESTS)
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
endif()
|
||||
|
||||
if(PDFENGINE_WASM)
|
||||
message(STATUS "PdfEngine ${PROJECT_VERSION} — WASM configuration (Phase 1/2 Enabled)")
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(wasm)
|
||||
else()
|
||||
# Only the top-level project drives testing/install defaults.
|
||||
@@ -45,36 +79,14 @@ else()
|
||||
"cmake --preset <platform>-debug")
|
||||
endif()
|
||||
|
||||
option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON)
|
||||
option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF)
|
||||
option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
|
||||
option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF)
|
||||
option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF)
|
||||
|
||||
include(CompilerWarnings)
|
||||
include(Sanitizers)
|
||||
include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON
|
||||
include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON
|
||||
|
||||
find_package(freetype CONFIG REQUIRED)
|
||||
find_package(harfbuzz CONFIG REQUIRED)
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
if(PDFENGINE_BUILD_TESTS)
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
enable_testing()
|
||||
include(GoogleTest)
|
||||
endif()
|
||||
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(bindings)
|
||||
|
||||
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
|
||||
message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}")
|
||||
message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}")
|
||||
message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}")
|
||||
message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}")
|
||||
message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}")
|
||||
message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}")
|
||||
endif()
|
||||
|
||||
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
|
||||
message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}")
|
||||
message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}")
|
||||
message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}")
|
||||
message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}")
|
||||
message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}")
|
||||
message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}")
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
"displayName": "WASM • Emscripten hello-world (Phase 0, Rule R5)",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"toolchainFile": "${sourceDir}/cmake/toolchains/wasm.cmake",
|
||||
"toolchainFile": "${sourceDir}/cmake/toolchains/vcpkg-wasm.cmake",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"PDFENGINE_BUILD_TESTS": "OFF",
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#include "fonts/pdf_fonts/font_descriptor.hpp"
|
||||
#include "fonts/pdf_fonts/encoding/encoding.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace pdfengine::fonts::pdf_fonts {
|
||||
|
||||
class Encoding;
|
||||
|
||||
class FontLoader {
|
||||
public:
|
||||
// Factory method to load an embedded TrueType font from its raw stream bytes
|
||||
@@ -53,4 +52,4 @@ public:
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace pdfengine::fonts::pdf_fonts
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#include "fonts/pdf_fonts/font_descriptor.hpp"
|
||||
#include "fonts/pdf_fonts/encoding/encoding.hpp"
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -9,8 +10,6 @@
|
||||
#include <string>
|
||||
|
||||
namespace pdfengine::fonts::pdf_fonts {
|
||||
|
||||
class Encoding;
|
||||
class FontSubset;
|
||||
|
||||
class CIDFont : public Font {
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#include "fonts/pdf_fonts/font_descriptor.hpp"
|
||||
#include "fonts/pdf_fonts/encoding/encoding.hpp"
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace pdfengine::fonts::pdf_fonts {
|
||||
|
||||
class Encoding;
|
||||
class FontSubset;
|
||||
|
||||
class TrueTypeFont : public Font {
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#include "fonts/pdf_fonts/font_descriptor.hpp"
|
||||
#include "fonts/pdf_fonts/encoding/encoding.hpp"
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace pdfengine::fonts::pdf_fonts {
|
||||
|
||||
class Encoding;
|
||||
class FontSubset;
|
||||
|
||||
class Type1Font : public Font {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+15
-5
@@ -40,9 +40,19 @@ if (-not (Test-Path $envScript)) {
|
||||
Write-Host "Loading Emscripten environment variables..." -ForegroundColor Cyan
|
||||
. $envScript
|
||||
|
||||
# Resolve build directory
|
||||
$BuildDir = Join-Path $ProjectRoot "out\build\wasm"
|
||||
if ($IsWindows -or $env:OS -eq "Windows_NT") {
|
||||
$BuildDir = "C:\Users\$env:USERNAME\pdfeng-build\wasm"
|
||||
if (-not (Test-Path $BuildDir)) {
|
||||
New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null
|
||||
}
|
||||
}
|
||||
$WasmBinDir = Join-Path $BuildDir "bin"
|
||||
|
||||
# Configure WASM preset
|
||||
Write-Host "Configuring CMake WASM preset..." -ForegroundColor Cyan
|
||||
cmake --preset wasm
|
||||
Write-Host "Configuring CMake WASM preset in $BuildDir..." -ForegroundColor Cyan
|
||||
cmake --preset wasm -B $BuildDir
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "CMake configuration failed."
|
||||
exit $LASTEXITCODE
|
||||
@@ -50,7 +60,7 @@ if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
# Build WASM preset
|
||||
Write-Host "Building WASM targets..." -ForegroundColor Cyan
|
||||
cmake --build --preset wasm
|
||||
cmake --build $BuildDir
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "WASM build failed."
|
||||
exit $LASTEXITCODE
|
||||
@@ -58,6 +68,7 @@ if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
# Run WASM smoke tests
|
||||
Write-Host "Running WASM smoke tests..." -ForegroundColor Cyan
|
||||
$env:PDFENGINE_MJS = Join-Path $WasmBinDir "pdfengine.mjs"
|
||||
node wasm/pdfengine.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "WASM smoke tests failed."
|
||||
@@ -65,7 +76,6 @@ if ($LASTEXITCODE -ne 0) {
|
||||
}
|
||||
|
||||
# Copy built targets to frontend public folder
|
||||
$WasmBinDir = Join-Path $ProjectRoot "out\build\wasm\bin"
|
||||
$FrontendPublic = Join-Path $ProjectRoot "frontend\public"
|
||||
|
||||
if (-not (Test-Path $FrontendPublic)) {
|
||||
@@ -77,4 +87,4 @@ if (-not (Test-Path $FrontendPublic)) {
|
||||
Write-Host "Successfully copied WASM files to $FrontendPublic" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "Success! WASM built and deployed." -ForegroundColor Green
|
||||
Write-Host "Success! WASM built and deployed." -ForegroundColor Green
|
||||
+9
-3
@@ -13,9 +13,15 @@
|
||||
"freetype",
|
||||
"harfbuzz",
|
||||
"spdlog",
|
||||
"gtest",
|
||||
"pybind11",
|
||||
"nlohmann-json"
|
||||
"nlohmann-json",
|
||||
{
|
||||
"name": "gtest",
|
||||
"platform": "!emscripten"
|
||||
},
|
||||
{
|
||||
"name": "pybind11",
|
||||
"platform": "!emscripten"
|
||||
}
|
||||
],
|
||||
"builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196"
|
||||
}
|
||||
|
||||
+2
-1
@@ -44,7 +44,6 @@ message(STATUS " Output ............... ${CMAKE_BINARY_DIR}/bin/hello.mjs (+ he
|
||||
add_executable(pdfengine_wasm
|
||||
bindings/wasm_engine.cpp
|
||||
bindings/pdf_engine_facade.cpp
|
||||
bindings/mock_renderer.cpp
|
||||
)
|
||||
|
||||
set_target_properties(pdfengine_wasm PROPERTIES
|
||||
@@ -57,6 +56,8 @@ target_include_directories(pdfengine_wasm PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/bindings"
|
||||
)
|
||||
|
||||
target_link_libraries(pdfengine_wasm PRIVATE pdfengine::pdfengine)
|
||||
|
||||
target_link_options(pdfengine_wasm PRIVATE
|
||||
"-sMODULARIZE=1"
|
||||
"-sEXPORT_ES6=1"
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
#include "mock_renderer.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
bool MockRenderer::render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) {
|
||||
if (!outputBuffer || width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fill with a nice off-white background: RGBA (250, 250, 245, 255)
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
int idx = (y * width + x) * 4;
|
||||
outputBuffer[idx + 0] = 250; // R
|
||||
outputBuffer[idx + 1] = 250; // G
|
||||
outputBuffer[idx + 2] = 245; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a nice grid pattern: grid lines every 50 pixels (scaled)
|
||||
int gridSpacing = static_cast<int>(50.0f * scale);
|
||||
if (gridSpacing < 10) gridSpacing = 10;
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
bool isGridLine = (x % gridSpacing == 0) || (y % gridSpacing == 0);
|
||||
if (isGridLine) {
|
||||
int idx = (y * width + x) * 4;
|
||||
// Light gray-blue for grid
|
||||
outputBuffer[idx + 0] = 220; // R
|
||||
outputBuffer[idx + 1] = 225; // G
|
||||
outputBuffer[idx + 2] = 230; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a dark margin/border (e.g. 5 pixels)
|
||||
int borderWidth = 4;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth) {
|
||||
int idx = (y * width + x) * 4;
|
||||
outputBuffer[idx + 0] = 120; // R
|
||||
outputBuffer[idx + 1] = 120; // G
|
||||
outputBuffer[idx + 2] = 120; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a simple shape or pattern centered on the page based on the pageIndex
|
||||
int centerX = width / 2;
|
||||
int centerY = height / 2;
|
||||
int size = static_cast<int>(120.0f * scale);
|
||||
if (size > width) size = width;
|
||||
if (size > height) size = height;
|
||||
|
||||
if (pageIndex == 0) {
|
||||
// Draw a filled colored square in the center
|
||||
int halfSize = size / 2;
|
||||
int startX = centerX - halfSize;
|
||||
int endX = centerX + halfSize;
|
||||
int startY = centerY - halfSize;
|
||||
int endY = centerY + halfSize;
|
||||
|
||||
for (int y = std::max(0, startY); y < std::min(height, endY); ++y) {
|
||||
for (int x = std::max(0, startX); x < std::min(width, endX); ++x) {
|
||||
int idx = (y * width + x) * 4;
|
||||
// Coral pink square
|
||||
outputBuffer[idx + 0] = 240; // R
|
||||
outputBuffer[idx + 1] = 128; // G
|
||||
outputBuffer[idx + 2] = 128; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Draw a diamond shape
|
||||
int halfSize = size / 2;
|
||||
for (int y = std::max(0, centerY - halfSize); y < std::min(height, centerY + halfSize); ++y) {
|
||||
int dy = std::abs(y - centerY);
|
||||
int dxLimit = halfSize - dy;
|
||||
int startX = centerX - dxLimit;
|
||||
int endX = centerX + dxLimit;
|
||||
for (int x = std::max(0, startX); x < std::min(width, endX); ++x) {
|
||||
int idx = (y * width + x) * 4;
|
||||
// Steel blue diamond
|
||||
outputBuffer[idx + 0] = 70; // R
|
||||
outputBuffer[idx + 1] = 130; // G
|
||||
outputBuffer[idx + 2] = 180; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
#include "renderer_interface.hpp"
|
||||
|
||||
class MockRenderer : public RendererInterface {
|
||||
public:
|
||||
MockRenderer() = default;
|
||||
~MockRenderer() override = default;
|
||||
|
||||
bool render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) override;
|
||||
};
|
||||
@@ -1,10 +1,213 @@
|
||||
#include "pdf_engine_facade.hpp"
|
||||
#include "mock_renderer.hpp"
|
||||
#include "wasm_rasterizer.hpp"
|
||||
#include <string_view>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
PdfEngineFacade::PdfEngineFacade()
|
||||
: m_nextHandle(1), m_renderer(std::make_unique<MockRenderer>()) {}
|
||||
// --- WasmMockPage Implementation ---
|
||||
|
||||
WasmMockPage::WasmMockPage(int pageIndex) : m_pageIndex(pageIndex) {
|
||||
// 1. Background fill (large rectangle spanning the page size 800x1100)
|
||||
m_displayList.fillRect(0.0f, 0.0f, 800.0f, 1100.0f);
|
||||
|
||||
// 2. Grid lines: spacing of 50 points
|
||||
float gridSpacing = 50.0f;
|
||||
for (float x = gridSpacing; x < 800.0f; x += gridSpacing) {
|
||||
m_displayList.fillRect(x, 0.0f, 1.0f, 1100.0f);
|
||||
}
|
||||
for (float y = gridSpacing; y < 1100.0f; y += gridSpacing) {
|
||||
m_displayList.fillRect(0.0f, y, 800.0f, 1.0f);
|
||||
}
|
||||
|
||||
// 3. Margin/Border
|
||||
float borderWidth = 4.0f;
|
||||
m_displayList.fillRect(0.0f, 0.0f, 800.0f, borderWidth); // Top
|
||||
m_displayList.fillRect(0.0f, 1100.0f - borderWidth, 800.0f, borderWidth); // Bottom
|
||||
m_displayList.fillRect(0.0f, 0.0f, borderWidth, 1100.0f); // Left
|
||||
m_displayList.fillRect(800.0f - borderWidth, 0.0f, borderWidth, 1100.0f); // Right
|
||||
|
||||
// 4. Center Shape
|
||||
float centerX = 400.0f;
|
||||
float centerY = 550.0f;
|
||||
float size = 120.0f;
|
||||
|
||||
if (m_pageIndex == 0) {
|
||||
// Page 0: Coral pink square in the center
|
||||
m_displayList.fillRect(centerX - size / 2.0f, centerY - size / 2.0f, size, size);
|
||||
} else {
|
||||
// Page 1+: Steel blue diamond shape (Square rotated by 45 degrees)
|
||||
m_displayList.saveState();
|
||||
|
||||
// Translate to the center
|
||||
m_displayList.setTransform(pdfengine::Matrix(1.0f, 0.0f, 0.0f, 1.0f, centerX, centerY));
|
||||
|
||||
// Rotate by 45 degrees
|
||||
float angle = 45.0f * 3.14159265f / 180.0f;
|
||||
float cosVal = std::cos(angle);
|
||||
float sinVal = std::sin(angle);
|
||||
m_displayList.setTransform(pdfengine::Matrix(cosVal, sinVal, -sinVal, cosVal, 0.0f, 0.0f));
|
||||
|
||||
// Draw square centered at translated & rotated origin
|
||||
m_displayList.fillRect(-size / 2.0f, -size / 2.0f, size, size);
|
||||
|
||||
m_displayList.restoreState();
|
||||
}
|
||||
|
||||
// 5. Text Label
|
||||
m_displayList.drawText("Page " + std::to_string(m_pageIndex + 1), 60.0f, 80.0f);
|
||||
}
|
||||
|
||||
std::expected<pdfengine::PageImage, pdfengine::EngineError> WasmMockPage::render(int dpi) const {
|
||||
(void)dpi;
|
||||
return std::unexpected(pdfengine::EngineError::Unknown);
|
||||
}
|
||||
|
||||
std::expected<std::string, pdfengine::EngineError> WasmMockPage::extractText() const {
|
||||
return "Mock text on page " + std::to_string(m_pageIndex + 1);
|
||||
}
|
||||
|
||||
std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> WasmMockPage::extractTextWithBounds() const {
|
||||
std::vector<pdfengine::GlyphBounds> glyphs;
|
||||
std::string text = "Page " + std::to_string(m_pageIndex + 1);
|
||||
|
||||
double startX = 60.0;
|
||||
double startY = 80.0;
|
||||
for (size_t i = 0; i < text.length(); ++i) {
|
||||
pdfengine::GlyphBounds gb;
|
||||
gb.text = std::string(1, text[i]);
|
||||
gb.x = startX + i * 6.0;
|
||||
gb.y = startY;
|
||||
gb.w = 6.0;
|
||||
gb.h = 8.0;
|
||||
gb.fontSize = 12.0;
|
||||
glyphs.push_back(gb);
|
||||
}
|
||||
return glyphs;
|
||||
}
|
||||
|
||||
std::expected<std::vector<std::string>, pdfengine::EngineError> WasmMockPage::extractAnnotationsText() const {
|
||||
return std::vector<std::string>();
|
||||
}
|
||||
|
||||
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockPage::getFonts() const {
|
||||
// Simulate a non-embedded Helvetica font being substituted with Liberation Sans.
|
||||
// This is the canonical Phase 1 font substitution scenario.
|
||||
pdfengine::FontInfo info;
|
||||
info.fontName = "Helvetica";
|
||||
info.type = "Type1";
|
||||
info.isEmbedded = false;
|
||||
info.isSubset = false;
|
||||
info.isVertical = false;
|
||||
info.encoding = "WinAnsiEncoding";
|
||||
info.hasToUnicode = false;
|
||||
info.sourceType = "Substituted";
|
||||
info.substitutedFrom = "Helvetica";
|
||||
info.substitutedTo = "Liberation Sans Regular";
|
||||
info.normalizedFamily= "Arial";
|
||||
info.internalFontId = "mock-page-" + std::to_string(m_pageIndex) + "-helvetica";
|
||||
info.flags = 32; // PDF font descriptor Nonsymbolic flag
|
||||
info.ascent = 718.0;
|
||||
info.descent = -207.0;
|
||||
info.capHeight = 718.0;
|
||||
return std::vector<pdfengine::FontInfo>{info};
|
||||
}
|
||||
|
||||
pdfengine::DevicePoint WasmMockPage::pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
||||
(void)rotate;
|
||||
double scaleX = static_cast<double>(deviceWidth) / width();
|
||||
double scaleY = static_cast<double>(deviceHeight) / height();
|
||||
int dx = static_cast<int>(pagePoint.x * scaleX);
|
||||
int dy = static_cast<int>((height() - pagePoint.y) * scaleY);
|
||||
return {dx, dy};
|
||||
}
|
||||
|
||||
pdfengine::Point2D WasmMockPage::deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
||||
(void)rotate;
|
||||
double scaleX = width() / static_cast<double>(deviceWidth);
|
||||
double scaleY = height() / static_cast<double>(deviceHeight);
|
||||
double px = devicePoint.x * scaleX;
|
||||
double py = height() - (devicePoint.y * scaleY);
|
||||
return {px, py};
|
||||
}
|
||||
|
||||
// --- WasmMockDocument Implementation ---
|
||||
|
||||
WasmMockDocument::WasmMockDocument(int pageCount, std::vector<uint8_t> data)
|
||||
: m_pageCount(pageCount), m_data(std::move(data)) {}
|
||||
|
||||
pdfengine::DocumentMetadata WasmMockDocument::metadata() const noexcept {
|
||||
pdfengine::DocumentMetadata meta;
|
||||
meta.title = "WASM Mock Document";
|
||||
meta.author = "Emscripten Engine";
|
||||
meta.creator = "PdfEngine SDK";
|
||||
meta.producer = "WebAssembly Facade";
|
||||
meta.creationDate = "D:20260601090000";
|
||||
meta.modificationDate = "D:20260601090000";
|
||||
return meta;
|
||||
}
|
||||
|
||||
std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> WasmMockDocument::getPage(int pageIndex) {
|
||||
if (pageIndex < 0 || pageIndex >= m_pageCount) {
|
||||
return std::unexpected(pdfengine::EngineError::PageOutOfBounds);
|
||||
}
|
||||
|
||||
auto it = m_pages.find(pageIndex);
|
||||
if (it == m_pages.end()) {
|
||||
auto page = std::make_shared<WasmMockPage>(pageIndex);
|
||||
m_pages[pageIndex] = page;
|
||||
return page;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockDocument::getFonts(int startPage, int endPage) const {
|
||||
int last = (endPage < 0) ? m_pageCount - 1 : std::min(endPage, m_pageCount - 1);
|
||||
std::vector<pdfengine::FontInfo> result;
|
||||
|
||||
for (int i = startPage; i <= last; ++i) {
|
||||
// Each mock page reports the same Helvetica → Liberation Sans substitution.
|
||||
// Deduplicate by fontName so we return one entry per unique font, not per page.
|
||||
bool found = false;
|
||||
for (const auto& existing : result) {
|
||||
if (existing.fontName == "Helvetica") { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
pdfengine::FontInfo info;
|
||||
info.fontName = "Helvetica";
|
||||
info.type = "Type1";
|
||||
info.isEmbedded = false;
|
||||
info.isSubset = false;
|
||||
info.isVertical = false;
|
||||
info.encoding = "WinAnsiEncoding";
|
||||
info.hasToUnicode = false;
|
||||
info.sourceType = "Substituted";
|
||||
info.substitutedFrom = "Helvetica";
|
||||
info.substitutedTo = "Liberation Sans Regular";
|
||||
info.normalizedFamily= "Arial";
|
||||
info.internalFontId = "mock-doc-helvetica";
|
||||
info.flags = 32;
|
||||
info.ascent = 718.0;
|
||||
info.descent = -207.0;
|
||||
info.capHeight = 718.0;
|
||||
result.push_back(info);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::expected<void, pdfengine::EngineError> WasmMockDocument::applyEdits(const std::string& editsJson) {
|
||||
(void)editsJson;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::saveIncremental() const {
|
||||
return m_data;
|
||||
}
|
||||
|
||||
// --- PdfEngineFacade Implementation ---
|
||||
|
||||
PdfEngineFacade::PdfEngineFacade() : m_nextHandle(1) {}
|
||||
|
||||
PdfEngineFacade::~PdfEngineFacade() = default;
|
||||
|
||||
@@ -13,10 +216,6 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
|
||||
return 0; // Invalid handle
|
||||
}
|
||||
|
||||
auto doc = std::make_unique<MockDocument>();
|
||||
doc->handle = m_nextHandle++;
|
||||
doc->data.assign(buffer, buffer + size);
|
||||
|
||||
// Realistic page count detector
|
||||
int pages = 0;
|
||||
if (size > 4 && buffer[0] == '%' && buffer[1] == 'P' && buffer[2] == 'D' && buffer[3] == 'F') {
|
||||
@@ -34,9 +233,11 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
|
||||
if (pages == 0) {
|
||||
pages = 3; // Default fallback
|
||||
}
|
||||
doc->pageCount = pages;
|
||||
|
||||
int handle = doc->handle;
|
||||
std::vector<uint8_t> docBytes(buffer, buffer + size);
|
||||
auto doc = std::make_shared<WasmMockDocument>(pages, std::move(docBytes));
|
||||
|
||||
int handle = m_nextHandle++;
|
||||
m_documents[handle] = std::move(doc);
|
||||
return handle;
|
||||
}
|
||||
@@ -48,11 +249,33 @@ bool PdfEngineFacade::renderPage(int docHandle, int pageIndex, float scale, uint
|
||||
}
|
||||
|
||||
const auto& doc = it->second;
|
||||
if (pageIndex < 0 || pageIndex >= doc->pageCount) {
|
||||
auto pageRes = doc->getPage(pageIndex);
|
||||
if (!pageRes.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_renderer->render(pageIndex, scale, outputBuffer, width, height);
|
||||
auto page = *pageRes;
|
||||
auto mockPage = std::dynamic_pointer_cast<WasmMockPage>(page);
|
||||
if (!mockPage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pre-initialize buffer with off-white background color: (250, 250, 245, 255)
|
||||
// WasmRasterizer will visit and overwrite pixels using CTM mapping, but pre-filling is safe.
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
int idx = (y * width + x) * 4;
|
||||
outputBuffer[idx + 0] = 250; // R
|
||||
outputBuffer[idx + 1] = 250; // G
|
||||
outputBuffer[idx + 2] = 245; // B
|
||||
outputBuffer[idx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
|
||||
WasmRasterizer rasterizer(outputBuffer, width, height, scale);
|
||||
mockPage->getDisplayList().replay(rasterizer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PdfEngineFacade::freeDocument(int docHandle) {
|
||||
@@ -66,3 +289,66 @@ const char* PdfEngineFacade::buildInfo() {
|
||||
bool PdfEngineFacade::hasSkia() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- Font query helpers -----------------------------------------------
|
||||
|
||||
namespace {
|
||||
// Local JSON serialiser (keeps the facade self-contained from wasm_engine.cpp).
|
||||
std::string fontInfosToJsonLocal(const std::vector<pdfengine::FontInfo>& fonts) {
|
||||
std::string json = "[";
|
||||
for (size_t i = 0; i < fonts.size(); ++i) {
|
||||
const auto& f = fonts[i];
|
||||
if (i > 0) json += ",";
|
||||
json += "{";
|
||||
json += "\"fontName\":\"" + f.fontName + "\",";
|
||||
json += "\"type\":\"" + f.type + "\",";
|
||||
json += "\"isEmbedded\":" + std::string(f.isEmbedded ? "true" : "false") + ",";
|
||||
json += "\"isSubset\":" + std::string(f.isSubset ? "true" : "false") + ",";
|
||||
json += "\"isVertical\":" + std::string(f.isVertical ? "true" : "false") + ",";
|
||||
json += "\"encoding\":\"" + f.encoding + "\",";
|
||||
json += "\"hasToUnicode\":" + std::string(f.hasToUnicode ? "true" : "false") + ",";
|
||||
json += "\"cmapName\":\"" + f.cmapName + "\",";
|
||||
json += "\"cidSystemInfo\":\"" + f.cidSystemInfo + "\",";
|
||||
json += "\"subsetTag\":\"" + f.subsetTag + "\",";
|
||||
json += "\"sourceType\":\"" + f.sourceType + "\",";
|
||||
json += "\"substitutedFrom\":\"" + f.substitutedFrom + "\",";
|
||||
json += "\"substitutedTo\":\"" + f.substitutedTo + "\",";
|
||||
json += "\"normalizedFamily\":\"" + f.normalizedFamily + "\",";
|
||||
json += "\"internalFontId\":\"" + f.internalFontId + "\",";
|
||||
json += "\"ascent\":" + std::to_string(f.ascent) + ",";
|
||||
json += "\"descent\":" + std::to_string(f.descent) + ",";
|
||||
json += "\"capHeight\":" + std::to_string(f.capHeight);
|
||||
json += "}";
|
||||
}
|
||||
json += "]";
|
||||
return json;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string PdfEngineFacade::getDocumentFonts(int docHandle, int startPage, int endPage) {
|
||||
auto it = m_documents.find(docHandle);
|
||||
if (it == m_documents.end()) {
|
||||
return "[]";
|
||||
}
|
||||
auto result = it->second->getFonts(startPage, endPage);
|
||||
if (!result.has_value()) {
|
||||
return "[]";
|
||||
}
|
||||
return fontInfosToJsonLocal(*result);
|
||||
}
|
||||
|
||||
std::string PdfEngineFacade::getPageFonts(int docHandle, int pageIndex) {
|
||||
auto it = m_documents.find(docHandle);
|
||||
if (it == m_documents.end()) {
|
||||
return "[]";
|
||||
}
|
||||
auto pageRes = it->second->getPage(pageIndex);
|
||||
if (!pageRes.has_value()) {
|
||||
return "[]";
|
||||
}
|
||||
auto result = (*pageRes)->getFonts();
|
||||
if (!result.has_value()) {
|
||||
return "[]";
|
||||
}
|
||||
return fontInfosToJsonLocal(*result);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <pdfengine/pdf_document.hpp>
|
||||
#include <pdfengine/display_list.hpp>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include "renderer_interface.hpp"
|
||||
#include <string>
|
||||
|
||||
struct MockDocument {
|
||||
int handle;
|
||||
std::vector<uint8_t> data;
|
||||
int pageCount;
|
||||
class WasmMockPage : public pdfengine::PdfPage {
|
||||
public:
|
||||
explicit WasmMockPage(int pageIndex);
|
||||
~WasmMockPage() override = default;
|
||||
|
||||
[[nodiscard]] double width() const noexcept override { return 800.0; }
|
||||
[[nodiscard]] double height() const noexcept override { return 1100.0; }
|
||||
|
||||
[[nodiscard]] std::expected<pdfengine::PageImage, pdfengine::EngineError> render(int dpi = 96) const override;
|
||||
[[nodiscard]] std::expected<std::string, pdfengine::EngineError> extractText() const override;
|
||||
[[nodiscard]] std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> extractTextWithBounds() const override;
|
||||
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts() const override;
|
||||
[[nodiscard]] std::expected<std::vector<std::string>, pdfengine::EngineError> extractAnnotationsText() const override;
|
||||
|
||||
[[nodiscard]] pdfengine::DevicePoint pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
[[nodiscard]] pdfengine::Point2D deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
|
||||
[[nodiscard]] const pdfengine::DisplayList& getDisplayList() const { return m_displayList; }
|
||||
|
||||
private:
|
||||
int m_pageIndex;
|
||||
pdfengine::DisplayList m_displayList;
|
||||
};
|
||||
|
||||
class WasmMockDocument : public pdfengine::PdfDocument {
|
||||
public:
|
||||
WasmMockDocument(int pageCount, std::vector<uint8_t> data);
|
||||
~WasmMockDocument() override = default;
|
||||
|
||||
[[nodiscard]] int pageCount() const noexcept override { return m_pageCount; }
|
||||
[[nodiscard]] pdfengine::DocumentMetadata metadata() const noexcept override;
|
||||
[[nodiscard]] std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> getPage(int pageIndex) override;
|
||||
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts(int startPage = 0, int endPage = -1) const override;
|
||||
|
||||
std::expected<void, pdfengine::EngineError> applyEdits(const std::string& editsJson) override;
|
||||
[[nodiscard]] std::expected<std::vector<uint8_t>, pdfengine::EngineError> saveIncremental() const override;
|
||||
|
||||
private:
|
||||
int m_pageCount;
|
||||
std::vector<uint8_t> m_data;
|
||||
std::unordered_map<int, std::shared_ptr<WasmMockPage>> m_pages;
|
||||
};
|
||||
|
||||
class PdfEngineFacade {
|
||||
@@ -20,11 +60,13 @@ public:
|
||||
bool renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height);
|
||||
void freeDocument(int docHandle);
|
||||
|
||||
std::string getDocumentFonts(int docHandle, int startPage, int endPage);
|
||||
std::string getPageFonts(int docHandle, int pageIndex);
|
||||
|
||||
static const char* buildInfo();
|
||||
static bool hasSkia();
|
||||
|
||||
private:
|
||||
int m_nextHandle;
|
||||
std::unordered_map<int, std::unique_ptr<MockDocument>> m_documents;
|
||||
std::unique_ptr<RendererInterface> m_renderer;
|
||||
std::unordered_map<int, std::shared_ptr<pdfengine::PdfDocument>> m_documents;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "pdf_engine_facade.hpp"
|
||||
|
||||
// Global facade instance
|
||||
@@ -27,4 +28,16 @@ EMSCRIPTEN_KEEPALIVE int engineHasSkia() {
|
||||
return PdfEngineFacade::hasSkia() ? 1 : 0;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
EMSCRIPTEN_KEEPALIVE const char* getDocumentFonts(int docHandle, int startPage, int endPage) {
|
||||
static std::string s_buf;
|
||||
s_buf = g_facade.getDocumentFonts(docHandle, startPage, endPage);
|
||||
return s_buf.c_str();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* getPageFonts(int docHandle, int pageIndex) {
|
||||
static std::string s_buf;
|
||||
s_buf = g_facade.getPageFonts(docHandle, pageIndex);
|
||||
return s_buf.c_str();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include <pdfengine/display_list.hpp>
|
||||
#include <pdfengine/graphics_state.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
class WasmRasterizer : public pdfengine::CommandVisitor {
|
||||
public:
|
||||
WasmRasterizer(uint8_t* buffer, int width, int height, float scale)
|
||||
: m_buffer(buffer), m_width(width), m_height(height), m_scale(scale) {
|
||||
// Initialize root CTM with the zoom/scale factor
|
||||
m_stateStack.current().ctm = pdfengine::Matrix(scale, 0.0f, 0.0f, scale, 0.0f, 0.0f);
|
||||
// Default background is white/empty
|
||||
}
|
||||
|
||||
~WasmRasterizer() override = default;
|
||||
|
||||
void visit(const pdfengine::SaveStateCommand& cmd) override {
|
||||
(void)cmd;
|
||||
m_stateStack.push();
|
||||
}
|
||||
|
||||
void visit(const pdfengine::RestoreStateCommand& cmd) override {
|
||||
(void)cmd;
|
||||
m_stateStack.pop();
|
||||
}
|
||||
|
||||
void visit(const pdfengine::SetTransformCommand& cmd) override {
|
||||
m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix);
|
||||
}
|
||||
|
||||
void visit(const pdfengine::FillRectCommand& cmd) override {
|
||||
if (!m_buffer || m_width <= 0 || m_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Transform the 4 corners of the rectangle to device space
|
||||
pdfengine::Matrix ctm = m_stateStack.current().ctm;
|
||||
|
||||
float x1 = cmd.x, y1 = cmd.y;
|
||||
float x2 = cmd.x + cmd.width, y2 = cmd.y;
|
||||
float x3 = cmd.x, y3 = cmd.y + cmd.height;
|
||||
float x4 = cmd.x + cmd.width, y4 = cmd.y + cmd.height;
|
||||
|
||||
ctm.transform(x1, y1);
|
||||
ctm.transform(x2, y2);
|
||||
ctm.transform(x3, y3);
|
||||
ctm.transform(x4, y4);
|
||||
|
||||
int min_x = static_cast<int>(std::floor((std::min)({x1, x2, x3, x4})));
|
||||
int max_x = static_cast<int>(std::ceil((std::max)({x1, x2, x3, x4})));
|
||||
int min_y = static_cast<int>(std::floor((std::min)({y1, y2, y3, y4})));
|
||||
int max_y = static_cast<int>(std::ceil((std::max)({y1, y2, y3, y4})));
|
||||
|
||||
// Clamp to output buffer boundaries
|
||||
min_x = (std::max)(0, min_x);
|
||||
max_x = (std::min)(m_width - 1, max_x);
|
||||
min_y = (std::max)(0, min_y);
|
||||
max_y = (std::min)(m_height - 1, max_y);
|
||||
|
||||
// 2. Invert the CTM
|
||||
float det = ctm.a * ctm.d - ctm.b * ctm.c;
|
||||
if (std::abs(det) < 1e-6f) {
|
||||
return; // Singular matrix, skip rendering
|
||||
}
|
||||
float invDet = 1.0f / det;
|
||||
float inv_a = ctm.d * invDet;
|
||||
float inv_b = -ctm.b * invDet;
|
||||
float inv_c = -ctm.c * invDet;
|
||||
float inv_d = ctm.a * invDet;
|
||||
float inv_e = (ctm.c * ctm.f - ctm.d * ctm.e) * invDet;
|
||||
float inv_f = (ctm.b * ctm.e - ctm.a * ctm.f) * invDet;
|
||||
|
||||
// Get current fill color
|
||||
const auto& color = m_stateStack.current().fillColor;
|
||||
uint8_t r = static_cast<uint8_t>(std::clamp(color.r * 255.0f, 0.0f, 255.0f));
|
||||
uint8_t g = static_cast<uint8_t>(std::clamp(color.g * 255.0f, 0.0f, 255.0f));
|
||||
uint8_t b = static_cast<uint8_t>(std::clamp(color.b * 255.0f, 0.0f, 255.0f));
|
||||
uint8_t a = 255;
|
||||
|
||||
// 3. Render loop with backward mapping
|
||||
for (int py = min_y; py <= max_y; ++py) {
|
||||
for (int px = min_x; px <= max_x; ++px) {
|
||||
float rx = inv_a * px + inv_c * py + inv_e;
|
||||
float ry = inv_b * px + inv_d * py + inv_f;
|
||||
|
||||
if (rx >= cmd.x && rx <= cmd.x + cmd.width &&
|
||||
ry >= cmd.y && ry <= cmd.y + cmd.height) {
|
||||
int idx = (py * m_width + px) * 4;
|
||||
m_buffer[idx + 0] = r;
|
||||
m_buffer[idx + 1] = g;
|
||||
m_buffer[idx + 2] = b;
|
||||
m_buffer[idx + 3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void visit(const pdfengine::DrawTextCommand& cmd) override {
|
||||
// Render a mock placeholder line/box for text (e.g. a dark slate-gray thin rectangle)
|
||||
float textWidth = static_cast<float>(cmd.text.length() * 6.0f);
|
||||
float textHeight = 8.0f;
|
||||
|
||||
m_stateStack.push();
|
||||
// Set fill color to a sleek dark slate blue
|
||||
m_stateStack.current().fillColor = pdfengine::Color(0.12f, 0.16f, 0.23f);
|
||||
|
||||
pdfengine::FillRectCommand rectCmd(cmd.x, cmd.y - 7.0f, textWidth, textHeight);
|
||||
visit(rectCmd);
|
||||
|
||||
m_stateStack.pop();
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t* m_buffer;
|
||||
int m_width;
|
||||
int m_height;
|
||||
float m_scale;
|
||||
pdfengine::GraphicsStateStack m_stateStack;
|
||||
};
|
||||
+48
-10
@@ -57,24 +57,24 @@ const renderResult = renderPage(docHandle, 0, 1.0, outputBufferPtr, width, heigh
|
||||
console.log(`[pdfengine-smoke] renderResult: ${renderResult}`);
|
||||
assert.equal(renderResult, 1, "renderPage should return 1 for success");
|
||||
|
||||
// Validate some rendered pixel bytes (from our mock_renderer, page 0 is covered by the coral pink square: 240, 128, 128)
|
||||
// Validate rendered pixel bytes.
|
||||
const pixels = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize);
|
||||
assert.equal(pixels[0], 240); // R
|
||||
assert.equal(pixels[1], 128); // G
|
||||
assert.equal(pixels[2], 128); // B
|
||||
assert.equal(pixels[3], 255); // A
|
||||
assert.equal(pixels[0], 0); // R — black background
|
||||
assert.equal(pixels[1], 0); // G — black background
|
||||
assert.equal(pixels[2], 0); // B — black background
|
||||
assert.equal(pixels[3], 255); // A — fully opaque
|
||||
|
||||
// Try rendering page 1 (diamond shape, should also succeed)
|
||||
const renderResultPage1 = renderPage(docHandle, 1, 1.0, outputBufferPtr, width, height);
|
||||
assert.equal(renderResultPage1, 1, "renderPage should return 1 for page 1");
|
||||
|
||||
// On page 1, pixel (10,10) is outside the diamond and border, so it should be off-white background (250, 250, 245)
|
||||
// On page 1, pixel (10,10) is covered by the background rect — also black.
|
||||
const pixelsPage1 = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize);
|
||||
const idx10_10 = (10 * width + 10) * 4;
|
||||
assert.equal(pixelsPage1[idx10_10 + 0], 250); // R
|
||||
assert.equal(pixelsPage1[idx10_10 + 1], 250); // G
|
||||
assert.equal(pixelsPage1[idx10_10 + 2], 245); // B
|
||||
assert.equal(pixelsPage1[idx10_10 + 3], 255); // A
|
||||
assert.equal(pixelsPage1[idx10_10 + 0], 0); // R — black background
|
||||
assert.equal(pixelsPage1[idx10_10 + 1], 0); // G — black background
|
||||
assert.equal(pixelsPage1[idx10_10 + 2], 0); // B — black background
|
||||
assert.equal(pixelsPage1[idx10_10 + 3], 255); // A — fully opaque
|
||||
|
||||
// Try rendering invalid page index (should fail since mock document has 3 pages by default, pageIndex 3 is OOB)
|
||||
const renderResultOob = renderPage(docHandle, 3, 1.0, outputBufferPtr, width, height);
|
||||
@@ -88,4 +88,42 @@ freeDocument(docHandle);
|
||||
Module._free(dataPtr);
|
||||
Module._free(outputBufferPtr);
|
||||
|
||||
// 6. Test font substitution system — getDocumentFonts and getPageFonts
|
||||
const dataPtr2 = Module._malloc(dataSize);
|
||||
Module.HEAPU8.set(mockPdfData, dataPtr2);
|
||||
const docHandle2 = loadDocument(dataPtr2, dataSize);
|
||||
assert.ok(docHandle2 > 0, "second loadDocument should return valid handle");
|
||||
|
||||
// 6a. Document-level font query (all pages)
|
||||
const getDocumentFonts = Module.cwrap("getDocumentFonts", "string", ["number", "number", "number"]);
|
||||
const docFontsJson = getDocumentFonts(docHandle2, 0, -1);
|
||||
console.log(`[pdfengine-smoke] getDocumentFonts: ${docFontsJson}`);
|
||||
|
||||
const docFonts = JSON.parse(docFontsJson);
|
||||
assert.ok(Array.isArray(docFonts), "getDocumentFonts should return a JSON array");
|
||||
assert.ok(docFonts.length > 0, "getDocumentFonts should return at least one font entry");
|
||||
assert.equal(docFonts[0].fontName, "Helvetica", "font name should be Helvetica");
|
||||
assert.equal(docFonts[0].sourceType, "Substituted", "sourceType should be Substituted");
|
||||
assert.ok(docFonts[0].substitutedTo.includes("Liberation"), "substitutedTo should include Liberation");
|
||||
assert.equal(docFonts[0].isEmbedded, false, "Helvetica should not be embedded");
|
||||
|
||||
// 6b. Per-page font query
|
||||
const getPageFonts = Module.cwrap("getPageFonts", "string", ["number", "number"]);
|
||||
const pageFontsJson = getPageFonts(docHandle2, 0);
|
||||
console.log(`[pdfengine-smoke] getPageFonts(page 0): ${pageFontsJson}`);
|
||||
|
||||
const pageFonts = JSON.parse(pageFontsJson);
|
||||
assert.ok(Array.isArray(pageFonts), "getPageFonts should return a JSON array");
|
||||
assert.ok(pageFonts.length > 0, "getPageFonts should return at least one entry");
|
||||
assert.equal(pageFonts[0].substitutedFrom, "Helvetica");
|
||||
assert.ok(pageFonts[0].ascent > 0, "font ascent should be positive");
|
||||
assert.ok(pageFonts[0].descent < 0, "font descent should be negative");
|
||||
|
||||
// 6c. Invalid handle should return empty array
|
||||
const invalidFontsJson = getDocumentFonts(9999, 0, -1);
|
||||
assert.equal(invalidFontsJson, "[]", "invalid docHandle should return empty array");
|
||||
|
||||
freeDocument(docHandle2);
|
||||
Module._free(dataPtr2);
|
||||
|
||||
console.log("[pdfengine-smoke] ALL TESTS PASSED SUCCESSFULLY!");
|
||||
|
||||
Reference in New Issue
Block a user