Skip to content

Commit d7e3cbd

Browse files
authored
Merge pull request #1 from PSPDFKit-labs/fix/html-input-conversion
fix: normalize nutrient_extract_text language schema for OpenClaw
2 parents d070ca3 + 32eee8d commit d7e3cbd

File tree

5 files changed

+88
-11
lines changed

5 files changed

+88
-11
lines changed

src/files.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export function buildFormData(
115115

116116
for (const [key, ref] of fileRefs.entries()) {
117117
if (ref.file) {
118-
const blob = new Blob([new Uint8Array(ref.file.buffer)]);
118+
const blob = ref.mimeType
119+
? new Blob([new Uint8Array(ref.file.buffer)], { type: ref.mimeType })
120+
: new Blob([new Uint8Array(ref.file.buffer)]);
119121
formData.append(key, blob, ref.name);
120122
}
121123
}

src/tools/convert-to-pdf.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ import {
1313
deriveOutputPath,
1414
} from '../files.js';
1515

16+
function isHtmlInput(filePath: string): boolean {
17+
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
18+
try {
19+
return /\.html?$/i.test(new URL(filePath).pathname);
20+
} catch {
21+
return /\.html?$/i.test(filePath);
22+
}
23+
}
24+
25+
return /\.html?$/i.test(filePath);
26+
}
27+
1628
export const convertToPdfTool: ToolDefinition = {
1729
name: 'nutrient_convert_to_pdf',
1830
description:
@@ -74,12 +86,20 @@ export const convertToPdfTool: ToolDefinition = {
7486
const outputPath = args.outputPath || deriveOutputPath(args.filePath, 'pdf');
7587
assertOutputDiffersFromInput(args.filePath, outputPath, ctx.sandboxDir);
7688

89+
const inputIsHtml = isHtmlInput(args.filePath);
7790
const fileRef = readFileReference(args.filePath, ctx.sandboxDir);
78-
const fileRefs = new Map<string, FileReference>([[fileRef.key, fileRef]]);
91+
const fileRefs = new Map<string, FileReference>([
92+
[
93+
fileRef.key,
94+
inputIsHtml && fileRef.file
95+
? { ...fileRef, mimeType: 'text/html' }
96+
: fileRef,
97+
],
98+
]);
7999

80100
// Build the part entry
81101
const part: Record<string, unknown> = {
82-
file: fileRef.url ?? fileRef.key,
102+
[inputIsHtml ? 'html' : 'file']: fileRef.url ?? fileRef.key,
83103
};
84104
if (args.password) part.password = args.password;
85105
if (args.pages) part.pages = args.pages;

src/tools/extract-text.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,36 @@ export const nutrient_extract_text: ToolDefinition = {
4242
"'key-values' for detected key-value pairs (phone numbers, emails, dates, etc.)",
4343
},
4444
language: {
45-
type: ['string', 'array'],
45+
type: 'string',
4646
description:
4747
"OCR language(s) for text extraction (default: 'english'). " +
48-
'Can be a single language string or an array of languages.',
48+
"Use a single language or a comma-separated list (for example: 'english,german').",
4949
default: 'english',
5050
},
5151
},
5252
},
5353

5454
async execute(
55-
args: { filePath: string; mode?: ExtractionMode; language?: string | string[] },
55+
args: { filePath: string; mode?: ExtractionMode; language?: string },
5656
ctx: ToolContext,
5757
): Promise<ToolResponse> {
5858
try {
5959
const { filePath, mode = 'text', language = 'english' } = args;
60+
const normalizedLanguage =
61+
typeof language === 'string' && language.includes(',')
62+
? language
63+
.split(',')
64+
.map((s) => s.trim())
65+
.filter(Boolean)
66+
: language;
6067

6168
const fileRef = readFileReference(filePath, ctx.sandboxDir);
6269
const fileRefs = new Map([[fileRef.key, fileRef]]);
6370

6471
const output: Record<string, unknown> = {
6572
type: 'json-content',
6673
[MODE_FLAGS[mode]]: true,
67-
language,
74+
language: normalizedLanguage,
6875
};
6976

7077
const instructions = {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export interface FileReference {
9090
buffer: Buffer;
9191
path: string;
9292
};
93+
mimeType?: string;
9394
url?: string;
9495
name: string;
9596
}

test/tools/convert-to-pdf.test.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import type { ToolContext } from '../../src/types.js';
66
import fs from 'node:fs';
77
import path from 'node:path';
88

9+
function getBuildBody(ctx: ToolContext): FormData | Record<string, unknown> {
10+
const call = (ctx.client.post as any).mock.calls.at(-1);
11+
expect(call[0]).toBe('build');
12+
return call[1];
13+
}
14+
15+
function getInstructions(body: FormData | Record<string, unknown>): Record<string, any> {
16+
if (body instanceof FormData) {
17+
const instructionsRaw = body.get('instructions');
18+
expect(typeof instructionsRaw).toBe('string');
19+
return JSON.parse(instructionsRaw as string);
20+
}
21+
22+
return body as Record<string, any>;
23+
}
24+
925
describe('nutrient_convert_to_pdf', () => {
1026
let ctx: ToolContext;
1127

@@ -26,7 +42,10 @@ describe('nutrient_convert_to_pdf', () => {
2642
);
2743
expect(result.success).toBe(true);
2844
expect(result.output).toContain('output.pdf');
29-
expect(ctx.client.post).toHaveBeenCalledWith('build', expect.anything());
45+
const body = getBuildBody(ctx);
46+
const instructions = getInstructions(body);
47+
expect(instructions.parts[0].file).toBeTruthy();
48+
expect(instructions.parts[0].html).toBeUndefined();
3049
expect(fs.existsSync(path.join(ctx.sandboxDir!, 'output.pdf'))).toBe(true);
3150
});
3251

@@ -41,8 +60,8 @@ describe('nutrient_convert_to_pdf', () => {
4160
expect(call[0]).toBe('build');
4261
});
4362

44-
it('forwards HTML layout options', async () => {
45-
writeSandboxFile(ctx.sandboxDir!, 'page.html');
63+
it('uses part.html for local HTML and keeps htmlLayout on part.layout', async () => {
64+
writeSandboxFile(ctx.sandboxDir!, 'page.html', '<html><body>test</body></html>');
4665
await convertToPdfTool.execute(
4766
{
4867
filePath: 'page.html',
@@ -51,7 +70,35 @@ describe('nutrient_convert_to_pdf', () => {
5170
},
5271
ctx,
5372
);
54-
expect(ctx.client.post).toHaveBeenCalled();
73+
74+
const body = getBuildBody(ctx);
75+
expect(body).toBeInstanceOf(FormData);
76+
77+
const instructions = getInstructions(body);
78+
expect(instructions.parts[0].html).toBeTruthy();
79+
expect(instructions.parts[0].file).toBeUndefined();
80+
expect(instructions.parts[0].layout).toEqual({ orientation: 'landscape', size: 'A4' });
81+
82+
const htmlPart = (body as FormData).get(instructions.parts[0].html);
83+
expect(htmlPart).toBeTruthy();
84+
expect(typeof htmlPart).not.toBe('string');
85+
if (htmlPart && typeof htmlPart !== 'string') {
86+
expect(htmlPart.type).toBe('text/html');
87+
}
88+
});
89+
90+
it('uses part.html for HTML URLs', async () => {
91+
const htmlUrl = 'https://example.com/page.html?utm=test';
92+
await convertToPdfTool.execute(
93+
{ filePath: htmlUrl, outputPath: 'out.pdf' },
94+
ctx,
95+
);
96+
97+
const body = getBuildBody(ctx);
98+
expect(body).not.toBeInstanceOf(FormData);
99+
const instructions = getInstructions(body);
100+
expect(instructions.parts[0].html).toBe(htmlUrl);
101+
expect(instructions.parts[0].file).toBeUndefined();
55102
});
56103

57104
it('forwards page ranges', async () => {

0 commit comments

Comments
 (0)