├── .babelrc ├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── lint-test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── accessibility.md ├── annotations.md ├── attachments.md ├── destinations.md ├── fonts │ ├── Alegreya-Bold.ttf │ ├── AlegreyaSans-Light.ttf │ ├── Chalkboard.ttc │ ├── GoodDog.ttf │ ├── Merriweather-Regular.ttf │ ├── SourceCodePro-Bold.ttf │ └── SourceCodePro-Regular.ttf ├── forms.md ├── generate.js ├── generate_website.js ├── getting_started.md ├── guide.pdf ├── images.md ├── images │ ├── acroforms.png │ └── test.jpeg ├── outline.md ├── paper_sizes.md ├── publish_website.js ├── table.md ├── template.pug ├── text.md ├── vector.md └── you_made_it.md ├── eslint.config.mjs ├── examples ├── attachment.js ├── attachment.pdf ├── browserify │ ├── browser.html │ └── browser.js ├── fonts │ ├── Chalkboard.ttc │ ├── DejaVuSans.ttf │ ├── GoodDog.ttf │ ├── Helvetica.dfont │ ├── Montserrat-Bold.otf │ └── PalatinoBold.ttf ├── form.js ├── form.pdf ├── images │ ├── dice.png │ ├── fish.png │ ├── interlaced-grayscale-8bit.png │ ├── interlaced-pallete-8bit.png │ ├── interlaced-rgb-16bit.png │ ├── interlaced-rgb-8bit.png │ ├── interlaced-rgb-alpha-8bit.png │ ├── pngsuite-gray-transparent-white.png │ ├── pngsuite-rgb-transparent-white.png │ ├── straight.png │ ├── test.jpeg │ ├── test.png │ ├── test2.png │ └── test3.png ├── kitchen-sink-accessible.js ├── kitchen-sink-accessible.pdf ├── kitchen-sink.js ├── kitchen-sink.pdf ├── text-link.js ├── text-link.pdf ├── tiger.js └── webpack │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── httpHelpers.js │ ├── index.html │ ├── index.js │ ├── lazy-assets │ │ └── test.jpeg │ ├── pdfkitHelpers.js │ ├── registerStaticFiles.js │ └── static-assets │ │ ├── fonts │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ └── Roboto-Regular.ttf │ │ └── images │ │ └── bee.png │ └── webpack.config.js ├── lib ├── abstract_reference.js ├── data.js ├── document.js ├── font.js ├── font │ ├── afm.js │ ├── data │ │ ├── Courier-Bold.afm │ │ ├── Courier-BoldOblique.afm │ │ ├── Courier-Oblique.afm │ │ ├── Courier.afm │ │ ├── Helvetica-Bold.afm │ │ ├── Helvetica-BoldOblique.afm │ │ ├── Helvetica-Oblique.afm │ │ ├── Helvetica.afm │ │ ├── MustRead.html │ │ ├── Symbol.afm │ │ ├── Times-Bold.afm │ │ ├── Times-BoldItalic.afm │ │ ├── Times-Italic.afm │ │ ├── Times-Roman.afm │ │ └── ZapfDingbats.afm │ ├── embedded.js │ └── standard.js ├── font_factory.js ├── gradient.js ├── image.js ├── image │ ├── jpeg.js │ └── png.js ├── line_wrapper.js ├── metadata.js ├── mixins │ ├── acroform.js │ ├── annotations.js │ ├── attachments.js │ ├── color.js │ ├── data │ │ └── sRGB_IEC61966_2_1.icc │ ├── fonts.js │ ├── images.js │ ├── markings.js │ ├── metadata.js │ ├── outline.js │ ├── pdfa.js │ ├── pdfua.js │ ├── subsets.js │ ├── table.js │ ├── text.js │ └── vector.js ├── name_tree.js ├── number_tree.js ├── object.js ├── outline.js ├── page.js ├── path.js ├── pattern.js ├── reference.js ├── saslprep │ ├── index.js │ └── lib │ │ ├── code-points.js │ │ └── util.js ├── security.js ├── spotcolor.js ├── structure_content.js ├── structure_element.js ├── table │ ├── accessibility.js │ ├── index.js │ ├── normalize.js │ ├── render.js │ ├── size.js │ ├── style.js │ └── utils.js ├── tree.js ├── utils.js └── virtual-fs.js ├── package.json ├── rollup.config.js ├── tests ├── fonts │ ├── Roboto-Italic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-MediumItalic.ttf │ └── Roboto-Regular.ttf ├── images │ ├── bee.jpg │ ├── bee.js │ ├── bee.png │ ├── fish.png │ ├── glassware-noisy.png │ ├── interlaced-grayscale-8bit.png │ ├── interlaced-pallete-8bit.png │ ├── interlaced-rgb-16bit.png │ ├── interlaced-rgb-8bit.png │ ├── interlaced-rgb-alpha-8bit.png │ ├── orientation-1.jpeg │ ├── orientation-2.jpeg │ ├── orientation-3.jpeg │ ├── orientation-4.jpeg │ ├── orientation-5.jpeg │ ├── orientation-6.jpeg │ ├── orientation-7.jpeg │ ├── orientation-8.jpeg │ ├── pngsuite-gray-transparent-black.png │ ├── pngsuite-gray-transparent-white.png │ ├── pngsuite-palette-transparent-white.png │ ├── pngsuite-rgb-transparent-white.png │ ├── sampleImage.jpg │ └── straight.png ├── unit │ ├── acroform.spec.js │ ├── annotations.spec.js │ ├── attachments.spec.js │ ├── color.spec.js │ ├── document.spec.js │ ├── font.spec.js │ ├── gradient.spec.js │ ├── helpers.js │ ├── image.spec.js │ ├── line_wrapper.spec.js │ ├── markings.spec.js │ ├── metadata.spec.js │ ├── object.spec.js │ ├── page.spec.js │ ├── pattern.spec.js │ ├── pdfa1.spec.js │ ├── pdfa2.spec.js │ ├── pdfa3.spec.js │ ├── pdfua.spec.js │ ├── png.spec.js │ ├── reference.spec.js │ ├── saslprep.spec.js │ ├── setupTests.js │ ├── table.spec.js │ ├── text.spec.js │ ├── toContainChunk │ │ └── index.js │ ├── toContainText │ │ └── index.js │ ├── trailer.spec.js │ ├── utils.spec.js │ ├── vector.spec.js │ └── virtual-fs.spec.js └── visual │ ├── __image_snapshots__ │ ├── fonts-spec-js-fonts-default-helvetica-1-snap.png │ ├── fonts-spec-js-fonts-helvetica-bold-1-snap.png │ ├── fonts-spec-js-fonts-roboto-1-snap.png │ ├── fonts-spec-js-fonts-roboto-bold-1-snap.png │ ├── images-spec-js-images-orientation-1-snap.png │ ├── images-spec-js-images-orientation-document-option-1-snap.png │ ├── images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png │ ├── images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png │ ├── table-spec-js-table-column-row-spans-1-snap.png │ ├── table-spec-js-table-defining-column-widths-1-snap.png │ ├── table-spec-js-table-defining-row-heights-1-snap.png │ ├── table-spec-js-table-iterables-1-snap.png │ ├── table-spec-js-table-line-flowing-rotated-text-1-snap.png │ ├── table-spec-js-table-multi-line-rotated-text-1-snap.png │ ├── table-spec-js-table-multi-page-table-1-snap.png │ ├── table-spec-js-table-multi-page-table-2-snap.png │ ├── table-spec-js-table-optional-border-1-snap.png │ ├── table-spec-js-table-rotated-text-1-snap.png │ ├── table-spec-js-table-simple-table-1-snap.png │ ├── table-spec-js-table-styling-tables-1-snap.png │ ├── table-spec-js-table-styling-tables-2-snap.png │ ├── text-spec-js-text-alignment-1-snap.png │ ├── text-spec-js-text-continued-text-with-open-type-features-1-snap.png │ ├── text-spec-js-text-decoration-1-snap.png │ ├── text-spec-js-text-list-1-snap.png │ ├── text-spec-js-text-list-lettered-1-snap.png │ ├── text-spec-js-text-list-numbered-1-snap.png │ ├── text-spec-js-text-list-with-line-breaks-in-items-1-snap.png │ ├── text-spec-js-text-list-with-sub-list-ordered-1-snap.png │ ├── text-spec-js-text-list-with-sub-list-unordered-1-snap.png │ ├── text-spec-js-text-rotated-multi-line-text-1-snap.png │ ├── text-spec-js-text-rotated-text-1-snap.png │ ├── text-spec-js-text-simple-text-1-snap.png │ ├── text-spec-js-text-soft-hyphen-1-snap.png │ ├── vector-spec-js-vector-complex-svg-1-snap.png │ ├── vector-spec-js-vector-simple-shapes-1-snap.png │ ├── vector-spec-js-vector-svg-path-1-snap.png │ └── vector-spec-js-vector-svg-path-2-snap.png │ ├── fonts.spec.js │ ├── helpers.js │ ├── images.spec.js │ ├── pdf2png.js │ ├── pdfmake │ ├── __image_snapshots__ │ │ ├── absolute-spec-js-pdfmake-absolute-1-snap.png │ │ ├── absolute-spec-js-pdfmake-absolute-2-snap.png │ │ ├── absolute-spec-js-pdfmake-absolute-3-snap.png │ │ ├── background-spec-js-pdfmake-background-1-snap.png │ │ ├── background-spec-js-pdfmake-background-2-snap.png │ │ ├── background-spec-js-pdfmake-background-3-snap.png │ │ ├── basics-spec-js-pdfmake-basics-1-snap.png │ │ ├── columns-simple-spec-js-pdfmake-columns-simple-1-snap.png │ │ ├── columns-simple-spec-js-pdfmake-columns-simple-2-snap.png │ │ ├── columns-simple-spec-js-pdfmake-columns-simple-3-snap.png │ │ ├── columns-simple-spec-js-pdfmake-columns-simple-4-snap.png │ │ ├── columns-simple-spec-js-pdfmake-columns-simple-5-snap.png │ │ ├── images-spec-js-pdfmake-images-1-snap.png │ │ ├── images-spec-js-pdfmake-images-2-snap.png │ │ ├── lists-spec-js-pdfmake-lists-1-snap.png │ │ ├── lists-spec-js-pdfmake-lists-2-snap.png │ │ ├── lists-spec-js-pdfmake-lists-3-snap.png │ │ ├── lists-spec-js-pdfmake-lists-4-snap.png │ │ ├── lists-spec-js-pdfmake-lists-5-snap.png │ │ ├── lists-spec-js-pdfmake-lists-6-snap.png │ │ ├── page-references-spec-js-pdfmake-page-references-1-snap.png │ │ ├── page-references-spec-js-pdfmake-page-references-2-snap.png │ │ ├── page-references-spec-js-pdfmake-page-references-3-snap.png │ │ ├── page-references-spec-js-pdfmake-page-references-4-snap.png │ │ ├── qrcode-spec-js-pdfmake-qrcode-1-snap.png │ │ ├── qrcode-spec-js-pdfmake-qrcode-2-snap.png │ │ ├── tables-spec-js-pdfmake-tables-1-snap.png │ │ ├── tables-spec-js-pdfmake-tables-2-snap.png │ │ ├── tables-spec-js-pdfmake-tables-3-snap.png │ │ ├── tables-spec-js-pdfmake-tables-4-snap.png │ │ ├── tables-spec-js-pdfmake-tables-5-snap.png │ │ ├── tables-spec-js-pdfmake-tables-6-snap.png │ │ ├── tables-spec-js-pdfmake-tables-7-snap.png │ │ ├── tables-spec-js-pdfmake-tables-8-snap.png │ │ ├── text-decorations-spec-js-pdfmake-text-decorations-1-snap.png │ │ ├── toc-spec-js-pdfmake-toc-1-snap.png │ │ ├── toc-spec-js-pdfmake-toc-2-snap.png │ │ ├── toc-spec-js-pdfmake-toc-3-snap.png │ │ ├── toc-spec-js-pdfmake-toc-4-snap.png │ │ ├── toc-spec-js-pdfmake-toc-5-snap.png │ │ └── watermark-spec-js-pdfmake-watermark-1-snap.png │ ├── absolute.spec.js │ ├── background.spec.js │ ├── basics.spec.js │ ├── columns_simple.spec.js │ ├── images.spec.js │ ├── lists.spec.js │ ├── page_references.spec.js │ ├── qrcode.spec.js │ ├── tables.spec.js │ ├── text_decorations.spec.js │ ├── toc.spec.js │ └── watermark.spec.js │ ├── table.spec.js │ ├── text.spec.js │ └── vector.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "node": "18" 9 | } 10 | } 11 | ] 12 | ], 13 | "env": { 14 | "test": { 15 | "presets": [ 16 | [ 17 | "@babel/preset-env", 18 | { 19 | "targets": { 20 | "node": "18" 21 | } 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | a76ab284a8d30c669b5ad2278bbcd050dea13abe -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report runtime errors or wrong PDF output 4 | --- 5 | 6 | # Bug Report 7 | 8 | 9 | ## Description of the problem 10 | 11 | 12 | ## Code sample 13 | 14 | 15 | 16 | ## Your environment 17 | 18 | * pdfkit version: 19 | * Node version: 20 | * Browser version (if applicable): 21 | * Operating System: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request new features 4 | --- 5 | 6 | # Feature Request 7 | 8 | ## Feature description 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask about pdfkit 4 | --- 5 | 6 | # Question 7 | 8 | ## Description 9 | 10 | 11 | 12 | ## Code sample 13 | 14 | 15 | ## Your environment 16 | 17 | * pdfkit version: 18 | * Node version: 19 | * Browser version (if applicable): 20 | * Operating System: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | **What kind of change does this PR introduce?** 12 | 13 | 14 | 15 | **Checklist**: 16 | 17 | 18 | 19 | 20 | - [ ] Unit Tests 21 | - [ ] Documentation 22 | - [ ] Update CHANGELOG.md 23 | - [ ] Ready to be merged 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Lint and Test 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install dependencies 24 | run: yarn --frozen-lockfile 25 | - name: Run Prettier check 26 | run: npm run prettier -- --check 27 | - name: Run ESLint 28 | run: npm run lint 29 | - name: Run tests 30 | run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/font/tables/.DS_Store 2 | .DS_Store 3 | /node_modules/ 4 | node-zlib/ 5 | playground/ 6 | build/ 7 | js/ 8 | .vscode 9 | coverage 10 | package-lock.json 11 | /examples/browserify/bundle.js 12 | index.html 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .git-blame-ignore-revs 3 | .prettierrc 4 | .prettierignore 5 | .babelrc 6 | node-zlib/ 7 | node-modules/ 8 | CONTRIBUTING.md 9 | examples/ 10 | src/ 11 | lib/ 12 | docs/ 13 | playground/ 14 | .vscode/ 15 | coverage/ 16 | tests/ 17 | index.js 18 | index.html 19 | yarn.lock 20 | rollup.config.js 21 | eslint.config.mjs 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **.html 2 | **.md 3 | demo/bundle.js 4 | tests/images/bee.js 5 | tests/visual/pdfmake/*.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to pdfkit 2 | 3 | ## Table of Contents 4 | 5 | - [Contributing to pdfkit](#contributing-to-pdfkit) 6 | - [Table of Contents](#table-of-contents) 7 | - [Code Organization](#code-organization) 8 | - [Setting Up the project locally](#setting-up-the-project-locally) 9 | - [Running and writing tests](#running-and-writing-tests) 10 | - [Submitting a Pull Request](#submitting-a-pull-request) 11 | 12 | 13 | ## Code Organization 14 | 15 | pdfkit is organized in the following folders: 16 | 17 | - `lib`: The actual source code. 18 | - `js`: The built / distributable code. 19 | - `docs`: Code and artifacts to generate documentation. 20 | - `demo`: Node and browser demos. 21 | - `tests/unit`: Tests behavior of specific classes / methods. 22 | - `tests/visual`: Compare the pdf output against a reference. 23 | 24 | **Working on your first Pull Request?** You can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 25 | 26 | ## Setting Up the project locally 27 | 28 | To install the project you need to have `node` 29 | 30 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork: 31 | 32 | ``` 33 | # Clone your fork 34 | git clone https://github.com//pdfkit.git 35 | 36 | # Navigate to the newly cloned directory 37 | cd pdfkit 38 | ``` 39 | 40 | 2. `npm install` to install dependencies 41 | 3. `npm run build` to build the library 42 | 4. `npm run demo` to run the demo (check demo/out.pdf) 43 | 5. `npm run browser-demo` to run the browser demo (check demo/browser.html) 44 | 45 | > Tip: Keep your `master` branch pointing at the original repository and make 46 | > pull requests from branches on your fork. To do this, run: 47 | > 48 | > ``` 49 | > git remote add upstream https://github.com/foliojs/pdfkit.git 50 | > git fetch upstream 51 | > git branch --set-upstream-to=upstream/master master 52 | > ``` 53 | > 54 | > This will add the original repository as a "remote" called "upstream," 55 | > then fetch the git information from that remote, then set your local `master` 56 | > branch to use the upstream master branch whenever you run `git pull`. 57 | > Then you can make all of your pull request branches based on this `master` 58 | > branch. Whenever you want to update your version of `master`, do a regular 59 | > `git pull`. 60 | 61 | ## Running and writing tests 62 | 63 | Tests are run using [Jest](http://jestjs.io/) and are categorized as unit and visual tests. 64 | 65 | Visual tests check the pdf image screenshot against a reference stored as snapshots. 66 | 67 | Unit tests check behavior of specific classes / methods isolatedly. 68 | 69 | Test commands 70 | * `npm run test`: Run all tests 71 | * `npm run test:unit`: Run unit tests 72 | * `npm run test:visual`: Run visual tests 73 | * `npm run lint`: Run linter 74 | 75 | To write new tests, look for the *.spec.js files at `test/unit` and `test/visual` as examples 76 | 77 | > Visual tests should use an embedded font, instead of system fonts, to ensure uniform rendering between different environments 78 | 79 | ## Documentation 80 | 81 | See `README.md` in the `docs` subdirectory for more information. 82 | 83 | ## Submitting a Pull Request 84 | 85 | Please go through existing issues and pull requests to check if somebody else is already working on it. 86 | 87 | Also, make sure to run the tests and lint the code before you commit your changes. 88 | 89 | > Tests should be added to check the changed behavior even if is a bug fix. 90 | 91 | If the proposed change affects document structure a unit test should be added, if affects rendering, add a visual test 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | Copyright (c) 2014 Devon Govett 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | css/ 3 | img/ 4 | js/ -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PDFKit Guide 2 | 3 | The PDFKit guide can be read a number of ways. The first is online at [pdfkit.org](http://pdfkit.org/). 4 | You can also read the guide in PDF form, in this directory or [online](http://pdfkit.org/docs/guide.pdf). 5 | 6 | Both the website and the PDF guide are generated from the Markdown files 7 | in this directory. Just run `npm run docs` to generate them. 8 | You will need to have ImageMagick 7 installed so the `magick` command is 9 | on your `$PATH`. 10 | 11 | The examples are actually run when generating the PDF in order to show the results inline. 12 | The `generate.js` file in this directory is actually quite short. It parses the markdown files into a 13 | tree structure using [markdown-js](https://github.com/evilstreak/markdown-js), syntax highlights the code 14 | examples using [codemirror](https://github.com/marijnh/codemirror), compiles and runs the code examples and puts the results 15 | inline, and generates the PDF using PDFKit. You can read the generator script source code to get a feeling for 16 | how you might do something slightly more complex than the guide itself shows. 17 | 18 | The markdown syntax used is pretty much standard, with a couple tweaks. 19 | 20 | 1. Code example output is references using the image notation, using the alt text as the example number starting from 21 | zero in the current file, and the title as the example output height. E.g. `![x](name "height")`. 22 | 23 | 2. Page breaks are added before `h1` and `h2`s, unless there are two in a row. `h3` is treated the same as `h2` but 24 | can be used to avoid this in the case you need multiple `h2`s on the same page. 25 | 26 | 3. The horizontal rule syntax (`* * *`) denotes an explicit page break 27 | -------------------------------------------------------------------------------- /docs/annotations.md: -------------------------------------------------------------------------------- 1 | # Annotations in PDFKit 2 | 3 | Annotations are interactive features of the PDF format, and they make it 4 | possible to include things like links and attached notes, or to highlight, 5 | underline or strikeout portions of text. Annotations are added using the 6 | various helper methods, and each type of annotation is defined by a rectangle 7 | and some other properties. Here is a list of the available annotation methods: 8 | 9 | * `note(x, y, width, height, contents, options)` 10 | * `link(x, y, width, height, url, options)` 11 | * `goTo(x, y, w, h, name, options)` 12 | * `highlight(x, y, width, height, options)` 13 | * `underline(x, y, width, height, options)` 14 | * `strike(x, y, width, height, options)` 15 | * `lineAnnotation(x1, y1, x2, y2, options)` 16 | * `rectAnnotation(x, y, width, height, options)` 17 | * `ellipseAnnotation(x, y, width, height, options)` 18 | * `textAnnotation(x, y, width, height, text, options)` 19 | * `fileAnnotation(x, y, width, height, file, options)` 20 | 21 | Many of the annotations have a `color` option that you can specify. You can 22 | use an array of RGB values, a hex color, a named CSS color value, or a named 23 | spot color value for that option. 24 | 25 | A custom icon can be set using option `Name` property. Possible values are: 26 | `'Comment'`, `'Key'`, `'Note'`, `'Help'`, `'NewParagraph'`, `'Paragraph'` 27 | and `'Insert'`. 28 | 29 | If you are adding an annotation to a piece of text, such as a link or 30 | underline, you will need to know the width and height of the text in order to 31 | create the required rectangle for the annotation. There are two methods that 32 | you can use to do that. To get the width of any piece of text in the current 33 | font, just call the `widthOfString` method with the string you want to 34 | measure. To get the line height in the current font, just call the 35 | `currentLineHeight` method. 36 | 37 | You must remember that annotations have a stacking order. If you are putting 38 | more than one annotation on a single area and one of those annotations is a 39 | link, make sure that the link is the last one you add, otherwise it will be 40 | covered by another annotation and the user won't be able to click it. 41 | 42 | * * * 43 | 44 | Here is an example that uses a few of the annotation types. 45 | 46 | // Add the link text 47 | doc.fontSize(25) 48 | .fillColor('blue') 49 | .text('This is a link!', 20, 0); 50 | 51 | // Measure the text 52 | const width = doc.widthOfString('This is a link!'); 53 | const height = doc.currentLineHeight(); 54 | 55 | // Add the underline and link annotations 56 | doc.underline(20, 0, width, height, {color: 'blue'}) 57 | .link(20, 0, width, height, 'http://google.com/'); 58 | 59 | // Create the highlighted text 60 | doc.moveDown() 61 | .fillColor('black') 62 | .highlight(20, doc.y, doc.widthOfString('This text is highlighted!'), height) 63 | .text('This text is highlighted!'); 64 | 65 | // Create text with a spot color 66 | doc.addSpotColor('PANTONE185C', 0, 100, 78, 9) 67 | doc.moveDown() 68 | .fillColor('PANTONE185C') 69 | .text('This text uses spot color!'); 70 | 71 | // Create the crossed out text 72 | doc.moveDown() 73 | .strike(20, doc.y, doc.widthOfString('STRIKE!'), height) 74 | .text('STRIKE!'); 75 | 76 | // Create note 77 | doc.note(10, 30, 30, 30, "Text of note"); 78 | 79 | // Create note with custom options 80 | doc.note(10, 80, 30, 30, "Text of custom note", {Name: 'Key', color: 'red'}); 81 | 82 | // Adding go to as annotation 83 | doc.goTo(20, doc.y, 10, 20, 'LINK', {}); 84 | 85 | The output of this example looks like this. 86 | 87 | ![0](images/annotations.png) 88 | 89 | Annotations are currently not the easiest things to add to PDF documents, but 90 | that is the fault of the PDF spec itself. Calculating a rectangle manually isn't 91 | fun, but PDFKit makes it easier for a few common annotations applied to text, including 92 | links, underlines, and strikes. Here's an example showing two of them: 93 | 94 | doc.fontSize(20) 95 | .fillColor('red') 96 | .text('Another link!', 20, 0, { 97 | link: 'http://apple.com/', 98 | underline: true 99 | } 100 | ); 101 | 102 | The output is as you'd expect: 103 | 104 | ![1]() 105 | -------------------------------------------------------------------------------- /docs/attachments.md: -------------------------------------------------------------------------------- 1 | # Attachments in PDFKit 2 | 3 | ## Embedded Files 4 | 5 | Embedded files make it possible to embed any external file into a PDF. 6 | Adding an embedded file is as simple as calling the `file` method and specifying a filepath. 7 | 8 | doc.file(path.join(__dirname, 'example.txt')) 9 | 10 | It is also possible to embed data directly as a Buffer, ArrayBuffer or base64 encoded string. 11 | If you are embedding data, it is recommended you also specify a filename like this: 12 | 13 | doc.file(Buffer.from('this will be a text file'), { name: 'example.txt' }) 14 | 15 | When embedding a data URL, the `type` option will be set to the data URL's MIME type automatically: 16 | 17 | doc.file('data:text/plain;base64,YmFzZTY0IHN0cmluZw==', { name: 'base64.txt' }) 18 | 19 | There are a few other options for `doc.file`: 20 | 21 | * `name` - specify the embedded file's name 22 | * `type` - specify the embedded file's subtype as a [MIME-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) 23 | * `description` - add descriptive text for the embedded file 24 | * `hidden` - if true, do not show file in the list of embedded files 25 | * `creationDate` - override the date and time the file was created 26 | * `modifiedDate` - override the date and time the file was last updated 27 | * `relationship` - relationship between the PDF document and its attached file. Can be 'Alternative', 'Data', 'Source', 'Supplement' or 'Unspecified'. 28 | 29 | If you are attaching a file from your file system, creationDate and modifiedDate will be set to the source file's creationDate and modifiedDate. 30 | 31 | Setting the `hidden` option prevents this file from showing up in the pdf viewer's attachment panel. 32 | While this may not be very useful for embedded files, it is absolutely necessary for file annotations, to prevent them from showing up twice in the attachment panel. 33 | 34 | ## File Annotations 35 | 36 | A file annotation contains a reference to an embedded file that can be placed anywhere in the document. 37 | File annotations show up in your reader's annotation panel as well as the attachment panel. 38 | 39 | In order to add a file annotation, you should first read the chapter on annotations. 40 | Like other annotations, you specify position and size with `x`, `y`, `width` and `height`, unlike other annotations you must also specify a file object. 41 | The file object may contain the same options as `doc.file` in the previous section with the addition of the source file or buffered data in `src`. 42 | 43 | Here is an example of adding a file annotation: 44 | 45 | const file = { 46 | src: path.join(__dirname, 'example.txt'), 47 | name: 'example.txt', 48 | description: 'file annotation description' 49 | } 50 | const options = { Name: 'Paperclip' } 51 | 52 | doc.fileAnnotation(100, 100, 100, 100, file, options) 53 | 54 | The annotation's appearance may be changed by setting the `Name` option to one of the three predefined icons `GraphPush`, `Paperclip` or `Push` (default value). 55 | -------------------------------------------------------------------------------- /docs/destinations.md: -------------------------------------------------------------------------------- 1 | # Destinations 2 | 3 | Anchor may specify a destination by `addNamedDestination(name, ...args)`, which consists of a page, the location of the display window on that page, and the zoom factor to use when displaying that page. 4 | 5 | Examples of creating anchor: 6 | 7 | // Insert anchor for current page 8 | doc.addNamedDestination('LINK'); 9 | 10 | // Insert anchor for current page with only horizontal magnified to fit where vertical top is 100 11 | doc.addNamedDestination('LINK', 'FitH', 100); 12 | 13 | // Insert anchor to display a portion of the current page, 1/2 inch in from the top and left and zoomed 50% 14 | doc.addNamedDestination('LINK', 'XYZ', 36, 36, 50); 15 | 16 | // Insert anchor for this text 17 | doc.text('End of paragraph', { destination: 'ENDP' }); 18 | 19 | 20 | Examples of go to link to anchor: 21 | 22 | // Go to annotation 23 | doc.goTo(10, 10, 100, 20, 'LINK') 24 | 25 | // Go to annotation for this text 26 | doc.text('Another goto', 20, 0, { 27 | goTo: 'ENDP', 28 | underline: true 29 | }); 30 | -------------------------------------------------------------------------------- /docs/fonts/Alegreya-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/Alegreya-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/AlegreyaSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/AlegreyaSans-Light.ttf -------------------------------------------------------------------------------- /docs/fonts/Chalkboard.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/Chalkboard.ttc -------------------------------------------------------------------------------- /docs/fonts/GoodDog.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/GoodDog.ttf -------------------------------------------------------------------------------- /docs/fonts/Merriweather-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/Merriweather-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /docs/guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/guide.pdf -------------------------------------------------------------------------------- /docs/images.md: -------------------------------------------------------------------------------- 1 | # Images in PDFKit 2 | 3 | Adding images to PDFKit documents is an easy task. Just pass an image path, buffer, or data uri with base64 encoded data to 4 | the `image` method along with some optional arguments. PDFKit supports the 5 | JPEG and PNG formats. If an X and Y position are not provided, the image is 6 | rendered at the current point in the text flow (below the last line of text). 7 | Otherwise, it is positioned absolutely at the specified point. The image will 8 | be scaled according to the following options. 9 | 10 | - Neither `width` or `height` provided - image is rendered at full size 11 | - `width` provided but not `height` - image is scaled proportionally to fit in the provided `width` 12 | - `height` provided but not `width` - image is scaled proportionally to fit in the provided `height` 13 | - Both `width` and `height` provided - image is stretched to the dimensions provided 14 | - `scale` factor provided - image is scaled proportionally by the provided scale factor 15 | - `fit` array provided - image is scaled proportionally to fit within the passed width and height 16 | - `cover` array provided - image is scaled proportionally to completely cover the rectangle defined by the passed width and height 17 | - `link` - a URL to link this image to (shortcut to create an annotation) 18 | - `goTo` - go to anchor (shortcut to create an annotation) 19 | - `destination` - create anchor to this image 20 | - `ignoreOrientation` - (true/false) ignore JPEG EXIF orientation. By default, images with JPEG EXIF orientation are properly rotated and/or flipped. Defaults to `false`, unless `ignoreOrientation` option set to `true` when creating the `PDFDocument` object (e.g. `new PDFDocument({ignoreOrientation: true})`) 21 | 22 | When a `fit` or `cover` array is provided, PDFKit accepts these additional options: 23 | 24 | - `align` - horizontally align the image, the possible values are `'left'`, `'center'` and `'right'` 25 | - `valign` - vertically align the image, the possible values are `'top'`, `'center'` and `'bottom'` 26 | 27 | Here is an example showing some of these options. 28 | 29 | // Scale proprotionally to the specified width 30 | doc.image('images/test.jpeg', 0, 15, {width: 300}) 31 | .text('Proportional to width', 0, 0); 32 | 33 | // Fit the image within the dimensions 34 | doc.image('images/test.jpeg', 320, 15, {fit: [100, 100]}) 35 | .rect(320, 15, 100, 100) 36 | .stroke() 37 | .text('Fit', 320, 0); 38 | 39 | // Stretch the image 40 | doc.image('images/test.jpeg', 320, 145, {width: 200, height: 100}) 41 | .text('Stretch', 320, 130); 42 | 43 | // Scale the image 44 | doc.image('images/test.jpeg', 320, 280, {scale: 0.25}) 45 | .text('Scale', 320, 265); 46 | 47 | // Fit the image in the dimensions, and center it both horizontally and vertically 48 | doc.image('images/test.jpeg', 430, 15, {fit: [100, 100], align: 'center', valign: 'center'}) 49 | .rect(430, 15, 100, 100).stroke() 50 | .text('Centered', 430, 0); 51 | 52 | --- 53 | 54 | This example produces the following output: 55 | 56 | ![0](images/images.png '400') 57 | 58 | That is all there is to adding images to your PDF documents with PDFKit. Now 59 | let's look at adding outlines. 60 | -------------------------------------------------------------------------------- /docs/images/acroforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/images/acroforms.png -------------------------------------------------------------------------------- /docs/images/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/docs/images/test.jpeg -------------------------------------------------------------------------------- /docs/outline.md: -------------------------------------------------------------------------------- 1 | # Outlines in PDFKit 2 | 3 | Outlines are the heirachical bookmarks that display in some PDF readers. Currently only page bookmarks are supported, but more may be added in the future. They are simple to add and only require a single method: 4 | 5 | * `addItem(title, options)` 6 | 7 | Here is an example of adding a bookmark with a single child bookmark. 8 | 9 | // Get a reference to the Outline root 10 | const { outline } = doc; 11 | 12 | // Add a top-level bookmark 13 | const top = outline.addItem('Top Level'); 14 | 15 | // Add a sub-section 16 | top.addItem('Sub-section'); 17 | 18 | ## Options 19 | 20 | The `options` parameter currently only has one property: `expanded`. If this value is set to `true` then all of that section's children will be visible by default. This value defaults to `false`. 21 | 22 | In this example the 'Top Level' section will be expanded to show 'Sub-section'. 23 | 24 | // Add a top-level bookmark 25 | const top = outline.addItem('Top Level', { expanded: true }); 26 | 27 | // Add a sub-section 28 | top.addItem('Sub-section'); -------------------------------------------------------------------------------- /docs/paper_sizes.md: -------------------------------------------------------------------------------- 1 | # Paper Sizes 2 | 3 | When creating a new document or adding a new page to your current document, PDFKit allows you to set the page dimensions. To improve convenience, PDFKit has a number of predefined page sizes. These sizes are based on the most commonly used standard page sizes. 4 | 5 | ### Predefined Page Sizes 6 | 7 | The following predefined sizes are based on the ISO (International) standards. All the dimensions in brackets are in PostScript points. 8 | 9 | #### A-series 10 | 11 | * A0 (2383.94 x 3370.39) 12 | * A1 (1683.78 x 2383.94) 13 | * A2 (1190.55 x 1683.78) 14 | * A3 (841.89 x 1190.55) 15 | * A4 (595.28 x 841.89) 16 | * A5 (419.53 x 595.28) 17 | * A6 (297.64 x 419.53) 18 | * A7 (209.76 x 297.64) 19 | * A8 (147.40 x 209.76) 20 | * A9 (104.88 x 147.40) 21 | * A10 (73.70 x 104.88) 22 | 23 | #### B-series 24 | 25 | * B0 (2834.65 x 4008.19) 26 | * B1 (2004.09 x 2834.65) 27 | * B2 (1417.32 x 2004.09) 28 | * B3 (1000.63 x 1417.32) 29 | * B4 (708.66 x 1000.63) 30 | * B5 (498.90 x 708.66) 31 | * B6 (354.33 x 498.90) 32 | * B7 (249.45 x 354.33) 33 | * B8 (175.75 x 249.45) 34 | * B9 (124.72 x 175.75) 35 | * B10 (87.87 x 124.72) 36 | 37 | #### C-series 38 | 39 | * C0 (2599.37 x 3676.54) 40 | * C1 (1836.85 x 2599.37) 41 | * C2 (1298.27 x 1836.85) 42 | * C3 (918.43 x 1298.27) 43 | * C4 (649.13 x 918.43) 44 | * C5 (459.21 x 649.13) 45 | * C6 (323.15 x 459.21) 46 | * C7 (229.61 x 323.15) 47 | * C8 (161.57 x 229.61) 48 | * C9 (113.39 x 161.57) 49 | * C10 (79.37 x 113.39) 50 | 51 | #### RA-series 52 | 53 | * RA0 (2437.80 x 3458.27) 54 | * RA1 (1729.13 x 2437.80) 55 | * RA2 (1218.90 x 1729.13) 56 | * RA3 (864.57 x 1218.90) 57 | * RA4 (609.45 x 864.57) 58 | 59 | #### SRA-series 60 | 61 | * SRA0 (2551.18 x 3628.35) 62 | * SRA1 (1814.17 x 2551.18) 63 | * SRA2 (1275.59 x 1814.17) 64 | * SRA3 (907.09 x 1275.59) 65 | * SRA4 (637.80 x 907.09) 66 | 67 | The following predefined sizes are based on the common paper sizes used mainly in the United States of America and Canada. The dimensions in brackets are in PostScript points. 68 | 69 | * EXECUTIVE (521.86 x 756.00) 70 | * LEGAL (612.00 x 1008.00) 71 | * LETTER (612.00 X 792.00) 72 | * TABLOID (792.00 X 1224.00) 73 | 74 | PDFKit supports also the following paper sizes. The dimensions in brackets are in PostScript points. 75 | 76 | * 4A0 (4767.89 x 6740.79) 77 | * 2A0 (3370.39 x 4767.87) 78 | * FOLIO (612.00 X 936.00) 79 | 80 | ### Setting the page size 81 | 82 | In order to use the predefined sizes, the name of the size (as named in the lists above) should be passed to either the `PDFDocument` constructor or the `addPage()` function in the `size` property of the `options` object, as shown in the example below, using `A7` as the preferred size. 83 | 84 | // Passing size to the constructor 85 | const doc = new PDFDocument({size: 'A7'}); 86 | 87 | // Passing size to the addPage function 88 | doc.addPage({size: 'A7'}); 89 | -------------------------------------------------------------------------------- /docs/publish_website.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages'); 2 | 3 | const { argv } = process; 4 | 5 | const message = argv.length > 2 ? argv[2] : undefined; 6 | 7 | ghpages.publish( 8 | '.', 9 | { 10 | src: [ 11 | 'index.html', 12 | 'docs/*.html', 13 | 'docs/img/*.png', 14 | 'docs/guide.pdf', 15 | 'examples/browserify/browser.html', 16 | 'examples/browserify/bundle.js', 17 | 'examples/kitchen-sink.pdf', 18 | ], 19 | add: true, 20 | message, 21 | }, 22 | function (err) { 23 | if (err) { 24 | console.error(err); 25 | } 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /docs/template.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | title= title 6 | link(rel='stylesheet', href='http://fonts.googleapis.com/css?family=Source+Code+Pro:400,700|Alegreya:700|Merriweather') 7 | link(rel='stylesheet', href='/docs/css/index.css') 8 | link(rel='stylesheet', href='/docs/css/github.css') 9 | body 10 | nav(class='sidebar') 11 | ul 12 | li 13 | a(href='/', class=(/index/.test(url) ? 'selected' : '')) Home 14 | li 15 | a(href=pages[0].url) Documentation 16 | ul 17 | each page in pages.slice(1) 18 | li 19 | a(href=page.url, class=page.title == title ? 'selected' : '') 20 | = page.title.replace(/(with|in) PDFKit/, '') 21 | 22 | - if (page.title == title && headers.length) 23 | ul 24 | each header in headers 25 | li 26 | a(href='#' + header.hash)= header.title 27 | 28 | li 29 | a(href='/docs/guide.pdf') PDF Guide 30 | li 31 | a(href='/examples/kitchen-sink.pdf') Example PDF 32 | li 33 | a(href='/examples/browserify/browser.html') Interactive Browser Demo 34 | li 35 | a(href='http://github.com/foliojs/pdfkit') Source Code 36 | 37 | .main 38 | != content 39 | nav 40 | - if (index > 0) 41 | a(href=pages[index - 1].url, class='previous') Previous 42 | 43 | - if (index < pages.length - 1) 44 | a(href=pages[index + 1].url, class='next') Next 45 | 46 | script(src='https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js') 47 | script(src='/docs/js/scroll.js') 48 | script(src='/docs/js/highlight.pack.js') 49 | script. 50 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 51 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 52 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 53 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 54 | ga('create', 'UA-48340245-1', 'pdfkit.org'); 55 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /docs/you_made_it.md: -------------------------------------------------------------------------------- 1 | # You made it! 2 | 3 | That's all there is to creating PDF documents in PDFKit. It's really quite 4 | simple to create beautiful multi-page printable documents using Node.js! 5 | 6 | This guide was generated from Markdown files using a 7 | PDFKit generation script. The examples are actually run to generate the output shown 8 | inline. The script generates both the website and the PDF guide, and 9 | can be found [on Github](https://github.com/foliojs/pdfkit/blob/master/docs/generate.js). 10 | Check it out if you want to see an example of a slightly more complicated renderer using 11 | a parser for Markdown and a syntax highlighter. 12 | 13 | If you have any questions about what you've learned in this guide, please don't 14 | hesitate to [ask the author](https://twitter.com/devongovett) or post an issue 15 | on [Github](https://github.com/foliojs/pdfkit/issues). Enjoy! -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import js from "@eslint/js"; 3 | 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | globals: globals.node, 9 | sourceType: "module", 10 | }, 11 | }, 12 | { 13 | files: ["tests/**/*.js"], 14 | languageOptions: { 15 | globals: { 16 | ...globals.jest, 17 | }, 18 | } 19 | }, 20 | ]; -------------------------------------------------------------------------------- /examples/attachment.js: -------------------------------------------------------------------------------- 1 | const PDFDocument = require('../'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const doc = new PDFDocument({ pdfVersion: '1.4' }); 6 | 7 | doc.pipe(fs.createWriteStream('attachment.pdf')); 8 | 9 | doc.info['Title'] = 'Attachment Test'; 10 | 11 | // add an embedded file from file system 12 | doc.file(path.join(__dirname, 'images', 'test.png'), { 13 | name: 'test.png', 14 | type: 'image/png', 15 | description: 'this is a test image' 16 | }); 17 | 18 | // add some text 19 | doc.text(`This PDF contains three text files: 20 | Two file attachment annotations and one embedded file. 21 | If you can see them (not every PDF viewer supports embedded files), 22 | hover over the paperclip to see its description!`); 23 | 24 | // add a file attachment annotation 25 | // first, declare the file to be attached 26 | const file = { 27 | src: Buffer.from('buffered input!'), 28 | name: 'embedded.txt', 29 | creationDate: new Date(2020, 3, 1) 30 | }; 31 | // then, add the annotation 32 | doc.fileAnnotation(100, 150, 10, doc.currentLineHeight(), file); 33 | 34 | // declared files can be reused, but they will show up separately in the PDF Viewer's attachments panel 35 | // we're going to use the paperclip icon for this one together with a short description 36 | // be aware that some PDF Viewers may not render the icon correctly — or not at all 37 | doc.fileAnnotation(150, 150, 10, doc.currentLineHeight(), file, { 38 | Name: 'Paperclip', 39 | Contents: 'Paperclip attachment' 40 | }); 41 | 42 | doc.end(); 43 | -------------------------------------------------------------------------------- /examples/attachment.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/attachment.pdf -------------------------------------------------------------------------------- /examples/browserify/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 33 | 34 | 35 | 36 |

PDFKit Browser Demo

37 |

Bundled with Browserify

38 |

Website | Github

39 |
40 | 41 | 42 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/browserify/browser.js: -------------------------------------------------------------------------------- 1 | var PDFDocument = require('../..'); 2 | var blobStream = require('blob-stream'); 3 | var ace = require('brace'); 4 | require('brace/mode/javascript'); 5 | require('brace/theme/monokai'); 6 | 7 | var lorem = 8 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam in suscipit purus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus nec hendrerit felis. Morbi aliquam facilisis risus eu lacinia. Sed eu leo in turpis fringilla hendrerit. Ut nec accumsan nisl. Suspendisse rhoncus nisl posuere tortor tempus et dapibus elit porta. Cras leo neque, elementum a rhoncus ut, vestibulum non nibh. Phasellus pretium justo turpis. Etiam vulputate, odio vitae tincidunt ultricies, eros odio dapibus nisi, ut tincidunt lacus arcu eu elit. Aenean velit erat, vehicula eget lacinia ut, dignissim non tellus. Aliquam nec lacus mi, sed vestibulum nunc. Suspendisse potenti. Curabitur vitae sem turpis. Vestibulum sed neque eget dolor dapibus porttitor at sit amet sem. Fusce a turpis lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;\nMauris at ante tellus. Vestibulum a metus lectus. Praesent tempor purus a lacus blandit eget gravida ante hendrerit. Cras et eros metus. Sed commodo malesuada eros, vitae interdum augue semper quis. Fusce id magna nunc. Curabitur sollicitudin placerat semper. Cras et mi neque, a dignissim risus. Nulla venenatis porta lacus, vel rhoncus lectus tempor vitae. Duis sagittis venenatis rutrum. Curabitur tempor massa tortor.'; 9 | 10 | function makePDF(PDFDocument, blobStream, lorem, iframe) { 11 | // create a document and pipe to a blob 12 | var doc = new PDFDocument(); 13 | var stream = doc.pipe(blobStream()); 14 | 15 | // draw some text 16 | doc.fontSize(25).text('Here is some vector graphics...', 100, 80); 17 | 18 | // some vector graphics 19 | doc 20 | .save() 21 | .moveTo(100, 150) 22 | .lineTo(100, 250) 23 | .lineTo(200, 250) 24 | .fill('#FF3300'); 25 | 26 | doc.circle(280, 200, 50).fill('#6600FF'); 27 | 28 | // an SVG path 29 | doc 30 | .scale(0.6) 31 | .translate(470, 130) 32 | .path('M 250,75 L 323,301 131,161 369,161 177,301 z') 33 | .fill('red', 'even-odd') 34 | .restore(); 35 | 36 | doc.save(); 37 | // a gradient fill 38 | var gradient = doc 39 | .linearGradient(100, 300, 200, 300) 40 | .stop(0, 'green') 41 | .stop(0.5, 'red') 42 | .stop(1, 'green'); 43 | doc.rect(100, 300, 100, 100).fill(gradient); 44 | 45 | // stroke & fill uncolored tiling pattern 46 | 47 | var stripe45d = doc.pattern( 48 | [1, 1, 4, 4], 49 | 3, 50 | 3, 51 | '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s' 52 | ); 53 | doc.circle(280, 350, 50).fill([stripe45d, 'blue']); 54 | 55 | doc 56 | .rect(380, 300, 100, 100) 57 | .fillColor('lime') 58 | .strokeColor([stripe45d, 'orange']) 59 | .lineWidth(5) 60 | .fillAndStroke(); 61 | doc.restore(); 62 | 63 | // and some justified text wrapped into columns 64 | doc 65 | .text('And here is some wrapped text...', 100, 450) 66 | .font('Times-Roman', 13) 67 | .moveDown() 68 | .text(lorem, { 69 | width: 412, 70 | align: 'justify', 71 | indent: 30, 72 | columns: 2, 73 | height: 300, 74 | ellipsis: true 75 | }); 76 | 77 | // end and display the document in the iframe to the right 78 | doc.end(); 79 | stream.on('finish', function() { 80 | iframe.src = stream.toBlobURL('application/pdf'); 81 | }); 82 | } 83 | 84 | var editor = ace.edit('editor'); 85 | editor.setTheme('ace/theme/monokai'); 86 | editor.getSession().setMode('ace/mode/javascript'); 87 | editor.setValue( 88 | makePDF 89 | .toString() 90 | .split('\n') 91 | .slice(1, -1) 92 | .join('\n') 93 | .replace(/^ /gm, '') 94 | ); 95 | editor 96 | .getSession() 97 | .getSelection() 98 | .clearSelection(); 99 | 100 | var iframe = document.querySelector('iframe'); 101 | makePDF(PDFDocument, blobStream, lorem, iframe); 102 | 103 | let debounceTimeout; 104 | 105 | editor.getSession().on('change', function() { 106 | try { 107 | if (debounceTimeout) { 108 | clearTimeout(debounceTimeout); 109 | } 110 | var fn = new Function( 111 | 'PDFDocument', 112 | 'blobStream', 113 | 'lorem', 114 | 'iframe', 115 | editor.getValue() 116 | ); 117 | debounceTimeout = setTimeout(() => { 118 | fn(PDFDocument, blobStream, lorem, iframe); 119 | debounceTimeout = undefined; 120 | }, 100); 121 | } catch (e) { 122 | console.log(e); 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /examples/fonts/Chalkboard.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/Chalkboard.ttc -------------------------------------------------------------------------------- /examples/fonts/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/DejaVuSans.ttf -------------------------------------------------------------------------------- /examples/fonts/GoodDog.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/GoodDog.ttf -------------------------------------------------------------------------------- /examples/fonts/Helvetica.dfont: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/Helvetica.dfont -------------------------------------------------------------------------------- /examples/fonts/Montserrat-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/Montserrat-Bold.otf -------------------------------------------------------------------------------- /examples/fonts/PalatinoBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/fonts/PalatinoBold.ttf -------------------------------------------------------------------------------- /examples/form.js: -------------------------------------------------------------------------------- 1 | var PDFDocument = require('../'); 2 | var fs = require('fs'); 3 | 4 | // Create a new PDFDocument 5 | var doc = new PDFDocument(); 6 | 7 | doc.pipe(fs.createWriteStream('form.pdf')); 8 | 9 | // Set some meta data 10 | doc.info['Title'] = 'Test Form Document'; 11 | doc.info['Author'] = 'test-acroform.js'; 12 | 13 | doc.font('Helvetica'); // establishes the default font for forms 14 | doc.initForm(); 15 | 16 | let rootField = doc.formField('rootField'); 17 | doc.font('Courier'); 18 | let child1Field = doc.formField('child1Field', { parent: rootField }); 19 | doc.font('Helvetica'); 20 | let child2Field = doc.formField('child2Field', { parent: rootField }); 21 | 22 | let y = 10; 23 | doc.formText('leaf1', 10, y, 200, 20, { 24 | parent: child1Field, 25 | value: '1999-12-31', 26 | format: { 27 | type: 'date', 28 | param: 'yyyy-mm-dd' 29 | }, 30 | align: 'center' 31 | }); 32 | 33 | y += 30; 34 | opts = { 35 | parent: child1Field, 36 | value: 32.98, 37 | format: { 38 | type: 'number', 39 | nDec: 2, 40 | currency: '$', 41 | currencyPrepend: true 42 | }, 43 | align: 'right' 44 | }; 45 | doc.formText('dollar', 10, y, 200, 20, opts); 46 | 47 | y += 30; 48 | doc.formText('leaf2', 10, y, 200, 40, { 49 | parent: child1Field, 50 | multiline: true, 51 | align: 'right' 52 | }); 53 | y += 50; 54 | doc.formText('leaf3', 10, y, 200, 80, { 55 | parent: child2Field, 56 | multiline: true 57 | }); 58 | 59 | y += 90; 60 | var opts = { 61 | backgroundColor: 'yellow', 62 | label: 'Test Button' 63 | }; 64 | doc.formPushButton('btn1', 10, y, 100, 30, opts); 65 | 66 | y += 40; 67 | opts = { 68 | borderColor: 'black', 69 | select: ['Select Option', 'github', 'bitbucket', 'gitlab'], 70 | value: 'Select Option', 71 | defaultValue: 'Select Option', 72 | align: 'center', 73 | edit: true 74 | }; 75 | doc.formCombo('ch1', 10, y, 100, 20, opts); 76 | 77 | y += 30; 78 | opts = { 79 | borderColor: '#808080', 80 | select: ['github', 'bitbucket', 'gitlab', 'sourcesafe', 'perforce'], 81 | sort: true 82 | }; 83 | doc.formList('ch2', 10, y, 100, 45, opts); 84 | 85 | doc.end(); 86 | -------------------------------------------------------------------------------- /examples/form.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/form.pdf -------------------------------------------------------------------------------- /examples/images/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/dice.png -------------------------------------------------------------------------------- /examples/images/fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/fish.png -------------------------------------------------------------------------------- /examples/images/interlaced-grayscale-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/interlaced-grayscale-8bit.png -------------------------------------------------------------------------------- /examples/images/interlaced-pallete-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/interlaced-pallete-8bit.png -------------------------------------------------------------------------------- /examples/images/interlaced-rgb-16bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/interlaced-rgb-16bit.png -------------------------------------------------------------------------------- /examples/images/interlaced-rgb-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/interlaced-rgb-8bit.png -------------------------------------------------------------------------------- /examples/images/interlaced-rgb-alpha-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/interlaced-rgb-alpha-8bit.png -------------------------------------------------------------------------------- /examples/images/pngsuite-gray-transparent-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/pngsuite-gray-transparent-white.png -------------------------------------------------------------------------------- /examples/images/pngsuite-rgb-transparent-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/pngsuite-rgb-transparent-white.png -------------------------------------------------------------------------------- /examples/images/straight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/straight.png -------------------------------------------------------------------------------- /examples/images/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/test.jpeg -------------------------------------------------------------------------------- /examples/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/test.png -------------------------------------------------------------------------------- /examples/images/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/test2.png -------------------------------------------------------------------------------- /examples/images/test3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/images/test3.png -------------------------------------------------------------------------------- /examples/kitchen-sink-accessible.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/kitchen-sink-accessible.pdf -------------------------------------------------------------------------------- /examples/kitchen-sink.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/kitchen-sink.pdf -------------------------------------------------------------------------------- /examples/text-link.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var PDFDocument = require('../'); 3 | 4 | var doc = new PDFDocument({ bufferPages: true }); 5 | doc.pipe(fs.createWriteStream('text-link.pdf')); 6 | doc.addPage(); 7 | doc.addPage(); 8 | doc.switchToPage(0); 9 | doc.text('First Page', { paragraphGap: 4 }); 10 | doc.text('Click here to go to Second Page', { link: 1, paragraphGap: 4 }); 11 | doc 12 | .text('Continued text ', { continued: true }) 13 | .text('with a link inside', { 14 | continued: true, 15 | link: 'http://pdfkit.org', 16 | underline: true 17 | }) 18 | .text(' and remaining text', { 19 | continued: false, 20 | link: null, 21 | underline: false 22 | }); 23 | doc.switchToPage(1); 24 | doc.text('Go To First Page', { link: 0 }); 25 | doc.text('Link to google', { link: 'https://www.google.com/' }); 26 | doc.end(); 27 | -------------------------------------------------------------------------------- /examples/text-link.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/text-link.pdf -------------------------------------------------------------------------------- /examples/webpack/.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | dist 5 | node_modules 6 | npm-debug.log 7 | /yarn-error.log 8 | -------------------------------------------------------------------------------- /examples/webpack/README.md: -------------------------------------------------------------------------------- 1 | # pdfkit-webpack-example 2 | 3 | Simple example of using PdfKit with webpack 4 | 5 | ### Features 6 | 7 | - Minimal webpack 5 setup 8 | - Automatically register binary files added to static-assets folder 9 | - Register AFM fonts provided by pdfkit 10 | - Shows how to load and register files lazily 11 | 12 | ### Technical details 13 | 14 | [`webpack.config.js`](webpack.config.js) 15 | 16 | - add alias to map `fs` calls to pdfkit virtual file system [implementation](../../lib/virtual-fs.js) 17 | - ignore crypto package to save bundle file size 18 | - add aliases to native node packages (buffer, stream, zlib, util, assert) 19 | - configure `*.afm` files to be imported as text 20 | - configure all files in `src/static-assets` folder to be imported encoded as base64 21 | - configure all files in `src/lazy-assets` folder to be imported as URLs 22 | - convert binary files used by linebreak and fontkit to base64 23 | 24 | ### Caveats 25 | 26 | The strategy to register binary files and AFM fonts inlines them in source code, increasing the bundle size significantly 27 | -------------------------------------------------------------------------------- /examples/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "dependencies": { 4 | "assert": "^2.1.0", 5 | "brace": "^0.11.1", 6 | "browserify-zlib": "^0.2.0", 7 | "buffer": "^6.0.3", 8 | "pdfkit": "^0.15.0", 9 | "process": "^0.11.10", 10 | "readable-stream": "^4.5.2", 11 | "util": "^0.12.5" 12 | }, 13 | "devDependencies": { 14 | "html-webpack-plugin": "^5.6.0", 15 | "transform-loader": "^0.2.4", 16 | "webpack": "^5.91.0", 17 | "webpack-cli": "^5.1.4" 18 | }, 19 | "scripts": { 20 | "dev": "webpack --mode development", 21 | "prod": "webpack --mode production" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/webpack/src/httpHelpers.js: -------------------------------------------------------------------------------- 1 | function createFetchError(fileURL, error) { 2 | const result = new Error(`Fetching "${fileURL}" failed: ${error}`); 3 | result.name = 'FetchError'; 4 | return result; 5 | } 6 | 7 | export function fetchFile(fileURL, { type = 'arraybuffer' } = {}) { 8 | return new Promise((resolve, reject) => { 9 | const request = new XMLHttpRequest(); 10 | request.open('GET', fileURL, true); 11 | request.responseType = type; 12 | 13 | request.onload = function(e) { 14 | if (request.status === 200) { 15 | resolve(request.response); 16 | } else { 17 | reject(createFetchError(fileURL, request.statusText)); 18 | } 19 | }; 20 | 21 | request.onerror = error => reject(createFetchError(fileURL, error)); 22 | 23 | request.send(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /examples/webpack/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 32 | 33 | 34 | 35 |

PDFKit Browser Demo

36 |

Bundled with Webpack

37 |

Website | Github

38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/webpack/src/lazy-assets/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/lazy-assets/test.jpeg -------------------------------------------------------------------------------- /examples/webpack/src/pdfkitHelpers.js: -------------------------------------------------------------------------------- 1 | export const waitForData = async doc => { 2 | return new Promise((resolve, reject) => { 3 | const buffers = []; 4 | doc.on('data', buffers.push.bind(buffers)); 5 | doc.on('end', async () => { 6 | const pdfBuffer = Buffer.concat(buffers); 7 | const pdfBase64 = pdfBuffer.toString('base64'); 8 | resolve(`data:application/pdf;base64,${pdfBase64}`); 9 | }); 10 | doc.on('error', reject); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /examples/webpack/src/registerStaticFiles.js: -------------------------------------------------------------------------------- 1 | // the fs here is not node fs but the provided virtual one 2 | import fs from 'fs'; 3 | // the content file is returned as is (webpack is configured to load *.afm files as asset/source) 4 | import Courier from 'pdfkit/js/data/Courier.afm'; 5 | import CourierBold from 'pdfkit/js/data/Courier-Bold.afm'; 6 | 7 | function registerBinaryFiles(ctx) { 8 | ctx.keys().forEach(key => { 9 | // extracts "./" from beginning of the key 10 | fs.writeFileSync(key.substring(2), ctx(key)); 11 | }); 12 | } 13 | 14 | function registerAFMFonts(ctx) { 15 | ctx.keys().forEach(key => { 16 | const match = key.match(/([^/]*\.afm$)/); 17 | if (match) { 18 | // afm files must be stored on data path 19 | fs.writeFileSync(`data/${match[0]}`, ctx(key)); 20 | } 21 | }); 22 | } 23 | 24 | // register all files found in assets folder (relative to src) 25 | registerBinaryFiles(require.context('./static-assets', true)); 26 | 27 | // register AFM fonts distributed with pdfkit 28 | // is good practice to register only required fonts to avoid the bundle size increase too much 29 | registerAFMFonts(require.context('pdfkit/js/data', false, /Helvetica.*\.afm$/)); 30 | 31 | // register files imported directly 32 | fs.writeFileSync('data/Courier.afm', Courier); 33 | fs.writeFileSync('data/Courier-Bold.afm', CourierBold); 34 | -------------------------------------------------------------------------------- /examples/webpack/src/static-assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/static-assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /examples/webpack/src/static-assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/static-assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /examples/webpack/src/static-assets/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/static-assets/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /examples/webpack/src/static-assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/static-assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /examples/webpack/src/static-assets/images/bee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/examples/webpack/src/static-assets/images/bee.png -------------------------------------------------------------------------------- /examples/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | plugins: [ 7 | new HtmlWebpackPlugin({ 8 | template: path.resolve(__dirname, 'src/index.html') 9 | }), 10 | new webpack.ProvidePlugin({ 11 | Buffer: ['buffer', 'Buffer'], 12 | process: 'process/browser' 13 | }) 14 | ], 15 | resolve: { 16 | symlinks: false, 17 | alias: { 18 | // maps fs to a virtual one allowing to register file content dynamically 19 | fs: __dirname + '/../../js/virtual-fs.js' 20 | }, 21 | fallback: { 22 | // crypto module is not necessary at browser 23 | crypto: false, 24 | // fallbacks for native node libraries 25 | buffer: require.resolve('buffer/'), 26 | stream: require.resolve('readable-stream'), 27 | zlib: require.resolve('browserify-zlib'), 28 | util: require.resolve('util/'), 29 | assert: require.resolve('assert/') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | // bundle and load afm files verbatim 35 | { test: /\.afm$/, type: 'asset/source' }, 36 | // bundle and load binary files inside static-assets folder as base64 37 | { 38 | test: /src[/\\]static-assets/, 39 | type: 'asset/inline', 40 | generator: { 41 | dataUrl: content => { 42 | return content.toString('base64'); 43 | } 44 | } 45 | }, 46 | // load binary files inside lazy-assets folder as an URL 47 | { 48 | test: /src[/\\]lazy-assets/, 49 | type: 'asset/resource' 50 | } 51 | ] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /lib/abstract_reference.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFAbstractReference - abstract class for PDF reference 3 | */ 4 | 5 | class PDFAbstractReference { 6 | toString() { 7 | throw new Error('Must be implemented by subclasses'); 8 | } 9 | } 10 | 11 | export default PDFAbstractReference; 12 | -------------------------------------------------------------------------------- /lib/font.js: -------------------------------------------------------------------------------- 1 | class PDFFont { 2 | constructor() {} 3 | 4 | encode() { 5 | throw new Error('Must be implemented by subclasses'); 6 | } 7 | 8 | widthOfString() { 9 | throw new Error('Must be implemented by subclasses'); 10 | } 11 | 12 | ref() { 13 | return this.dictionary != null 14 | ? this.dictionary 15 | : (this.dictionary = this.document.ref()); 16 | } 17 | 18 | finalize() { 19 | if (this.embedded || this.dictionary == null) { 20 | return; 21 | } 22 | 23 | this.embed(); 24 | this.embedded = true; 25 | } 26 | 27 | embed() { 28 | throw new Error('Must be implemented by subclasses'); 29 | } 30 | 31 | lineHeight(size, includeGap = false) { 32 | const gap = includeGap ? this.lineGap : 0; 33 | return ((this.ascender + gap - this.descender) / 1000) * size; 34 | } 35 | } 36 | 37 | export default PDFFont; 38 | -------------------------------------------------------------------------------- /lib/font/data/MustRead.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Core 14 AFM Files - ReadMe 7 | 8 | 9 | 10 | or 11 | 12 | 13 | 14 | 15 | 16 |
This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and distributed for any purpose and without charge, with or without modification, provided that all copyright notices are retained; that the AFM files are not distributed without this file; that all modifications to this file or any of the AFM files are prominently noted in the modified file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or obligation to support the use of the AFM files. Col
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/font/standard.js: -------------------------------------------------------------------------------- 1 | import AFMFont from './afm'; 2 | import PDFFont from '../font'; 3 | import fs from 'fs'; 4 | 5 | // This insanity is so bundlers can inline the font files 6 | const STANDARD_FONTS = { 7 | Courier() { 8 | return fs.readFileSync(__dirname + '/data/Courier.afm', 'utf8'); 9 | }, 10 | 'Courier-Bold'() { 11 | return fs.readFileSync(__dirname + '/data/Courier-Bold.afm', 'utf8'); 12 | }, 13 | 'Courier-Oblique'() { 14 | return fs.readFileSync(__dirname + '/data/Courier-Oblique.afm', 'utf8'); 15 | }, 16 | 'Courier-BoldOblique'() { 17 | return fs.readFileSync(__dirname + '/data/Courier-BoldOblique.afm', 'utf8'); 18 | }, 19 | Helvetica() { 20 | return fs.readFileSync(__dirname + '/data/Helvetica.afm', 'utf8'); 21 | }, 22 | 'Helvetica-Bold'() { 23 | return fs.readFileSync(__dirname + '/data/Helvetica-Bold.afm', 'utf8'); 24 | }, 25 | 'Helvetica-Oblique'() { 26 | return fs.readFileSync(__dirname + '/data/Helvetica-Oblique.afm', 'utf8'); 27 | }, 28 | 'Helvetica-BoldOblique'() { 29 | return fs.readFileSync( 30 | __dirname + '/data/Helvetica-BoldOblique.afm', 31 | 'utf8', 32 | ); 33 | }, 34 | 'Times-Roman'() { 35 | return fs.readFileSync(__dirname + '/data/Times-Roman.afm', 'utf8'); 36 | }, 37 | 'Times-Bold'() { 38 | return fs.readFileSync(__dirname + '/data/Times-Bold.afm', 'utf8'); 39 | }, 40 | 'Times-Italic'() { 41 | return fs.readFileSync(__dirname + '/data/Times-Italic.afm', 'utf8'); 42 | }, 43 | 'Times-BoldItalic'() { 44 | return fs.readFileSync(__dirname + '/data/Times-BoldItalic.afm', 'utf8'); 45 | }, 46 | Symbol() { 47 | return fs.readFileSync(__dirname + '/data/Symbol.afm', 'utf8'); 48 | }, 49 | ZapfDingbats() { 50 | return fs.readFileSync(__dirname + '/data/ZapfDingbats.afm', 'utf8'); 51 | }, 52 | }; 53 | 54 | class StandardFont extends PDFFont { 55 | constructor(document, name, id) { 56 | super(); 57 | this.document = document; 58 | this.name = name; 59 | this.id = id; 60 | this.font = new AFMFont(STANDARD_FONTS[this.name]()); 61 | ({ 62 | ascender: this.ascender, 63 | descender: this.descender, 64 | bbox: this.bbox, 65 | lineGap: this.lineGap, 66 | xHeight: this.xHeight, 67 | capHeight: this.capHeight, 68 | } = this.font); 69 | } 70 | 71 | embed() { 72 | this.dictionary.data = { 73 | Type: 'Font', 74 | BaseFont: this.name, 75 | Subtype: 'Type1', 76 | Encoding: 'WinAnsiEncoding', 77 | }; 78 | 79 | return this.dictionary.end(); 80 | } 81 | 82 | encode(text) { 83 | const encoded = this.font.encodeText(text); 84 | const glyphs = this.font.glyphsForString(`${text}`); 85 | const advances = this.font.advancesForGlyphs(glyphs); 86 | const positions = []; 87 | for (let i = 0; i < glyphs.length; i++) { 88 | const glyph = glyphs[i]; 89 | positions.push({ 90 | xAdvance: advances[i], 91 | yAdvance: 0, 92 | xOffset: 0, 93 | yOffset: 0, 94 | advanceWidth: this.font.widthOfGlyph(glyph), 95 | }); 96 | } 97 | 98 | return [encoded, positions]; 99 | } 100 | 101 | widthOfString(string, size) { 102 | const glyphs = this.font.glyphsForString(`${string}`); 103 | const advances = this.font.advancesForGlyphs(glyphs); 104 | 105 | let width = 0; 106 | for (let advance of advances) { 107 | width += advance; 108 | } 109 | 110 | const scale = size / 1000; 111 | return width * scale; 112 | } 113 | 114 | static isStandardFont(name) { 115 | return name in STANDARD_FONTS; 116 | } 117 | } 118 | 119 | export default StandardFont; 120 | -------------------------------------------------------------------------------- /lib/font_factory.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as fontkit from 'fontkit'; 3 | import StandardFont from './font/standard'; 4 | import EmbeddedFont from './font/embedded'; 5 | 6 | class PDFFontFactory { 7 | static open(document, src, family, id) { 8 | let font; 9 | if (typeof src === 'string') { 10 | if (StandardFont.isStandardFont(src)) { 11 | return new StandardFont(document, src, id); 12 | } 13 | 14 | src = fs.readFileSync(src); 15 | } 16 | if (src instanceof Uint8Array) { 17 | font = fontkit.create(src, family); 18 | } else if (src instanceof ArrayBuffer) { 19 | font = fontkit.create(new Uint8Array(src), family); 20 | } 21 | 22 | if (font == null) { 23 | throw new Error('Not a supported font format or standard PDF font.'); 24 | } 25 | 26 | return new EmbeddedFont(document, font, id); 27 | } 28 | } 29 | 30 | export default PDFFontFactory; 31 | -------------------------------------------------------------------------------- /lib/image.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFImage - embeds images in PDF documents 3 | By Devon Govett 4 | */ 5 | 6 | import fs from 'fs'; 7 | import JPEG from './image/jpeg'; 8 | import PNG from './image/png'; 9 | 10 | class PDFImage { 11 | static open(src, label) { 12 | let data; 13 | if (Buffer.isBuffer(src)) { 14 | data = src; 15 | } else if (src instanceof ArrayBuffer) { 16 | data = Buffer.from(new Uint8Array(src)); 17 | } else { 18 | const match = /^data:.+?;base64,(.*)$/.exec(src); 19 | if (match) { 20 | data = Buffer.from(match[1], 'base64'); 21 | } else { 22 | data = fs.readFileSync(src); 23 | if (!data) { 24 | return; 25 | } 26 | } 27 | } 28 | 29 | if (data[0] === 0xff && data[1] === 0xd8) { 30 | return new JPEG(data, label); 31 | } else if (data[0] === 0x89 && data.toString('ascii', 1, 4) === 'PNG') { 32 | return new PNG(data, label); 33 | } else { 34 | throw new Error('Unknown image format.'); 35 | } 36 | } 37 | } 38 | 39 | export default PDFImage; 40 | -------------------------------------------------------------------------------- /lib/image/jpeg.js: -------------------------------------------------------------------------------- 1 | import exif from 'jpeg-exif'; 2 | 3 | const MARKERS = [ 4 | 0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc5, 0xffc6, 0xffc7, 0xffc8, 0xffc9, 5 | 0xffca, 0xffcb, 0xffcc, 0xffcd, 0xffce, 0xffcf, 6 | ]; 7 | 8 | const COLOR_SPACE_MAP = { 9 | 1: 'DeviceGray', 10 | 3: 'DeviceRGB', 11 | 4: 'DeviceCMYK', 12 | }; 13 | 14 | class JPEG { 15 | constructor(data, label) { 16 | let marker; 17 | this.data = data; 18 | this.label = label; 19 | if (this.data.readUInt16BE(0) !== 0xffd8) { 20 | throw 'SOI not found in JPEG'; 21 | } 22 | 23 | // Parse the EXIF orientation 24 | this.orientation = exif.fromBuffer(this.data).Orientation || 1; 25 | 26 | let pos = 2; 27 | while (pos < this.data.length) { 28 | marker = this.data.readUInt16BE(pos); 29 | pos += 2; 30 | if (MARKERS.includes(marker)) { 31 | break; 32 | } 33 | pos += this.data.readUInt16BE(pos); 34 | } 35 | 36 | if (!MARKERS.includes(marker)) { 37 | throw 'Invalid JPEG.'; 38 | } 39 | pos += 2; 40 | 41 | this.bits = this.data[pos++]; 42 | this.height = this.data.readUInt16BE(pos); 43 | pos += 2; 44 | 45 | this.width = this.data.readUInt16BE(pos); 46 | pos += 2; 47 | 48 | const channels = this.data[pos++]; 49 | this.colorSpace = COLOR_SPACE_MAP[channels]; 50 | 51 | this.obj = null; 52 | } 53 | 54 | embed(document) { 55 | if (this.obj) { 56 | return; 57 | } 58 | 59 | this.obj = document.ref({ 60 | Type: 'XObject', 61 | Subtype: 'Image', 62 | BitsPerComponent: this.bits, 63 | Width: this.width, 64 | Height: this.height, 65 | ColorSpace: this.colorSpace, 66 | Filter: 'DCTDecode', 67 | }); 68 | 69 | // add extra decode params for CMYK images. By swapping the 70 | // min and max values from the default, we invert the colors. See 71 | // section 4.8.4 of the spec. 72 | if (this.colorSpace === 'DeviceCMYK') { 73 | this.obj.data['Decode'] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]; 74 | } 75 | 76 | this.obj.end(this.data); 77 | 78 | // free memory 79 | return (this.data = null); 80 | } 81 | } 82 | 83 | export default JPEG; 84 | -------------------------------------------------------------------------------- /lib/metadata.js: -------------------------------------------------------------------------------- 1 | class PDFMetadata { 2 | constructor() { 3 | this._metadata = ` 4 | 5 | 6 | 7 | `; 8 | } 9 | 10 | _closeTags() { 11 | this._metadata = this._metadata.concat(` 12 | 13 | 14 | 15 | `); 16 | } 17 | 18 | append(xml, newline = true) { 19 | this._metadata = this._metadata.concat(xml); 20 | if (newline) this._metadata = this._metadata.concat('\n'); 21 | } 22 | 23 | getXML() { 24 | return this._metadata; 25 | } 26 | 27 | getLength() { 28 | return this._metadata.length; 29 | } 30 | 31 | end() { 32 | this._closeTags(); 33 | this._metadata = this._metadata.trim(); 34 | } 35 | } 36 | 37 | export default PDFMetadata; 38 | -------------------------------------------------------------------------------- /lib/mixins/data/sRGB_IEC61966_2_1.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/lib/mixins/data/sRGB_IEC61966_2_1.icc -------------------------------------------------------------------------------- /lib/mixins/metadata.js: -------------------------------------------------------------------------------- 1 | import PDFMetadata from '../metadata'; 2 | 3 | export default { 4 | initMetadata() { 5 | this.metadata = new PDFMetadata(); 6 | }, 7 | 8 | appendXML(xml, newline = true) { 9 | this.metadata.append(xml, newline); 10 | }, 11 | 12 | _addInfo() { 13 | this.appendXML(` 14 | 15 | ${this.info.CreationDate.toISOString().split('.')[0] + 'Z'} 16 | ${this.info.Creator} 17 | 18 | `); 19 | 20 | if (this.info.Title || this.info.Author || this.info.Subject) { 21 | this.appendXML(` 22 | 23 | `); 24 | 25 | if (this.info.Title) { 26 | this.appendXML(` 27 | 28 | 29 | ${this.info.Title} 30 | 31 | 32 | `); 33 | } 34 | 35 | if (this.info.Author) { 36 | this.appendXML(` 37 | 38 | 39 | ${this.info.Author} 40 | 41 | 42 | `); 43 | } 44 | 45 | if (this.info.Subject) { 46 | this.appendXML(` 47 | 48 | 49 | ${this.info.Subject} 50 | 51 | 52 | `); 53 | } 54 | 55 | this.appendXML(` 56 | 57 | `); 58 | } 59 | 60 | this.appendXML( 61 | ` 62 | 63 | ${this.info.Creator}`, 64 | false, 65 | ); 66 | 67 | if (this.info.Keywords) { 68 | this.appendXML( 69 | ` 70 | ${this.info.Keywords}`, 71 | false, 72 | ); 73 | } 74 | 75 | this.appendXML(` 76 | 77 | `); 78 | }, 79 | 80 | endMetadata() { 81 | this._addInfo(); 82 | 83 | this.metadata.end(); 84 | 85 | /* 86 | Metadata was introduced in PDF 1.4, so adding it to 1.3 87 | will likely only take up more space. 88 | */ 89 | if (this.version != 1.3) { 90 | this.metadataRef = this.ref({ 91 | length: this.metadata.getLength(), 92 | Type: 'Metadata', 93 | Subtype: 'XML', 94 | }); 95 | this.metadataRef.compress = false; 96 | this.metadataRef.write(Buffer.from(this.metadata.getXML(), 'utf-8')); 97 | this.metadataRef.end(); 98 | this._root.data.Metadata = this.metadataRef; 99 | } 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /lib/mixins/outline.js: -------------------------------------------------------------------------------- 1 | import PDFOutline from '../outline'; 2 | 3 | export default { 4 | initOutline() { 5 | this.outline = new PDFOutline(this, null, null, null); 6 | }, 7 | 8 | endOutline() { 9 | this.outline.endOutline(); 10 | if (this.outline.children.length > 0) { 11 | this._root.data.Outlines = this.outline.dictionary; 12 | return (this._root.data.PageMode = 'UseOutlines'); 13 | } 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /lib/mixins/pdfa.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export default { 4 | initPDFA(pSubset) { 5 | if (pSubset.charAt(pSubset.length - 3) === '-') { 6 | this.subset_conformance = pSubset 7 | .charAt(pSubset.length - 1) 8 | .toUpperCase(); 9 | this.subset = parseInt(pSubset.charAt(pSubset.length - 2)); 10 | } else { 11 | // Default to Basic conformance when user doesn't specify 12 | this.subset_conformance = 'B'; 13 | this.subset = parseInt(pSubset.charAt(pSubset.length - 1)); 14 | } 15 | }, 16 | 17 | endSubset() { 18 | this._addPdfaMetadata(); 19 | this._addColorOutputIntent(); 20 | }, 21 | 22 | _addColorOutputIntent() { 23 | const iccProfile = fs.readFileSync( 24 | `${__dirname}/data/sRGB_IEC61966_2_1.icc`, 25 | ); 26 | 27 | const colorProfileRef = this.ref({ 28 | Length: iccProfile.length, 29 | N: 3, 30 | }); 31 | colorProfileRef.write(iccProfile); 32 | colorProfileRef.end(); 33 | 34 | const intentRef = this.ref({ 35 | Type: 'OutputIntent', 36 | S: 'GTS_PDFA1', 37 | Info: new String('sRGB IEC61966-2.1'), 38 | OutputConditionIdentifier: new String('sRGB IEC61966-2.1'), 39 | DestOutputProfile: colorProfileRef, 40 | }); 41 | intentRef.end(); 42 | 43 | this._root.data.OutputIntents = [intentRef]; 44 | }, 45 | 46 | _getPdfaid() { 47 | return ` 48 | 49 | ${this.subset} 50 | ${this.subset_conformance} 51 | 52 | `; 53 | }, 54 | 55 | _addPdfaMetadata() { 56 | this.appendXML(this._getPdfaid()); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /lib/mixins/pdfua.js: -------------------------------------------------------------------------------- 1 | export default { 2 | initPDFUA() { 3 | this.subset = 1; 4 | }, 5 | 6 | endSubset() { 7 | this._addPdfuaMetadata(); 8 | }, 9 | 10 | _addPdfuaMetadata() { 11 | this.appendXML(this._getPdfuaid()); 12 | }, 13 | 14 | _getPdfuaid() { 15 | return ` 16 | 17 | ${this.subset} 18 | 19 | `; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/mixins/subsets.js: -------------------------------------------------------------------------------- 1 | import PDFA from './pdfa'; 2 | import PDFUA from './pdfua'; 3 | 4 | export default { 5 | _importSubset(subset) { 6 | Object.assign(this, subset); 7 | }, 8 | 9 | initSubset(options) { 10 | switch (options.subset) { 11 | case 'PDF/A-1': 12 | case 'PDF/A-1a': 13 | case 'PDF/A-1b': 14 | case 'PDF/A-2': 15 | case 'PDF/A-2a': 16 | case 'PDF/A-2b': 17 | case 'PDF/A-3': 18 | case 'PDF/A-3a': 19 | case 'PDF/A-3b': 20 | this._importSubset(PDFA); 21 | this.initPDFA(options.subset); 22 | break; 23 | case 'PDF/UA': 24 | this._importSubset(PDFUA); 25 | this.initPDFUA(); 26 | break; 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/mixins/table.js: -------------------------------------------------------------------------------- 1 | import PDFTable from '../table/index'; 2 | 3 | export default { 4 | initTables() { 5 | this._tableIndex = 0; 6 | }, 7 | /** 8 | * @param {Table} [opts] 9 | * @returns {PDFTable} returns the table object unless `data` is set, 10 | * then it returns the underlying document 11 | */ 12 | table(opts) { 13 | return new PDFTable(this, opts); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /lib/name_tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFNameTree - represents a name tree object 3 | */ 4 | 5 | import PDFTree from './tree'; 6 | 7 | class PDFNameTree extends PDFTree { 8 | _compareKeys(a, b) { 9 | return a.localeCompare(b); 10 | } 11 | 12 | _keysName() { 13 | return 'Names'; 14 | } 15 | 16 | _dataForKey(k) { 17 | return new String(k); 18 | } 19 | } 20 | 21 | export default PDFNameTree; 22 | -------------------------------------------------------------------------------- /lib/number_tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFNumberTree - represents a number tree object 3 | */ 4 | 5 | import PDFTree from './tree'; 6 | 7 | class PDFNumberTree extends PDFTree { 8 | _compareKeys(a, b) { 9 | return parseInt(a) - parseInt(b); 10 | } 11 | 12 | _keysName() { 13 | return 'Nums'; 14 | } 15 | 16 | _dataForKey(k) { 17 | return parseInt(k); 18 | } 19 | } 20 | 21 | export default PDFNumberTree; 22 | -------------------------------------------------------------------------------- /lib/object.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFObject - converts JavaScript types into their corresponding PDF types. 3 | By Devon Govett 4 | */ 5 | 6 | import PDFAbstractReference from './abstract_reference'; 7 | import PDFTree from './tree'; 8 | import SpotColor from './spotcolor'; 9 | 10 | const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length); 11 | 12 | const escapableRe = /[\n\r\t\b\f()\\]/g; 13 | const escapable = { 14 | '\n': '\\n', 15 | '\r': '\\r', 16 | '\t': '\\t', 17 | '\b': '\\b', 18 | '\f': '\\f', 19 | '\\': '\\\\', 20 | '(': '\\(', 21 | ')': '\\)', 22 | }; 23 | 24 | // Convert little endian UTF-16 to big endian 25 | const swapBytes = function (buff) { 26 | const l = buff.length; 27 | if (l & 0x01) { 28 | throw new Error('Buffer length must be even'); 29 | } else { 30 | for (let i = 0, end = l - 1; i < end; i += 2) { 31 | const a = buff[i]; 32 | buff[i] = buff[i + 1]; 33 | buff[i + 1] = a; 34 | } 35 | } 36 | 37 | return buff; 38 | }; 39 | 40 | class PDFObject { 41 | static convert(object, encryptFn = null) { 42 | // String literals are converted to the PDF name type 43 | if (typeof object === 'string') { 44 | return `/${object}`; 45 | 46 | // String objects are converted to PDF strings (UTF-16) 47 | } else if (object instanceof String) { 48 | let string = object; 49 | // Detect if this is a unicode string 50 | let isUnicode = false; 51 | for (let i = 0, end = string.length; i < end; i++) { 52 | if (string.charCodeAt(i) > 0x7f) { 53 | isUnicode = true; 54 | break; 55 | } 56 | } 57 | 58 | // If so, encode it as big endian UTF-16 59 | let stringBuffer; 60 | if (isUnicode) { 61 | stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le')); 62 | } else { 63 | stringBuffer = Buffer.from(string.valueOf(), 'ascii'); 64 | } 65 | 66 | // Encrypt the string when necessary 67 | if (encryptFn) { 68 | string = encryptFn(stringBuffer).toString('binary'); 69 | } else { 70 | string = stringBuffer.toString('binary'); 71 | } 72 | 73 | // Escape characters as required by the spec 74 | string = string.replace(escapableRe, (c) => escapable[c]); 75 | 76 | return `(${string})`; 77 | 78 | // Buffers are converted to PDF hex strings 79 | } else if (Buffer.isBuffer(object)) { 80 | return `<${object.toString('hex')}>`; 81 | } else if ( 82 | object instanceof PDFAbstractReference || 83 | object instanceof PDFTree || 84 | object instanceof SpotColor 85 | ) { 86 | return object.toString(); 87 | } else if (object instanceof Date) { 88 | let string = 89 | `D:${pad(object.getUTCFullYear(), 4)}` + 90 | pad(object.getUTCMonth() + 1, 2) + 91 | pad(object.getUTCDate(), 2) + 92 | pad(object.getUTCHours(), 2) + 93 | pad(object.getUTCMinutes(), 2) + 94 | pad(object.getUTCSeconds(), 2) + 95 | 'Z'; 96 | 97 | // Encrypt the string when necessary 98 | if (encryptFn) { 99 | string = encryptFn(Buffer.from(string, 'ascii')).toString('binary'); 100 | 101 | // Escape characters as required by the spec 102 | string = string.replace(escapableRe, (c) => escapable[c]); 103 | } 104 | 105 | return `(${string})`; 106 | } else if (Array.isArray(object)) { 107 | const items = object 108 | .map((e) => PDFObject.convert(e, encryptFn)) 109 | .join(' '); 110 | return `[${items}]`; 111 | } else if ({}.toString.call(object) === '[object Object]') { 112 | const out = ['<<']; 113 | for (let key in object) { 114 | const val = object[key]; 115 | out.push(`/${key} ${PDFObject.convert(val, encryptFn)}`); 116 | } 117 | 118 | out.push('>>'); 119 | return out.join('\n'); 120 | } else if (typeof object === 'number') { 121 | return PDFObject.number(object); 122 | } else { 123 | return `${object}`; 124 | } 125 | } 126 | 127 | static number(n) { 128 | if (n > -1e21 && n < 1e21) { 129 | return Math.round(n * 1e6) / 1e6; 130 | } 131 | 132 | throw new Error(`unsupported number: ${n}`); 133 | } 134 | } 135 | 136 | export default PDFObject; 137 | -------------------------------------------------------------------------------- /lib/outline.js: -------------------------------------------------------------------------------- 1 | class PDFOutline { 2 | constructor(document, parent, title, dest, options = { expanded: false }) { 3 | this.document = document; 4 | this.options = options; 5 | this.outlineData = {}; 6 | 7 | if (dest !== null) { 8 | this.outlineData['Dest'] = [dest.dictionary, 'Fit']; 9 | } 10 | 11 | if (parent !== null) { 12 | this.outlineData['Parent'] = parent; 13 | } 14 | 15 | if (title !== null) { 16 | this.outlineData['Title'] = new String(title); 17 | } 18 | 19 | this.dictionary = this.document.ref(this.outlineData); 20 | this.children = []; 21 | } 22 | 23 | addItem(title, options = { expanded: false }) { 24 | const result = new PDFOutline( 25 | this.document, 26 | this.dictionary, 27 | title, 28 | this.document.page, 29 | options, 30 | ); 31 | this.children.push(result); 32 | 33 | return result; 34 | } 35 | 36 | endOutline() { 37 | if (this.children.length > 0) { 38 | if (this.options.expanded) { 39 | this.outlineData.Count = this.children.length; 40 | } 41 | 42 | const first = this.children[0], 43 | last = this.children[this.children.length - 1]; 44 | this.outlineData.First = first.dictionary; 45 | this.outlineData.Last = last.dictionary; 46 | 47 | for (let i = 0, len = this.children.length; i < len; i++) { 48 | const child = this.children[i]; 49 | if (i > 0) { 50 | child.outlineData.Prev = this.children[i - 1].dictionary; 51 | } 52 | if (i < this.children.length - 1) { 53 | child.outlineData.Next = this.children[i + 1].dictionary; 54 | } 55 | child.endOutline(); 56 | } 57 | } 58 | 59 | return this.dictionary.end(); 60 | } 61 | } 62 | 63 | export default PDFOutline; 64 | -------------------------------------------------------------------------------- /lib/pattern.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDF tiling pattern support. Uncolored only. 3 | */ 4 | 5 | const underlyingColorSpaces = ['DeviceCMYK', 'DeviceRGB']; 6 | 7 | class PDFTilingPattern { 8 | constructor(doc, bBox, xStep, yStep, stream) { 9 | this.doc = doc; 10 | this.bBox = bBox; 11 | this.xStep = xStep; 12 | this.yStep = yStep; 13 | this.stream = stream; 14 | } 15 | 16 | createPattern() { 17 | // no resources needed for our current usage 18 | // required entry 19 | const resources = this.doc.ref(); 20 | resources.end(); 21 | // apply default transform matrix (flipped in the default doc._ctm) 22 | // see document.js & gradient.js 23 | const [m0, m1, m2, m3, m4, m5] = this.doc._ctm; 24 | const [m11, m12, m21, m22, dx, dy] = [1, 0, 0, 1, 0, 0]; 25 | const m = [ 26 | m0 * m11 + m2 * m12, 27 | m1 * m11 + m3 * m12, 28 | m0 * m21 + m2 * m22, 29 | m1 * m21 + m3 * m22, 30 | m0 * dx + m2 * dy + m4, 31 | m1 * dx + m3 * dy + m5, 32 | ]; 33 | const pattern = this.doc.ref({ 34 | Type: 'Pattern', 35 | PatternType: 1, // tiling 36 | PaintType: 2, // 1-colored, 2-uncolored 37 | TilingType: 2, // 2-no distortion 38 | BBox: this.bBox, 39 | XStep: this.xStep, 40 | YStep: this.yStep, 41 | Matrix: m.map((v) => +v.toFixed(5)), 42 | Resources: resources, 43 | }); 44 | pattern.end(this.stream); 45 | return pattern; 46 | } 47 | 48 | embedPatternColorSpaces() { 49 | // map each pattern to an underlying color space 50 | // and embed on each page 51 | underlyingColorSpaces.forEach((csName) => { 52 | const csId = this.getPatternColorSpaceId(csName); 53 | 54 | if (this.doc.page.colorSpaces[csId]) return; 55 | const cs = this.doc.ref(['Pattern', csName]); 56 | cs.end(); 57 | this.doc.page.colorSpaces[csId] = cs; 58 | }); 59 | } 60 | 61 | getPatternColorSpaceId(underlyingColorspace) { 62 | return `CsP${underlyingColorspace}`; 63 | } 64 | 65 | embed() { 66 | if (!this.id) { 67 | this.doc._patternCount = this.doc._patternCount + 1; 68 | this.id = 'P' + this.doc._patternCount; 69 | this.pattern = this.createPattern(); 70 | } 71 | 72 | // patterns are embedded in each page 73 | if (!this.doc.page.patterns[this.id]) { 74 | this.doc.page.patterns[this.id] = this.pattern; 75 | } 76 | } 77 | 78 | apply(stroke, patternColor) { 79 | // do any embedding/creating that might be needed 80 | this.embedPatternColorSpaces(); 81 | this.embed(); 82 | 83 | const normalizedColor = this.doc._normalizeColor(patternColor); 84 | if (!normalizedColor) 85 | throw Error(`invalid pattern color. (value: ${patternColor})`); 86 | 87 | // select one of the pattern color spaces 88 | const csId = this.getPatternColorSpaceId( 89 | this.doc._getColorSpace(normalizedColor), 90 | ); 91 | this.doc._setColorSpace(csId, stroke); 92 | 93 | // stroke/fill using the pattern and color (in the above underlying color space) 94 | const op = stroke ? 'SCN' : 'scn'; 95 | return this.doc.addContent( 96 | `${normalizedColor.join(' ')} /${this.id} ${op}`, 97 | ); 98 | } 99 | } 100 | 101 | export default { PDFTilingPattern }; 102 | -------------------------------------------------------------------------------- /lib/reference.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFReference - represents a reference to another object in the PDF object heirarchy 3 | By Devon Govett 4 | */ 5 | 6 | import zlib from 'zlib'; 7 | import PDFAbstractReference from './abstract_reference'; 8 | import PDFObject from './object'; 9 | 10 | class PDFReference extends PDFAbstractReference { 11 | constructor(document, id, data = {}) { 12 | super(); 13 | this.document = document; 14 | this.id = id; 15 | this.data = data; 16 | this.gen = 0; 17 | this.compress = this.document.compress && !this.data.Filter; 18 | this.uncompressedLength = 0; 19 | this.buffer = []; 20 | } 21 | 22 | write(chunk) { 23 | if (!(chunk instanceof Uint8Array)) { 24 | chunk = Buffer.from(chunk + '\n', 'binary'); 25 | } 26 | 27 | this.uncompressedLength += chunk.length; 28 | if (this.data.Length == null) { 29 | this.data.Length = 0; 30 | } 31 | this.buffer.push(chunk); 32 | this.data.Length += chunk.length; 33 | if (this.compress) { 34 | this.data.Filter = 'FlateDecode'; 35 | } 36 | } 37 | 38 | end(chunk) { 39 | if (chunk) { 40 | this.write(chunk); 41 | } 42 | this.finalize(); 43 | } 44 | 45 | finalize() { 46 | this.offset = this.document._offset; 47 | 48 | const encryptFn = this.document._security 49 | ? this.document._security.getEncryptFn(this.id, this.gen) 50 | : null; 51 | 52 | if (this.buffer.length) { 53 | this.buffer = Buffer.concat(this.buffer); 54 | if (this.compress) { 55 | this.buffer = zlib.deflateSync(this.buffer); 56 | } 57 | 58 | if (encryptFn) { 59 | this.buffer = encryptFn(this.buffer); 60 | } 61 | 62 | this.data.Length = this.buffer.length; 63 | } 64 | 65 | this.document._write(`${this.id} ${this.gen} obj`); 66 | this.document._write(PDFObject.convert(this.data, encryptFn)); 67 | 68 | if (this.buffer.length) { 69 | this.document._write('stream'); 70 | this.document._write(this.buffer); 71 | 72 | this.buffer = []; // free up memory 73 | this.document._write('\nendstream'); 74 | } 75 | 76 | this.document._write('endobj'); 77 | this.document._refEnd(this); 78 | } 79 | toString() { 80 | return `${this.id} ${this.gen} R`; 81 | } 82 | } 83 | 84 | export default PDFReference; 85 | -------------------------------------------------------------------------------- /lib/saslprep/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | isUnassignedCodePoint, 3 | isCommonlyMappedToNothing, 4 | isNonASCIISpaceCharacter, 5 | isProhibitedCharacter, 6 | isBidirectionalRAL, 7 | isBidirectionalL, 8 | } from './lib/code-points'; 9 | 10 | // 2.1. Mapping 11 | 12 | /** 13 | * non-ASCII space characters [StringPrep, C.1.2] that can be 14 | * mapped to SPACE (U+0020) 15 | */ 16 | const mapping2space = isNonASCIISpaceCharacter; 17 | 18 | /** 19 | * the "commonly mapped to nothing" characters [StringPrep, B.1] 20 | * that can be mapped to nothing. 21 | */ 22 | const mapping2nothing = isCommonlyMappedToNothing; 23 | 24 | // utils 25 | const getCodePoint = (character) => character.codePointAt(0); 26 | const first = (x) => x[0]; 27 | const last = (x) => x[x.length - 1]; 28 | 29 | /** 30 | * Convert provided string into an array of Unicode Code Points. 31 | * Based on https://stackoverflow.com/a/21409165/1556249 32 | * and https://www.npmjs.com/package/code-point-at. 33 | * @param {string} input 34 | * @returns {number[]} 35 | */ 36 | function toCodePoints(input) { 37 | const codepoints = []; 38 | const size = input.length; 39 | 40 | for (let i = 0; i < size; i += 1) { 41 | const before = input.charCodeAt(i); 42 | 43 | if (before >= 0xd800 && before <= 0xdbff && size > i + 1) { 44 | const next = input.charCodeAt(i + 1); 45 | 46 | if (next >= 0xdc00 && next <= 0xdfff) { 47 | codepoints.push((before - 0xd800) * 0x400 + next - 0xdc00 + 0x10000); 48 | i += 1; 49 | continue; 50 | } 51 | } 52 | 53 | codepoints.push(before); 54 | } 55 | 56 | return codepoints; 57 | } 58 | 59 | /** 60 | * SASLprep. 61 | * @param {string} input 62 | * @param {Object} opts 63 | * @param {boolean} opts.allowUnassigned 64 | * @returns {string} 65 | */ 66 | function saslprep(input, opts = {}) { 67 | if (typeof input !== 'string') { 68 | throw new TypeError('Expected string.'); 69 | } 70 | 71 | if (input.length === 0) { 72 | return ''; 73 | } 74 | 75 | // 1. Map 76 | const mapped_input = toCodePoints(input) 77 | // 1.1 mapping to space 78 | .map((character) => (mapping2space(character) ? 0x20 : character)) 79 | // 1.2 mapping to nothing 80 | .filter((character) => !mapping2nothing(character)); 81 | 82 | // 2. Normalize 83 | const normalized_input = String.fromCodePoint 84 | .apply(null, mapped_input) 85 | .normalize('NFKC'); 86 | 87 | const normalized_map = toCodePoints(normalized_input); 88 | 89 | // 3. Prohibit 90 | const hasProhibited = normalized_map.some(isProhibitedCharacter); 91 | 92 | if (hasProhibited) { 93 | throw new Error( 94 | 'Prohibited character, see https://tools.ietf.org/html/rfc4013#section-2.3', 95 | ); 96 | } 97 | 98 | // Unassigned Code Points 99 | if (opts.allowUnassigned !== true) { 100 | const hasUnassigned = normalized_map.some(isUnassignedCodePoint); 101 | 102 | if (hasUnassigned) { 103 | throw new Error( 104 | 'Unassigned code point, see https://tools.ietf.org/html/rfc4013#section-2.5', 105 | ); 106 | } 107 | } 108 | 109 | // 4. check bidi 110 | 111 | const hasBidiRAL = normalized_map.some(isBidirectionalRAL); 112 | 113 | const hasBidiL = normalized_map.some(isBidirectionalL); 114 | 115 | // 4.1 If a string contains any RandALCat character, the string MUST NOT 116 | // contain any LCat character. 117 | if (hasBidiRAL && hasBidiL) { 118 | throw new Error( 119 | 'String must not contain RandALCat and LCat at the same time,' + 120 | ' see https://tools.ietf.org/html/rfc3454#section-6', 121 | ); 122 | } 123 | 124 | /** 125 | * 4.2 If a string contains any RandALCat character, a RandALCat 126 | * character MUST be the first character of the string, and a 127 | * RandALCat character MUST be the last character of the string. 128 | */ 129 | 130 | const isFirstBidiRAL = isBidirectionalRAL( 131 | getCodePoint(first(normalized_input)), 132 | ); 133 | const isLastBidiRAL = isBidirectionalRAL( 134 | getCodePoint(last(normalized_input)), 135 | ); 136 | 137 | if (hasBidiRAL && !(isFirstBidiRAL && isLastBidiRAL)) { 138 | throw new Error( 139 | 'Bidirectional RandALCat character must be the first and the last' + 140 | ' character of the string, see https://tools.ietf.org/html/rfc3454#section-6', 141 | ); 142 | } 143 | 144 | return normalized_input; 145 | } 146 | 147 | export default saslprep; 148 | -------------------------------------------------------------------------------- /lib/saslprep/lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if value is in a range group. 3 | * @param {number} value 4 | * @param {number[]} rangeGroup 5 | * @returns {boolean} 6 | */ 7 | function inRange(value, rangeGroup) { 8 | if (value < rangeGroup[0]) return false; 9 | let startRange = 0; 10 | let endRange = rangeGroup.length / 2; 11 | while (startRange <= endRange) { 12 | const middleRange = Math.floor((startRange + endRange) / 2); 13 | 14 | // actual array index 15 | const arrayIndex = middleRange * 2; 16 | 17 | // Check if value is in range pointed by actual index 18 | if ( 19 | value >= rangeGroup[arrayIndex] && 20 | value <= rangeGroup[arrayIndex + 1] 21 | ) { 22 | return true; 23 | } 24 | 25 | if (value > rangeGroup[arrayIndex + 1]) { 26 | // Search Right Side Of Array 27 | startRange = middleRange + 1; 28 | } else { 29 | // Search Left Side Of Array 30 | endRange = middleRange - 1; 31 | } 32 | } 33 | return false; 34 | } 35 | 36 | export { inRange }; 37 | -------------------------------------------------------------------------------- /lib/spotcolor.js: -------------------------------------------------------------------------------- 1 | export default class SpotColor { 2 | constructor(doc, name, C, M, Y, K) { 3 | this.id = 'CS' + Object.keys(doc.spotColors).length; 4 | this.name = name; 5 | this.values = [C, M, Y, K]; 6 | this.ref = doc.ref([ 7 | 'Separation', 8 | this.name, 9 | 'DeviceCMYK', 10 | { 11 | Range: [0, 1, 0, 1, 0, 1, 0, 1], 12 | C0: [0, 0, 0, 0], 13 | C1: this.values.map((value) => value / 100), 14 | FunctionType: 2, 15 | Domain: [0, 1], 16 | N: 1, 17 | }, 18 | ]); 19 | this.ref.end(); 20 | } 21 | 22 | toString() { 23 | return `${this.ref.id} 0 R`; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/structure_content.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFStructureContent - a reference to a marked structure content 3 | By Ben Schmidt 4 | */ 5 | 6 | class PDFStructureContent { 7 | constructor(pageRef, mcid) { 8 | this.refs = [{ pageRef, mcid }]; 9 | } 10 | 11 | push(structContent) { 12 | structContent.refs.forEach((ref) => this.refs.push(ref)); 13 | } 14 | } 15 | 16 | export default PDFStructureContent; 17 | -------------------------------------------------------------------------------- /lib/table/index.js: -------------------------------------------------------------------------------- 1 | import { normalizeRow, normalizeTable } from './normalize'; 2 | import { measure, ensure } from './size'; 3 | import { renderRow } from './render'; 4 | import { accommodateCleanup, accommodateTable } from './accessibility'; 5 | 6 | class PDFTable { 7 | /** 8 | * @param {PDFDocument} document 9 | * @param {Table} [opts] 10 | */ 11 | constructor(document, opts = {}) { 12 | this.document = document; 13 | this.opts = Object.freeze(opts); 14 | 15 | normalizeTable.call(this); 16 | accommodateTable.call(this); 17 | 18 | this._currRowIndex = 0; 19 | this._ended = false; 20 | 21 | // Render cells if present 22 | if (opts.data) { 23 | for (const row of opts.data) this.row(row); 24 | return this.end(); 25 | } 26 | } 27 | 28 | /** 29 | * Render a new row in the table 30 | * 31 | * @param {Iterable} row - The cells to render 32 | * @param {boolean} lastRow - Whether this row is the last row 33 | * @returns {this} returns the table, unless lastRow is `true` then returns the `PDFDocument` 34 | */ 35 | row(row, lastRow = false) { 36 | if (this._ended) { 37 | throw new Error(`Table was marked as ended on row ${this._currRowIndex}`); 38 | } 39 | 40 | // Convert the iterable into an array 41 | row = Array.from(row); 42 | // Transform row 43 | row = normalizeRow.call(this, row, this._currRowIndex); 44 | if (this._currRowIndex === 0) ensure.call(this, row); 45 | const { newPage, toRender } = measure.call(this, row, this._currRowIndex); 46 | if (newPage) this.document.continueOnNewPage(); 47 | const yPos = renderRow.call(this, toRender, this._currRowIndex); 48 | 49 | // Position document at base of new row 50 | this.document.x = this._position.x; 51 | this.document.y = yPos; 52 | 53 | if (lastRow) return this.end(); 54 | 55 | this._currRowIndex++; 56 | return this; 57 | } 58 | 59 | /** 60 | * Indicates to the table that it is finished, 61 | * allowing the table to flush its cell buffer (which should be empty unless there is rowSpans) 62 | * 63 | * @returns {PDFDocument} the document 64 | */ 65 | end() { 66 | // Flush any remaining cells 67 | while (this._rowBuffer?.size) this.row([]); 68 | this._ended = true; 69 | accommodateCleanup.call(this); 70 | return this.document; 71 | } 72 | } 73 | 74 | export default PDFTable; 75 | -------------------------------------------------------------------------------- /lib/tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | PDFTree - abstract base class for name and number tree objects 3 | */ 4 | 5 | import PDFObject from './object'; 6 | 7 | class PDFTree { 8 | constructor(options = {}) { 9 | this._items = {}; 10 | // disable /Limits output for this tree 11 | this.limits = typeof options.limits === 'boolean' ? options.limits : true; 12 | } 13 | 14 | add(key, val) { 15 | return (this._items[key] = val); 16 | } 17 | 18 | get(key) { 19 | return this._items[key]; 20 | } 21 | 22 | toString() { 23 | // Needs to be sorted by key 24 | const sortedKeys = Object.keys(this._items).sort((a, b) => 25 | this._compareKeys(a, b), 26 | ); 27 | 28 | const out = ['<<']; 29 | if (this.limits && sortedKeys.length > 1) { 30 | const first = sortedKeys[0], 31 | last = sortedKeys[sortedKeys.length - 1]; 32 | out.push( 33 | ` /Limits ${PDFObject.convert([this._dataForKey(first), this._dataForKey(last)])}`, 34 | ); 35 | } 36 | out.push(` /${this._keysName()} [`); 37 | for (let key of sortedKeys) { 38 | out.push( 39 | ` ${PDFObject.convert(this._dataForKey(key))} ${PDFObject.convert( 40 | this._items[key], 41 | )}`, 42 | ); 43 | } 44 | out.push(']'); 45 | out.push('>>'); 46 | return out.join('\n'); 47 | } 48 | 49 | _compareKeys(/*a, b*/) { 50 | throw new Error('Must be implemented by subclasses'); 51 | } 52 | 53 | _keysName() { 54 | throw new Error('Must be implemented by subclasses'); 55 | } 56 | 57 | _dataForKey(/*k*/) { 58 | throw new Error('Must be implemented by subclasses'); 59 | } 60 | } 61 | 62 | export default PDFTree; 63 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fArray = new Float32Array(1); 2 | const uArray = new Uint32Array(fArray.buffer); 3 | 4 | export function PDFNumber(n) { 5 | // PDF numbers are strictly 32bit 6 | // so convert this number to a 32bit number 7 | // @see ISO 32000-1 Annex C.2 (real numbers) 8 | const rounded = Math.fround(n); 9 | if (rounded <= n) return rounded; 10 | 11 | // Will have to perform 32bit float truncation 12 | fArray[0] = n; 13 | 14 | // Get the 32-bit representation as integer and shift bits 15 | if (n <= 0) { 16 | uArray[0] += 1; 17 | } else { 18 | uArray[0] -= 1; 19 | } 20 | 21 | // Return the float value 22 | return fArray[0]; 23 | } 24 | 25 | /** 26 | * Measurement of size 27 | * 28 | * @typedef {number | `${number}` | `${number}${'em' | 'in' | 'px' | 'cm' | 'mm' | 'pc' | 'ex' | 'ch' | 'rem' | 'vw' | 'vmin' | 'vmax' | '%' | 'pt'}`} Size 29 | */ 30 | 31 | /** 32 | * @typedef {Array | string | Array} PDFColor 33 | */ 34 | 35 | /** @typedef {string | Buffer | Uint8Array | ArrayBuffer} PDFFontSource */ 36 | /** 37 | * Side definitions 38 | * - To define all sides, use a single value 39 | * - To define up-down left-right, use a `[Y, X]` array 40 | * - To define each side, use `[top, right, bottom, left]` array 41 | * - Or `{vertical: SideValue, horizontal: SideValue}` 42 | * - Or `{top: SideValue, right: SideValue, bottom: SideValue, left: SideValue}` 43 | * 44 | * @template T 45 | * @typedef {T | [T, T] | [T, T, T, T] | { vertical: T; horizontal: T } | ExpandedSideDefinition} SideDefinition 46 | **/ 47 | 48 | /** 49 | * @template T 50 | * @typedef {{ top: T; right: T; bottom: T; left: T }} ExpandedSideDefinition 51 | */ 52 | 53 | /** 54 | * Convert any side definition into a static structure 55 | * 56 | * @template S 57 | * @template D 58 | * @template O 59 | * @template {S | D} T 60 | * @param {SideDefinition} sides - The sides to convert 61 | * @param {SideDefinition} defaultDefinition - The value to use when no definition is provided 62 | * @param {function(T): O} transformer - The transformation to apply to the sides once normalized 63 | * @returns {ExpandedSideDefinition} 64 | */ 65 | export function normalizeSides( 66 | sides, 67 | defaultDefinition = undefined, 68 | transformer = (v) => v, 69 | ) { 70 | if ( 71 | sides == null || 72 | (typeof sides === 'object' && Object.keys(sides).length === 0) 73 | ) { 74 | sides = defaultDefinition; 75 | } 76 | if (sides == null || typeof sides !== 'object') { 77 | sides = { top: sides, right: sides, bottom: sides, left: sides }; 78 | } else if (Array.isArray(sides)) { 79 | if (sides.length === 2) { 80 | sides = { vertical: sides[0], horizontal: sides[1] }; 81 | } else { 82 | sides = { 83 | top: sides[0], 84 | right: sides[1], 85 | bottom: sides[2], 86 | left: sides[3], 87 | }; 88 | } 89 | } 90 | 91 | if ('vertical' in sides || 'horizontal' in sides) { 92 | sides = { 93 | top: sides.vertical, 94 | right: sides.horizontal, 95 | bottom: sides.vertical, 96 | left: sides.horizontal, 97 | }; 98 | } 99 | 100 | return { 101 | top: transformer(sides.top), 102 | right: transformer(sides.right), 103 | bottom: transformer(sides.bottom), 104 | left: transformer(sides.left), 105 | }; 106 | } 107 | 108 | export const MM_TO_CM = 1 / 10; // 1MM = 1CM 109 | export const CM_TO_IN = 1 / 2.54; // 1CM = 1/2.54 IN 110 | export const PX_TO_IN = 1 / 96; // 1 PX = 1/96 IN 111 | export const IN_TO_PT = 72; // 1 IN = 72 PT 112 | export const PC_TO_PT = 12; // 1 PC = 12 PT 113 | 114 | /** 115 | * Get cosine in degrees of a 116 | * 117 | * Rounding errors are handled 118 | * @param a 119 | * @returns {number} 120 | */ 121 | export function cosine(a) { 122 | if (a === 0) return 1; 123 | if (a === 90) return 0; 124 | if (a === 180) return -1; 125 | if (a === 270) return 0; 126 | return Math.cos((a * Math.PI) / 180); 127 | } 128 | 129 | /** 130 | * Get sine in degrees of a 131 | * 132 | * Rounding errors are handled 133 | * @param a 134 | * @returns {number} 135 | */ 136 | export function sine(a) { 137 | if (a === 0) return 0; 138 | if (a === 90) return 1; 139 | if (a === 180) return 0; 140 | if (a === 270) return -1; 141 | return Math.sin((a * Math.PI) / 180); 142 | } 143 | -------------------------------------------------------------------------------- /lib/virtual-fs.js: -------------------------------------------------------------------------------- 1 | class VirtualFileSystem { 2 | constructor() { 3 | this.fileData = {}; 4 | } 5 | 6 | readFileSync(fileName, options = {}) { 7 | const encoding = typeof options === 'string' ? options : options.encoding; 8 | const virtualFileName = normalizeFilename(fileName); 9 | 10 | const data = this.fileData[virtualFileName]; 11 | if (data == null) { 12 | throw new Error( 13 | `File '${virtualFileName}' not found in virtual file system`, 14 | ); 15 | } 16 | 17 | if (encoding) { 18 | // return a string 19 | return typeof data === 'string' ? data : data.toString(encoding); 20 | } 21 | 22 | return Buffer.from(data, typeof data === 'string' ? 'base64' : undefined); 23 | } 24 | 25 | writeFileSync(fileName, content) { 26 | this.fileData[normalizeFilename(fileName)] = content; 27 | } 28 | 29 | bindFileData(data = {}, options = {}) { 30 | if (options.reset) { 31 | this.fileData = data; 32 | } else { 33 | Object.assign(this.fileData, data); 34 | } 35 | } 36 | } 37 | 38 | function normalizeFilename(fileName) { 39 | if (fileName.indexOf(__dirname) === 0) { 40 | fileName = fileName.substring(__dirname.length); 41 | } 42 | 43 | if (fileName.indexOf('/') === 0) { 44 | fileName = fileName.substring(1); 45 | } 46 | 47 | return fileName; 48 | } 49 | 50 | export default new VirtualFileSystem(); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdfkit", 3 | "description": "A PDF generation library for Node.js", 4 | "keywords": [ 5 | "pdf", 6 | "pdf writer", 7 | "pdf generator", 8 | "graphics", 9 | "document", 10 | "vector" 11 | ], 12 | "version": "0.17.1", 13 | "homepage": "http://pdfkit.org/", 14 | "author": { 15 | "name": "Devon Govett", 16 | "email": "devongovett@gmail.com", 17 | "url": "http://badassjs.com/" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/foliojs/pdfkit.git" 23 | }, 24 | "bugs": "https://github.com/foliojs/pdfkit/issues", 25 | "devDependencies": { 26 | "@babel/core": "^7.26.0", 27 | "@babel/plugin-external-helpers": "^7.25.9", 28 | "@babel/preset-env": "^7.26.0", 29 | "@eslint/js": "^9.17.0", 30 | "@rollup/plugin-babel": "^6.0.4", 31 | "babel-jest": "^29.7.0", 32 | "blob-stream": "^0.1.3", 33 | "brace": "^0.11.1", 34 | "brfs": "~2.0.2", 35 | "browserify": "^17.0.1", 36 | "canvas": "^3.1.0", 37 | "codemirror": "~5.65.18", 38 | "eslint": "^9.17.0", 39 | "gh-pages": "^6.2.0", 40 | "globals": "^15.14.0", 41 | "jest": "^29.7.0", 42 | "jest-environment-jsdom": "^29.7.0", 43 | "jest-image-snapshot": "^6.4.0", 44 | "markdown": "~0.5.0", 45 | "pdfjs-dist": "^2.14.305", 46 | "prettier": "3.4.2", 47 | "pug": "^3.0.3", 48 | "rollup": "^2.79.2", 49 | "rollup-plugin-copy": "^3.5.0" 50 | }, 51 | "dependencies": { 52 | "crypto-js": "^4.2.0", 53 | "fontkit": "^2.0.4", 54 | "jpeg-exif": "^1.1.4", 55 | "linebreak": "^1.1.0", 56 | "png-js": "^1.0.0" 57 | }, 58 | "scripts": { 59 | "prepublishOnly": "npm run build", 60 | "build": "rollup -c && npm run build-standalone", 61 | "build-standalone": "browserify --standalone PDFDocument --ignore crypto js/pdfkit.js > js/pdfkit.standalone.js", 62 | "browserify-example": "browserify examples/browserify/browser.js > examples/browserify/bundle.js", 63 | "pdf-guide": "node docs/generate.js", 64 | "website": "node docs/generate_website.js", 65 | "publish-website": "node docs/publish_website.js", 66 | "docs": "npm run pdf-guide && npm run website && npm run browserify-example", 67 | "lint": "eslint {lib,tests}/**/*.js", 68 | "prettier": "prettier lib tests docs", 69 | "test": "jest -i --env=node", 70 | "test:visual": "jest visual/ -i --env=node", 71 | "test:unit": "jest unit/ --env=node" 72 | }, 73 | "main": "js/pdfkit.js", 74 | "module": "js/pdfkit.es.js", 75 | "browserify": { 76 | "transform": [ 77 | "brfs" 78 | ] 79 | }, 80 | "engine": [ 81 | "node >= v18.0.0" 82 | ], 83 | "jest": { 84 | "testEnvironment": "jest-environment-jsdom", 85 | "testPathIgnorePatterns": [ 86 | "/node_modules/", 87 | "/examples/" 88 | ], 89 | "setupFilesAfterEnv": [ 90 | "/tests/unit/setupTests.js" 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | import { babel } from '@rollup/plugin-babel'; 3 | import copy from 'rollup-plugin-copy'; 4 | 5 | const external = [ 6 | 'stream', 7 | 'fs', 8 | 'zlib', 9 | 'fontkit', 10 | 'events', 11 | 'linebreak', 12 | 'png-js', 13 | 'crypto-js', 14 | 'saslprep', 15 | 'jpeg-exif' 16 | ]; 17 | 18 | const supportedBrowsers = [ 19 | 'Firefox 102', // ESR from 2022 20 | 'iOS 14', // from 2020 21 | 'Safari 14' // from 2020 22 | ]; 23 | 24 | export default [ 25 | // CommonJS build for Node 26 | { 27 | input: 'lib/document.js', 28 | external, 29 | output: { 30 | name: 'pdfkit', 31 | file: pkg.main, 32 | format: 'cjs', 33 | sourcemap: true, 34 | interop: false 35 | }, 36 | plugins: [ 37 | babel({ 38 | babelHelpers: 'bundled', 39 | babelrc: false, 40 | presets: [ 41 | [ 42 | '@babel/preset-env', 43 | { 44 | modules: false, 45 | targets: { 46 | node: '18' 47 | } 48 | } 49 | ] 50 | ], 51 | comments: false 52 | }), 53 | copy({ 54 | targets: [ 55 | { src: ['lib/font/data/*.afm', 'lib/mixins/data/*.icc'], dest: 'js/data' }, 56 | ] 57 | }) 58 | ] 59 | }, 60 | // ES for green browsers 61 | { 62 | input: 'lib/document.js', 63 | external, 64 | output: { 65 | name: 'pdfkit.es', 66 | file: pkg.module, 67 | format: 'es', 68 | sourcemap: true 69 | }, 70 | plugins: [ 71 | babel({ 72 | babelHelpers: 'bundled', 73 | babelrc: false, 74 | presets: [ 75 | [ 76 | '@babel/preset-env', 77 | { 78 | modules: false, 79 | targets: { 80 | browsers: supportedBrowsers 81 | } 82 | } 83 | ] 84 | ], 85 | comments: false 86 | }) 87 | ] 88 | }, 89 | { 90 | input: 'lib/virtual-fs.js', 91 | external, 92 | output: { 93 | name: 'virtual-fs', 94 | file: 'js/virtual-fs.js', 95 | format: 'cjs', 96 | sourcemap: false 97 | }, 98 | plugins: [ 99 | babel({ 100 | babelHelpers: 'bundled', 101 | babelrc: false, 102 | presets: [ 103 | [ 104 | '@babel/preset-env', 105 | { 106 | loose: true, 107 | modules: false, 108 | targets: { 109 | browsers: supportedBrowsers 110 | } 111 | } 112 | ] 113 | ] 114 | }) 115 | ] 116 | } 117 | ]; 118 | -------------------------------------------------------------------------------- /tests/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /tests/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /tests/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /tests/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /tests/images/bee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/bee.jpg -------------------------------------------------------------------------------- /tests/images/bee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/bee.png -------------------------------------------------------------------------------- /tests/images/fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/fish.png -------------------------------------------------------------------------------- /tests/images/glassware-noisy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/glassware-noisy.png -------------------------------------------------------------------------------- /tests/images/interlaced-grayscale-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/interlaced-grayscale-8bit.png -------------------------------------------------------------------------------- /tests/images/interlaced-pallete-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/interlaced-pallete-8bit.png -------------------------------------------------------------------------------- /tests/images/interlaced-rgb-16bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/interlaced-rgb-16bit.png -------------------------------------------------------------------------------- /tests/images/interlaced-rgb-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/interlaced-rgb-8bit.png -------------------------------------------------------------------------------- /tests/images/interlaced-rgb-alpha-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/interlaced-rgb-alpha-8bit.png -------------------------------------------------------------------------------- /tests/images/orientation-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-1.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-2.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-3.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-4.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-5.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-6.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-7.jpeg -------------------------------------------------------------------------------- /tests/images/orientation-8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/orientation-8.jpeg -------------------------------------------------------------------------------- /tests/images/pngsuite-gray-transparent-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/pngsuite-gray-transparent-black.png -------------------------------------------------------------------------------- /tests/images/pngsuite-gray-transparent-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/pngsuite-gray-transparent-white.png -------------------------------------------------------------------------------- /tests/images/pngsuite-palette-transparent-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/pngsuite-palette-transparent-white.png -------------------------------------------------------------------------------- /tests/images/pngsuite-rgb-transparent-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/pngsuite-rgb-transparent-white.png -------------------------------------------------------------------------------- /tests/images/sampleImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/sampleImage.jpg -------------------------------------------------------------------------------- /tests/images/straight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/images/straight.png -------------------------------------------------------------------------------- /tests/unit/annotations.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import PDFSecurity from '../../lib/security'; 3 | import { logData } from './helpers'; 4 | 5 | // manual mock for PDFSecurity to ensure stored id will be the same accross different systems 6 | PDFSecurity.generateFileID = () => { 7 | return Buffer.from('mocked-pdf-id'); 8 | }; 9 | 10 | describe('Annotations', () => { 11 | let document; 12 | 13 | beforeEach(() => { 14 | document = new PDFDocument({ 15 | info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, 16 | }); 17 | }); 18 | 19 | describe('link', () => { 20 | test('using page index', () => { 21 | document.addPage(); 22 | 23 | const docData = logData(document); 24 | 25 | document.text('Go To First Page', { link: 0 }); 26 | 27 | expect(docData).toContainChunk([ 28 | `11 0 obj`, 29 | `<< 30 | /S /GoTo 31 | /D [7 0 R /XYZ null null null] 32 | >>`, 33 | ]); 34 | }); 35 | 36 | test('using url', () => { 37 | document.addPage(); 38 | 39 | const docData = logData(document); 40 | 41 | document.text('Go to url', { link: 'http://www.example.com' }); 42 | 43 | expect(docData).toContainChunk([ 44 | `11 0 obj`, 45 | `<< 46 | /S /URI 47 | /URI (http://www.example.com) 48 | >>`, 49 | ]); 50 | }); 51 | 52 | test('using url on continue', () => { 53 | document.addPage(); 54 | 55 | const docData = logData(document); 56 | 57 | document.text('Go to url', { 58 | link: 'http://www.example.com', 59 | continued: true, 60 | }); 61 | document.text('continued link'); 62 | 63 | expect(docData).toContainChunk([ 64 | `11 0 obj`, 65 | `<< 66 | /S /URI 67 | /URI (http://www.example.com) 68 | >>`, 69 | ]); 70 | 71 | expect(docData).toContainChunk([ 72 | `14 0 obj`, 73 | `<< 74 | /S /URI 75 | /URI (http://www.example.com) 76 | >>`, 77 | ]); 78 | }); 79 | 80 | test('using url on continue', () => { 81 | document.addPage(); 82 | 83 | const docData = logData(document); 84 | 85 | document.text('Go to url', { 86 | link: 'http://www.example.com', 87 | continued: true, 88 | }); 89 | document.text('no continued link', { link: null }); 90 | 91 | // console.log(docData); 92 | expect(docData).toContainChunk([ 93 | `11 0 obj`, 94 | `<< 95 | /S /URI 96 | /URI (http://www.example.com) 97 | >>`, 98 | ]); 99 | 100 | expect(docData).not.toContainChunk([`14 0 obj`]); 101 | }); 102 | }); 103 | 104 | describe('fileAnnotation', () => { 105 | test('creating a fileAnnotation', () => { 106 | const docData = logData(document); 107 | 108 | document.fileAnnotation(100, 100, 20, 20, { 109 | src: Buffer.from('example text'), 110 | name: 'file.txt', 111 | }); 112 | 113 | expect(docData).toContainChunk([ 114 | `10 0 obj`, 115 | `<< 116 | /Subtype /FileAttachment 117 | /FS 9 0 R 118 | /Type /Annot 119 | /Rect [100 672 120 692] 120 | /Border [0 0 0] 121 | /C [0 0 0] 122 | >>`, 123 | ]); 124 | }); 125 | 126 | test("using the file's description", () => { 127 | const docData = logData(document); 128 | 129 | document.fileAnnotation(100, 100, 20, 20, { 130 | src: Buffer.from('example text'), 131 | name: 'file.txt', 132 | description: 'file description', 133 | }); 134 | 135 | expect(docData).toContainChunk([ 136 | `10 0 obj`, 137 | `<< 138 | /Subtype /FileAttachment 139 | /FS 9 0 R 140 | /Contents (file description) 141 | /Type /Annot 142 | /Rect [100 672 120 692] 143 | /Border [0 0 0] 144 | /C [0 0 0] 145 | >>`, 146 | ]); 147 | }); 148 | 149 | test("overriding the file's description", () => { 150 | const docData = logData(document); 151 | 152 | document.fileAnnotation( 153 | 100, 154 | 100, 155 | 20, 156 | 20, 157 | { 158 | src: Buffer.from('example text'), 159 | name: 'file.txt', 160 | description: 'file description', 161 | }, 162 | { 163 | Contents: 'other description', 164 | }, 165 | ); 166 | 167 | expect(docData).toContainChunk([ 168 | `10 0 obj`, 169 | `<< 170 | /Contents (other description) 171 | /Subtype /FileAttachment 172 | /FS 9 0 R 173 | /Type /Annot 174 | /Rect [100 672 120 692] 175 | /Border [0 0 0] 176 | /C [0 0 0] 177 | >>`, 178 | ]); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /tests/unit/color.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData } from './helpers'; 3 | 4 | describe('color', function () { 5 | test('normalize', function () { 6 | const doc = new PDFDocument(); 7 | 8 | expect(doc._normalizeColor('#FFF')).toEqual([1, 1, 1]); 9 | expect(doc._normalizeColor('#FFFFFF')).toEqual([1, 1, 1]); 10 | expect(doc._normalizeColor('#000')).toEqual([0, 0, 0]); 11 | expect(doc._normalizeColor('#000000')).toEqual([0, 0, 0]); 12 | expect(doc._normalizeColor('#6F6FEF')).toEqual([ 13 | 0.43529411764705883, 0.43529411764705883, 0.9372549019607843, 14 | ]); 15 | 16 | expect(doc._normalizeColor([255, 255, 255])).toEqual([1, 1, 1]); 17 | expect(doc._normalizeColor([255, 255, 255, 255])).toEqual([ 18 | 2.55, 2.55, 2.55, 2.55, 19 | ]); 20 | expect(doc._normalizeColor([0, 0, 0])).toEqual([0, 0, 0]); 21 | expect(doc._normalizeColor([0, 0, 0, 0])).toEqual([0, 0, 0, 0]); 22 | expect(doc._normalizeColor([128, 10, 18])).toEqual([ 23 | 0.5019607843137255, 0.0392156862745098, 0.07058823529411765, 24 | ]); 25 | expect(doc._normalizeColor([128, 10, 18, 100])).toEqual([ 26 | 1.28, 0.1, 0.18, 1, 27 | ]); 28 | }); 29 | 30 | test('normalize with spot color', function () { 31 | const doc = new PDFDocument(); 32 | doc.addSpotColor('PANTONE 123 C', 0.1, 0.2, 0.3, 0.4); 33 | 34 | const color = doc._normalizeColor('PANTONE 123 C'); 35 | expect(color.id).toEqual('CS0'); 36 | expect(color.values).toEqual([0.1, 0.2, 0.3, 0.4]); 37 | }); 38 | 39 | test('spot color', function () { 40 | const doc = new PDFDocument(); 41 | const data = logData(doc); 42 | doc.addSpotColor('PANTONE185C', 0, 100, 78, 9); 43 | doc.fillColor('PANTONE185C').text('This text uses spot color!'); 44 | doc.end(); 45 | 46 | expect(data).toContainChunk([ 47 | `6 0 obj`, 48 | '<<\n' + 49 | '/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n' + 50 | '/ColorSpace <<\n' + 51 | '/CS0 8 0 R\n' + 52 | '>>\n' + 53 | '/Font <<\n' + 54 | '/F1 9 0 R\n' + 55 | '>>\n' + 56 | '>>', 57 | ]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/unit/document.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData } from './helpers'; 3 | 4 | describe('PDFDocument', () => { 5 | describe('font option', () => { 6 | let fontSpy; 7 | 8 | beforeEach(() => { 9 | fontSpy = jest.spyOn(PDFDocument.prototype, 'font').mockReturnThis(); 10 | }); 11 | 12 | afterEach(() => { 13 | fontSpy.mockRestore(); 14 | }); 15 | 16 | test('not defined', () => { 17 | new PDFDocument(); 18 | 19 | expect(fontSpy).toBeCalledWith('Helvetica', null); 20 | }); 21 | 22 | test('a string value', () => { 23 | new PDFDocument({ font: 'Roboto' }); 24 | 25 | expect(fontSpy).toBeCalledWith('Roboto', null); 26 | }); 27 | 28 | test('a falsy value', () => { 29 | new PDFDocument({ font: null }); 30 | new PDFDocument({ font: false }); 31 | new PDFDocument({ font: '' }); 32 | 33 | expect(fontSpy).not.toBeCalled(); 34 | }); 35 | }); 36 | 37 | describe('document info', () => { 38 | test('accepts properties with value undefined', () => { 39 | expect(() => new PDFDocument({ info: { Title: undefined } })).not.toThrow( 40 | new TypeError("Cannot read property 'toString' of undefined"), 41 | ); 42 | }); 43 | 44 | test('accepts properties with value null', () => { 45 | expect(() => new PDFDocument({ info: { Title: null } })).not.toThrow( 46 | new TypeError("Cannot read property 'toString' of null"), 47 | ); 48 | }); 49 | }); 50 | 51 | test('metadata is present for PDF 1.4', () => { 52 | let doc = new PDFDocument({ pdfVersion: '1.4' }); 53 | const data = logData(doc); 54 | doc.end(); 55 | 56 | let catalog = data[data.length - 28]; 57 | 58 | expect(catalog).toContain('/Metadata'); 59 | }); 60 | 61 | test('metadata is NOT present for PDF 1.3', () => { 62 | let doc = new PDFDocument({ pdfVersion: '1.3' }); 63 | const data = logData(doc); 64 | doc.end(); 65 | 66 | let catalog = data[data.length - 27]; 67 | 68 | expect(catalog).not.toContain('/Metadata'); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/unit/font.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import PDFFontFactory from '../../lib/font_factory'; 3 | import { logData } from './helpers'; 4 | 5 | describe('EmbeddedFont', () => { 6 | test('no fontLayoutCache option', () => { 7 | const document = new PDFDocument(); 8 | const font = PDFFontFactory.open( 9 | document, 10 | 'tests/fonts/Roboto-Regular.ttf', 11 | ); 12 | const runSpy = jest.spyOn(font, 'layoutRun'); 13 | 14 | font.layout('test'); 15 | font.layout('test'); 16 | font.layout('test'); 17 | font.layout('test'); 18 | 19 | expect(runSpy).toBeCalledTimes(1); 20 | }); 21 | 22 | test('fontLayoutCache = false', () => { 23 | const document = new PDFDocument({ fontLayoutCache: false }); 24 | const font = PDFFontFactory.open( 25 | document, 26 | 'tests/fonts/Roboto-Regular.ttf', 27 | ); 28 | const runSpy = jest.spyOn(font, 'layoutRun'); 29 | 30 | font.layout('test'); 31 | font.layout('test'); 32 | font.layout('test'); 33 | font.layout('test'); 34 | 35 | expect(runSpy).toBeCalledTimes(4); 36 | }); 37 | 38 | describe('emebed', () => { 39 | test('sets BaseName based on font id and postscript name', () => { 40 | const document = new PDFDocument(); 41 | const font = PDFFontFactory.open( 42 | document, 43 | 'tests/fonts/Roboto-Regular.ttf', 44 | undefined, 45 | 'F1099', 46 | ); 47 | const dictionary = { 48 | end: () => {}, 49 | }; 50 | font.dictionary = dictionary; 51 | font.embed(); 52 | 53 | expect(dictionary.data.BaseFont).toBe('BAJJZZ+Roboto-Regular'); 54 | }); 55 | }); 56 | 57 | describe('toUnicodeMap', () => { 58 | test('bfrange lines should not cross highcode boundary', () => { 59 | const doc = new PDFDocument({ compress: false }); 60 | const font = PDFFontFactory.open( 61 | doc, 62 | 'tests/fonts/Roboto-Regular.ttf', 63 | undefined, 64 | 'F1099', 65 | ); 66 | 67 | // 398 different glyphs 68 | font.encode('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 69 | font.encode('abcdefghijklmnopqrstuvwxyz'); 70 | font.encode('ÁÀÂÄÅÃÆÇÐÉÈÊËÍÌÎÏÑÓÒÔÖÕØŒÞÚÙÛÜÝŸ'); 71 | font.encode('áàâäãåæçðéèêëíìîïıñóòôöõøœßþúùûüýÿ'); 72 | font.encode('ĀĂĄĆČĎĐĒĖĘĚĞĢĪĮİĶŁĹĻĽŃŅŇŌŐŔŖŘŠŚŞȘŢȚŤŪŮŰŲŽŹŻ'); 73 | font.encode('āăąćčďđēėęěğģīįķłĺļľńņňōőŕŗřšśşșţțťūůűųžźż'); 74 | font.encode('ΑΒΓ∆ΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΆΈΉΊΌΎΏΪΫ'); 75 | font.encode('αβγδεζηθικλµνξοπρςστυφχψωάέήίόύώϊϋΐΰ'); 76 | font.encode('АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'); 77 | font.encode('абвгдежзийклмнопрстуфхцчшщъыьэюя'); 78 | font.encode('ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏҐӁҒҖҚҢҮҰҲҶҺӘӢӨӮ'); 79 | font.encode('ѐёђѓєѕіїјљњћќѝўџґӂғҗқңүұҳҷһәӣөӯ'); 80 | 81 | const docData = logData(doc); 82 | font.toUnicodeCmap(); 83 | const text = docData.map((d) => d.toString('utf8')).join(''); 84 | 85 | let glyphs = 0; 86 | for (const block of text.matchAll( 87 | /beginbfrange\n((?:.|\n)*?)\nendbfrange/g, 88 | )) { 89 | for (const line of block[1].matchAll( 90 | /^<([0-9a-f]+)>\s+<([0-9a-f]+)>\s+\[/gim, 91 | )) { 92 | const low = parseInt(line[1], 16); 93 | const high = parseInt(line[2], 16); 94 | glyphs += high - low + 1; 95 | expect(high & 0xffffff00).toBe(low & 0xffffff00); 96 | } 97 | } 98 | 99 | expect(glyphs).toBe(398 + 1); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('sizeToPoint', () => { 105 | let doc; 106 | beforeEach(() => { 107 | doc = new PDFDocument({ 108 | font: 'Helvetica', 109 | fontSize: 12, 110 | size: [250, 500], 111 | margin: { top: 10, right: 5, bottom: 10, left: 5 }, 112 | }); 113 | }); 114 | 115 | test.each([ 116 | [1, 1], 117 | ['1', 1], 118 | [true, 1], 119 | [false, 0], 120 | ['1em', 12], 121 | ['1in', 72], 122 | ['1px', 0.75], 123 | ['1cm', 28.3465], 124 | ['1mm', 2.8346], 125 | ['1pc', 12], 126 | ['1ex', 11.1], 127 | ['1ch', 6.672], 128 | ['1vw', 2.5], 129 | ['1vh', 5], 130 | ['1vmin', 2.5], 131 | ['1vmax', 5], 132 | ['1%', 0.12], 133 | ['1pt', 1], 134 | ])('%o -> %s', (size, expected) => { 135 | expect(doc.sizeToPoint(size)).toBeCloseTo(expected, 4); 136 | }); 137 | 138 | test('1rem -> 12', () => { 139 | doc.fontSize(15); 140 | expect(doc.sizeToPoint('1em')).toEqual(15); 141 | expect(doc.sizeToPoint('1rem')).toEqual(12); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /tests/unit/gradient.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData } from './helpers'; 3 | 4 | describe('Gradient', function () { 5 | let document; 6 | 7 | beforeEach(() => { 8 | document = new PDFDocument({ 9 | info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, 10 | }); 11 | }); 12 | 13 | test('Multiple stops', () => { 14 | const docData = logData(document); 15 | const gradient = document.linearGradient(0, 0, 300, 0); 16 | gradient.stop(0, 'green').stop(0.5, 'red').stop(1, 'green'); 17 | document.rect(0, 0, 300, 300).fill(gradient); 18 | document.end(); 19 | 20 | expect(docData).toContainChunk([ 21 | '8 0 obj', 22 | `<< 23 | /FunctionType 2 24 | /Domain [0 1] 25 | /C0 [0 0.501961 0] 26 | /C1 [1 0 0] 27 | /N 1 28 | >>`, 29 | ]); 30 | expect(docData).toContainChunk([ 31 | '9 0 obj', 32 | `<< 33 | /FunctionType 2 34 | /Domain [0 1] 35 | /C0 [1 0 0] 36 | /C1 [0 0.501961 0] 37 | /N 1 38 | >>`, 39 | ]); 40 | 41 | expect(docData).toContainChunk([ 42 | '10 0 obj', 43 | `<< 44 | /FunctionType 3 45 | /Domain [0 1] 46 | /Functions [8 0 R 9 0 R] 47 | /Bounds [0.5] 48 | /Encode [0 1 0 1] 49 | >>`, 50 | ]); 51 | 52 | expect(docData).toContainChunk([ 53 | '11 0 obj', 54 | `<< 55 | /ShadingType 2 56 | /ColorSpace /DeviceRGB 57 | /Coords [0 0 300 0] 58 | /Function 10 0 R 59 | /Extend [true true] 60 | >>`, 61 | ]); 62 | 63 | expect(docData).toContainChunk([ 64 | '12 0 obj', 65 | `<< 66 | /Type /Pattern 67 | /PatternType 2 68 | /Shading 11 0 R 69 | /Matrix [1 0 0 -1 0 792] 70 | >>`, 71 | ]); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/unit/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import PDFDocument from '../../lib/document'; 3 | */ 4 | 5 | /** 6 | * @typedef {object} TextStream 7 | * @property {string} text 8 | * @property {string} font 9 | * @property {number} fontSize 10 | * 11 | * @typedef {string | Buffer} PDFDataItem 12 | * @typedef {Array} PDFData 13 | * 14 | * @typedef {object} PDFDataObject 15 | * @property {PDFDataItem[]} items 16 | */ 17 | 18 | /** 19 | * @param {PDFDocument} doc 20 | * @return {PDFData} 21 | */ 22 | function logData(doc) { 23 | const loggedData = []; 24 | const originalMethod = doc._write; 25 | doc._write = function (data) { 26 | loggedData.push(data); 27 | originalMethod.call(this, data); 28 | }; 29 | return loggedData; 30 | } 31 | 32 | function escapeRegExp(string) { 33 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 34 | } 35 | 36 | function joinTokens(...args) { 37 | let a = args.map((i) => escapeRegExp(i)); 38 | let r = new RegExp('^' + a.join('\\s*') + '$'); 39 | return r; 40 | } 41 | 42 | /** 43 | * @description 44 | * Returns an array of objects from the PDF data. Object items are surrounded by /\d 0 obj/ and 'endobj'. 45 | * @param {PDFData} data 46 | * @return {Array} 47 | */ 48 | function getObjects(data) { 49 | const objects = []; 50 | let currentObject = null; 51 | for (const item of data) { 52 | if (item instanceof Buffer) { 53 | if (currentObject) { 54 | currentObject.items.push(item); 55 | } 56 | } else if (typeof item === 'string') { 57 | if (/^\d+\s0\sobj/.test(item)) { 58 | currentObject = { items: [] }; 59 | objects.push(currentObject); 60 | } else if (item === 'endobj') { 61 | currentObject = null; 62 | } else if (currentObject) { 63 | currentObject.items.push(item); 64 | } 65 | } 66 | } 67 | return objects; 68 | } 69 | 70 | /** 71 | * @param {Buffer} textStream 72 | * @return {TextStream | undefined} 73 | */ 74 | function parseTextStream(textStream) { 75 | const decodedStream = textStream.toString('utf8'); 76 | 77 | // Extract font and font size 78 | const fontMatch = decodedStream.match(/\/([A-Za-z0-9]+)\s+(\d+)\s+Tf/); 79 | 80 | if (!fontMatch) { 81 | return undefined; 82 | } 83 | 84 | const font = fontMatch[1]; 85 | const fontSize = parseInt(fontMatch[2], 10); 86 | 87 | // Extract hex strings inside TJ array 88 | const tjMatch = decodedStream.match(/\[([^\]]+)\]\s+TJ/); 89 | if (!tjMatch) { 90 | return undefined; 91 | } 92 | let text = ''; 93 | 94 | // this is a simplified version 95 | // the correct way is to retrieve the encoding from /Resources /Font dictionary and decode using it 96 | // https://stackoverflow.com/a/29468049/5724645 97 | 98 | // Match all hex strings like <...> 99 | const hexMatches = [...tjMatch[1].matchAll(/<([0-9a-fA-F]+)>/g)]; 100 | for (const m of hexMatches) { 101 | // Convert hex to string 102 | const hex = m[1]; 103 | for (let i = 0; i < hex.length; i += 2) { 104 | const code = parseInt(hex.substr(i, 2), 16); 105 | let char = String.fromCharCode(code); 106 | // Handle special cases 107 | if (code === 0x0a) { 108 | char = '\n'; // Newline 109 | } else if (code === 0x0d) { 110 | char = '\r'; // Carriage return 111 | } else if (code === 0x85) { 112 | char = '...'; 113 | } 114 | text += char; 115 | } 116 | } 117 | 118 | return { text, font, fontSize }; 119 | } 120 | 121 | export { logData, joinTokens, parseTextStream, getObjects }; 122 | -------------------------------------------------------------------------------- /tests/unit/image.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | 3 | describe('Image', function () { 4 | /** 5 | * @type {PDFDocument} 6 | */ 7 | let document; 8 | 9 | beforeEach(() => { 10 | document = new PDFDocument({ 11 | info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, 12 | }); 13 | }); 14 | 15 | test('y position should be updated', () => { 16 | const originalY = document.y; 17 | const imageHeight = 400; 18 | document.image('./tests/images/bee.png'); 19 | expect(document.y).toBe(originalY + imageHeight); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/line_wrapper.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import LineWrapper from '../../lib/line_wrapper'; 3 | 4 | describe('LineWrapper', () => { 5 | let document; 6 | 7 | beforeEach(() => { 8 | document = new PDFDocument({ 9 | compress: false, 10 | margin: 0, 11 | }); 12 | }); 13 | 14 | test('ellipsis is present only on last line of multiline text', () => { 15 | // There is a weird edge case where ellipsis occurs on lines 16 | // in the middle of text due to number rounding errors 17 | // 18 | // There is probably a simpler combination of values but this is one I found in the wild 19 | document.y = 402.1999999999999; 20 | document.fontSize(7.26643598615917); 21 | const wrapper = new LineWrapper(document, { 22 | width: 300, 23 | height: 50.399999999999864, 24 | ellipsis: true, 25 | }); 26 | let wrapperOutput = ''; 27 | wrapper.on('line', (buffer) => { 28 | wrapperOutput += buffer; 29 | document.y += document.currentLineHeight(true); 30 | }); 31 | wrapper.wrap('- A\n- B\n- C\n- D\n- E\n- F', {}); 32 | expect(wrapperOutput).toBe('- A\n- B\n- C\n- D\n- E\n- F'); 33 | }); 34 | 35 | test('line break is handled correctly when at weird heights', () => { 36 | // There is probably a simpler combination of values but this is one I found in the wild 37 | document.y = 1 / 3; 38 | document.fontSize(Math.fround(42.3 / 3)); 39 | let lineHeight = document.currentLineHeight(true); 40 | const wrapper = new LineWrapper(document, { 41 | width: 300, 42 | height: lineHeight * 3, 43 | }); 44 | let wrapperOutput = ''; 45 | wrapper.on('line', (buffer) => { 46 | wrapperOutput += buffer; 47 | document.y += lineHeight; 48 | }); 49 | // Limit to 3/4 lines 50 | wrapper.wrap('A\nB\nC\nD', {}); 51 | expect(wrapperOutput).toBe('A\nB\nC\n'); 52 | }); 53 | 54 | test('line break is handled correctly with ellipsis', () => { 55 | // There is probably a simpler combination of values but this is one I found in the wild 56 | document.y = 1 / 3; 57 | document.fontSize(Math.fround(42.3 / 3)); 58 | let lineHeight = document.currentLineHeight(true); 59 | const wrapper = new LineWrapper(document, { 60 | width: 300, 61 | height: lineHeight * 3, 62 | ellipsis: true, 63 | }); 64 | let wrapperOutput = ''; 65 | wrapper.on('line', (buffer) => { 66 | wrapperOutput += buffer; 67 | document.y += lineHeight; 68 | }); 69 | // Limit to 3/4 lines 70 | wrapper.wrap('A\nB\nC\nD', {}); 71 | expect(wrapperOutput).toBe('A\nB\nC…'); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/unit/metadata.spec.js: -------------------------------------------------------------------------------- 1 | import PDFMetadata from '../../lib/metadata'; 2 | 3 | describe('PDFMetadata', () => { 4 | let metadata; 5 | beforeEach(() => { 6 | metadata = new PDFMetadata(); 7 | }); 8 | 9 | test('initialising metadata', () => { 10 | expect(metadata._metadata).toBeDefined(); 11 | expect(metadata.getLength()).toBeGreaterThan(0); 12 | expect(typeof metadata._metadata).toBe('string'); 13 | }); 14 | 15 | test('contains appended XML', () => { 16 | let xml = ` 17 | 18 | 19 | Test 20 | 21 | 22 | `; 23 | metadata.append(xml); 24 | expect(metadata.getXML()).toContain(xml); 25 | }); 26 | 27 | test('closing tags', () => { 28 | let length = metadata.getLength(); 29 | metadata.end(); 30 | expect(metadata.getLength()).toBeGreaterThan(length); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/unit/object.spec.js: -------------------------------------------------------------------------------- 1 | import PDFObject from '../../lib/object'; 2 | 3 | describe('PDFObject', () => { 4 | describe('convert', () => { 5 | test('string literal', () => { 6 | expect(PDFObject.convert('test')).toEqual('/test'); 7 | }); 8 | 9 | test('string literal with unicode', () => { 10 | expect(PDFObject.convert('αβγδ')).toEqual('/αβγδ'); 11 | }); 12 | 13 | test('String object', () => { 14 | expect(PDFObject.convert(new String('test'))).toEqual('(test)'); 15 | }); 16 | 17 | test('String object with unicode', () => { 18 | const result = PDFObject.convert(new String('αβγδ')); 19 | expect(result.length).toEqual(12); 20 | expect(result).toMatchInlineSnapshot(`"(þÿ±²³´)"`); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/unit/page.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | 3 | describe('page', function () { 4 | test('cascade page options', function () { 5 | const doc = new PDFDocument({ 6 | autoFirstPage: false, 7 | bufferPages: true, 8 | }); 9 | doc.addPage({ size: [50, 50], margin: 0 }); 10 | doc.text(Array(10).fill('TEST').join('\n')); 11 | doc._pageBuffer.forEach((page) => { 12 | expect(page.size).toEqual([50, 50]); 13 | expect(page.margins).toEqual({ top: 0, right: 0, bottom: 0, left: 0 }); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/pattern.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData } from './helpers'; 3 | 4 | describe('Pattern', function () { 5 | let document; 6 | 7 | beforeEach(() => { 8 | document = new PDFDocument({ 9 | info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, 10 | compress: false, 11 | }); 12 | }); 13 | 14 | test('Uncolored tiling pattern', () => { 15 | const docData = logData(document); 16 | const patternStream = '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s'; 17 | const binaryStream = Buffer.from(`${patternStream}\n`, 'binary'); 18 | const pattern = document.pattern([1, 1, 4, 4], 3, 3, patternStream); 19 | document.rect(0, 0, 100, 100).fill([pattern, 'blue']).end(); 20 | 21 | // empty resources 22 | expect(docData).toContainChunk(['10 0 obj', `<<\n>>`]); 23 | 24 | // pattern dictionary 25 | expect(docData).toContainChunk([ 26 | '11 0 obj', 27 | `<< 28 | /Type /Pattern 29 | /PatternType 1 30 | /PaintType 2 31 | /TilingType 2 32 | /BBox [1 1 4 4] 33 | /XStep 3 34 | /YStep 3 35 | /Matrix [1 0 0 -1 0 792] 36 | /Resources 10 0 R 37 | /Length 32 38 | >>`, 39 | 'stream', 40 | binaryStream, 41 | '\nendstream', 42 | ]); 43 | 44 | // page resource dictionary with color space and pattern 45 | expect(docData).toContainChunk([ 46 | '6 0 obj', 47 | `<< 48 | /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] 49 | /ColorSpace << 50 | /CsPDeviceCMYK 8 0 R 51 | /CsPDeviceRGB 9 0 R 52 | >> 53 | /Pattern << 54 | /P1 11 0 R 55 | >> 56 | >>`, 57 | ]); 58 | // map to the underlying color space 59 | expect(docData).toContainChunk(['8 0 obj', `[/Pattern /DeviceCMYK]`]); 60 | expect(docData).toContainChunk(['9 0 obj', `[/Pattern /DeviceRGB]`]); 61 | // graphics 62 | const graphicsStream = Buffer.from( 63 | `1 0 0 -1 0 792 cm 64 | 0 0 100 100 re 65 | /CsPDeviceRGB cs 66 | 0 0 1 /P1 scn 67 | f\n`, 68 | 'binary', 69 | ); 70 | expect(docData).toContainChunk([ 71 | '5 0 obj', 72 | `<< 73 | /Length 66 74 | >>`, 75 | 'stream', 76 | graphicsStream, 77 | '\nendstream', 78 | ]); 79 | }); 80 | 81 | test('Pattern naming', () => { 82 | const docData = logData(document); 83 | const pattern1 = document.pattern( 84 | [1, 1, 4, 4], 85 | 3, 86 | 3, 87 | '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s', 88 | ); 89 | const pattern2 = document.pattern( 90 | [1, 1, 7, 7], 91 | 6, 92 | 6, 93 | '1 w 0 1 m 7 8 l s 5 0 m 8 3 l s', 94 | ); 95 | document.rect(0, 0, 100, 100).fill([pattern1, 'blue']); 96 | document.rect(0, 0, 100, 100).fill([pattern2, 'red']).end(); 97 | 98 | // patterns P1 and P2 99 | expect(docData).toContainChunk([ 100 | '6 0 obj', 101 | `<< 102 | /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] 103 | /ColorSpace << 104 | /CsPDeviceCMYK 8 0 R 105 | /CsPDeviceRGB 9 0 R 106 | >> 107 | /Pattern << 108 | /P1 11 0 R 109 | /P2 13 0 R 110 | >> 111 | >>`, 112 | ]); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/unit/pdfa1.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData, joinTokens } from './helpers'; 3 | 4 | describe('PDF/A-1', () => { 5 | test('metadata is present', () => { 6 | let options = { 7 | autoFirstPage: false, 8 | pdfVersion: '1.4', 9 | subset: 'PDF/A-1', 10 | }; 11 | let doc = new PDFDocument(options); 12 | const data = logData(doc); 13 | doc.end(); 14 | expect(data).toContainChunk([ 15 | `11 0 obj`, 16 | `<<\n/length 892\n/Type /Metadata\n/Subtype /XML\n/Length 894\n>>`, 17 | ]); 18 | }); 19 | 20 | test('color profile is present', () => { 21 | const expected = [ 22 | `10 0 obj`, 23 | joinTokens( 24 | '<<', 25 | '/Type /OutputIntent', 26 | '/S /GTS_PDFA1', 27 | '/Info (sRGB IEC61966-2.1)', 28 | '/OutputConditionIdentifier (sRGB IEC61966-2.1)', 29 | '/DestOutputProfile 9 0 R', 30 | '>>', 31 | ), 32 | ]; 33 | let options = { 34 | autoFirstPage: false, 35 | pdfVersion: '1.4', 36 | subset: 'PDF/A-1', 37 | }; 38 | let doc = new PDFDocument(options); 39 | const data = logData(doc); 40 | doc.end(); 41 | expect(data).toContainChunk(expected); 42 | }); 43 | 44 | test('metadata contains pdfaid part and conformance', () => { 45 | let options = { 46 | autoFirstPage: false, 47 | pdfVersion: '1.4', 48 | subset: 'PDF/A-1', 49 | }; 50 | let doc = new PDFDocument(options); 51 | const data = logData(doc); 52 | doc.end(); 53 | let metadata = Buffer.from(data[27]).toString(); 54 | 55 | expect(metadata).toContain('pdfaid:part>1'); 56 | expect(metadata).toContain('pdfaid:conformance'); 57 | }); 58 | 59 | test('metadata pdfaid conformance B', () => { 60 | let options = { 61 | autoFirstPage: false, 62 | pdfVersion: '1.4', 63 | subset: 'PDF/A-1b', 64 | }; 65 | let doc = new PDFDocument(options); 66 | const data = logData(doc); 67 | doc.end(); 68 | let metadata = Buffer.from(data[27]).toString(); 69 | 70 | expect(metadata).toContain('pdfaid:conformance>B'); 71 | }); 72 | 73 | test('metadata pdfaid conformance A', () => { 74 | let options = { 75 | autoFirstPage: false, 76 | pdfVersion: '1.4', 77 | subset: 'PDF/A-1a', 78 | }; 79 | let doc = new PDFDocument(options); 80 | const data = logData(doc); 81 | doc.end(); 82 | let metadata = Buffer.from(data[27]).toString(); 83 | 84 | expect(metadata).toContain('pdfaid:conformance>A'); 85 | }); 86 | 87 | test('font data contains CIDSet', () => { 88 | let options = { 89 | autoFirstPage: false, 90 | pdfVersion: '1.4', 91 | subset: 'PDF/A-1a', 92 | }; 93 | let doc = new PDFDocument(options); 94 | const data = logData(doc); 95 | doc.addPage(); 96 | doc.registerFont('Roboto', 'tests/fonts/Roboto-Regular.ttf'); 97 | doc.font('Roboto'); 98 | doc.text('Text'); 99 | doc.end(); 100 | 101 | let fontDescriptor = data.find((v) => { 102 | return v.includes('/Type /FontDescriptor'); 103 | }); 104 | 105 | expect(fontDescriptor).not.toBeUndefined(); 106 | 107 | expect(fontDescriptor).toContain('/CIDSet'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/unit/pdfa2.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData, joinTokens } from './helpers'; 3 | 4 | describe('PDF/A-2', () => { 5 | test('metadata is present', () => { 6 | let options = { 7 | autoFirstPage: false, 8 | pdfVersion: '1.7', 9 | subset: 'PDF/A-2', 10 | }; 11 | let doc = new PDFDocument(options); 12 | const data = logData(doc); 13 | doc.end(); 14 | expect(data).toContainChunk([ 15 | `11 0 obj`, 16 | `<<\n/length 892\n/Type /Metadata\n/Subtype /XML\n/Length 894\n>>`, 17 | ]); 18 | }); 19 | 20 | test('color profile is present', () => { 21 | const expected = [ 22 | `10 0 obj`, 23 | joinTokens( 24 | '<<', 25 | '/Type /OutputIntent', 26 | '/S /GTS_PDFA1', 27 | '/Info (sRGB IEC61966-2.1)', 28 | '/OutputConditionIdentifier (sRGB IEC61966-2.1)', 29 | '/DestOutputProfile 9 0 R', 30 | '>>', 31 | ), 32 | ]; 33 | let options = { 34 | autoFirstPage: false, 35 | pdfVersion: '1.7', 36 | subset: 'PDF/A-2', 37 | }; 38 | let doc = new PDFDocument(options); 39 | const data = logData(doc); 40 | doc.end(); 41 | expect(data).toContainChunk(expected); 42 | }); 43 | 44 | test('metadata contains pdfaid part and conformance', () => { 45 | let options = { 46 | autoFirstPage: false, 47 | pdfVersion: '1.7', 48 | subset: 'PDF/A-2', 49 | }; 50 | let doc = new PDFDocument(options); 51 | const data = logData(doc); 52 | doc.end(); 53 | let metadata = Buffer.from(data[27]).toString(); 54 | 55 | expect(metadata).toContain('pdfaid:part>2'); 56 | expect(metadata).toContain('pdfaid:conformance'); 57 | }); 58 | 59 | test('metadata pdfaid conformance B', () => { 60 | let options = { 61 | autoFirstPage: false, 62 | pdfVersion: '1.7', 63 | subset: 'PDF/A-2b', 64 | }; 65 | let doc = new PDFDocument(options); 66 | const data = logData(doc); 67 | doc.end(); 68 | let metadata = Buffer.from(data[27]).toString(); 69 | 70 | expect(metadata).toContain('pdfaid:conformance>B'); 71 | }); 72 | 73 | test('metadata pdfaid conformance A', () => { 74 | let options = { 75 | autoFirstPage: false, 76 | pdfVersion: '1.7', 77 | subset: 'PDF/A-2a', 78 | }; 79 | let doc = new PDFDocument(options); 80 | const data = logData(doc); 81 | doc.end(); 82 | let metadata = Buffer.from(data[27]).toString(); 83 | 84 | expect(metadata).toContain('pdfaid:conformance>A'); 85 | }); 86 | 87 | test('font data NOT contains CIDSet', () => { 88 | let options = { 89 | autoFirstPage: false, 90 | pdfVersion: '1.4', 91 | subset: 'PDF/A-2a', 92 | }; 93 | let doc = new PDFDocument(options); 94 | const data = logData(doc); 95 | doc.addPage(); 96 | doc.registerFont('Roboto', 'tests/fonts/Roboto-Regular.ttf'); 97 | doc.font('Roboto'); 98 | doc.text('Text'); 99 | doc.end(); 100 | 101 | let fontDescriptor = data.find((v) => { 102 | return v.includes('/Type /FontDescriptor'); 103 | }); 104 | 105 | expect(fontDescriptor).not.toBeUndefined(); 106 | 107 | expect(fontDescriptor).not.toContain('/CIDSet'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/unit/pdfa3.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData, joinTokens } from './helpers'; 3 | 4 | describe('PDF/A-3', () => { 5 | test('metadata is present', () => { 6 | let options = { 7 | autoFirstPage: false, 8 | pdfVersion: '1.7', 9 | subset: 'PDF/A-3', 10 | }; 11 | let doc = new PDFDocument(options); 12 | const data = logData(doc); 13 | doc.end(); 14 | expect(data).toContainChunk([ 15 | `11 0 obj`, 16 | `<<\n/length 892\n/Type /Metadata\n/Subtype /XML\n/Length 894\n>>`, 17 | ]); 18 | }); 19 | 20 | test('color profile is present', () => { 21 | const expected = [ 22 | `10 0 obj`, 23 | joinTokens( 24 | '<<', 25 | '/Type /OutputIntent', 26 | '/S /GTS_PDFA1', 27 | '/Info (sRGB IEC61966-2.1)', 28 | '/OutputConditionIdentifier (sRGB IEC61966-2.1)', 29 | '/DestOutputProfile 9 0 R', 30 | '>>', 31 | ), 32 | ]; 33 | let options = { 34 | autoFirstPage: false, 35 | pdfVersion: '1.7', 36 | subset: 'PDF/A-3', 37 | }; 38 | let doc = new PDFDocument(options); 39 | const data = logData(doc); 40 | doc.end(); 41 | expect(data).toContainChunk(expected); 42 | }); 43 | 44 | test('metadata contains pdfaid part and conformance', () => { 45 | let options = { 46 | autoFirstPage: false, 47 | pdfVersion: '1.7', 48 | subset: 'PDF/A-3', 49 | }; 50 | let doc = new PDFDocument(options); 51 | const data = logData(doc); 52 | doc.end(); 53 | let metadata = Buffer.from(data[27]).toString(); 54 | 55 | expect(metadata).toContain('pdfaid:part>3'); 56 | expect(metadata).toContain('pdfaid:conformance'); 57 | }); 58 | 59 | test('metadata pdfaid conformance B', () => { 60 | let options = { 61 | autoFirstPage: false, 62 | pdfVersion: '1.7', 63 | subset: 'PDF/A-3b', 64 | }; 65 | let doc = new PDFDocument(options); 66 | const data = logData(doc); 67 | doc.end(); 68 | let metadata = Buffer.from(data[27]).toString(); 69 | 70 | expect(metadata).toContain('pdfaid:conformance>B'); 71 | }); 72 | 73 | test('metadata pdfaid conformance A', () => { 74 | let options = { 75 | autoFirstPage: false, 76 | pdfVersion: '1.7', 77 | subset: 'PDF/A-3a', 78 | }; 79 | let doc = new PDFDocument(options); 80 | const data = logData(doc); 81 | doc.end(); 82 | let metadata = Buffer.from(data[27]).toString(); 83 | 84 | expect(metadata).toContain('pdfaid:conformance>A'); 85 | }); 86 | 87 | test('font data NOT contains CIDSet', () => { 88 | let options = { 89 | autoFirstPage: false, 90 | pdfVersion: '1.4', 91 | subset: 'PDF/A-3a', 92 | }; 93 | let doc = new PDFDocument(options); 94 | const data = logData(doc); 95 | doc.addPage(); 96 | doc.registerFont('Roboto', 'tests/fonts/Roboto-Regular.ttf'); 97 | doc.font('Roboto'); 98 | doc.text('Text'); 99 | doc.end(); 100 | 101 | let fontDescriptor = data.find((v) => { 102 | return v.includes('/Type /FontDescriptor'); 103 | }); 104 | 105 | expect(fontDescriptor).not.toBeUndefined(); 106 | 107 | expect(fontDescriptor).not.toContain('/CIDSet'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/unit/pdfua.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { logData } from './helpers'; 3 | 4 | describe('PDF/UA', () => { 5 | test('metadata is present', () => { 6 | let options = { 7 | autoFirstPage: false, 8 | pdfVersion: '1.7', 9 | subset: 'PDF/UA', 10 | tagged: true, 11 | }; 12 | let doc = new PDFDocument(options); 13 | const data = logData(doc); 14 | doc.end(); 15 | expect(data).toContainChunk([ 16 | `11 0 obj`, 17 | `<<\n/length 841\n/Type /Metadata\n/Subtype /XML\n/Length 843\n>>`, 18 | ]); 19 | }); 20 | 21 | test('metadata constains pdfuaid part', () => { 22 | let options = { 23 | autoFirstPage: false, 24 | pdfVersion: '1.7', 25 | subset: 'PDF/UA', 26 | tagged: true, 27 | }; 28 | let doc = new PDFDocument(options); 29 | const data = logData(doc); 30 | doc.end(); 31 | let metadata = Buffer.from(data[24]).toString(); 32 | 33 | expect(metadata).toContain('pdfuaid:part>1'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/reference.spec.js: -------------------------------------------------------------------------------- 1 | import PDFReference from '../../lib/reference'; 2 | import PDFDocument from '../../lib/document'; 3 | import zlib from 'zlib'; 4 | import { logData } from './helpers'; 5 | 6 | describe('PDFReference', () => { 7 | let document; 8 | beforeEach(() => { 9 | document = new PDFDocument(); 10 | }); 11 | 12 | test('instantiated without data', () => { 13 | const ref = new PDFReference(document, 1); 14 | 15 | expect(ref.id).toBeDefined(); 16 | expect(ref.data).toBeDefined(); 17 | expect(ref.data).toBeInstanceOf(Object); 18 | }); 19 | 20 | test('instantiated with data', () => { 21 | const refData = { Pages: 0 }; 22 | const ref = new PDFReference(document, 1, refData); 23 | 24 | expect(ref.id).toBe(1); 25 | expect(ref.data).toBe(refData); 26 | }); 27 | 28 | test('written data of empty reference', () => { 29 | const ref = new PDFReference(document, 1); 30 | 31 | const docData = logData(document); 32 | ref.finalize(); 33 | 34 | expect(docData).toContainChunk(['1 0 obj', '<<\n>>', 'endobj']); 35 | }); 36 | 37 | test('written data of reference with uncompressed data', () => { 38 | const docData = logData(document); 39 | const chunk = Buffer.from('test'); 40 | const ref = new PDFReference(document, 1); 41 | ref.compress = false; 42 | ref.write(chunk); 43 | ref.finalize(); 44 | expect(docData).toContainChunk([ 45 | '1 0 obj', 46 | `<< 47 | /Length ${chunk.length} 48 | >>`, 49 | 'stream', 50 | chunk, 51 | '\nendstream', 52 | 'endobj', 53 | ]); 54 | }); 55 | 56 | test('written data of reference with compressed data', () => { 57 | const docData = logData(document); 58 | const chunk = Buffer.from('test'); 59 | const compressed = zlib.deflateSync(chunk); 60 | const ref = new PDFReference(document, 1); 61 | ref.write(chunk); 62 | 63 | ref.finalize(); 64 | expect(docData).toContainChunk([ 65 | '1 0 obj', 66 | `<< 67 | /Length ${compressed.length} 68 | /Filter /FlateDecode 69 | >>`, 70 | 'stream', 71 | compressed, 72 | '\nendstream', 73 | 'endobj', 74 | ]); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/unit/saslprep.spec.js: -------------------------------------------------------------------------------- 1 | import saslprep from '../../lib/saslprep'; 2 | 3 | const chr = String.fromCodePoint; 4 | 5 | test('should work with liatin letters', () => { 6 | const str = 'user'; 7 | expect(saslprep(str)).toEqual(str); 8 | }); 9 | 10 | test('should work be case preserved', () => { 11 | const str = 'USER'; 12 | expect(saslprep(str)).toEqual(str); 13 | }); 14 | 15 | test('should work with high code points (> U+FFFF)', () => { 16 | const str = '\uD83D\uDE00'; 17 | expect(saslprep(str, { allowUnassigned: true })).toEqual(str); 18 | }); 19 | 20 | test('should remove `mapped to nothing` characters', () => { 21 | expect(saslprep('I\u00ADX')).toEqual('IX'); 22 | }); 23 | 24 | test('should replace `Non-ASCII space characters` with space', () => { 25 | expect(saslprep('a\u00A0b')).toEqual('a\u0020b'); 26 | }); 27 | 28 | test('should normalize as NFKC', () => { 29 | expect(saslprep('\u00AA')).toEqual('a'); 30 | expect(saslprep('\u2168')).toEqual('IX'); 31 | }); 32 | 33 | test('should throws when prohibited characters', () => { 34 | // C.2.1 ASCII control characters 35 | expect(() => saslprep('a\u007Fb')).toThrow(); 36 | 37 | // C.2.2 Non-ASCII control characters 38 | expect(() => saslprep('a\u06DDb')).toThrow(); 39 | 40 | // C.3 Private use 41 | expect(() => saslprep('a\uE000b')).toThrow(); 42 | 43 | // C.4 Non-character code points 44 | expect(() => saslprep(`a${chr(0x1fffe)}b`)).toThrow(); 45 | 46 | // C.5 Surrogate codes 47 | expect(() => saslprep('a\uD800b')).toThrow(); 48 | 49 | // C.6 Inappropriate for plain text 50 | expect(() => saslprep('a\uFFF9b')).toThrow(); 51 | 52 | // C.7 Inappropriate for canonical representation 53 | expect(() => saslprep('a\u2FF0b')).toThrow(); 54 | 55 | // C.8 Change display properties or are deprecated 56 | expect(() => saslprep('a\u200Eb')).toThrow(); 57 | 58 | // C.9 Tagging characters 59 | expect(() => saslprep(`a${chr(0xe0001)}b`)).toThrow(); 60 | }); 61 | 62 | test('should not containt RandALCat and LCat bidi', () => { 63 | expect(() => saslprep('a\u06DD\u00AAb')).toThrow(); 64 | }); 65 | 66 | test('RandALCat should be first and last', () => { 67 | expect(() => saslprep('\u0627\u0031\u0628')).not.toThrow(); 68 | expect(() => saslprep('\u0627\u0031')).toThrow(); 69 | }); 70 | 71 | test('should handle unassigned code points', () => { 72 | expect(() => saslprep('a\u0487')).toThrow(); 73 | expect(() => saslprep('a\u0487', { allowUnassigned: true })).not.toThrow(); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/unit/setupTests.js: -------------------------------------------------------------------------------- 1 | import toContainChunk from './toContainChunk'; 2 | import toContainText from './toContainText'; 3 | import { toMatchImageSnapshot } from 'jest-image-snapshot'; 4 | 5 | expect.extend(toContainChunk); 6 | expect.extend(toContainText); 7 | expect.extend({ toMatchImageSnapshot }); 8 | -------------------------------------------------------------------------------- /tests/unit/table.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import PDFTable from '../../lib/table'; 3 | import { deepMerge } from '../../lib/table/utils'; 4 | 5 | describe('table', () => { 6 | test('created', () => { 7 | const document = new PDFDocument(); 8 | expect(document.table()).toBeInstanceOf(PDFTable); 9 | expect(document.table({ data: [] })).toBe(document); 10 | }); 11 | test('row', () => { 12 | const document = new PDFDocument(); 13 | const table = document.table(); 14 | table.row(['A', 'B', 'C']); 15 | expect(table._columnWidths.length).toBe(3); 16 | }); 17 | }); 18 | 19 | describe('utils', () => { 20 | describe('deepMerge', () => { 21 | test.each([ 22 | [{ a: 'hello' }, { b: 'world' }, { a: 'hello', b: 'world' }], 23 | [{ a: 'hello' }, { a: 'world' }, { a: 'world' }], 24 | [{}, { a: 'hello' }, { a: 'hello' }], 25 | [{ a: 'hello' }, undefined, { a: 'hello' }], 26 | [undefined, null, undefined], 27 | [1, 2, 1], 28 | [1, {}, 1], 29 | [{ a: 'hello' }, { a: {} }, { a: 'hello' }], 30 | [{ a: { b: 'hello' } }, { a: { b: 'world' } }, { a: { b: 'world' } }], 31 | ])('%o -> %o', function () { 32 | const opts = Array.from(arguments); 33 | const expected = opts.splice(-1, 1)[0]; 34 | expect(deepMerge(...opts)).toEqual(expected); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/toContainChunk/index.js: -------------------------------------------------------------------------------- 1 | import { diff } from 'jest-diff'; 2 | 3 | const buildMessage = (utils, data, chunk, headIndex) => { 4 | let message; 5 | if (headIndex !== -1) { 6 | const received = data.slice(headIndex, headIndex + chunk.length); 7 | const difference = diff(chunk, received); 8 | message = `Difference:\n\n${difference}`; 9 | } else { 10 | message = 11 | 'Expected data to contain chunk:\n' + ` ${utils.printExpected(chunk)}\n`; 12 | } 13 | return message; 14 | }; 15 | 16 | const passMessage = (utils, data, chunk, headIndex) => () => { 17 | return ( 18 | utils.matcherHint('.not.toContainChunk', 'data', 'chunk') + 19 | '\n\n' + 20 | buildMessage(utils, data, chunk, headIndex) 21 | ); 22 | }; 23 | 24 | const failMessage = (utils, data, chunk, headIndex) => () => { 25 | return ( 26 | utils.matcherHint('.toContainChunk', 'data', 'chunk') + 27 | '\n\n' + 28 | buildMessage(utils, data, chunk, headIndex) 29 | ); 30 | }; 31 | 32 | export default { 33 | toContainChunk(data, chunk) { 34 | const headIndex = data.indexOf(chunk[0]); 35 | let pass = headIndex !== -1; 36 | if (pass) { 37 | for (let i = 1; i < chunk.length; ++i) { 38 | if (chunk[i] instanceof RegExp) { 39 | pass = pass && chunk[i].test(data[headIndex + i]); 40 | } else { 41 | pass = pass && this.equals(data[headIndex + i], chunk[i]); 42 | } 43 | } 44 | } 45 | 46 | if (pass) { 47 | return { 48 | pass: true, 49 | message: passMessage(this.utils, data, chunk, headIndex), 50 | }; 51 | } 52 | 53 | return { 54 | pass: false, 55 | message: failMessage(this.utils, data, chunk, headIndex), 56 | }; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /tests/unit/toContainText/index.js: -------------------------------------------------------------------------------- 1 | import { getObjects, parseTextStream } from '../helpers.js'; 2 | 3 | /** 4 | * @import { TextStream, PDFDataObject } from '../helpers.js'; 5 | * @import JestMatchedUtils from 'jest-matcher-utils'; 6 | */ 7 | 8 | /** 9 | * @param {JestMatchedUtils} utils 10 | * @param {TextStream} argument 11 | * @return {string} 12 | */ 13 | const passMessage = (utils, argument) => () => { 14 | return ( 15 | utils.matcherHint('.not.toContainText', 'data', 'textStream') + 16 | '\n\n' + 17 | `Expected data not to contain text:\n\n${utils.printExpected(argument)}` 18 | ); 19 | }; 20 | 21 | /** 22 | * @param {JestMatchedUtils} utils 23 | * @param {TextStream[]} received 24 | * @param {TextStream} argument 25 | * @return {string} 26 | */ 27 | const failMessage = (utils, received, argument) => () => { 28 | return ( 29 | utils.matcherHint('.toContainText', 'data', 'textStream') + 30 | '\n\n' + 31 | `Expected data to contain text:\n\n${utils.printExpected(argument)}\n\nFound:\n\n${utils.printReceived(received)}` 32 | ); 33 | }; 34 | 35 | function textStreamMatches(expected, actual) { 36 | if (expected.text !== actual.text) { 37 | return false; 38 | } 39 | 40 | if (expected.font && expected.font !== actual.font) { 41 | return false; 42 | } 43 | 44 | if (expected.fontSize && expected.fontSize !== actual.fontSize) { 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | /** 52 | * @param {PDFDataObject} object 53 | * @return {TextStream | undefined} 54 | */ 55 | function getTextStream(object) { 56 | // text stream objects have 4 items 57 | // first item is a string containing the Length of the stream 58 | // second item 'stream' 59 | // third item is the stream content Buffer 60 | // fourth item is 'endstream' 61 | 62 | if (object.items.length !== 4) { 63 | return; 64 | } 65 | if (typeof object.items[0] !== 'string') { 66 | return; 67 | } 68 | if (object.items[1] !== 'stream') { 69 | return; 70 | } 71 | if (!(object.items[2] instanceof Buffer)) { 72 | return; 73 | } 74 | if (!/endstream/.test(object.items[3])) { 75 | return; 76 | } 77 | 78 | return parseTextStream(object.items[2]); 79 | } 80 | 81 | export default { 82 | /** 83 | * 84 | * @param {(string | Buffer)[]} data 85 | * @param {Partial} textStream 86 | * @returns 87 | */ 88 | toContainText(data, textStream) { 89 | const objects = getObjects(data); 90 | const foundTextStreams = []; 91 | let pass = false; 92 | 93 | for (const object of objects) { 94 | const objectTextStream = getTextStream(object, textStream); 95 | if (!objectTextStream) { 96 | continue; 97 | } 98 | foundTextStreams.push(objectTextStream); 99 | if (textStreamMatches(textStream, objectTextStream)) { 100 | pass = true; 101 | break; 102 | } 103 | } 104 | 105 | if (pass) { 106 | return { 107 | pass: true, 108 | message: passMessage(this.utils, textStream), 109 | }; 110 | } 111 | 112 | return { 113 | pass: false, 114 | message: failMessage(this.utils, foundTextStreams, textStream), 115 | }; 116 | }, 117 | }; 118 | -------------------------------------------------------------------------------- /tests/unit/trailer.spec.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import PDFSecurity from '../../lib/security'; 3 | import { logData } from './helpers'; 4 | 5 | // manual mock for PDFSecurity to ensure stored id will be the same accross different systems 6 | PDFSecurity.generateFileID = () => { 7 | return Buffer.from('mocked-pdf-id'); 8 | }; 9 | 10 | describe('Document trailer', () => { 11 | let document; 12 | 13 | beforeEach(() => { 14 | document = new PDFDocument({ 15 | info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, 16 | }); 17 | }); 18 | 19 | test('', () => { 20 | const docData = logData(document); 21 | document.end(); 22 | expect(docData).toContainChunk([ 23 | '8 0 obj', 24 | '<<\n/Producer 9 0 R\n/Creator 10 0 R\n/CreationDate 11 0 R\n>>', 25 | ]); 26 | expect(docData).toContainChunk(['9 0 obj', '(PDFKit)']); 27 | expect(docData).toContainChunk(['10 0 obj', '(PDFKit)']); 28 | expect(docData).toContainChunk(['11 0 obj', '(D:20180201000000Z)']); 29 | expect(docData).toContainChunk([ 30 | 'trailer', 31 | `<<\n/Size 12\n/Root 3 0 R\n/Info 8 0 R\n/ID [<6d6f636b65642d7064662d6964> <6d6f636b65642d7064662d6964>]\n>>`, 32 | ]); 33 | }); 34 | 35 | test('written empty data of destinations', () => { 36 | const docData = logData(document); 37 | document.end(); 38 | expect(docData).toContainChunk([ 39 | '2 0 obj', 40 | '<<\n/Dests <<\n /Names [\n]\n>>\n>>', 41 | ]); 42 | }); 43 | 44 | test('written data of destinations', () => { 45 | const docData = logData(document); 46 | document.addNamedDestination('LINK1'); 47 | document.addNamedDestination('LINK2', 'FitH', 100); 48 | document.addNamedDestination('LINK3', 'XYZ', 36, 36, 50); 49 | document.goTo(10, 10, 100, 20, 'LINK1'); 50 | document.end(); 51 | 52 | expect(docData).toContainChunk([ 53 | '2 0 obj', 54 | `<< 55 | /Dests << 56 | /Limits [(LINK1) (LINK3)] 57 | /Names [ 58 | (LINK1) [7 0 R /XYZ null null null] 59 | (LINK2) [7 0 R /FitH 100] 60 | (LINK3) [7 0 R /XYZ 36 756 50] 61 | ] 62 | >> 63 | >>`, 64 | ]); 65 | expect(docData).toContainChunk([ 66 | '7 0 obj', 67 | `<< 68 | /Type /Page 69 | /Parent 1 0 R 70 | /MediaBox [0 0 612 792] 71 | /Contents 5 0 R 72 | /Resources 6 0 R 73 | /Annots [9 0 R] 74 | >>`, 75 | ]); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/unit/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { normalizeSides, PDFNumber } from '../../lib/utils'; 2 | 3 | describe('normalizeSides', () => { 4 | test.each([ 5 | [1, { top: 1, right: 1, bottom: 1, left: 1 }], 6 | [[1, 2], { top: 1, right: 2, bottom: 1, left: 2 }], 7 | [ 8 | { vertical: 1, horizontal: 2 }, 9 | { top: 1, right: 2, bottom: 1, left: 2 }, 10 | ], 11 | [[1, 2, 3, 4], { top: 1, right: 2, bottom: 3, left: 4 }], 12 | [ 13 | { top: 1, right: 2, bottom: 3, left: 4 }, 14 | { top: 1, right: 2, bottom: 3, left: 4 }, 15 | ], 16 | [ 17 | { a: 'hi' }, 18 | { top: undefined, right: undefined, bottom: undefined, left: undefined }, 19 | ], 20 | [ 21 | { vertical: 'hi' }, 22 | { top: 'hi', right: undefined, bottom: 'hi', left: undefined }, 23 | ], 24 | [ 25 | { top: undefined }, 26 | { top: undefined, right: undefined, bottom: undefined, left: undefined }, 27 | ], 28 | [ 29 | null, 30 | { top: undefined, right: undefined, bottom: undefined, left: undefined }, 31 | ], 32 | [ 33 | undefined, 34 | { top: undefined, right: undefined, bottom: undefined, left: undefined }, 35 | ], 36 | [true, { top: true, right: true, bottom: true, left: true }], 37 | [false, { top: false, right: false, bottom: false, left: false }], 38 | ])('%s -> %s', (size, expected) => { 39 | expect(normalizeSides(size)).toEqual(expected); 40 | }); 41 | 42 | test('with transformer', () => { 43 | expect( 44 | normalizeSides( 45 | undefined, 46 | { top: '1', right: '2', bottom: '3', left: '4' }, 47 | Number, 48 | ), 49 | ).toEqual({ 50 | top: 1, 51 | right: 2, 52 | bottom: 3, 53 | left: 4, 54 | }); 55 | }); 56 | }); 57 | 58 | describe('PDFNumber', () => { 59 | test.each([ 60 | [0, 0], 61 | [0.04999999701976776], //float32 rounded down 62 | [0.05], 63 | [0.05000000074505806], //float32 rounded up 64 | [1], 65 | [-1], 66 | [-5.05], 67 | [5.05], 68 | ])('PDFNumber(%f) -> %f', (n) => { 69 | expect(PDFNumber(n)).toBeLessThanOrEqual(n); 70 | expect(PDFNumber(n, false)).toBeLessThanOrEqual(n); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/unit/virtual-fs.spec.js: -------------------------------------------------------------------------------- 1 | import fs from '../../lib/virtual-fs'; 2 | 3 | function checkMissingFiles(files) { 4 | for (let file of files) { 5 | expect(() => fs.readFileSync(`files/${file}`)).toThrow( 6 | `File 'files/${file}' not found in virtual file system`, 7 | ); 8 | } 9 | } 10 | 11 | describe('virtual-fs', function () { 12 | beforeEach(() => { 13 | fs.fileData = {}; 14 | }); 15 | 16 | test('readFileSync', function () { 17 | checkMissingFiles(['encoded', 'raw', 'binary']); 18 | 19 | fs.bindFileData({ 20 | 'files/binary': Buffer.from('Buffer content'), 21 | }); 22 | 23 | const base64Data = fs.readFileSync('files/binary', 'base64'); 24 | expect(base64Data).toEqual('QnVmZmVyIGNvbnRlbnQ='); 25 | }); 26 | 27 | test('writeFileSync', function () { 28 | checkMissingFiles(['encoded', 'raw', 'binary']); 29 | 30 | fs.writeFileSync( 31 | 'files/encoded', 32 | Buffer.from('File content').toString('base64'), 33 | ); 34 | fs.writeFileSync('files/raw', 'File content'); 35 | fs.writeFileSync('files/binary', new Uint8Array([4, 3, 1, 2])); 36 | 37 | const encodedData = fs.readFileSync('files/encoded'); 38 | expect(encodedData).toBeInstanceOf(Buffer); 39 | expect(encodedData.toString('utf8')).toEqual('File content'); 40 | 41 | const rawData = fs.readFileSync('files/raw', 'utf8'); 42 | expect(rawData).toEqual('File content'); 43 | 44 | const binaryData = fs.readFileSync('files/binary'); 45 | expect(binaryData).toBeInstanceOf(Buffer); 46 | expect(binaryData.toJSON()).toEqual({ data: [4, 3, 1, 2], type: 'Buffer' }); 47 | }); 48 | 49 | test('bindFileData', function () { 50 | checkMissingFiles(['encoded', 'raw', 'binary']); 51 | 52 | fs.bindFileData({ 53 | 'files/encoded': Buffer.from('File content').toString('base64'), 54 | }); 55 | 56 | fs.bindFileData({ 57 | 'files/raw': 'File content', 58 | 'files/binary': new Uint8Array([4, 3, 1, 2]), 59 | }); 60 | 61 | const encodedData = fs.readFileSync('files/encoded'); 62 | expect(encodedData).toBeInstanceOf(Buffer); 63 | expect(encodedData.toString('utf8')).toEqual('File content'); 64 | 65 | let rawData = fs.readFileSync('files/raw', 'utf8'); 66 | expect(rawData).toEqual('File content'); 67 | 68 | let binaryData = fs.readFileSync('files/binary'); 69 | expect(binaryData).toBeInstanceOf(Buffer); 70 | expect(binaryData.toJSON()).toEqual({ data: [4, 3, 1, 2], type: 'Buffer' }); 71 | 72 | // reset option 73 | fs.bindFileData( 74 | { 75 | 'files/raw': 'New File content', 76 | 'files/binary2': new Uint8Array([4, 3, 1, 2]), 77 | }, 78 | { reset: true }, 79 | ); 80 | 81 | checkMissingFiles(['encoded', 'binary']); 82 | 83 | rawData = fs.readFileSync('files/raw', 'utf8'); 84 | expect(rawData).toEqual('New File content'); 85 | 86 | binaryData = fs.readFileSync('files/binary2'); 87 | expect(binaryData).toBeInstanceOf(Buffer); 88 | expect(binaryData.toJSON()).toEqual({ data: [4, 3, 1, 2], type: 'Buffer' }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/fonts-spec-js-fonts-default-helvetica-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/fonts-spec-js-fonts-default-helvetica-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/fonts-spec-js-fonts-helvetica-bold-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/fonts-spec-js-fonts-helvetica-bold-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/fonts-spec-js-fonts-roboto-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/fonts-spec-js-fonts-roboto-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/fonts-spec-js-fonts-roboto-bold-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/fonts-spec-js-fonts-roboto-bold-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/images-spec-js-images-orientation-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/images-spec-js-images-orientation-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/images-spec-js-images-orientation-document-option-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/images-spec-js-images-orientation-document-option-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-cover-and-alignment-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/images-spec-js-images-orientation-with-fit-and-alignment-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-column-row-spans-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-column-row-spans-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-defining-column-widths-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-defining-column-widths-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-defining-row-heights-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-defining-row-heights-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-iterables-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-iterables-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-line-flowing-rotated-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-line-flowing-rotated-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-multi-line-rotated-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-multi-line-rotated-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-multi-page-table-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-multi-page-table-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-multi-page-table-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-multi-page-table-2-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-optional-border-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-optional-border-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-rotated-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-rotated-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-simple-table-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-simple-table-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-styling-tables-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-styling-tables-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/table-spec-js-table-styling-tables-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/table-spec-js-table-styling-tables-2-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-alignment-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-alignment-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-continued-text-with-open-type-features-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-continued-text-with-open-type-features-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-decoration-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-decoration-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-lettered-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-lettered-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-numbered-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-numbered-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-with-line-breaks-in-items-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-with-line-breaks-in-items-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-with-sub-list-ordered-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-with-sub-list-ordered-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-list-with-sub-list-unordered-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-list-with-sub-list-unordered-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-rotated-multi-line-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-rotated-multi-line-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-rotated-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-rotated-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-simple-text-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-simple-text-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/text-spec-js-text-soft-hyphen-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/text-spec-js-text-soft-hyphen-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/vector-spec-js-vector-complex-svg-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/vector-spec-js-vector-complex-svg-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/vector-spec-js-vector-simple-shapes-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/vector-spec-js-vector-simple-shapes-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/vector-spec-js-vector-svg-path-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/vector-spec-js-vector-svg-path-1-snap.png -------------------------------------------------------------------------------- /tests/visual/__image_snapshots__/vector-spec-js-vector-svg-path-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/__image_snapshots__/vector-spec-js-vector-svg-path-2-snap.png -------------------------------------------------------------------------------- /tests/visual/fonts.spec.js: -------------------------------------------------------------------------------- 1 | import { runDocTest } from './helpers'; 2 | 3 | const characters = `Latin 4 | ABCDEFGHIJKLMNOPQRSTUVWXYZ 5 | abcdefghijklmnopqrstuvwxyz 6 | 7 | Latin 1 (Western) 8 | ÁÀÂÄÅÃÆÇÐÉÈÊËÍÌÎÏÑÓÒÔÖÕØŒÞÚÙÛÜÝŸ 9 | áàâäãåæçðéèêëíìîïıñóòôöõøœßþúùûüýÿ 10 | 11 | Latin 2 (Eastern) 12 | ĀĂĄĆČĎĐĒĖĘĚĞĢĪĮİĶŁĹĻĽŃŅŇŌŐŔŖŘŠŚŞȘŢȚŤŪŮŰŲŽŹŻ 13 | āăąćčďđēėęěğģīįķłĺļľńņňōőŕŗřšśşșţțťūůűųžźż 14 | 15 | Greek (Modern) 16 | ΑΒΓ∆ΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΆΈΉΊΌΎΏΪΫ 17 | αβγδεζηθικλµνξοπρςστυφχψωάέήίόύώϊϋΐΰ 18 | 19 | Cyrillic 1 (Russian) 20 | АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ 21 | абвгдежзийклмнопрстуфхцчшщъыьэюя 22 | 23 | Cyrillic 2 (Extended) 24 | ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏҐӁҒҖҚҢҮҰҲҶҺӘӢӨӮ 25 | ѐёђѓєѕіїјљњћќѝўџґӂғҗқңүұҳҷһәӣөӯ`; 26 | 27 | describe('fonts', function () { 28 | test.skip('default (Helvetica)', function () { 29 | return runDocTest({ systemFonts: true }, function (doc) { 30 | doc.text(characters, 10, 10); 31 | }); 32 | }); 33 | 34 | test.skip('Helvetica Bold', function () { 35 | return runDocTest({ systemFonts: true }, function (doc) { 36 | doc.font('Helvetica-Bold'); 37 | doc.text(characters, 10, 10); 38 | }); 39 | }); 40 | 41 | test('Roboto', function () { 42 | return runDocTest(function (doc) { 43 | doc.font('tests/fonts/Roboto-Regular.ttf'); 44 | doc.text(characters, 10, 10); 45 | }); 46 | }); 47 | 48 | test('Roboto Bold', function () { 49 | return runDocTest(function (doc) { 50 | doc.font('tests/fonts/Roboto-Medium.ttf'); 51 | doc.text(characters, 10, 10); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/visual/helpers.js: -------------------------------------------------------------------------------- 1 | import PDFDocument from '../../lib/document'; 2 | import { pdf2png } from './pdf2png.js'; 3 | 4 | function runDocTest(options, fn) { 5 | if (typeof options === 'function') { 6 | fn = options; 7 | options = {}; 8 | } 9 | if (!options.info) { 10 | options.info = {}; 11 | } 12 | 13 | return new Promise((resolve, reject) => { 14 | const doc = new PDFDocument(options); 15 | const buffers = []; 16 | 17 | (async () => { 18 | await fn(doc); 19 | })() 20 | .then(() => { 21 | doc.on('error', (err) => reject(err)); 22 | doc.on('data', buffers.push.bind(buffers)); 23 | doc.on('end', async () => { 24 | try { 25 | const pdfData = Buffer.concat(buffers); 26 | const { systemFonts = false } = options; 27 | const images = await pdf2png(pdfData, { systemFonts }); 28 | for (let image of images) { 29 | expect(image).toMatchImageSnapshot(options); 30 | } 31 | resolve(); 32 | } catch (err) { 33 | reject(err); 34 | } 35 | }); 36 | doc.end(); 37 | }) 38 | .catch((err) => reject(err)); 39 | }); 40 | } 41 | 42 | export { runDocTest }; 43 | -------------------------------------------------------------------------------- /tests/visual/pdf2png.js: -------------------------------------------------------------------------------- 1 | import Canvas from 'canvas'; 2 | import { strict as assert } from 'assert'; 3 | import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf'; 4 | 5 | // adapted from https://github.com/mozilla/pdf.js/tree/master/examples/node/pdf2png 6 | 7 | class NodeCanvasFactory { 8 | create(width, height) { 9 | assert(width > 0 && height > 0, 'Invalid canvas size'); 10 | const canvas = Canvas.createCanvas(width, height); 11 | const context = canvas.getContext('2d'); 12 | return { 13 | canvas, 14 | context, 15 | }; 16 | } 17 | 18 | reset({ canvas }, width, height) { 19 | assert(canvas, 'Canvas is not specified'); 20 | assert(width > 0 && height > 0, 'Invalid canvas size'); 21 | canvas.width = width; 22 | canvas.height = height; 23 | } 24 | 25 | destroy(canvasAndContext) { 26 | assert(canvasAndContext.canvas, 'Canvas is not specified'); 27 | 28 | // Zeroing the width and height cause Firefox to release graphics 29 | // resources immediately, which can greatly reduce memory consumption. 30 | canvasAndContext.canvas.width = 0; 31 | canvasAndContext.canvas.height = 0; 32 | canvasAndContext.canvas = null; 33 | canvasAndContext.context = null; 34 | } 35 | } 36 | 37 | async function pdf2png(data, { systemFonts } = {}) { 38 | // Load the PDF file. 39 | const loadingTask = pdfjsLib.getDocument({ 40 | data, 41 | disableFontFace: !systemFonts, 42 | }); 43 | 44 | const pdfDocument = await loadingTask.promise; 45 | 46 | const pageCount = pdfDocument.numPages; 47 | 48 | const images = []; 49 | 50 | for (let i = 1; i <= pageCount; i++) { 51 | const page = await pdfDocument.getPage(i); 52 | // Render the page on a Node canvas with 100% scale. 53 | const viewport = page.getViewport({ scale: 2.0 }); 54 | const canvasFactory = new NodeCanvasFactory(); 55 | const canvasAndContext = canvasFactory.create( 56 | viewport.width, 57 | viewport.height, 58 | ); 59 | const renderContext = { 60 | canvasContext: canvasAndContext.context, 61 | viewport, 62 | canvasFactory, 63 | }; 64 | const renderTask = page.render(renderContext); 65 | await renderTask.promise; 66 | // Convert the canvas to an image buffer. 67 | const image = canvasAndContext.canvas.toBuffer(); 68 | images.push(image); 69 | canvasFactory.destroy(canvasAndContext); 70 | } 71 | 72 | return images; 73 | } 74 | 75 | export { pdf2png }; 76 | -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/absolute-spec-js-pdfmake-absolute-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/background-spec-js-pdfmake-background-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/basics-spec-js-pdfmake-basics-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/basics-spec-js-pdfmake-basics-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-4-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/columns-simple-spec-js-pdfmake-columns-simple-5-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/images-spec-js-pdfmake-images-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/images-spec-js-pdfmake-images-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/images-spec-js-pdfmake-images-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/images-spec-js-pdfmake-images-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-4-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-5-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-6-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/lists-spec-js-pdfmake-lists-6-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/page-references-spec-js-pdfmake-page-references-4-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/qrcode-spec-js-pdfmake-qrcode-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/qrcode-spec-js-pdfmake-qrcode-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/qrcode-spec-js-pdfmake-qrcode-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/qrcode-spec-js-pdfmake-qrcode-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-4-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-5-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-6-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-6-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-7-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-7-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-8-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/tables-spec-js-pdfmake-tables-8-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/text-decorations-spec-js-pdfmake-text-decorations-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/text-decorations-spec-js-pdfmake-text-decorations-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-1-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-2-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-3-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-4-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/toc-spec-js-pdfmake-toc-5-snap.png -------------------------------------------------------------------------------- /tests/visual/pdfmake/__image_snapshots__/watermark-spec-js-pdfmake-watermark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foliojs/pdfkit/d837a13569ccfac604e9ec42e420ebe55637a8cc/tests/visual/pdfmake/__image_snapshots__/watermark-spec-js-pdfmake-watermark-1-snap.png -------------------------------------------------------------------------------- /tests/visual/vector.spec.js: -------------------------------------------------------------------------------- 1 | import { runDocTest } from './helpers'; 2 | var tiger = require('../../examples/tiger'); 3 | 4 | describe('vector', function () { 5 | test('simple shapes', function () { 6 | return runDocTest(function (doc) { 7 | doc 8 | .save() 9 | .moveTo(100, 150) 10 | .lineTo(100, 250) 11 | .lineTo(200, 250) 12 | .fill('#FF3300'); 13 | 14 | doc.circle(280, 200, 50).fill('#6600FF'); 15 | 16 | // an SVG path 17 | doc 18 | .scale(0.6) 19 | .translate(470, 130) 20 | .path('M 250,75 L 323,301 131,161 369,161 177,301 z') 21 | .fill('red', 'even-odd') 22 | .restore(); 23 | }); 24 | }); 25 | 26 | test('complex svg', function () { 27 | return runDocTest(function (doc) { 28 | var i, len, part; 29 | doc.translate(220, 300); 30 | for (i = 0, len = tiger.length; i < len; i++) { 31 | part = tiger[i]; 32 | doc.save(); 33 | doc.path(part.path); 34 | if (part['stroke-width']) { 35 | doc.lineWidth(part['stroke-width']); 36 | } 37 | if (part.fill !== 'none' && part.stroke !== 'none') { 38 | doc.fillAndStroke(part.fill, part.stroke); 39 | } else { 40 | if (part.fill !== 'none') { 41 | doc.fill(part.fill); 42 | } 43 | if (part.stroke !== 'none') { 44 | doc.stroke(part.stroke); 45 | } 46 | } 47 | doc.restore(); 48 | } 49 | }); 50 | }); 51 | 52 | test('svg path', function () { 53 | return runDocTest(function (doc) { 54 | // extracted from https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths 55 | // lines 56 | doc.path('M10 10 H 90 V 90 H 10 L 10 10'); 57 | doc.stroke('#000'); 58 | doc.translate(0, 100); 59 | 60 | doc.path('M10 10 H 90 V 90 H 10 Z'); 61 | doc.stroke('#000'); 62 | doc.translate(0, 100); 63 | 64 | doc.path('M10 10 h 80 v 80 h -80 Z'); 65 | doc.stroke('#000'); 66 | doc.translate(0, 100); 67 | 68 | // bezier curves 69 | 70 | doc.path('M10 10 C 20 20, 40 20, 50 10'); 71 | doc.stroke('#000'); 72 | 73 | doc.path('M70 10 C 70 20, 110 20, 110 10'); 74 | doc.stroke('#000'); 75 | 76 | doc.path('M130 10 C 120 20, 180 20, 170 10'); 77 | doc.stroke('#000'); 78 | 79 | doc.path('M10 60 C 20 80, 40 80, 50 60'); 80 | doc.stroke('#000'); 81 | 82 | doc.path('M70 60 C 70 80, 110 80, 110 60'); 83 | doc.stroke('#000'); 84 | 85 | doc.path('M130 60 C 120 80, 180 80, 170 60'); 86 | doc.stroke('#000'); 87 | 88 | doc.path('M10 110 C 20 140, 40 140, 50 110'); 89 | doc.stroke('#000'); 90 | 91 | doc.path('M70 110 C 70 140, 110 140, 110 110'); 92 | doc.stroke('#000'); 93 | 94 | doc.path('M130 110 C 120 140, 180 140, 170 110'); 95 | doc.stroke('#000'); 96 | 97 | doc.translate(0, 120); 98 | doc.path('M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80'); 99 | doc.stroke('#000'); 100 | 101 | doc.translate(0, 120); 102 | doc.path('M10 80 Q 95 10 180 80'); 103 | doc.stroke('#000'); 104 | 105 | doc.translate(0, 120); 106 | doc.path('M10 80 Q 52.5 10, 95 80 T 180 80'); 107 | doc.stroke('#000'); 108 | 109 | // arcs 110 | doc.addPage(); 111 | doc.path(`M10 315 112 | L 110 215 113 | A 30 50 0 0 1 162.55 162.45 114 | L 172.55 152.45 115 | A 30 50 -45 0 1 215.1 109.9 116 | L 315 10`); 117 | doc.fillAndStroke('#73B373', '#000'); 118 | 119 | doc.translate(0, 180); 120 | doc.path(`M10 315 121 | L 110 215 122 | A 36 60 0 0 1 150.71 170.29 123 | L 172.55 152.45 124 | A 30 50 -45 0 1 215.1 109.9 125 | L 315 10`); 126 | doc.fillAndStroke('#73B373', '#000'); 127 | 128 | doc.translate(0, 180); 129 | doc.path(`M80 80 130 | A 45 45, 0, 0, 0, 125 125 131 | L 125 80 Z`); 132 | doc.fillAndStroke('#73B373', '#000'); 133 | 134 | doc.path(`M230 80 135 | A 45 45, 0, 1, 0, 275 125 136 | L 275 80 Z`); 137 | doc.fillAndStroke('#F67676', '#000'); 138 | 139 | doc.path(`M80 230 140 | A 45 45, 0, 0, 1, 125 275 141 | L 125 230 Z`); 142 | doc.fillAndStroke('#AF6FAF', '#000'); 143 | 144 | doc.path(`M230 230 145 | A 45 45, 0, 1, 1, 275 275 146 | L 275 230 Z`); 147 | doc.fillAndStroke('#6F6FEF', '#000'); 148 | }); 149 | }); 150 | }); 151 | --------------------------------------------------------------------------------