mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-10 01:14:17 +01:00
test: cover the text editor engine and FreeText flattening
Adds a node-side harness that drives the engine wasm directly, fixtures that build PDFs in-memory, and cases for font fallback and flattening. The engine loader now tolerates a runtime without import.meta.resolve so the suite can load it outside a browser.
This commit is contained in:
1 parent
5392c1be76
commit
aee3e0a65a
10 files changed
+850
-24
No files matched your search
@@ -0,0 +1,166 @@
|
||||
export interface ModelRun {
|
||||
text: string;
|
||||
family: string;
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
size: number;
|
||||
rgba: number;
|
||||
underline: boolean;
|
||||
strike: boolean;
|
||||
script: number;
|
||||
renderMode: number;
|
||||
strokeRgba: number;
|
||||
strokeWidth: number;
|
||||
hScale: number;
|
||||
rise: number;
|
||||
fallback?: string;
|
||||
}
|
||||
|
||||
export interface ModelLine {
|
||||
y: number;
|
||||
x: number;
|
||||
w: number;
|
||||
off: number;
|
||||
px: number;
|
||||
}
|
||||
|
||||
export interface ModelBox {
|
||||
x: number;
|
||||
top: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface ModelFormat {
|
||||
align: number;
|
||||
lineSpacing: number;
|
||||
charSpacing: number;
|
||||
paraSpacing: number;
|
||||
wordSpacing: number;
|
||||
firstIndent: number;
|
||||
hangIndent: number;
|
||||
dir: number;
|
||||
listLevel: number;
|
||||
}
|
||||
|
||||
export interface ModelParagraph {
|
||||
id: number;
|
||||
editable: boolean;
|
||||
lockReason: number;
|
||||
box: ModelBox;
|
||||
rotation: number;
|
||||
marker: boolean;
|
||||
vertical: boolean;
|
||||
format: ModelFormat;
|
||||
firstBaseline: number;
|
||||
lines: ModelLine[];
|
||||
runs: ModelRun[];
|
||||
}
|
||||
|
||||
export interface CommitResult {
|
||||
ok?: boolean;
|
||||
box?: ModelBox;
|
||||
}
|
||||
|
||||
export interface Engine {
|
||||
pageCount: number;
|
||||
pageIndex: number;
|
||||
open(bytes: Uint8Array): void;
|
||||
loadPage(index: number): void;
|
||||
close(): void;
|
||||
buildModel(): ModelParagraph[];
|
||||
commitParagraph(
|
||||
id: number,
|
||||
runs: ModelRun[],
|
||||
fmt: ModelFormat
|
||||
): CommitResult | null;
|
||||
previewParagraph(
|
||||
id: number,
|
||||
runs: ModelRun[],
|
||||
fmt: ModelFormat
|
||||
): CommitResult | null;
|
||||
deleteParagraph(id: number): unknown;
|
||||
moveParagraph(id: number, dx: number, dy: number): unknown;
|
||||
saveSpliced(opts?: unknown): Promise<Uint8Array | null>;
|
||||
}
|
||||
|
||||
interface EditCoreModule {
|
||||
PdfEngine: { create(): Promise<Engine> };
|
||||
scriptFallbackFamilies(codepoints: number[]): string[];
|
||||
}
|
||||
|
||||
let cached: EditCoreModule | null = null;
|
||||
|
||||
export async function loadEditCore(): Promise<EditCoreModule> {
|
||||
if (!cached) {
|
||||
cached =
|
||||
(await import('../../js/editcore/core.js')) as unknown as EditCoreModule;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
export async function createEngine(): Promise<Engine> {
|
||||
const mod = await loadEditCore();
|
||||
return mod.PdfEngine.create();
|
||||
}
|
||||
|
||||
export function modelOf(
|
||||
engine: Engine,
|
||||
bytes: Uint8Array,
|
||||
pageIndex = 0
|
||||
): ModelParagraph[] {
|
||||
engine.open(bytes);
|
||||
engine.loadPage(pageIndex);
|
||||
return engine.buildModel();
|
||||
}
|
||||
|
||||
export function paragraphText(p: ModelParagraph): string {
|
||||
return p.runs.map((r) => r.text).join('');
|
||||
}
|
||||
|
||||
export function allText(paragraphs: ModelParagraph[]): string {
|
||||
return paragraphs.map(paragraphText).join('\n');
|
||||
}
|
||||
|
||||
export function normalizedText(paragraphs: ModelParagraph[]): string {
|
||||
return allText(paragraphs).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
export function boxContains(outer: ModelBox, inner: ModelBox): boolean {
|
||||
const eps = 0.5;
|
||||
return (
|
||||
inner.x >= outer.x - eps &&
|
||||
inner.x + inner.w <= outer.x + outer.w + eps &&
|
||||
inner.top <= outer.top + eps &&
|
||||
inner.top - inner.h >= outer.top - outer.h - eps
|
||||
);
|
||||
}
|
||||
|
||||
export function boxOverlapArea(a: ModelBox, b: ModelBox): number {
|
||||
const w = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
|
||||
const h = Math.min(a.top, b.top) - Math.max(a.top - a.h, b.top - b.h);
|
||||
return w > 0 && h > 0 ? w * h : 0;
|
||||
}
|
||||
|
||||
export function overlappingPairs(
|
||||
paragraphs: ModelParagraph[],
|
||||
minRatio = 0.25
|
||||
): Array<[number, number]> {
|
||||
const hits: Array<[number, number]> = [];
|
||||
for (let i = 0; i < paragraphs.length; i++) {
|
||||
for (let j = i + 1; j < paragraphs.length; j++) {
|
||||
const a = paragraphs[i];
|
||||
const b = paragraphs[j];
|
||||
const area = boxOverlapArea(a.box, b.box);
|
||||
if (area <= 0) continue;
|
||||
const smallest = Math.min(a.box.w * a.box.h, b.box.w * b.box.h);
|
||||
if (smallest > 0 && area / smallest >= minRatio) hits.push([a.id, b.id]);
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function runsFrom(p: ModelParagraph, text: string): ModelRun[] {
|
||||
const base = p.runs[0];
|
||||
return [{ ...base, text }];
|
||||
}
|
||||
Reference in new issue
Block a user