@hcu-lab.me/mcp-device 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { getDocumentProxy, extractImages } from 'unpdf';
|
|
2
|
+
/**
|
|
3
|
+
* Optimized image extraction from PDF using unpdf's built-in extractImages method
|
|
4
|
+
* @param pdfBuffer PDF file as Uint8Array
|
|
5
|
+
* @param pageNumbers Optional array of specific page numbers to process
|
|
6
|
+
* @param compressionOptions Image compression settings
|
|
7
|
+
* @returns Record of page numbers to extracted images
|
|
8
|
+
*/
|
|
9
|
+
export async function extractImagesFromPdf(pdfBuffer, pageNumbers, compressionOptions = {}) {
|
|
10
|
+
const pdfDocument = await getDocumentProxy(pdfBuffer);
|
|
11
|
+
const pagesToProcess = pageNumbers || Array.from({ length: pdfDocument.numPages }, (_, i) => i + 1);
|
|
12
|
+
const pageResults = {};
|
|
13
|
+
try {
|
|
14
|
+
// Process pages in parallel batches for better performance
|
|
15
|
+
const batchSize = 5; // Process 5 pages at a time
|
|
16
|
+
const batches = [];
|
|
17
|
+
for (let i = 0; i < pagesToProcess.length; i += batchSize) {
|
|
18
|
+
batches.push(pagesToProcess.slice(i, i + batchSize));
|
|
19
|
+
}
|
|
20
|
+
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
|
21
|
+
const batch = batches[batchIndex];
|
|
22
|
+
const batchPromises = batch.map(async (pageNum) => {
|
|
23
|
+
if (pageNum < 1 || pageNum > pdfDocument.numPages) {
|
|
24
|
+
return { pageNum, images: [] };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// Use unpdf's built-in extractImages
|
|
28
|
+
const extractedImages = await extractImages(pdfDocument, pageNum);
|
|
29
|
+
const pageImages = [];
|
|
30
|
+
for (let index = 0; index < extractedImages.length; index++) {
|
|
31
|
+
const img = extractedImages[index];
|
|
32
|
+
const originalSize = img.data.length;
|
|
33
|
+
try {
|
|
34
|
+
const compressionResult = await convertRawImageToBase64(img.data, img.width, img.height, img.channels, compressionOptions);
|
|
35
|
+
if (compressionResult) {
|
|
36
|
+
pageImages.push({
|
|
37
|
+
objId: index, // Use index as objId since unpdf doesn't provide original objId
|
|
38
|
+
width: img.width,
|
|
39
|
+
height: img.height,
|
|
40
|
+
data: compressionResult.data,
|
|
41
|
+
mimeType: compressionResult.mimeType,
|
|
42
|
+
originalSize,
|
|
43
|
+
compressedSize: Math.round(compressionResult.data.length * 0.75) // Approximate base64 overhead
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// Ignore conversion errors as requested
|
|
49
|
+
console.warn(`Failed to convert image ${index} on page ${pageNum}:`, err instanceof Error ? err.message : String(err));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { pageNum, images: pageImages };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.warn(`Failed to extract images from page ${pageNum}:`, error instanceof Error ? error.message : String(error));
|
|
56
|
+
return { pageNum, images: [] };
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Wait for the current batch to complete
|
|
60
|
+
const batchResults = await Promise.all(batchPromises);
|
|
61
|
+
// Store results
|
|
62
|
+
for (const { pageNum, images } of batchResults) {
|
|
63
|
+
pageResults[pageNum] = images;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
// Clean up document
|
|
69
|
+
try {
|
|
70
|
+
if (typeof pdfDocument.cleanup === 'function') {
|
|
71
|
+
await pdfDocument.cleanup(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) { /* Ignore cleanup errors */ }
|
|
75
|
+
try {
|
|
76
|
+
if (typeof pdfDocument.destroy === 'function') {
|
|
77
|
+
await pdfDocument.destroy();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) { /* Ignore cleanup errors */ }
|
|
81
|
+
}
|
|
82
|
+
return pageResults;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert raw image data to compressed base64 using sharp
|
|
86
|
+
*/
|
|
87
|
+
async function convertRawImageToBase64(data, width, height, channels, options = {}) {
|
|
88
|
+
const { format = 'webp', quality = 85, maxDimension = 1200 } = options;
|
|
89
|
+
// Smart resizing - only resize large images
|
|
90
|
+
let targetWidth = width;
|
|
91
|
+
let targetHeight = height;
|
|
92
|
+
if (width > maxDimension || height > maxDimension) {
|
|
93
|
+
const scale = maxDimension / Math.max(width, height);
|
|
94
|
+
targetWidth = Math.round(width * scale);
|
|
95
|
+
targetHeight = Math.round(height * scale);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
// Try to dynamically import sharp
|
|
99
|
+
const sharp = (await import('sharp')).default;
|
|
100
|
+
// sharp takes Buffer, Uint8Array, etc.
|
|
101
|
+
// unpdf returns Uint8ClampedArray, which works with Buffer.from()
|
|
102
|
+
let pipeline = sharp(Buffer.from(data), {
|
|
103
|
+
raw: {
|
|
104
|
+
width,
|
|
105
|
+
height,
|
|
106
|
+
channels: channels
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
if (targetWidth !== width || targetHeight !== height) {
|
|
110
|
+
pipeline = pipeline.resize(targetWidth, targetHeight);
|
|
111
|
+
}
|
|
112
|
+
let outputBuffer;
|
|
113
|
+
let mimeType;
|
|
114
|
+
if (format === 'jpeg') {
|
|
115
|
+
outputBuffer = await pipeline.jpeg({ quality }).toBuffer();
|
|
116
|
+
mimeType = 'image/jpeg';
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Default to webp
|
|
120
|
+
outputBuffer = await pipeline.webp({ quality }).toBuffer();
|
|
121
|
+
mimeType = 'image/webp';
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
data: outputBuffer.toString('base64'),
|
|
125
|
+
mimeType
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn('Image conversion failed (likely missing sharp or invalid data):', error instanceof Error ? error.message : String(error));
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { editPdf } from './manipulations.js';
|
|
2
|
+
export type { PdfOperations, PdfInsertOperation, PdfDeleteOperation } from './manipulations.js';
|
|
3
|
+
export { parsePdfToMarkdown, parseMarkdownToPdf } from './markdown.js';
|
|
4
|
+
export type { PdfMetadata, PdfPageItem } from './lib/pdf2md.js';
|
|
5
|
+
export { extractImagesFromPdf } from './extract-images.js';
|
|
6
|
+
export type { ImageInfo, PageImages } from './extract-images.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ImageInfo } from '../extract-images.js';
|
|
2
|
+
/**
|
|
3
|
+
* PDF metadata structure
|
|
4
|
+
*/
|
|
5
|
+
export interface PdfMetadata {
|
|
6
|
+
fileSize?: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
title?: string;
|
|
9
|
+
author?: string;
|
|
10
|
+
creator?: string;
|
|
11
|
+
producer?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
creationDate?: string;
|
|
14
|
+
modificationDate?: string;
|
|
15
|
+
isEncrypted?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PdfPageItem {
|
|
18
|
+
text: string;
|
|
19
|
+
images: ImageInfo[];
|
|
20
|
+
pageNumber: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PdfParseResult {
|
|
23
|
+
pages: PdfPageItem[];
|
|
24
|
+
metadata: PdfMetadata;
|
|
25
|
+
}
|
|
26
|
+
export type PageRange = {
|
|
27
|
+
offset: number;
|
|
28
|
+
length: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Reads a PDF and converts it to Markdown, returning structured data.
|
|
32
|
+
* @param pdfBuffer The PDF buffer to convert.
|
|
33
|
+
* @param pageNumbers The page numbers to extract. If empty, all pages are extracted.
|
|
34
|
+
* @returns A Promise that resolves to a PdfParseResult object containing the parsed data.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pdf2md(pdfBuffer: Uint8Array, pageNumbers?: number[] | PageRange): Promise<PdfParseResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { generatePageNumbers } from '../utils.js';
|
|
3
|
+
import { extractImagesFromPdf } from '../extract-images.js';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { parse } = require('@opendocsg/pdf2md/lib/util/pdf');
|
|
6
|
+
const { makeTransformations, transform } = require('@opendocsg/pdf2md/lib/util/transformations');
|
|
7
|
+
/**
|
|
8
|
+
* Extracts metadata from a PDF document.
|
|
9
|
+
* @param pdfDocument The PDF document to extract metadata from.
|
|
10
|
+
* @returns A PdfMetadata object containing the extracted metadata.
|
|
11
|
+
*/
|
|
12
|
+
const extractMetadata = ({ pdfDocument, metadata }) => ({
|
|
13
|
+
totalPages: pdfDocument.numPages,
|
|
14
|
+
title: metadata.Title,
|
|
15
|
+
author: metadata.Author,
|
|
16
|
+
creator: metadata.Creator,
|
|
17
|
+
producer: metadata.Producer,
|
|
18
|
+
version: metadata.PDFFormatVersion,
|
|
19
|
+
creationDate: metadata.CreationDate,
|
|
20
|
+
modificationDate: metadata.ModDate,
|
|
21
|
+
isEncrypted: metadata.IsEncrypted,
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Reads a PDF and converts it to Markdown, returning structured data.
|
|
25
|
+
* @param pdfBuffer The PDF buffer to convert.
|
|
26
|
+
* @param pageNumbers The page numbers to extract. If empty, all pages are extracted.
|
|
27
|
+
* @returns A Promise that resolves to a PdfParseResult object containing the parsed data.
|
|
28
|
+
*/
|
|
29
|
+
export async function pdf2md(pdfBuffer, pageNumbers = []) {
|
|
30
|
+
const result = await parse(pdfBuffer);
|
|
31
|
+
const { fonts, pages, pdfDocument } = result;
|
|
32
|
+
// Calculate which pages to process
|
|
33
|
+
const filterPageNumbers = Array.isArray(pageNumbers) ?
|
|
34
|
+
pageNumbers :
|
|
35
|
+
generatePageNumbers(pageNumbers.offset, pageNumbers.length, pages.length);
|
|
36
|
+
// Filter and transform pages
|
|
37
|
+
const pagesToProcess = filterPageNumbers.length === 0 ?
|
|
38
|
+
pages :
|
|
39
|
+
pages.filter((_, index) => filterPageNumbers.includes(index + 1));
|
|
40
|
+
const pageNumberMap = filterPageNumbers.length === 0 ?
|
|
41
|
+
pages.map((_, index) => index + 1) :
|
|
42
|
+
filterPageNumbers.filter(pageNum => pageNum >= 1 && pageNum <= pages.length);
|
|
43
|
+
const transformations = makeTransformations(fonts.map);
|
|
44
|
+
const parseResult = transform(pagesToProcess, transformations);
|
|
45
|
+
// Extract images
|
|
46
|
+
const imagesByPage = await extractImagesFromPdf(pdfBuffer, pageNumberMap, { format: 'webp', quality: 85 });
|
|
47
|
+
// Create pages without images for now
|
|
48
|
+
const processedPages = parseResult.pages.map((page, index) => {
|
|
49
|
+
const pageNumber = pageNumberMap[index];
|
|
50
|
+
return {
|
|
51
|
+
pageNumber,
|
|
52
|
+
text: page.items.join('\n') + '\n',
|
|
53
|
+
images: imagesByPage[pageNumber] || [],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
const metadata = extractMetadata(result);
|
|
57
|
+
try {
|
|
58
|
+
return { pages: processedPages, metadata };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (pdfDocument) {
|
|
62
|
+
try {
|
|
63
|
+
if (typeof pdfDocument.cleanup === 'function') {
|
|
64
|
+
await pdfDocument.cleanup(false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) { }
|
|
68
|
+
try {
|
|
69
|
+
if (typeof pdfDocument.destroy === 'function') {
|
|
70
|
+
await pdfDocument.destroy();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) { }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PdfInsertOperationSchema, PdfDeleteOperationSchema, PdfOperationSchema } from '../schemas.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
type PdfInsertOperation = z.infer<typeof PdfInsertOperationSchema>;
|
|
4
|
+
type PdfDeleteOperation = z.infer<typeof PdfDeleteOperationSchema>;
|
|
5
|
+
type PdfOperations = z.infer<typeof PdfOperationSchema>;
|
|
6
|
+
export type { PdfOperations, PdfInsertOperation, PdfDeleteOperation };
|
|
7
|
+
/**
|
|
8
|
+
* Edit an existing PDF by deleting or inserting pages
|
|
9
|
+
* @param pdfPath Path to the PDF file to edit
|
|
10
|
+
* @param operations List of operations to perform
|
|
11
|
+
* @returns The modified PDF as a Uint8Array
|
|
12
|
+
*/
|
|
13
|
+
export declare function editPdf(pdfPath: string, operations: PdfOperations[]): Promise<Uint8Array>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { normalizePageIndexes } from './utils.js';
|
|
4
|
+
import { parseMarkdownToPdf } from './markdown.js';
|
|
5
|
+
// Use createRequire to load pdf-lib as CJS (works around Node 25 ESM resolution issues)
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { PDFDocument } = require('pdf-lib');
|
|
8
|
+
async function loadPdfDocumentFromBuffer(filePathOrBuffer) {
|
|
9
|
+
const buffer = typeof filePathOrBuffer === 'string' ? await fs.readFile(filePathOrBuffer) : filePathOrBuffer;
|
|
10
|
+
const pdfBytes = new Uint8Array(buffer);
|
|
11
|
+
return await PDFDocument.load(pdfBytes);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Delete pages from a PDF document
|
|
15
|
+
* @param pdfDoc PDF document to delete pages from
|
|
16
|
+
* @param pageIndexes Page indices to delete, negative indices are from end
|
|
17
|
+
*/
|
|
18
|
+
function deletePages(pdfDoc, pageIndexes) {
|
|
19
|
+
const pageCount = pdfDoc.getPageCount();
|
|
20
|
+
// Transform negative indices to absolute and filter valid ones
|
|
21
|
+
const normalizedIndexes = normalizePageIndexes(pageIndexes, pageCount).sort((a, b) => b - a);
|
|
22
|
+
for (const pageIndex of normalizedIndexes) {
|
|
23
|
+
pdfDoc.removePage(pageIndex);
|
|
24
|
+
}
|
|
25
|
+
return pdfDoc;
|
|
26
|
+
}
|
|
27
|
+
function getPageLayout(page) {
|
|
28
|
+
const { width, height } = page.getSize();
|
|
29
|
+
const mediaBox = page.getMediaBox(); // Full page area
|
|
30
|
+
const cropBox = page.getCropBox(); // Visible area (may indicate margins)
|
|
31
|
+
// Calculate margins (if CropBox differs from MediaBox)
|
|
32
|
+
let marginLeft = cropBox.x - mediaBox.x;
|
|
33
|
+
let marginBottom = cropBox.y - mediaBox.y;
|
|
34
|
+
let marginRight = (mediaBox.x + mediaBox.width) - (cropBox.x + cropBox.width);
|
|
35
|
+
let marginTop = (mediaBox.y + mediaBox.height) - (cropBox.y + cropBox.height);
|
|
36
|
+
if (marginLeft === 0 && marginRight === 0 && marginTop === 0 && marginBottom === 0) {
|
|
37
|
+
marginLeft = 72;
|
|
38
|
+
marginBottom = 72;
|
|
39
|
+
marginRight = 72;
|
|
40
|
+
marginTop = 72;
|
|
41
|
+
}
|
|
42
|
+
// Convert points to inches (1 inch = 72 points)
|
|
43
|
+
// Puppeteer requires standard units and doesn't accept decimal points
|
|
44
|
+
const pointsToInches = (pts) => (pts / 72).toFixed(4);
|
|
45
|
+
return {
|
|
46
|
+
format: undefined, // Explicitly disable format to use custom dimensions
|
|
47
|
+
width: `${pointsToInches(width)}in`,
|
|
48
|
+
height: `${pointsToInches(height)}in`,
|
|
49
|
+
margin: {
|
|
50
|
+
top: `${pointsToInches(marginTop)}in`,
|
|
51
|
+
right: `${pointsToInches(marginRight)}in`,
|
|
52
|
+
bottom: `${pointsToInches(marginBottom)}in`,
|
|
53
|
+
left: `${pointsToInches(marginLeft)}in`
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function insertPages(destPdfDocument, pageIndex, sourcePdfDocument) {
|
|
58
|
+
let insertPosition = pageIndex < 0 ? destPdfDocument.getPageCount() + pageIndex : pageIndex;
|
|
59
|
+
if (insertPosition < 0 || insertPosition > destPdfDocument.getPageCount()) {
|
|
60
|
+
throw new Error('Invalid page index');
|
|
61
|
+
}
|
|
62
|
+
const copiedPages = await destPdfDocument.copyPages(sourcePdfDocument, sourcePdfDocument.getPageIndices());
|
|
63
|
+
for (let i = 0; i < copiedPages.length; i++) {
|
|
64
|
+
destPdfDocument.insertPage(insertPosition + i, copiedPages[i]);
|
|
65
|
+
}
|
|
66
|
+
return destPdfDocument;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Edit an existing PDF by deleting or inserting pages
|
|
70
|
+
* @param pdfPath Path to the PDF file to edit
|
|
71
|
+
* @param operations List of operations to perform
|
|
72
|
+
* @returns The modified PDF as a Uint8Array
|
|
73
|
+
*/
|
|
74
|
+
export async function editPdf(pdfPath, operations) {
|
|
75
|
+
const pdfDoc = await loadPdfDocumentFromBuffer(pdfPath);
|
|
76
|
+
// Get page layout from the ORIGINAL first page
|
|
77
|
+
const pageLayout = pdfDoc.getPageCount() > 0 ? getPageLayout(pdfDoc.getPage(0)) : undefined;
|
|
78
|
+
for (const op of operations) {
|
|
79
|
+
if (op.type === 'delete') {
|
|
80
|
+
deletePages(pdfDoc, op.pageIndexes);
|
|
81
|
+
}
|
|
82
|
+
else if (op.type == 'insert') {
|
|
83
|
+
let sourcePdfDocument;
|
|
84
|
+
if (op.markdown !== undefined) {
|
|
85
|
+
const pdfOptions = pageLayout ? { pdf_options: pageLayout } : undefined;
|
|
86
|
+
const pdfBuffer = await parseMarkdownToPdf(op.markdown, pdfOptions);
|
|
87
|
+
sourcePdfDocument = await loadPdfDocumentFromBuffer(pdfBuffer);
|
|
88
|
+
}
|
|
89
|
+
else if (op.sourcePdfPath) {
|
|
90
|
+
sourcePdfDocument = await loadPdfDocumentFromBuffer(op.sourcePdfPath);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw new Error('No source provided for insert operation');
|
|
94
|
+
}
|
|
95
|
+
await insertPages(pdfDoc, op.pageIndex, sourcePdfDocument);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return await pdfDoc.save();
|
|
99
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PageRange } from './lib/pdf2md.js';
|
|
2
|
+
import { PdfParseResult } from './lib/pdf2md.js';
|
|
3
|
+
interface CachedPuppeteerChrome {
|
|
4
|
+
executablePath: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find Chrome in puppeteer's cache directory
|
|
8
|
+
* Returns the executable path if found, undefined otherwise
|
|
9
|
+
*/
|
|
10
|
+
export declare function findPuppeteerChrome(cacheDir?: string): CachedPuppeteerChrome | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Remove stale Puppeteer Chrome builds while preserving the active build.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pruneOldPuppeteerChromeBuilds(activeExecutablePath: string, cacheDir?: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Preemptively ensure Chrome is available for PDF generation.
|
|
17
|
+
* Call this at server startup to trigger download in background if needed.
|
|
18
|
+
* Returns immediately, download happens in background.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureChromeAvailable(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Convert PDF to Markdown using @opendocsg/pdf2md
|
|
23
|
+
*/
|
|
24
|
+
export declare function parsePdfToMarkdown(source: string, pageNumbers?: number[] | PageRange): Promise<PdfParseResult>;
|
|
25
|
+
export declare function parseMarkdownToPdf(markdown: string, options?: any): Promise<Buffer>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
4
|
+
import { mdToPdf } from 'md-to-pdf';
|
|
5
|
+
import { pdf2md } from './lib/pdf2md.js';
|
|
6
|
+
import { CONFIG_FILE } from '../../config.js';
|
|
7
|
+
const isUrl = (source) => source.startsWith('http://') || source.startsWith('https://');
|
|
8
|
+
// Cached Chrome path to avoid repeated lookups
|
|
9
|
+
let cachedChromePath = null; // null = not checked yet
|
|
10
|
+
let chromeCheckPromise = null;
|
|
11
|
+
/**
|
|
12
|
+
* Get Desktop Commander's private Puppeteer cache directory.
|
|
13
|
+
*/
|
|
14
|
+
function getPuppeteerCacheDir() {
|
|
15
|
+
return join(dirname(CONFIG_FILE), 'puppeteer-cache');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the cache path where Puppeteer stores Chrome for Testing builds.
|
|
19
|
+
*/
|
|
20
|
+
function getPuppeteerChromeDir(cacheDir = getPuppeteerCacheDir()) {
|
|
21
|
+
return join(cacheDir, 'chrome');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the platform-specific executable within a cached Chrome build directory.
|
|
25
|
+
*/
|
|
26
|
+
function getChromeExecutablePath(chromeDir, version) {
|
|
27
|
+
const chromePath = process.platform === 'win32'
|
|
28
|
+
? join(chromeDir, version, 'chrome-win64', 'chrome.exe')
|
|
29
|
+
: process.platform === 'darwin'
|
|
30
|
+
? join(chromeDir, version, 'chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')
|
|
31
|
+
: join(chromeDir, version, 'chrome-linux64', 'chrome');
|
|
32
|
+
if (existsSync(chromePath)) {
|
|
33
|
+
return chromePath;
|
|
34
|
+
}
|
|
35
|
+
// Also check for arm64 mac
|
|
36
|
+
if (process.platform === 'darwin') {
|
|
37
|
+
const armPath = join(chromeDir, version, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
|
|
38
|
+
if (existsSync(armPath)) {
|
|
39
|
+
return armPath;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the cached Chrome build directory that owns an executable path.
|
|
46
|
+
*/
|
|
47
|
+
function getCachedChromeBuildDir(chromeDir, executablePath) {
|
|
48
|
+
const relativePath = relative(chromeDir, executablePath);
|
|
49
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const [buildDir] = relativePath.split(sep);
|
|
53
|
+
return buildDir ? join(chromeDir, buildDir) : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Find Chrome in puppeteer's cache directory
|
|
57
|
+
* Returns the executable path if found, undefined otherwise
|
|
58
|
+
*/
|
|
59
|
+
export function findPuppeteerChrome(cacheDir = getPuppeteerCacheDir()) {
|
|
60
|
+
const chromeDir = getPuppeteerChromeDir(cacheDir);
|
|
61
|
+
if (!existsSync(chromeDir)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// Look for chrome directories (e.g., win64-143.0.7499.169)
|
|
66
|
+
const versions = readdirSync(chromeDir, { withFileTypes: true })
|
|
67
|
+
.filter(entry => entry.isDirectory())
|
|
68
|
+
.map(entry => entry.name)
|
|
69
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
70
|
+
for (const version of versions) {
|
|
71
|
+
const executablePath = getChromeExecutablePath(chromeDir, version);
|
|
72
|
+
if (executablePath) {
|
|
73
|
+
return { executablePath };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Ignore errors reading cache directory
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Remove stale Puppeteer Chrome builds while preserving the active build.
|
|
84
|
+
*/
|
|
85
|
+
export async function pruneOldPuppeteerChromeBuilds(activeExecutablePath, cacheDir = getPuppeteerCacheDir()) {
|
|
86
|
+
const chromeDir = getPuppeteerChromeDir(cacheDir);
|
|
87
|
+
const activeBuildDir = getCachedChromeBuildDir(chromeDir, activeExecutablePath);
|
|
88
|
+
if (!activeBuildDir) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = await fs.readdir(chromeDir, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await Promise.all(entries
|
|
99
|
+
.filter(entry => entry.isDirectory())
|
|
100
|
+
.map(async (entry) => {
|
|
101
|
+
const buildDir = join(chromeDir, entry.name);
|
|
102
|
+
if (resolve(buildDir) === resolve(activeBuildDir)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error(`Failed to remove old Chrome cache build at ${buildDir}:`, error);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Find system-installed Chrome/Chromium browser
|
|
115
|
+
* Returns the executable path if found, undefined otherwise
|
|
116
|
+
*/
|
|
117
|
+
function findSystemChrome() {
|
|
118
|
+
const paths = process.platform === 'win32'
|
|
119
|
+
? [
|
|
120
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
121
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
122
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
123
|
+
'C:\\Program Files\\Chromium\\Application\\chrome.exe',
|
|
124
|
+
'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe',
|
|
125
|
+
]
|
|
126
|
+
: process.platform === 'darwin'
|
|
127
|
+
? [
|
|
128
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
129
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
130
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
131
|
+
]
|
|
132
|
+
: [
|
|
133
|
+
// Linux paths
|
|
134
|
+
'/usr/bin/google-chrome',
|
|
135
|
+
'/usr/bin/google-chrome-stable',
|
|
136
|
+
'/usr/bin/chromium',
|
|
137
|
+
'/usr/bin/chromium-browser',
|
|
138
|
+
'/snap/bin/chromium',
|
|
139
|
+
];
|
|
140
|
+
return paths.find(p => existsSync(p));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Download and install Chrome using @puppeteer/browsers
|
|
144
|
+
* Returns the executable path after installation
|
|
145
|
+
*/
|
|
146
|
+
async function installChrome() {
|
|
147
|
+
// Dynamic import to avoid loading if not needed
|
|
148
|
+
const { install, Browser, detectBrowserPlatform, resolveBuildId } = await import('@puppeteer/browsers');
|
|
149
|
+
const cacheDir = getPuppeteerCacheDir();
|
|
150
|
+
const platform = detectBrowserPlatform();
|
|
151
|
+
const buildId = await resolveBuildId(Browser.CHROME, platform, 'stable');
|
|
152
|
+
console.error('Downloading Chrome for PDF generation (this may take a few minutes)...');
|
|
153
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
154
|
+
const installedBrowser = await install({
|
|
155
|
+
browser: Browser.CHROME,
|
|
156
|
+
buildId,
|
|
157
|
+
cacheDir,
|
|
158
|
+
downloadProgressCallback: (downloadedBytes, totalBytes) => {
|
|
159
|
+
const percent = Math.round((downloadedBytes / totalBytes) * 100);
|
|
160
|
+
process.stderr.write(`\rDownloading Chrome: ${percent}%`);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
console.error('\nChrome download complete.');
|
|
164
|
+
return {
|
|
165
|
+
executablePath: installedBrowser.executablePath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Find or install Chrome for PDF generation
|
|
170
|
+
* Priority: 1. Puppeteer cache, 2. System Chrome, 3. Install Chrome
|
|
171
|
+
* Results are cached to avoid repeated lookups
|
|
172
|
+
*/
|
|
173
|
+
async function getChromePath() {
|
|
174
|
+
// Return cached result if available
|
|
175
|
+
if (cachedChromePath !== null) {
|
|
176
|
+
return cachedChromePath;
|
|
177
|
+
}
|
|
178
|
+
// If a check is already in progress, wait for it
|
|
179
|
+
if (chromeCheckPromise) {
|
|
180
|
+
return chromeCheckPromise;
|
|
181
|
+
}
|
|
182
|
+
// Start the check
|
|
183
|
+
chromeCheckPromise = (async () => {
|
|
184
|
+
// 1. Check puppeteer cache first (exact compatible version)
|
|
185
|
+
const cachedChrome = findPuppeteerChrome();
|
|
186
|
+
if (cachedChrome) {
|
|
187
|
+
await pruneOldPuppeteerChromeBuilds(cachedChrome.executablePath);
|
|
188
|
+
cachedChromePath = cachedChrome.executablePath;
|
|
189
|
+
return cachedChrome.executablePath;
|
|
190
|
+
}
|
|
191
|
+
// 2. Check system Chrome
|
|
192
|
+
const systemChrome = findSystemChrome();
|
|
193
|
+
if (systemChrome) {
|
|
194
|
+
cachedChromePath = systemChrome;
|
|
195
|
+
return systemChrome;
|
|
196
|
+
}
|
|
197
|
+
// 3. Install Chrome as last resort
|
|
198
|
+
try {
|
|
199
|
+
const installedChrome = await installChrome();
|
|
200
|
+
await pruneOldPuppeteerChromeBuilds(installedChrome.executablePath);
|
|
201
|
+
cachedChromePath = installedChrome.executablePath;
|
|
202
|
+
return installedChrome.executablePath;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error('Failed to install Chrome:', error);
|
|
206
|
+
cachedChromePath = undefined;
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
const result = await chromeCheckPromise;
|
|
211
|
+
chromeCheckPromise = null;
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Preemptively ensure Chrome is available for PDF generation.
|
|
216
|
+
* Call this at server startup to trigger download in background if needed.
|
|
217
|
+
* Returns immediately, download happens in background.
|
|
218
|
+
*/
|
|
219
|
+
export function ensureChromeAvailable() {
|
|
220
|
+
// Don't await - let it run in background
|
|
221
|
+
getChromePath().catch((error) => {
|
|
222
|
+
console.error('Background Chrome check failed:', error);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async function loadPdfToBuffer(source) {
|
|
226
|
+
if (isUrl(source)) {
|
|
227
|
+
const response = await fetch(source);
|
|
228
|
+
return await response.arrayBuffer();
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return await fs.readFile(source);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Convert PDF to Markdown using @opendocsg/pdf2md
|
|
236
|
+
*/
|
|
237
|
+
export async function parsePdfToMarkdown(source, pageNumbers = []) {
|
|
238
|
+
try {
|
|
239
|
+
const data = await loadPdfToBuffer(source);
|
|
240
|
+
// @ts-ignore: Type definition mismatch for ESM usage
|
|
241
|
+
return await pdf2md(new Uint8Array(data), pageNumbers);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error("Error converting PDF to Markdown (v3):", error);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function parseMarkdownToPdf(markdown, options = {}) {
|
|
249
|
+
try {
|
|
250
|
+
// Find Chrome: puppeteer cache -> system Chrome -> install
|
|
251
|
+
const chromePath = await getChromePath();
|
|
252
|
+
if (chromePath) {
|
|
253
|
+
options = {
|
|
254
|
+
...options,
|
|
255
|
+
launch_options: {
|
|
256
|
+
...options.launch_options,
|
|
257
|
+
executablePath: chromePath,
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const pdf = await mdToPdf({ content: markdown }, options);
|
|
262
|
+
return pdf.content;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
// Provide helpful error message if Chrome is not found
|
|
266
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
267
|
+
if (errorMessage.includes('Could not find Chrome')) {
|
|
268
|
+
throw new Error('PDF generation requires Chrome or Chromium browser. ' +
|
|
269
|
+
'Please install Google Chrome from https://www.google.com/chrome/ ' +
|
|
270
|
+
'or Chromium, then try again.');
|
|
271
|
+
}
|
|
272
|
+
console.error('Error creating PDF:', error);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|