mindee 4.22.0 → 4.23.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/CHANGELOG.md +7 -0
- package/package.json +8 -2
- package/src/errors/mindeeError.d.ts +6 -0
- package/src/errors/mindeeError.js +15 -1
- package/src/http/apiSettings.d.ts +1 -0
- package/src/http/apiSettings.js +14 -2
- package/src/imageOperations/imageCompressor.d.ts +9 -0
- package/src/imageOperations/imageCompressor.js +36 -0
- package/src/imageOperations/index.d.ts +1 -0
- package/src/imageOperations/index.js +3 -1
- package/src/input/sources/localInputSource.d.ts +18 -0
- package/src/input/sources/localInputSource.js +42 -2
- package/src/parsing/common/asyncPredictResponse.d.ts +1 -1
- package/src/pdf/index.d.ts +2 -0
- package/src/pdf/index.js +5 -1
- package/src/pdf/pdfCompressor.d.ts +9 -0
- package/src/pdf/pdfCompressor.js +247 -0
- package/src/pdf/pdfUtils.d.ts +30 -0
- package/src/pdf/pdfUtils.js +55 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mindee",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.23.0",
|
|
4
4
|
"description": "Mindee Client Library for Node.js",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": "bin/mindee.js",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@types/chai": "^4.3.4",
|
|
44
44
|
"@types/mocha": "^10.0.9",
|
|
45
45
|
"@types/node": "^18.15.11",
|
|
46
|
+
"@types/tmp": "^0.2.6",
|
|
46
47
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
47
48
|
"@typescript-eslint/parser": "^8.15.0",
|
|
48
49
|
"chai": "^4.3.10",
|
|
@@ -55,10 +56,15 @@
|
|
|
55
56
|
"typescript": "^5.6.3"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
59
|
+
"canvas": "^3.0.1",
|
|
58
60
|
"commander": "~9.4.1",
|
|
59
61
|
"file-type": "~16.5.4",
|
|
60
62
|
"form-data": "~3.0.1",
|
|
61
|
-
"
|
|
63
|
+
"node-poppler": "^7.2.2",
|
|
64
|
+
"pdf-lib": "^1.17.1",
|
|
65
|
+
"pdf.js-extract": "^0.2.1",
|
|
66
|
+
"sharp": "^0.33.5",
|
|
67
|
+
"tmp": "^0.2.3"
|
|
62
68
|
},
|
|
63
69
|
"keywords": [
|
|
64
70
|
"typescript",
|
|
@@ -10,3 +10,9 @@ export declare class MindeeError extends Error {
|
|
|
10
10
|
export declare class MindeeMimeTypeError extends MindeeError {
|
|
11
11
|
constructor(message: string);
|
|
12
12
|
}
|
|
13
|
+
export declare class MindeeImageError extends MindeeError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class MindeePdfError extends MindeeError {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MindeeMimeTypeError = exports.MindeeError = void 0;
|
|
3
|
+
exports.MindeePdfError = exports.MindeeImageError = exports.MindeeMimeTypeError = exports.MindeeError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Main Mindee Error custom class.
|
|
6
6
|
*/
|
|
@@ -21,3 +21,17 @@ class MindeeMimeTypeError extends MindeeError {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
exports.MindeeMimeTypeError = MindeeMimeTypeError;
|
|
24
|
+
class MindeeImageError extends MindeeError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "MindeeImageError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.MindeeImageError = MindeeImageError;
|
|
31
|
+
class MindeePdfError extends MindeeError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "MindeePdfError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.MindeePdfError = MindeePdfError;
|
package/src/http/apiSettings.js
CHANGED
|
@@ -33,7 +33,6 @@ exports.API_HOST_ENVVAR_NAME = "MINDEE_API_HOST";
|
|
|
33
33
|
exports.STANDARD_API_OWNER = "mindee";
|
|
34
34
|
exports.TIMEOUT_DEFAULT = 120;
|
|
35
35
|
const DEFAULT_MINDEE_API_HOST = "api.mindee.net";
|
|
36
|
-
const USER_AGENT = `mindee-api-nodejs@v${package_json_1.version} nodejs-${process.version} ${os.type().toLowerCase()}`;
|
|
37
36
|
class ApiSettings {
|
|
38
37
|
constructor({ apiKey = "", }) {
|
|
39
38
|
if (!apiKey || apiKey.length === 0) {
|
|
@@ -47,12 +46,25 @@ class ApiSettings {
|
|
|
47
46
|
+ `You can set this using the ${exports.API_KEY_ENVVAR_NAME} environment variable.`);
|
|
48
47
|
}
|
|
49
48
|
this.baseHeaders = {
|
|
50
|
-
"User-Agent":
|
|
49
|
+
"User-Agent": this.getUserAgent(),
|
|
51
50
|
Authorization: `Token ${this.apiKey}`,
|
|
52
51
|
};
|
|
53
52
|
this.hostname = this.hostnameFromEnv();
|
|
54
53
|
this.timeout = process.env.MINDEE_REQUEST_TIMEOUT ? parseInt(process.env.MINDEE_REQUEST_TIMEOUT) : exports.TIMEOUT_DEFAULT;
|
|
55
54
|
}
|
|
55
|
+
getUserAgent() {
|
|
56
|
+
let platform = os.type().toLowerCase();
|
|
57
|
+
if (platform.includes("darwin")) {
|
|
58
|
+
platform = "macos";
|
|
59
|
+
}
|
|
60
|
+
else if (platform.includes("window")) {
|
|
61
|
+
platform = "windows";
|
|
62
|
+
}
|
|
63
|
+
else if (platform.includes("bsd")) {
|
|
64
|
+
platform = "bsd";
|
|
65
|
+
}
|
|
66
|
+
return `mindee-api-nodejs@v${package_json_1.version} nodejs-${process.version} ${platform}`;
|
|
67
|
+
}
|
|
56
68
|
apiKeyFromEnv() {
|
|
57
69
|
const envVarValue = process.env[exports.API_KEY_ENVVAR_NAME];
|
|
58
70
|
if (envVarValue) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compresses an image with the given parameters.
|
|
3
|
+
*
|
|
4
|
+
* @param imageBuffer Buffer representation of an image.
|
|
5
|
+
* @param quality Quality to apply to the image (JPEG).
|
|
6
|
+
* @param maxWidth Maximum bound for width.
|
|
7
|
+
* @param maxHeight Maximum bound for height.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compressImage(imageBuffer: Buffer, quality?: number, maxWidth?: number | null, maxHeight?: number | null): Promise<Buffer>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.compressImage = compressImage;
|
|
7
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
8
|
+
const mindeeError_1 = require("../errors/mindeeError");
|
|
9
|
+
/**
|
|
10
|
+
* Compresses an image with the given parameters.
|
|
11
|
+
*
|
|
12
|
+
* @param imageBuffer Buffer representation of an image.
|
|
13
|
+
* @param quality Quality to apply to the image (JPEG).
|
|
14
|
+
* @param maxWidth Maximum bound for width.
|
|
15
|
+
* @param maxHeight Maximum bound for height.
|
|
16
|
+
*/
|
|
17
|
+
async function compressImage(imageBuffer, quality = 85, maxWidth = null, maxHeight = null) {
|
|
18
|
+
let sharpImage = (0, sharp_1.default)(imageBuffer);
|
|
19
|
+
const metadata = await sharpImage.metadata();
|
|
20
|
+
if (metadata.width === undefined || metadata.height === undefined) {
|
|
21
|
+
throw new mindeeError_1.MindeeImageError("Source image has invalid dimensions.");
|
|
22
|
+
}
|
|
23
|
+
maxWidth ?? (maxWidth = metadata.width);
|
|
24
|
+
maxHeight ?? (maxHeight = metadata.height);
|
|
25
|
+
if (maxWidth || maxHeight) {
|
|
26
|
+
sharpImage = sharpImage.resize({
|
|
27
|
+
width: maxWidth,
|
|
28
|
+
height: maxHeight,
|
|
29
|
+
fit: "inside",
|
|
30
|
+
withoutEnlargement: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return await sharpImage
|
|
34
|
+
.jpeg({ quality: quality })
|
|
35
|
+
.toBuffer();
|
|
36
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ExtractedInvoiceSplitterImage = exports.extractInvoices = exports.ExtractedMultiReceiptImage = exports.extractReceipts = void 0;
|
|
3
|
+
exports.compressImage = exports.ExtractedInvoiceSplitterImage = exports.extractInvoices = exports.ExtractedMultiReceiptImage = exports.extractReceipts = void 0;
|
|
4
4
|
var multiReceiptsExtractor_1 = require("./multiReceiptsExtractor");
|
|
5
5
|
Object.defineProperty(exports, "extractReceipts", { enumerable: true, get: function () { return multiReceiptsExtractor_1.extractReceipts; } });
|
|
6
6
|
Object.defineProperty(exports, "ExtractedMultiReceiptImage", { enumerable: true, get: function () { return multiReceiptsExtractor_1.ExtractedMultiReceiptImage; } });
|
|
7
7
|
var invoiceSplitterExtractor_1 = require("./invoiceSplitterExtractor");
|
|
8
8
|
Object.defineProperty(exports, "extractInvoices", { enumerable: true, get: function () { return invoiceSplitterExtractor_1.extractInvoices; } });
|
|
9
9
|
Object.defineProperty(exports, "ExtractedInvoiceSplitterImage", { enumerable: true, get: function () { return invoiceSplitterExtractor_1.ExtractedInvoiceSplitterImage; } });
|
|
10
|
+
var imageCompressor_1 = require("./imageCompressor");
|
|
11
|
+
Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return imageCompressor_1.compressImage; } });
|
|
@@ -17,4 +17,22 @@ export declare abstract class LocalInputSource extends InputSource {
|
|
|
17
17
|
* @param pageOptions
|
|
18
18
|
*/
|
|
19
19
|
cutPdf(pageOptions: PageOptions): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Compresses the file object, either as a PDF or an image.
|
|
22
|
+
*
|
|
23
|
+
* @param quality Quality of the compression. For images, this is the JPEG quality.
|
|
24
|
+
* For PDFs, this affects image quality within the PDF.
|
|
25
|
+
* @param maxWidth Maximum width for image resizing. Ignored for PDFs.
|
|
26
|
+
* @param maxHeight Maximum height for image resizing. Ignored for PDFs.
|
|
27
|
+
* @param forceSourceText For PDFs, whether to force compression even if source text is present.
|
|
28
|
+
* @param disableSourceText For PDFs, whether to disable source text during compression.
|
|
29
|
+
*
|
|
30
|
+
* @returns A Promise that resolves when the compression is complete.
|
|
31
|
+
*/
|
|
32
|
+
compress(quality?: number, maxWidth?: number | null, maxHeight?: number | null, forceSourceText?: boolean, disableSourceText?: boolean): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if the object is a PDF and has source text. False otherwise.
|
|
35
|
+
* @return boolean
|
|
36
|
+
*/
|
|
37
|
+
hasSourceText(): Promise<boolean>;
|
|
20
38
|
}
|
|
@@ -29,9 +29,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.LocalInputSource = void 0;
|
|
30
30
|
const handler_1 = require("../../errors/handler");
|
|
31
31
|
const logger_1 = require("../../logger");
|
|
32
|
+
const imageOperations_1 = require("../../imageOperations");
|
|
33
|
+
const pdf_1 = require("../../pdf");
|
|
32
34
|
const path_1 = __importDefault(require("path"));
|
|
33
35
|
const fileType = __importStar(require("file-type"));
|
|
34
|
-
const
|
|
36
|
+
const pdf_2 = require("../../pdf");
|
|
35
37
|
const inputSource_1 = require("./inputSource");
|
|
36
38
|
const MIMETYPES = new Map([
|
|
37
39
|
[".pdf", "application/pdf"],
|
|
@@ -103,8 +105,46 @@ class LocalInputSource extends inputSource_1.InputSource {
|
|
|
103
105
|
if (!(this.fileObject instanceof Buffer)) {
|
|
104
106
|
throw new Error(`Cannot modify an input source of type ${this.inputType}.`);
|
|
105
107
|
}
|
|
106
|
-
const processedPdf = await (0,
|
|
108
|
+
const processedPdf = await (0, pdf_2.extractPages)(this.fileObject, pageOptions);
|
|
107
109
|
this.fileObject = processedPdf.file;
|
|
108
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Compresses the file object, either as a PDF or an image.
|
|
113
|
+
*
|
|
114
|
+
* @param quality Quality of the compression. For images, this is the JPEG quality.
|
|
115
|
+
* For PDFs, this affects image quality within the PDF.
|
|
116
|
+
* @param maxWidth Maximum width for image resizing. Ignored for PDFs.
|
|
117
|
+
* @param maxHeight Maximum height for image resizing. Ignored for PDFs.
|
|
118
|
+
* @param forceSourceText For PDFs, whether to force compression even if source text is present.
|
|
119
|
+
* @param disableSourceText For PDFs, whether to disable source text during compression.
|
|
120
|
+
*
|
|
121
|
+
* @returns A Promise that resolves when the compression is complete.
|
|
122
|
+
*/
|
|
123
|
+
async compress(quality = 85, maxWidth = null, maxHeight = null, forceSourceText = false, disableSourceText = true) {
|
|
124
|
+
let buffer;
|
|
125
|
+
if (typeof this.fileObject === "string") {
|
|
126
|
+
buffer = Buffer.from(this.fileObject);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
buffer = this.fileObject;
|
|
130
|
+
}
|
|
131
|
+
if (this.isPdf()) {
|
|
132
|
+
this.fileObject = await (0, pdf_1.compressPdf)(buffer, quality, forceSourceText, disableSourceText);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.fileObject = await (0, imageOperations_1.compressImage)(buffer, quality, maxWidth, maxHeight);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Returns true if the object is a PDF and has source text. False otherwise.
|
|
140
|
+
* @return boolean
|
|
141
|
+
*/
|
|
142
|
+
async hasSourceText() {
|
|
143
|
+
if (!this.isPdf()) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const buffer = typeof this.fileObject === "string" ? Buffer.from(this.fileObject) : this.fileObject;
|
|
147
|
+
return (0, pdf_2.hasSourceText)(buffer);
|
|
148
|
+
}
|
|
109
149
|
}
|
|
110
150
|
exports.LocalInputSource = LocalInputSource;
|
|
@@ -17,7 +17,7 @@ export declare class Job {
|
|
|
17
17
|
/** ID of the job. */
|
|
18
18
|
id: string;
|
|
19
19
|
/** Status of the job. */
|
|
20
|
-
status?: "waiting" | "processing" | "completed";
|
|
20
|
+
status?: "waiting" | "processing" | "completed" | "failed";
|
|
21
21
|
/** The time taken to process the job, in milliseconds. */
|
|
22
22
|
milliSecsTaken?: number;
|
|
23
23
|
constructor(jsonResponse: StringDict);
|
package/src/pdf/index.d.ts
CHANGED
package/src/pdf/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.countPages = exports.extractPages = void 0;
|
|
3
|
+
exports.hasSourceText = exports.compressPdf = exports.countPages = exports.extractPages = void 0;
|
|
4
4
|
var pdfOperation_1 = require("./pdfOperation");
|
|
5
5
|
Object.defineProperty(exports, "extractPages", { enumerable: true, get: function () { return pdfOperation_1.extractPages; } });
|
|
6
6
|
Object.defineProperty(exports, "countPages", { enumerable: true, get: function () { return pdfOperation_1.countPages; } });
|
|
7
|
+
var pdfCompressor_1 = require("./pdfCompressor");
|
|
8
|
+
Object.defineProperty(exports, "compressPdf", { enumerable: true, get: function () { return pdfCompressor_1.compressPdf; } });
|
|
9
|
+
var pdfUtils_1 = require("./pdfUtils");
|
|
10
|
+
Object.defineProperty(exports, "hasSourceText", { enumerable: true, get: function () { return pdfUtils_1.hasSourceText; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compresses each page of a provided PDF buffer.
|
|
3
|
+
* @param pdfData The input PDF as a Buffer.
|
|
4
|
+
* @param imageQuality Compression quality (70-100 for most JPG images).
|
|
5
|
+
* @param forceSourceTextCompression If true, attempts to re-write detected text.
|
|
6
|
+
* @param disableSourceText If true, doesn't re-apply source text to the output PDF.
|
|
7
|
+
* @returns A Promise resolving to the compressed PDF as a Buffer.
|
|
8
|
+
*/
|
|
9
|
+
export declare function compressPdf(pdfData: Buffer, imageQuality?: number, forceSourceTextCompression?: boolean, disableSourceText?: boolean): Promise<Buffer>;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.compressPdf = compressPdf;
|
|
30
|
+
const logger_1 = require("../logger");
|
|
31
|
+
const tmp_1 = __importDefault(require("tmp"));
|
|
32
|
+
const pdfUtils_1 = require("./pdfUtils");
|
|
33
|
+
const fs = __importStar(require("node:fs"));
|
|
34
|
+
const node_poppler_1 = require("node-poppler");
|
|
35
|
+
const pdf_lib_1 = require("pdf-lib");
|
|
36
|
+
const imageOperations_1 = require("../imageOperations");
|
|
37
|
+
/**
|
|
38
|
+
* Compresses each page of a provided PDF buffer.
|
|
39
|
+
* @param pdfData The input PDF as a Buffer.
|
|
40
|
+
* @param imageQuality Compression quality (70-100 for most JPG images).
|
|
41
|
+
* @param forceSourceTextCompression If true, attempts to re-write detected text.
|
|
42
|
+
* @param disableSourceText If true, doesn't re-apply source text to the output PDF.
|
|
43
|
+
* @returns A Promise resolving to the compressed PDF as a Buffer.
|
|
44
|
+
*/
|
|
45
|
+
async function compressPdf(pdfData, imageQuality = 85, forceSourceTextCompression = false, disableSourceText = true) {
|
|
46
|
+
handleCompressionWarnings(forceSourceTextCompression, disableSourceText);
|
|
47
|
+
if (await (0, pdfUtils_1.hasSourceText)(pdfData)) {
|
|
48
|
+
if (forceSourceTextCompression) {
|
|
49
|
+
if (!disableSourceText) {
|
|
50
|
+
logger_1.logger.warn("Re-writing PDF source-text is an EXPERIMENTAL feature.");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
logger_1.logger.warn("Source file contains text, but disable_source_text flag. " +
|
|
54
|
+
"is set to false. Resulting file will not contain any embedded text.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
logger_1.logger.warn("Found text inside of the provided PDF file. Compression operation aborted since disableSourceText "
|
|
59
|
+
+ "is set to 'true'.");
|
|
60
|
+
return pdfData;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const extractedText = disableSourceText ? await (0, pdfUtils_1.extractTextFromPdf)(pdfData) : null;
|
|
64
|
+
const extractedPdfInfo = await (0, pdfUtils_1.extractTextFromPdf)(pdfData);
|
|
65
|
+
const compressedPages = await compressPdfPages(pdfData, extractedPdfInfo, imageQuality, disableSourceText, extractedText);
|
|
66
|
+
if (!compressedPages) {
|
|
67
|
+
logger_1.logger.warn("Could not compress PDF to a smaller size. Returning original PDF.");
|
|
68
|
+
return pdfData;
|
|
69
|
+
}
|
|
70
|
+
return createNewPdfFromCompressedPages(compressedPages);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Handles compression warnings based on the provided parameters.
|
|
74
|
+
* @param forceSourceTextCompression If true, attempts to re-write detected text.
|
|
75
|
+
* @param disableSourceText If true, doesn't re-apply source text to the output PDF.
|
|
76
|
+
*/
|
|
77
|
+
function handleCompressionWarnings(forceSourceTextCompression, disableSourceText) {
|
|
78
|
+
if (forceSourceTextCompression) {
|
|
79
|
+
if (!disableSourceText) {
|
|
80
|
+
logger_1.logger.warn("Re-writing PDF source-text is an EXPERIMENTAL feature.");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
logger_1.logger.warn("Source file contains text, but the disable_source_text is set to false. "
|
|
84
|
+
+ "Resulting file will not contain any embedded text.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compresses PDF pages and returns an array of compressed page buffers.
|
|
90
|
+
* @param pdfData The input PDF as a Buffer.
|
|
91
|
+
* @param extractedPdfInfo Extracted PDF information.
|
|
92
|
+
* @param imageQuality Initial compression quality.
|
|
93
|
+
* @param disableSourceText If true, doesn't re-apply source text to the output PDF.
|
|
94
|
+
* @param extractedText Extracted text from the PDF.
|
|
95
|
+
* @returns A Promise resolving to an array of compressed page buffers, or null if compression fails.
|
|
96
|
+
*/
|
|
97
|
+
async function compressPdfPages(pdfData, extractedPdfInfo, imageQuality, disableSourceText, extractedText) {
|
|
98
|
+
const originalSize = pdfData.length;
|
|
99
|
+
const MIN_QUALITY = 1;
|
|
100
|
+
let imageQualityLoop = imageQuality;
|
|
101
|
+
while (imageQualityLoop >= MIN_QUALITY) {
|
|
102
|
+
const compressedPages = await compressPagesWithQuality(pdfData, extractedPdfInfo, imageQualityLoop, disableSourceText, extractedText);
|
|
103
|
+
const totalCompressedSize = calculateTotalCompressedSize(compressedPages);
|
|
104
|
+
if (isCompressionSuccessful(totalCompressedSize, originalSize, imageQuality)) {
|
|
105
|
+
return compressedPages;
|
|
106
|
+
}
|
|
107
|
+
imageQualityLoop -= Math.round(lerp(1, 10, imageQualityLoop / 100));
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compresses pages with a specific quality.
|
|
113
|
+
* @param pdfData The input PDF as a Buffer.
|
|
114
|
+
* @param extractedPdfInfo Extracted PDF information.
|
|
115
|
+
* @param imageQuality Compression quality.
|
|
116
|
+
* @param disableSourceText If true, doesn't re-apply source text to the output PDF.
|
|
117
|
+
* @param extractedText Extracted text from the PDF.
|
|
118
|
+
* @returns A Promise resolving to an array of compressed page buffers.
|
|
119
|
+
*/
|
|
120
|
+
async function compressPagesWithQuality(pdfData, extractedPdfInfo, imageQuality, disableSourceText, extractedText) {
|
|
121
|
+
const pdfDoc = await pdf_lib_1.PDFDocument.load(pdfData);
|
|
122
|
+
const compressedPages = [];
|
|
123
|
+
for (let i = 0; i < extractedPdfInfo.pages.length; i++) {
|
|
124
|
+
const page = pdfDoc.getPages()[i];
|
|
125
|
+
const rasterizedPage = await rasterizePage(pdfData, i + 1, imageQuality);
|
|
126
|
+
const compressedImage = await (0, imageOperations_1.compressImage)(Buffer.from(rasterizedPage, "binary"), imageQuality);
|
|
127
|
+
if (!disableSourceText) {
|
|
128
|
+
await addTextToPdfPage(page, extractedText);
|
|
129
|
+
}
|
|
130
|
+
compressedPages.push(compressedImage);
|
|
131
|
+
}
|
|
132
|
+
return compressedPages;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Calculates the total size of compressed pages.
|
|
136
|
+
* @param compressedPages Array of compressed page buffers.
|
|
137
|
+
* @returns The total size of compressed pages.
|
|
138
|
+
*/
|
|
139
|
+
function calculateTotalCompressedSize(compressedPages) {
|
|
140
|
+
return compressedPages.reduce((sum, page) => sum + page.length, 0);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Checks if the compression was successful based on the compressed size and original size.
|
|
144
|
+
* Note: Not quite sure how or why the rasterization quality ratio is correlated with the overhead generated by the
|
|
145
|
+
* image's inclusion into the pdf data, but this makes the following lerp() necessary if we want consistency during
|
|
146
|
+
* compression.
|
|
147
|
+
*
|
|
148
|
+
* @param totalCompressedSize Total size of compressed pages.
|
|
149
|
+
* @param originalSize Original PDF size.
|
|
150
|
+
* @param imageQuality Compression quality.
|
|
151
|
+
* @returns True if compression was successful, false otherwise.
|
|
152
|
+
*/
|
|
153
|
+
function isCompressionSuccessful(totalCompressedSize, originalSize, imageQuality) {
|
|
154
|
+
const overhead = lerp(0.54, 0.18, imageQuality / 100);
|
|
155
|
+
return totalCompressedSize + totalCompressedSize * overhead < originalSize;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Creates a new PDF document from compressed page buffers.
|
|
159
|
+
* @param compressedPages Array of compressed page buffers.
|
|
160
|
+
* @returns A Promise resolving to the new PDF as a Buffer.
|
|
161
|
+
*/
|
|
162
|
+
async function createNewPdfFromCompressedPages(compressedPages) {
|
|
163
|
+
const newPdfDoc = await pdf_lib_1.PDFDocument.create();
|
|
164
|
+
for (const compressedPage of compressedPages) {
|
|
165
|
+
const image = await newPdfDoc.embedJpg(compressedPage);
|
|
166
|
+
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
|
167
|
+
newPage.drawImage(image, {
|
|
168
|
+
x: 0,
|
|
169
|
+
y: 0,
|
|
170
|
+
width: image.width,
|
|
171
|
+
height: image.height,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const compressedPdfBytes = await newPdfDoc.save();
|
|
175
|
+
return Buffer.from(compressedPdfBytes);
|
|
176
|
+
}
|
|
177
|
+
async function addTextToPdfPage(page, textInfo) {
|
|
178
|
+
if (textInfo === null) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const textPages of textInfo.pages) {
|
|
182
|
+
for (const textPage of textPages.content) {
|
|
183
|
+
page.drawText(textPage.str, {
|
|
184
|
+
x: textPage.x,
|
|
185
|
+
y: textPage.y,
|
|
186
|
+
size: textPage.height,
|
|
187
|
+
color: (0, pdf_lib_1.rgb)(0, 0, 0),
|
|
188
|
+
font: await getFontFromName(textPage.fontName)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function getFontFromName(fontName) {
|
|
194
|
+
const pdfDoc = await pdf_lib_1.PDFDocument.create();
|
|
195
|
+
let font;
|
|
196
|
+
if (Object.values(pdf_lib_1.StandardFonts).map(value => value.toString()).includes(fontName)) {
|
|
197
|
+
font = await pdfDoc.embedFont(fontName);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
font = await pdfDoc.embedFont(pdf_lib_1.StandardFonts.Helvetica);
|
|
201
|
+
}
|
|
202
|
+
return font;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Rasterizes a PDF page.
|
|
206
|
+
*
|
|
207
|
+
* @param pdfData Buffer representation of the entire PDF file.
|
|
208
|
+
* @param index Index of the page to rasterize.
|
|
209
|
+
* @param quality Quality to apply during rasterization.
|
|
210
|
+
*/
|
|
211
|
+
async function rasterizePage(pdfData, index, quality = 85) {
|
|
212
|
+
const poppler = new node_poppler_1.Poppler();
|
|
213
|
+
const tmpPdf = tmp_1.default.fileSync();
|
|
214
|
+
const tempPdfPath = tmpPdf.name;
|
|
215
|
+
const antialiasOption = "best";
|
|
216
|
+
try {
|
|
217
|
+
await fs.promises.writeFile(tempPdfPath, pdfData);
|
|
218
|
+
const options = {
|
|
219
|
+
antialias: antialiasOption,
|
|
220
|
+
firstPageToConvert: index,
|
|
221
|
+
lastPageToConvert: index,
|
|
222
|
+
jpegFile: true,
|
|
223
|
+
jpegOptions: `quality=${quality}`,
|
|
224
|
+
singleFile: true
|
|
225
|
+
};
|
|
226
|
+
const jpegBuffer = await poppler.pdfToCairo(tempPdfPath, undefined, options);
|
|
227
|
+
await fs.promises.unlink(tempPdfPath);
|
|
228
|
+
return jpegBuffer;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error("Error rasterizing PDF:", error);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
tmpPdf.removeCallback();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Performs linear interpolation between two numbers.
|
|
240
|
+
* @param start The starting value.
|
|
241
|
+
* @param end The ending value.
|
|
242
|
+
* @param t The interpolation factor (0 to 1).
|
|
243
|
+
* @returns The interpolated value.
|
|
244
|
+
*/
|
|
245
|
+
function lerp(start, end, t) {
|
|
246
|
+
return start * (1 - t) + end * t;
|
|
247
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface PageTextInfo {
|
|
2
|
+
pageNumber: number;
|
|
3
|
+
content: Array<{
|
|
4
|
+
str: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
fontName: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export interface ExtractedPdfInfo {
|
|
13
|
+
pages: PageTextInfo[];
|
|
14
|
+
getConcatenatedText: () => string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extracts text from a full PDF document.
|
|
18
|
+
*
|
|
19
|
+
* @returns A Promise containing the extracted text as a string.
|
|
20
|
+
* @param pdfBuffer PDF handle, as a buffer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractTextFromPdf(pdfBuffer: Buffer): Promise<ExtractedPdfInfo>;
|
|
23
|
+
/**
|
|
24
|
+
* Checks if a PDF contains source text.
|
|
25
|
+
*
|
|
26
|
+
* @param pdfData Buffer representing the content of the PDF file.
|
|
27
|
+
*
|
|
28
|
+
* @returns A Promise containing a boolean indicating if the PDF has source text.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasSourceText(pdfData: Buffer): Promise<boolean>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractTextFromPdf = extractTextFromPdf;
|
|
4
|
+
exports.hasSourceText = hasSourceText;
|
|
5
|
+
const pdf_js_extract_1 = require("pdf.js-extract");
|
|
6
|
+
const mindeeError_1 = require("../errors/mindeeError");
|
|
7
|
+
function getConcatenatedText(pages) {
|
|
8
|
+
return pages.flatMap(page => page.content.map(item => item.str)).join(" ");
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extracts text from a full PDF document.
|
|
12
|
+
*
|
|
13
|
+
* @returns A Promise containing the extracted text as a string.
|
|
14
|
+
* @param pdfBuffer PDF handle, as a buffer.
|
|
15
|
+
*/
|
|
16
|
+
async function extractTextFromPdf(pdfBuffer) {
|
|
17
|
+
const pdfExtract = new pdf_js_extract_1.PDFExtract();
|
|
18
|
+
const options = {};
|
|
19
|
+
const pdf = await new Promise((resolve, reject) => {
|
|
20
|
+
pdfExtract.extractBuffer(pdfBuffer, options, (err, result) => {
|
|
21
|
+
if (err)
|
|
22
|
+
reject(err);
|
|
23
|
+
if (result === undefined)
|
|
24
|
+
reject(new mindeeError_1.MindeePdfError("Couldn't process result."));
|
|
25
|
+
else
|
|
26
|
+
resolve(result);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
const pages = pdf.pages.map((page, index) => ({
|
|
30
|
+
pageNumber: index + 1,
|
|
31
|
+
content: page.content.map(item => ({
|
|
32
|
+
str: item.str,
|
|
33
|
+
x: item.x,
|
|
34
|
+
y: item.y,
|
|
35
|
+
width: item.width,
|
|
36
|
+
height: item.height,
|
|
37
|
+
fontName: item.fontName,
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
return {
|
|
41
|
+
pages,
|
|
42
|
+
getConcatenatedText: () => getConcatenatedText(pages),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Checks if a PDF contains source text.
|
|
47
|
+
*
|
|
48
|
+
* @param pdfData Buffer representing the content of the PDF file.
|
|
49
|
+
*
|
|
50
|
+
* @returns A Promise containing a boolean indicating if the PDF has source text.
|
|
51
|
+
*/
|
|
52
|
+
async function hasSourceText(pdfData) {
|
|
53
|
+
const text = await extractTextFromPdf(pdfData);
|
|
54
|
+
return text.getConcatenatedText().trim().length > 0;
|
|
55
|
+
}
|