diff --git a/src/api/PDFDocument.ts b/src/api/PDFDocument.ts index 1eaa3fece..2a3140c33 100644 --- a/src/api/PDFDocument.ts +++ b/src/api/PDFDocument.ts @@ -50,6 +50,7 @@ import { CreateOptions, EmbedFontOptions, SetTitleOptions, + FileSaveOptions, } from './PDFDocumentOptions'; import PDFObject from '../core/objects/PDFObject'; import PDFRef from '../core/objects/PDFRef'; @@ -1358,28 +1359,10 @@ export default class PDFDocument { * @returns Resolves with the bytes of the serialized document. */ async save(options: SaveOptions = {}): Promise { - const { - useObjectStreams = true, - addDefaultPage = true, - objectsPerTick = 50, - updateFieldAppearances = true, - } = options; - - assertIs(useObjectStreams, 'useObjectStreams', ['boolean']); - assertIs(addDefaultPage, 'addDefaultPage', ['boolean']); - assertIs(objectsPerTick, 'objectsPerTick', ['number']); - assertIs(updateFieldAppearances, 'updateFieldAppearances', ['boolean']); - - if (addDefaultPage && this.getPageCount() === 0) this.addPage(); - - if (updateFieldAppearances) { - const form = this.formCache.getValue(); - if (form) form.updateFieldAppearances(); - } - - await this.flush(); + const defaultOptions = this.getDefaultSaveOptions(options); + const { objectsPerTick } = defaultOptions; - const Writer = useObjectStreams ? PDFStreamWriter : PDFWriter; + const Writer = await this.validateAndGetAdaptWriter(defaultOptions); return Writer.forContext(this.context, objectsPerTick).serializeToBuffer(); } @@ -1406,6 +1389,32 @@ export default class PDFDocument { return dataUri ? `data:application/pdf;base64,${base64}` : base64; } + /** + * + * Serialize this document to specific directory path formed with A PDF file + * For example: + * ```js + * const pdfBuffer = await saveToTargetPath { destPath: "/some/your/directory.pdf" } + * ``` + * + * @param options The options are used to determine which path to write + * @returns Serialized Buffer Array from input Destination Path which is located The PDF file + * + */ + async saveToTargetPath(options: FileSaveOptions): Promise { + options && options.outputPath && assertIsValidString(options.outputPath); + const resolvedSaveOptions = this.getDefaultSaveOptions(options); + const { objectsPerTick } = resolvedSaveOptions; + + const Writer = await this.validateAndGetAdaptWriter(resolvedSaveOptions); + const { outputPath, forceWrite } = options; + + return Writer.forContext(this.context, objectsPerTick).writeToTargetPath({ + outputPath, + forceWrite: forceWrite !== undefined ? forceWrite : true, + }); + } + findPageForAnnotationRef(ref: PDFRef): PDFPage | undefined { const pages = this.getPages(); for (let idx = 0, len = pages.length; idx < len; idx++) { @@ -1420,6 +1429,53 @@ export default class PDFDocument { return undefined; } + private async validateAndGetAdaptWriter( + options: SaveOptions, + ): Promise { + const { + useObjectStreams, + addDefaultPage, + objectsPerTick, + updateFieldAppearances, + } = options; + + assertIs(useObjectStreams, 'useObjectStreams', ['boolean']); + assertIs(addDefaultPage, 'addDefaultPage', ['boolean']); + assertIs(objectsPerTick, 'objectsPerTick', ['number']); + assertIs(updateFieldAppearances, 'updateFieldAppearances', ['boolean']); + + if (addDefaultPage && this.getPageCount() === 0) this.addPage(); + + if (updateFieldAppearances) { + const form = this.formCache.getValue(); + if (form) form.updateFieldAppearances(); + } + + await this.flush(); + return useObjectStreams ? PDFStreamWriter : PDFWriter; + } + + private getDefaultSaveOptions(options: SaveOptions): { + useObjectStreams: boolean; + addDefaultPage: boolean; + objectsPerTick: number; + updateFieldAppearances: boolean; + } { + const { + useObjectStreams = true, + addDefaultPage = true, + objectsPerTick = 50, + updateFieldAppearances = true, + } = options; + + return { + useObjectStreams, + addDefaultPage, + objectsPerTick, + updateFieldAppearances, + }; + } + private async embedAll(embeddables: Embeddable[]): Promise { for (let idx = 0, len = embeddables.length; idx < len; idx++) { await embeddables[idx].embed(); @@ -1486,3 +1542,7 @@ function assertIsLiteralOrHexString( throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject); } } + +function assertIsValidString(str?: any): str is string { + return str !== null && str !== undefined && typeof str === 'string'; +} diff --git a/src/api/PDFDocumentOptions.ts b/src/api/PDFDocumentOptions.ts index 172314ef0..dc558fda6 100644 --- a/src/api/PDFDocumentOptions.ts +++ b/src/api/PDFDocumentOptions.ts @@ -21,6 +21,11 @@ export interface Base64SaveOptions extends SaveOptions { dataUri?: boolean; } +export interface FileSaveOptions extends SaveOptions { + outputPath: string; + forceWrite?: boolean; +} + export interface LoadOptions { ignoreEncryption?: boolean; parseSpeed?: ParseSpeeds | number; diff --git a/src/core/document/PDFCrossRefSection.ts b/src/core/document/PDFCrossRefSection.ts index 3c019d41a..58b8432b9 100644 --- a/src/core/document/PDFCrossRefSection.ts +++ b/src/core/document/PDFCrossRefSection.ts @@ -1,6 +1,11 @@ +import { Writable } from 'stream'; import PDFRef from '../objects/PDFRef'; import CharCodes from '../syntax/CharCodes'; -import { copyStringIntoBuffer, padStart } from '../../utils'; +import { + convertStringToUnicodeArray, + copyStringIntoBuffer, + padStart, +} from '../../utils'; export interface Entry { ref: PDFRef; @@ -82,6 +87,45 @@ class PDFCrossRefSection { return size; } + writeBytesInto(stream: Writable) { + stream.write( + Buffer.from([ + CharCodes.x, + CharCodes.r, + CharCodes.e, + CharCodes.f, + CharCodes.Newline, + ]), + ); + + const writeEntriesIntoStream = (entries: Entry[], stream: Writable) => { + entries.forEach((entry) => { + const entryOffset = padStart(String(entry.offset), 10, '0'); + stream.write(convertStringToUnicodeArray(entryOffset)); + stream.write(Buffer.from([CharCodes.Space])); + + const entryGen = padStart(String(entry.ref.generationNumber), 5, '0'); + stream.write(convertStringToUnicodeArray(entryGen)); + stream.write(Buffer.from([CharCodes.Space])); + stream.write(Buffer.from([entry.deleted ? CharCodes.f : CharCodes.n])); + stream.write(Buffer.from([CharCodes.Space])); + stream.write(Buffer.from([CharCodes.Newline])); + }); + }; + + // NOTE: String그냥쓰는 코드 있는지 확인해야해.... + this.subsections.forEach((subsection) => { + stream.write( + convertStringToUnicodeArray(String(subsection[0].ref.objectNumber)), + ); + stream.write(Buffer.from([CharCodes.Space])); + + stream.write(convertStringToUnicodeArray(String(subsection.length))); + stream.write(Buffer.from([CharCodes.Newline])); + writeEntriesIntoStream(subsection, stream); + }); + } + copyBytesInto(buffer: Uint8Array, offset: number): number { const initialOffset = offset; diff --git a/src/core/document/PDFHeader.ts b/src/core/document/PDFHeader.ts index 6c686380d..3449a0324 100644 --- a/src/core/document/PDFHeader.ts +++ b/src/core/document/PDFHeader.ts @@ -1,5 +1,10 @@ +import { Writable } from 'stream'; import CharCodes from '../syntax/CharCodes'; -import { charFromCode, copyStringIntoBuffer } from '../../utils'; +import { + charFromCode, + convertStringToUnicodeArray, + copyStringIntoBuffer, +} from '../../utils'; class PDFHeader { static forVersion = (major: number, minor: number) => @@ -48,6 +53,23 @@ class PDFHeader { return offset - initialOffset; } + + writeBytesInto(stream: Writable): void { + stream.write( + Buffer.from([ + CharCodes.Percent, + CharCodes.P, + CharCodes.D, + CharCodes.F, + CharCodes.Dash, + ]), + ); + stream.write(convertStringToUnicodeArray(this.major)); + stream.write(Buffer.from([CharCodes.Period])); + stream.write(convertStringToUnicodeArray(this.minor)); + stream.write(Buffer.from([CharCodes.Newline])); + stream.write(Buffer.from([CharCodes.Percent, 129, 129, 129, 129])); + } } export default PDFHeader; diff --git a/src/core/document/PDFTrailer.ts b/src/core/document/PDFTrailer.ts index d6fa5f911..301913c20 100644 --- a/src/core/document/PDFTrailer.ts +++ b/src/core/document/PDFTrailer.ts @@ -1,5 +1,6 @@ import CharCodes from '../syntax/CharCodes'; -import { copyStringIntoBuffer } from '../../utils'; +import { convertStringToUnicodeArray, copyStringIntoBuffer } from '../../utils'; +import { Writable } from 'stream'; class PDFTrailer { static forLastCrossRefSectionOffset = (offset: number) => @@ -44,6 +45,36 @@ class PDFTrailer { return offset - initialOffset; } + + writeBytesInto(stream: Writable): void { + stream.write( + Buffer.from([ + CharCodes.s, + CharCodes.t, + CharCodes.a, + CharCodes.r, + CharCodes.t, + CharCodes.x, + CharCodes.r, + CharCodes.e, + CharCodes.f, + CharCodes.Newline, + ]), + ); + + stream.write(convertStringToUnicodeArray(this.lastXRefOffset)); + + stream.write( + Buffer.from([ + CharCodes.Newline, + CharCodes.Percent, + CharCodes.Percent, + CharCodes.E, + CharCodes.O, + CharCodes.F, + ]), + ); + } } export default PDFTrailer; diff --git a/src/core/document/PDFTrailerDict.ts b/src/core/document/PDFTrailerDict.ts index b0521a54f..e32094356 100644 --- a/src/core/document/PDFTrailerDict.ts +++ b/src/core/document/PDFTrailerDict.ts @@ -1,3 +1,4 @@ +import { Writable } from 'stream'; import PDFDict from '../objects/PDFDict'; import CharCodes from '../syntax/CharCodes'; @@ -34,6 +35,23 @@ class PDFTrailerDict { return offset - initialOffset; } + + writeBytesInto(stream: Writable): void { + stream.write( + Buffer.from([ + CharCodes.t, + CharCodes.r, + CharCodes.a, + CharCodes.i, + CharCodes.l, + CharCodes.e, + CharCodes.r, + CharCodes.Newline, + ]), + ); + + this.dict.writeBytesInto(stream); + } } export default PDFTrailerDict; diff --git a/src/core/objects/PDFArray.ts b/src/core/objects/PDFArray.ts index 50f674519..86310de9f 100644 --- a/src/core/objects/PDFArray.ts +++ b/src/core/objects/PDFArray.ts @@ -12,6 +12,7 @@ import PDFContext from '../PDFContext'; import CharCodes from '../syntax/CharCodes'; import { PDFArrayIsNotRectangleError } from '../errors'; import PDFRawStream from './PDFRawStream'; +import { Writable } from 'stream'; class PDFArray extends PDFObject { static withContext = (context: PDFContext) => new PDFArray(context); @@ -171,6 +172,17 @@ class PDFArray extends PDFObject { return offset - initialOffset; } + writeBytesInto(stream: Writable): void { + stream.write(Buffer.from([CharCodes.LeftSquareBracket, CharCodes.Space])); + + this.array.forEach((obj) => { + obj.writeBytesInto(stream); + stream.write(Buffer.from([CharCodes.Space])); + }); + + stream.write(Buffer.from([CharCodes.RightSquareBracket])); + } + scalePDFNumbers(x: number, y: number): void { for (let idx = 0, len = this.size(); idx < len; idx++) { const el = this.lookup(idx); diff --git a/src/core/objects/PDFBool.ts b/src/core/objects/PDFBool.ts index 4af89f938..e495630e6 100644 --- a/src/core/objects/PDFBool.ts +++ b/src/core/objects/PDFBool.ts @@ -1,6 +1,7 @@ import { PrivateConstructorError } from '../errors'; import PDFObject from './PDFObject'; import CharCodes from '../syntax/CharCodes'; +import { Writable } from 'stream'; const ENFORCER = {}; @@ -48,6 +49,22 @@ class PDFBool extends PDFObject { return 5; } } + + writeBytesInto(stream: Writable): void { + this.value + ? stream.write( + Buffer.from([CharCodes.t, CharCodes.r, CharCodes.u, CharCodes.e]), + ) + : stream.write( + Buffer.from([ + CharCodes.f, + CharCodes.a, + CharCodes.l, + CharCodes.s, + CharCodes.e, + ]), + ); + } } export default PDFBool; diff --git a/src/core/objects/PDFDict.ts b/src/core/objects/PDFDict.ts index 5a9997b5c..b57d066b1 100644 --- a/src/core/objects/PDFDict.ts +++ b/src/core/objects/PDFDict.ts @@ -1,3 +1,6 @@ +import { Writable } from 'stream'; +import PDFContext from '../PDFContext'; +import CharCodes from '../syntax/CharCodes'; import PDFArray from './PDFArray'; import PDFBool from './PDFBool'; import PDFHexString from './PDFHexString'; @@ -8,8 +11,6 @@ import PDFObject from './PDFObject'; import PDFRef from './PDFRef'; import PDFStream from './PDFStream'; import PDFString from './PDFString'; -import PDFContext from '../PDFContext'; -import CharCodes from '../syntax/CharCodes'; export type DictMap = Map; @@ -223,6 +224,22 @@ class PDFDict extends PDFObject { return offset - initialOffset; } + + writeBytesInto(stream: Writable): void { + stream.write( + Buffer.from([CharCodes.LessThan, CharCodes.LessThan, CharCodes.Newline]), + ); + + this.entries().forEach((ent) => { + const [key, value] = ent; + key.writeBytesInto(stream); + stream.write(Buffer.from([CharCodes.Space])); + value.writeBytesInto(stream); + stream.write(Buffer.from([CharCodes.Newline])); + }); + + stream.write(Buffer.from([CharCodes.GreaterThan, CharCodes.GreaterThan])); + } } export default PDFDict; diff --git a/src/core/objects/PDFHexString.ts b/src/core/objects/PDFHexString.ts index 92ccf5d8e..e1e5dbbf7 100644 --- a/src/core/objects/PDFHexString.ts +++ b/src/core/objects/PDFHexString.ts @@ -9,8 +9,10 @@ import { parseDate, hasUtf16BOM, byteArrayToHexString, + convertStringToUnicodeArray, } from '../../utils'; import { InvalidPDFDateStringError } from '../errors'; +import { Writable } from 'stream'; class PDFHexString extends PDFObject { static of = (value: string) => new PDFHexString(value); @@ -93,6 +95,12 @@ class PDFHexString extends PDFObject { buffer[offset++] = CharCodes.GreaterThan; return this.value.length + 2; } + + writeBytesInto(stream: Writable): void { + stream.write(Buffer.from([CharCodes.LessThan])); + stream.write(convertStringToUnicodeArray(this.value)); + stream.write(Buffer.from([CharCodes.GreaterThan])); + } } export default PDFHexString; diff --git a/src/core/objects/PDFInvalidObject.ts b/src/core/objects/PDFInvalidObject.ts index 51f93998e..9a78aaba9 100644 --- a/src/core/objects/PDFInvalidObject.ts +++ b/src/core/objects/PDFInvalidObject.ts @@ -1,3 +1,4 @@ +import { Writable } from 'stream'; import PDFObject from './PDFObject'; class PDFInvalidObject extends PDFObject { @@ -29,6 +30,10 @@ class PDFInvalidObject extends PDFObject { } return length; } + + writeBytesInto(stream: Writable): void { + stream.write(this.data); + } } export default PDFInvalidObject; diff --git a/src/core/objects/PDFName.ts b/src/core/objects/PDFName.ts index 86d6dc84c..b2e6f2758 100644 --- a/src/core/objects/PDFName.ts +++ b/src/core/objects/PDFName.ts @@ -4,10 +4,12 @@ import CharCodes from '../syntax/CharCodes'; import { IsIrregular } from '../syntax/Irregular'; import { charFromHexCode, + convertStringToUnicodeArray, copyStringIntoBuffer, toCharCode, toHexString, } from '../../utils'; +import { Writable } from 'stream'; const decodeName = (name: string) => name.replace(/#([\dABCDEF]{2})/g, (_, hex) => charFromHexCode(hex)); @@ -154,6 +156,10 @@ class PDFName extends PDFObject { offset += copyStringIntoBuffer(this.encodedName, buffer, offset); return this.encodedName.length; } + + writeBytesInto(stream: Writable): void { + stream.write(convertStringToUnicodeArray(this.encodedName)); + } } export default PDFName; diff --git a/src/core/objects/PDFNull.ts b/src/core/objects/PDFNull.ts index b9fc91a2f..ff1ed2bdd 100644 --- a/src/core/objects/PDFNull.ts +++ b/src/core/objects/PDFNull.ts @@ -1,5 +1,6 @@ import PDFObject from './PDFObject'; import CharCodes from '../syntax/CharCodes'; +import { Writable } from 'stream'; class PDFNull extends PDFObject { asNull(): null { @@ -25,6 +26,12 @@ class PDFNull extends PDFObject { buffer[offset++] = CharCodes.l; return 4; } + + writeBytesInto(stream: Writable): void { + stream.write( + Buffer.from([CharCodes.n, CharCodes.u, CharCodes.l, CharCodes.l]), + ); + } } export default new PDFNull(); diff --git a/src/core/objects/PDFNumber.ts b/src/core/objects/PDFNumber.ts index 4c0c24b3e..9dc3fb409 100644 --- a/src/core/objects/PDFNumber.ts +++ b/src/core/objects/PDFNumber.ts @@ -1,4 +1,9 @@ -import { copyStringIntoBuffer, numberToString } from '../../utils/index'; +import { Writable } from 'stream'; +import { + convertStringToUnicodeArray, + copyStringIntoBuffer, + numberToString, +} from '../../utils/index'; import PDFObject from './PDFObject'; @@ -39,6 +44,10 @@ class PDFNumber extends PDFObject { offset += copyStringIntoBuffer(this.stringValue, buffer, offset); return this.stringValue.length; } + + writeBytesInto(stream: Writable): void { + stream.write(convertStringToUnicodeArray(this.stringValue)); + } } export default PDFNumber; diff --git a/src/core/objects/PDFObject.ts b/src/core/objects/PDFObject.ts index e8af42cf3..244810e42 100644 --- a/src/core/objects/PDFObject.ts +++ b/src/core/objects/PDFObject.ts @@ -1,3 +1,4 @@ +import { Writable } from 'stream'; import { MethodNotImplementedError } from '../errors'; import PDFContext from '../PDFContext'; @@ -17,6 +18,13 @@ class PDFObject { copyBytesInto(_buffer: Uint8Array, _offset: number): number { throw new MethodNotImplementedError(this.constructor.name, 'copyBytesInto'); } + + writeBytesInto(_stream: Writable): void { + throw new MethodNotImplementedError( + this.constructor.name, + 'writeBytesInto', + ); + } } export default PDFObject; diff --git a/src/core/objects/PDFRef.ts b/src/core/objects/PDFRef.ts index 2868478a2..0909c959c 100644 --- a/src/core/objects/PDFRef.ts +++ b/src/core/objects/PDFRef.ts @@ -1,6 +1,7 @@ import { PrivateConstructorError } from '../errors'; import PDFObject from '../objects/PDFObject'; -import { copyStringIntoBuffer } from '../../utils'; +import { convertStringToUnicodeArray, copyStringIntoBuffer } from '../../utils'; +import { Writable } from 'stream'; const ENFORCER = {}; const pool = new Map(); @@ -50,6 +51,10 @@ class PDFRef extends PDFObject { offset += copyStringIntoBuffer(this.tag, buffer, offset); return this.tag.length; } + + writeBytesInto(stream: Writable): void { + stream.write(convertStringToUnicodeArray(this.tag)); + } } export default PDFRef; diff --git a/src/core/objects/PDFStream.ts b/src/core/objects/PDFStream.ts index 92590b32f..f2f4d952e 100644 --- a/src/core/objects/PDFStream.ts +++ b/src/core/objects/PDFStream.ts @@ -5,6 +5,7 @@ import PDFNumber from './PDFNumber'; import PDFObject from './PDFObject'; import PDFContext from '../PDFContext'; import CharCodes from '../syntax/CharCodes'; +import { Writable } from 'stream'; class PDFStream extends PDFObject { readonly dict: PDFDict; @@ -95,6 +96,44 @@ class PDFStream extends PDFObject { return offset - initialOffset; } + + writeBytesInto(stream: Writable): void { + this.updateDict(); + + this.dict.writeBytesInto(stream); + + stream.write( + Buffer.from([ + CharCodes.Newline, + CharCodes.s, + CharCodes.t, + CharCodes.r, + CharCodes.e, + CharCodes.a, + CharCodes.m, + CharCodes.Newline, + ]), + ); + + this.getContents().forEach((content) => + stream.write(Buffer.from([content])), + ); + + stream.write( + Buffer.from([ + CharCodes.Newline, + CharCodes.e, + CharCodes.n, + CharCodes.d, + CharCodes.s, + CharCodes.t, + CharCodes.r, + CharCodes.e, + CharCodes.a, + CharCodes.m, + ]), + ); + } } export default PDFStream; diff --git a/src/core/objects/PDFString.ts b/src/core/objects/PDFString.ts index 09931cedc..3efa97ec6 100644 --- a/src/core/objects/PDFString.ts +++ b/src/core/objects/PDFString.ts @@ -8,8 +8,10 @@ import { toCharCode, parseDate, hasUtf16BOM, + convertStringToUnicodeArray, } from '../../utils'; import { InvalidPDFDateStringError } from '../errors'; +import { Writable } from 'stream'; class PDFString extends PDFObject { // The PDF spec allows newlines and parens to appear directly within a literal @@ -113,6 +115,12 @@ class PDFString extends PDFObject { buffer[offset++] = CharCodes.RightParen; return this.value.length + 2; } + + writeBytesInto(stream: Writable): void { + stream.write(Buffer.from([CharCodes.LeftParen])); + stream.write(convertStringToUnicodeArray(this.value)); + stream.write(Buffer.from([CharCodes.RightParen])); + } } export default PDFString; diff --git a/src/core/writers/PDFWriter.ts b/src/core/writers/PDFWriter.ts index c30af2bdb..f9647d97b 100644 --- a/src/core/writers/PDFWriter.ts +++ b/src/core/writers/PDFWriter.ts @@ -1,3 +1,12 @@ +import fs from 'fs'; +import path from 'path'; +import { FileSaveOptions } from 'src/api'; +import { Writable } from 'stream'; +import { + convertStringToUnicodeArray, + copyStringIntoBuffer, + waitForTick, +} from '../../utils'; import PDFCrossRefSection from '../document/PDFCrossRefSection'; import PDFHeader from '../document/PDFHeader'; import PDFTrailer from '../document/PDFTrailer'; @@ -7,10 +16,9 @@ import PDFObject from '../objects/PDFObject'; import PDFRef from '../objects/PDFRef'; import PDFStream from '../objects/PDFStream'; import PDFContext from '../PDFContext'; -import PDFObjectStream from '../structures/PDFObjectStream'; import PDFSecurity from '../security/PDFSecurity'; +import PDFObjectStream from '../structures/PDFObjectStream'; import CharCodes from '../syntax/CharCodes'; -import { copyStringIntoBuffer, waitForTick } from '../../utils'; export interface SerializationInfo { size: number; @@ -35,6 +43,101 @@ class PDFWriter { this.objectsPerTick = objectsPerTick; } + async writeToTargetPath( + options: Pick, + ): Promise { + const { outputPath, forceWrite } = options; + const splitPath = outputPath.split('/'); + const fileName = splitPath.pop(); + const dirPath = splitPath.join('/'); + + if (!fileName) { + throw new Error('File name is Missing'); + } + + const match = fileName.match(/^(.+)\.([a-zA-Z0-9]+)$/); + if (!match || match[2] !== 'pdf') { + throw new Error('Invalid file extension. Only ".pdf" files are allowed.'); + } + + if (forceWrite) { + fs.mkdirSync(dirPath, { recursive: true }); + } else { + if (!fs.existsSync(outputPath)) { + throw Error('File does not exist'); + } + } + + const destWriteStream = fs.createWriteStream(path.join(dirPath, fileName)); + + await new Promise((res, rej) => { + destWriteStream.on('finish', res); + destWriteStream.on('error', rej); + + this.serializeToStream(destWriteStream) + .then(() => destWriteStream.end()) + .catch(rej); + }); + + return new Uint8Array(fs.readFileSync(outputPath)); + } + + async serializeToStream(destStream: Writable): Promise { + const { header, indirectObjects, xref, trailerDict, trailer } = + await this.computeBufferSize(); + + header.writeBytesInto(destStream); + destStream.write(Buffer.from([CharCodes.Newline, CharCodes.Newline])); + + for (let idx = 0, len = indirectObjects.length; idx < len; idx++) { + const [ref, object] = indirectObjects[idx]; + + const objectNumber = String(ref.objectNumber); + destStream.write(convertStringToUnicodeArray(objectNumber)); + destStream.write(Buffer.from([CharCodes.Space])); + + const generationNumber = String(ref.generationNumber); + destStream.write(convertStringToUnicodeArray(generationNumber)); + destStream.write(Buffer.from([CharCodes.Space])); + + destStream.write( + Buffer.from([CharCodes.o, CharCodes.b, CharCodes.j, CharCodes.Newline]), + ); + + object.writeBytesInto(destStream); + + destStream.write( + Buffer.from([ + CharCodes.Newline, + CharCodes.e, + CharCodes.n, + CharCodes.d, + CharCodes.o, + CharCodes.b, + CharCodes.j, + CharCodes.Newline, + CharCodes.Newline, + ]), + ); + + const n = + object instanceof PDFObjectStream ? object.getObjectsCount() : 1; + if (this.shouldWaitForTick(n)) await waitForTick(); + } + + if (xref) { + xref.writeBytesInto(destStream); + destStream.write(Buffer.from([CharCodes.Newline])); + } + + if (trailerDict) { + trailerDict.writeBytesInto(destStream); + destStream.write(Buffer.from([CharCodes.Newline, CharCodes.Newline])); + } + + trailer.writeBytesInto(destStream); + } + async serializeToBuffer(): Promise { const { size, header, indirectObjects, xref, trailerDict, trailer } = await this.computeBufferSize(); diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 019273c8f..4ae0e3d75 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -37,6 +37,10 @@ export const copyStringIntoBuffer = ( return length; }; +export const convertStringToUnicodeArray = (str: string): Buffer => { + return Buffer.from(str.split('').map((char) => char.charCodeAt(0))); +}; + export const addRandomSuffix = (prefix: string, suffixLength = 4) => `${prefix}-${Math.floor(Math.random() * 10 ** suffixLength)}`; diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts index b461ed43c..56dbd6396 100644 --- a/tests/api/PDFDocument.spec.ts +++ b/tests/api/PDFDocument.spec.ts @@ -14,7 +14,11 @@ import { PrintScaling, ReadingDirection, ViewerPreferences, + degrees, + rgb, + grayscale, } from '../../src/index'; +import path from 'path'; const examplePngImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TxaoVBzuIdMhQnSyIijhKFYtgobQVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5uak6CIl/i8ptIjx4Lgf7+497t4BQqPCVLNrAlA1y0jFY2I2tyr2vKIfAgLoRVhipp5IL2bgOb7u4ePrXZRneZ/7cwwoeZMBPpF4jumGRbxBPLNp6Zz3iUOsJCnE58TjBl2Q+JHrsstvnIsOCzwzZGRS88QhYrHYwXIHs5KhEk8TRxRVo3wh67LCeYuzWqmx1j35C4N5bSXNdZphxLGEBJIQIaOGMiqwEKVVI8VEivZjHv4Rx58kl0yuMhg5FlCFCsnxg//B727NwtSkmxSMAd0vtv0xCvTsAs26bX8f23bzBPA/A1da219tALOfpNfbWuQIGNwGLq7bmrwHXO4Aw0+6ZEiO5KcpFArA+xl9Uw4YugX61tzeWvs4fQAy1NXyDXBwCIwVKXvd492Bzt7+PdPq7wcdn3KFLu4iBAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlFJREFUeNrt289r02AYB/Dvk6Sl4EDKpllTlFKsnUdBHXgUBEHwqHj2IJ72B0zwKHhxJ08i/gDxX/AiRfSkBxELXTcVxTa2s2xTsHNN8ngQbQL70RZqG/Z9b29JnvflkydP37whghG3ZaegoxzfwB5vBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgwB5rstWPtnP0LqBX/vZNyLF6vVrpN/hucewhb4g+B2AyAwiwY7NGOXijviS9vBeYh6CEP4edBLDADCAAAQhAAAIQgAAEIAABCDAUAFF/GIN1DM+PBYCo/ohMXDQ1WPjoeUZH1mMBEEh0oqLGvsHCy0S4NzWVWotJBogbvZB+brDwQT7UWSmXy5sxyQB9HQEROdVv4HQ+vx+QmS4iXsWmCK7Usu8AhOqAXMzlcn3VgWTbugQgEYrxMkZ/gyUPgnuhe2C6/Stxvdeg2ezMJERvhOuoZ+JBrNYBRuDdBtDuXkDM25nCHLbZSv9X6A4VHU+DpwCcbvbjcetLtTaOANtuirrux08HM0euisjDEMKC7RQuq+C+pVJqpzx3NZ3+eeBza9I0rWJgyHnxg2sAJrqnaHUzFcyN60Jox13hprv8aNopZBS4GcqWWVHM+lAkN0zY7ncgkYBukRoKLPpiXVj9UFkfV4Bdl8Jf60u3IMZZAG/6iLuhkDvaSZ74VqtUx3kp3NN7gUZt8RmA43a2eEY1OCfQ04AcBpAGkAKwpkBLIG8BfQE/eNJsvG/G4VlARj0BfjDBx2ECEIAABCAAAQhAAAIQgAAE+P/tN8YvpvbTDBOlAAAAAElFTkSuQmCC'; @@ -22,6 +26,9 @@ const examplePngImage = const unencryptedPdfBytes = fs.readFileSync('assets/pdfs/normal.pdf'); const oldEncryptedPdfBytes1 = fs.readFileSync('assets/pdfs/encrypted_old.pdf'); +const validWriteTargetPath = 'assets/pdfs/stream/normal.pdf'; +const anotherValidWriteTargetPath = 'assets/pdfs/stream/normal_another.pdf'; + // Had to remove this file due to DMCA complaint, so commented this line out // along with the 2 tests that depend on it. Would be nice to find a new file // that we could drop in here, but the tests are for non-critical functionality, @@ -163,6 +170,39 @@ describe(`PDFDocument`, () => { }); }); + describe(`saveToTargetPath() method with embedFont()`, () => { + it(`should be same result comparing with [save()] result After Embedding font`, async () => { + const customFont = fs.readFileSync('assets/fonts/ubuntu/Ubuntu-B.ttf'); + const pdfDoc1 = await PDFDocument.create({ updateMetadata: false }); + const pdfDoc2 = await PDFDocument.create({ updateMetadata: false }); + const pdfDoc3 = await PDFDocument.create({ updateMetadata: false }); + + pdfDoc1.registerFontkit(fontkit); + pdfDoc2.registerFontkit(fontkit); + pdfDoc3.registerFontkit(fontkit); + + await pdfDoc1.embedFont(customFont); + await pdfDoc2.embedFont(customFont); + await pdfDoc3.embedFont(customFont); + + const savedDoc1 = await pdfDoc1.save(); + + const savedDoc2 = await pdfDoc2.saveToTargetPath({ + outputPath: validWriteTargetPath, + forceWrite: true, + }); + + const savedDoc3 = await pdfDoc3.saveToTargetPath({ + outputPath: anotherValidWriteTargetPath, + forceWrite: true, + }); + + expect(savedDoc1).toEqual(savedDoc2); + expect(savedDoc1).toEqual(savedDoc3); + expect(savedDoc2).toEqual(savedDoc3); + }); + }); + describe(`setLanguage() method`, () => { it(`sets the language of the document`, async () => { const pdfDoc = await PDFDocument.create(); @@ -528,6 +568,132 @@ describe(`PDFDocument`, () => { }); }); + describe(`saveToTargetPath() method`, () => { + const validDirPath = 'assets/pdfs/stream/'; + const invalidDirPath = '/invalid/directory/path/'; + const validFileName = 'valid_output.pdf'; + const invalidFileName = 'invalid_output.txt'; + + beforeEach(() => { + jest.clearAllMocks(); + // Ensure the valid directory exists + if (!fs.existsSync(validDirPath)) { + fs.mkdirSync(validDirPath, { recursive: true }); + } + }); + + afterEach(() => { + // Cleanup generated files + const testFiles = [validFileName, 'created_dir_test.pdf']; + testFiles.forEach((file) => { + const filePath = path.join(validDirPath, file); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + }); + }); + + it(`should throw an error when provided with an invalid target directory path`, async () => { + const pdfDoc = await PDFDocument.create(); + await expect( + pdfDoc.saveToTargetPath({ + outputPath: path.join(invalidDirPath, validFileName), + forceWrite: false, + }), + ).rejects.toThrow('File does not exist'); + }); + + it(`should throw an error when no file name is provided in the output path`, async () => { + const pdfDoc = await PDFDocument.create(); + await expect( + pdfDoc.saveToTargetPath({ + outputPath: validDirPath, // Directory only, no file name + forceWrite: true, + }), + ).rejects.toThrow('File name is Missing'); + }); + + it(`should throw an error when the file name does not have a .pdf extension`, async () => { + const pdfDoc = await PDFDocument.create(); + await expect( + pdfDoc.saveToTargetPath({ + outputPath: path.join(validDirPath, invalidFileName), + forceWrite: true, + }), + ).rejects.toThrow( + 'Invalid file extension. Only ".pdf" files are allowed.', + ); + }); + + it(`can create non-existing directory path when 'forceWrite' flag is enabled`, async () => { + const pdfDoc = await PDFDocument.create(); + const nonExistingDirPath = path.join(validDirPath, 'created_dir'); + const outputPath = path.join(nonExistingDirPath, 'created_dir_test.pdf'); + + await expect( + pdfDoc.saveToTargetPath({ + outputPath, + forceWrite: true, + }), + ).resolves.toBeInstanceOf(Uint8Array); + + // Ensure the directory and file are created + expect(fs.existsSync(nonExistingDirPath)).toBe(true); + expect(fs.existsSync(outputPath)).toBe(true); + }); + + it(`should throw an error when the outputPath directory does not exist and 'forceWrite' flag is disabled`, async () => { + const pdfDoc = await PDFDocument.create(); + const outputPath = path.join(invalidDirPath, validFileName); + + await expect( + pdfDoc.saveToTargetPath({ + outputPath, + forceWrite: false, + }), + ).rejects.toThrow('File does not exist'); + }); + + it(`should produce the same result as the save() method`, async () => { + const pdfDoc = await PDFDocument.create(); + pdfDoc.setTitle('Test PDF Document'); + + pdfDoc.addPage(); + pdfDoc.addPage(); + + pdfDoc.getPages().forEach((page) => { + page.drawRectangle({ + x: 25, + y: 75, + rx: 5, // This is the border radius + ry: 5, + width: 250, + height: 75, + rotate: degrees(-15), + borderWidth: 5, + borderColor: grayscale(0.5), + color: rgb(0.75, 0.2, 0.2), + opacity: 0.5, + borderOpacity: 0.75, + }); + }); + + const saveBytes = await pdfDoc.save(); + const saveToTargetBytes = await pdfDoc.saveToTargetPath({ + outputPath: path.join(validDirPath, validFileName), + forceWrite: true, + }); + + expect(saveToTargetBytes).toEqual(saveBytes); + + const writtenBytes = new Uint8Array( + fs.readFileSync(path.join(validDirPath, validFileName)), + ); + + expect(writtenBytes).toEqual(saveBytes); + }); + }); + describe(`copy() method`, () => { let pdfDoc: PDFDocument; let srcDoc: PDFDocument;