diff --git a/CHANGELOG.md b/CHANGELOG.md index 856dcdb0..9b6bcbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased -- Add support for PDF/A-1b and PDF/A-1a +- Add support for PDF/A-1b, PDF/A-1a, PDF/A-2b, PDF/A-2a, PDF/A-3b, PDF/A-3a ### [v0.13.0] - 2021-10-24 diff --git a/docs/getting_started.md b/docs/getting_started.md index 5a4f3ce8..e10ce2e9 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -272,16 +272,20 @@ The restrictions on PDF/A documents are: - Addition of XMP metadata - Must define color spaces -Currently, PDFKit aims to support PDF/A-1b and PDF/A-1a standards, also known as level B compliance and level A compliance, respectively. +Currently, PDFKit aims to support PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/A-1a, PDF/A-2a, PDF/A-3a standards, also known as level B conformance and level A conformance, respectively. -In order to create PDF/A documents, set `subset` to either `PDF/A-1` (`PDF/A-1a` or `PDF/A-1b` for A specific conformance level) when creating the `PDFDocument` in `options` object. If `PDF/A-1` is passed, conformance level `B` is used. +In order to create PDF/A documents, set `subset` to either `PDF/A-1` or `PDF/A-1b` for level B (basic) conformance, or `PDF/A-1a` for level A (accessible) conformance when creating the `PDFDocument` in `options` object. -Futhermore, you will need to specify the other options relevant to the PDF/A subset you wish to use, for PDFA-1 being: +Similary, use `PDF/A-2` or `PDF/A-2b` for PDF/A-2 level B conformance and `PDF/A-2a` for PDF/A-2 level A conformance. `PDF/A-3` or `PDF/A-3b` can be used for PDF/A-3 level B conformance and `PDF/A-3a` for PDF/A-3 level A conformance. + +Futhermore, you will need to specify the other options relevant to the PDF/A subset you wish to use, for PDF/A-1 being: - `pdfVersion` set to at least `1.4` - `tagged` set to `true` for PDF/A-1a -In order to verify PDF/A compliance, veraPDF is an excellent open source validator. +For PDF/A-2 and PDF/A-3, the `pdfVersion` needs to be set to at least `1.7` and `tagged` needs to be `true` for level A conformance. + +In order to verify the generated document for PDF/A and its subsets conformance, veraPDF is an excellent open source validator. ### Adding content diff --git a/lib/mixins/pdfa1.js b/lib/mixins/pdfa.js similarity index 58% rename from lib/mixins/pdfa1.js rename to lib/mixins/pdfa.js index 613ef0be..e7fca918 100644 --- a/lib/mixins/pdfa1.js +++ b/lib/mixins/pdfa.js @@ -2,27 +2,26 @@ import fs from 'fs'; export default { - initPDFA1(pOptions) { - switch(pOptions.subset) { - case 'PDF/A-1': - case 'PDF/A-1b': - this.subset = 1; - this.subset_conformance = 'B'; - break; - case 'PDF/A-1a': - this.subset = 1; - this.subset_conformance = 'A'; - break; + initPDFA(pSubset) { + if (pSubset.charAt(pSubset.length - 3) === '-') { + this.subset_conformance = pSubset.charAt(pSubset.length - 1).toUpperCase(); + this.subset = parseInt(pSubset.charAt(pSubset.length - 2)); + } else { + // Default to Basic conformance when user doesn't specify + this.subset_conformance = 'B'; + this.subset = parseInt(pSubset.charAt(pSubset.length - 1)); } }, endSubset() { this._addPdfaMetadata(); - this._addColorOutputIntent(); + const jsPath = `${__dirname}/data/sRGB_IEC61966_2_1.icc` + const jestPath = `${__dirname}/../color_profiles/sRGB_IEC61966_2_1.icc` + this._addColorOutputIntent(fs.existsSync(jsPath) ? jsPath : jestPath); }, - _addColorOutputIntent() { - const iccProfile = fs.readFileSync(__dirname + '/../color_profiles/sRGB_IEC61966_2_1.icc'); + _addColorOutputIntent(pICCPath) { + const iccProfile = fs.readFileSync(pICCPath); const colorProfileRef = this.ref({ Length: iccProfile.length, @@ -55,5 +54,5 @@ export default { _addPdfaMetadata() { this.appendXML(this._getPdfaid()); }, - + } \ No newline at end of file diff --git a/lib/mixins/subsets.js b/lib/mixins/subsets.js index f4ee828c..b076efa1 100644 --- a/lib/mixins/subsets.js +++ b/lib/mixins/subsets.js @@ -1,5 +1,4 @@ -import PDFA1 from './pdfa1'; -// import PDFA2 from './pdfa2'; +import PDFA from './pdfa'; export default { _importSubset(subset) { @@ -7,21 +6,20 @@ export default { }, initSubset(options) { + switch (options.subset) { case 'PDF/A-1': case 'PDF/A-1a': case 'PDF/A-1b': - this._importSubset(PDFA1); - this.initPDFA1(options); + case 'PDF/A-2': + case 'PDF/A-2a': + case 'PDF/A-2b': + case 'PDF/A-3': + case 'PDF/A-3a': + case 'PDF/A-3b': + this._importSubset(PDFA); + this.initPDFA(options.subset); break; - // case 'PDF/A-2': - // case 'PDF/A-2a': - // case 'PDF/A-2b': - // case 'PDF/A-2u': - // this._importSubset(PDFA2); - // this.initPDFA2(options); - // break; } } -} - +} \ No newline at end of file diff --git a/tests/unit/pdfa2.spec.js b/tests/unit/pdfa2.spec.js new file mode 100644 index 00000000..7a694252 --- /dev/null +++ b/tests/unit/pdfa2.spec.js @@ -0,0 +1,88 @@ +import PDFDocument from '../../lib/document'; +import { logData, joinTokens } from './helpers'; + +describe('PDF/A-2', () => { + + test('metadata is present', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-2' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + expect(data).toContainChunk([ + `11 0 obj`, + `<<\n/length 892\n/Type /Metadata\n/Subtype /XML\n/Length 894\n>>` + ]); + }); + + test('color profile is present', () => { + const expected = [ + `10 0 obj`, + joinTokens( + '<<', + '/Type /OutputIntent', + '/S /GTS_PDFA1', + '/Info (sRGB IEC61966-2.1)', + '/OutputConditionIdentifier (sRGB IEC61966-2.1)', + '/DestOutputProfile 9 0 R', + '>>' + ), + ]; + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-2' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + expect(data).toContainChunk(expected); + }); + + test('metadata contains pdfaid part and conformance', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-2' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:part>2'); + expect(metadata).toContain('pdfaid:conformance'); + }); + + test('metadata pdfaid conformance B', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-2b' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:conformance>B'); + }); + + test('metadata pdfaid conformance A', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-2a' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:conformance>A'); + }); + +}); \ No newline at end of file diff --git a/tests/unit/pdfa3.spec.js b/tests/unit/pdfa3.spec.js new file mode 100644 index 00000000..d93bc280 --- /dev/null +++ b/tests/unit/pdfa3.spec.js @@ -0,0 +1,88 @@ +import PDFDocument from '../../lib/document'; +import { logData, joinTokens } from './helpers'; + +describe('PDF/A-3', () => { + + test('metadata is present', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-3' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + expect(data).toContainChunk([ + `11 0 obj`, + `<<\n/length 892\n/Type /Metadata\n/Subtype /XML\n/Length 894\n>>` + ]); + }); + + test('color profile is present', () => { + const expected = [ + `10 0 obj`, + joinTokens( + '<<', + '/Type /OutputIntent', + '/S /GTS_PDFA1', + '/Info (sRGB IEC61966-2.1)', + '/OutputConditionIdentifier (sRGB IEC61966-2.1)', + '/DestOutputProfile 9 0 R', + '>>' + ), + ]; + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-3' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + expect(data).toContainChunk(expected); + }); + + test('metadata contains pdfaid part and conformance', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-3' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:part>3'); + expect(metadata).toContain('pdfaid:conformance'); + }); + + test('metadata pdfaid conformance B', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-3b' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:conformance>B'); + }); + + test('metadata pdfaid conformance A', () => { + let options = { + autoFirstPage: false, + pdfVersion: '1.7', + subset: 'PDF/A-3a' + }; + let doc = new PDFDocument(options); + const data = logData(doc); + doc.end(); + let metadata = Buffer.from(data[27]).toString(); + + expect(metadata).toContain('pdfaid:conformance>A'); + }); + +}); \ No newline at end of file