diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..5b4a1ef --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,24 @@ +env: + browser: true + es2021: true +extends: eslint:recommended +parserOptions: + ecmaVersion: latest + sourceType: module +rules: + linebreak-style: + - error + - unix + semi: + - error + - always + camelcase: + - warn + dot-notation: + - error + no-useless-concat: + - error + prefer-const: + - error + prefer-template: + - error diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..cb8c342 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,13 @@ +name: Run tests +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install modules + run: npm install + - name: Run Prettier + run: npm run prettier + - name: Run ESLint + run: npm run lint diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index b0d7e7c..adc333d 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -44,4 +44,4 @@ jobs: with: folder: dist # The folder the action should deploy. clean-exclude: pr-preview/ - force: false \ No newline at end of file + force: false diff --git a/.github/workflows/test-pages.yml b/.github/workflows/test-pages.yml index 3914f6e..232d1c0 100644 --- a/.github/workflows/test-pages.yml +++ b/.github/workflows/test-pages.yml @@ -7,9 +7,9 @@ on: workflow_dispatch: inputs: destination: - description: "Define destination base path" + description: 'Define destination base path' required: true - default: "manual_run" + default: 'manual_run' # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages permissions: @@ -44,4 +44,4 @@ jobs: with: source-dir: dist umbrella-dir: pr-preview - action: auto \ No newline at end of file + action: auto diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eada8f2..1d72e49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Install modules - run: npm install - - name: Run tests - run: npm run test \ No newline at end of file + - uses: actions/checkout@v2 + - name: Install modules + run: npm install + - name: Run tests + run: npm run test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d18260 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +public/old/* \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..12637cf --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,5 @@ +trailingComma: 'es5' +tabWidth: 2 +semi: true +singleQuote: true +printWidth: 100 diff --git a/.vscode/launch.json b/.vscode/launch.json index 9f9f828..a5bd05e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,30 +1,30 @@ { - "version": "0.2.0", - "configurations": [ + "version": "0.2.0", + "configurations": [ + { + "preLaunchTask": "npm: dev", + "postDebugTask": "Terminate All Tasks", + "type": "chrome", + "request": "launch", + "name": "Debug Chrome", + "url": "http://localhost:5173/bookbinder-js/", + "webRoot": "${workspaceFolder}" + }, + { + "preLaunchTask": "npm: dev", + "postDebugTask": "Terminate All Tasks", + "type": "firefox", + "request": "launch", + "name": "Debug Firefox", + "url": "http://localhost:5173/bookbinder-js/", + "webRoot": "${workspaceFolder}", + "tmpDir": "${workspaceFolder}/tmp", + "pathMappings": [ { - "preLaunchTask": "npm: dev", - "postDebugTask": "Terminate All Tasks", - "type": "chrome", - "request": "launch", - "name": "Debug Chrome", - "url": "http://localhost:5173/bookbinder-js/", - "webRoot": "${workspaceFolder}", - }, - { - "preLaunchTask": "npm: dev", - "postDebugTask": "Terminate All Tasks", - "type": "firefox", - "request": "launch", - "name": "Debug Firefox", - "url": "http://localhost:5173/bookbinder-js/", - "webRoot": "${workspaceFolder}", - "tmpDir": "${workspaceFolder}/tmp", - "pathMappings": [ - { - "url": "http://localhost:5173/bookbinder-js/src", - "path": "${workspaceFolder}/src" - } - ] + "url": "http://localhost:5173/bookbinder-js/src", + "path": "${workspaceFolder}/src" } - ] + ] + } + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6cc7c13..e14ff7b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,45 +1,45 @@ { - "version": "2.0.0", - "inputs": [ - { - "id": "terminate", - "type": "command", - "command": "workbench.action.tasks.terminate", - "args": "terminateAll" - } - ], - "tasks": [ - { - "label": "Terminate All Tasks", - "command": "echo ${input:terminate}", - "type": "shell", - "problemMatcher": [] - }, - { - "type": "npm", - "isBackground": true, - "script": "dev", - "label": "npm: dev", - "detail": "vite --port 5173", - // All this is needed so that VSCode doesn't wait for the prelaunch - // task to complete and just lets it run in the background - "problemMatcher": [ - { - "pattern": [ - { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": ".", - "endsPattern": ".", - } - } - ] - } - ] + "version": "2.0.0", + "inputs": [ + { + "id": "terminate", + "type": "command", + "command": "workbench.action.tasks.terminate", + "args": "terminateAll" + } + ], + "tasks": [ + { + "label": "Terminate All Tasks", + "command": "echo ${input:terminate}", + "type": "shell", + "problemMatcher": [] + }, + { + "type": "npm", + "isBackground": true, + "script": "dev", + "label": "npm: dev", + "detail": "vite --port 5173", + // All this is needed so that VSCode doesn't wait for the prelaunch + // task to complete and just lets it run in the background + "problemMatcher": [ + { + "pattern": [ + { + "regexp": ".", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".", + "endsPattern": "." + } + } + ] + } + ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2958c5..30231b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,22 @@ # How to Contribute to This Project + If you require more detailed instructions on any of these steps, please see the [detailed guide](/docs/contributing-details.md). + ## Getting Started + 1. Prerequisites: [`node`, `npm`,](https://nodejs.org/en/download/) and [`git`](https://docs.github.com/en/get-started/quickstart/set-up-git), as well as a web browser, somewhere to edit your code, and a way to run commands in the terminal 1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the repository, [clone](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository) down your copy, and navigate to the project directory (`bookbinder-js` unless you've renamed it) 1. Set the parent repository as the upstream remote for your project with `git remote add upstream https://github.com/momijizukamori/bookbinder-js.git` 1. Run `npm i` to install project dependencies ## Running the App + 1. Run the webpack build step with `npm run build` - this will update the generated `preload.js` file with the latest code from `src/` 1. At present, this project does not automatically rebuild to reflect your changes. If you have made changes to files in `src/` you will need to run `npm run build` to see them reflected in the app. 1. Open `index.html` in your browser and you're all set! (if you're not using a tool like VS Code's [LiveServer](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) extension you will need to refresh the page to see changes you've made) ## Branching and Committing + 1. Create a feature branch with `git checkout -b` for whatever you're working on, such as `feature/contribution-docs` or `bugfix/page-layout-issues` 1. Commit your changes as you go with meaningful commit messages 1. Ideally, write tests for any new behavior you're introducting @@ -19,10 +24,11 @@ If you require more detailed instructions on any of these steps, please see the 1. Push your changes up to the feature branch on your fork as you go with `git push origin [name of branch]`, for example, `git push origin feature/contribution-docs` ## Pull Requests + 1. When you're ready to make a request for your changes to be merged into the parent repository, [open a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request): - - first, [make sure your fork is still up-to-date](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-from-the-command-line) with the latest version of the upstream repository with `git merge upstream/main` - - address any conflicts that may arise with upstream updates (currently outside the scope of this guide, but you can read some docs [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts)). - - push all your latest changes, including any updates merged from upstream, up to your fork's feature branch as described above - - make sure tests are passing with `npm run test` - - open a pull request against the parent repository, as described in the link at the start of this section. Choose the parent repository's main branch as the 'base' and your fork's feature branch as the 'head'. Describe the changes you've made and review the changed files to make sure it's what you intended. See the following screenshot for an example: - ![pull request example](/docs/PR-example.png) + - first, [make sure your fork is still up-to-date](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-from-the-command-line) with the latest version of the upstream repository with `git merge upstream/main` + - address any conflicts that may arise with upstream updates (currently outside the scope of this guide, but you can read some docs [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts)). + - push all your latest changes, including any updates merged from upstream, up to your fork's feature branch as described above + - make sure tests are passing with `npm run test` + - open a pull request against the parent repository, as described in the link at the start of this section. Choose the parent repository's main branch as the 'base' and your fork's feature branch as the 'head'. Describe the changes you've made and review the changed files to make sure it's what you intended. See the following screenshot for an example: + ![pull request example](/docs/PR-example.png) diff --git a/README.md b/README.md index e1aeee5..a1956fa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # bookbinder-js + A JS application to format PDFs for bookbinding, based on [Bookbinder](http://quantumelephant.co.uk/bookbinder/bookbinder.html), rewritten to use [PDF-Lib](https://pdf-lib.js.org) as its backend library. Like Bookbinder, it is licensed under the [Mozilla Public License](https://www.mozilla.org/en-US/MPL/). ## Using + Go to [the project page](https://momijizukamori.github.io/bookbinder-js) to use the app online - saving a complete copy of the webpage will enable you to load it locally without a web connection, too. A very helpful guide on page size given layout and paper selection has been created and can be found [HERE](https://docs.google.com/spreadsheets/d/1Qi9Qlbd4QBj6lErnFaRe8rdBsrX0tD7cWf0iOW1V0Vs/edit#gid=0). @@ -9,21 +11,22 @@ A very helpful guide on page size given layout and paper selection has been crea Snapshot of the size chart as of 2022-08-11 ![Snapshot of sizes as of 2022-08-11](/docs/sizes_guide_snapshot_2022_08_11.png) - ## Building + ``` npm install npm run dev ``` + And load `index.html` in any modern web browser. ## Auditing Results In the [`/docs`](/docs) directory: - - There's 3 sample PDFs (and the `.tex` files to generate them) in [landscape](/docs/example_50cm_wide_10cm_tall.pdf)/[portrait](/docs/example_15cm_wide_40cm_tall.pdf)/[square proportions](/docs/example_20cm_square.pdf) filled with lorum ipsum and colored backgrounds to help test the positioning of the layouts. - - There's [a basic PDF](/docs/example_page_numbers.pdf) with just the numbers 1-120 writ large, used for figuring out page ordering. - - A basic export of the different layouts with proportional/snug settings have been recorded for comparison/reference as well as several shots of `centered` settings. These can be found in the [`/docs/examples`](/docs/examples) folder. +- There's 3 sample PDFs (and the `.tex` files to generate them) in [landscape](/docs/example_50cm_wide_10cm_tall.pdf)/[portrait](/docs/example_15cm_wide_40cm_tall.pdf)/[square proportions](/docs/example_20cm_square.pdf) filled with lorum ipsum and colored backgrounds to help test the positioning of the layouts. +- There's [a basic PDF](/docs/example_page_numbers.pdf) with just the numbers 1-120 writ large, used for figuring out page ordering. +- A basic export of the different layouts with proportional/snug settings have been recorded for comparison/reference as well as several shots of `centered` settings. These can be found in the [`/docs/examples`](/docs/examples) folder. Snapshot of layout proof summary as of 2022-08-14 ![Snapshot of layout proof summary 2022-08-14](/docs/examples_summary_snapshot_2022_08_14.png) diff --git a/docs/contribution-help.md b/docs/contribution-help.md index 5771972..69a63bb 100644 --- a/docs/contribution-help.md +++ b/docs/contribution-help.md @@ -1,17 +1,19 @@ # Contribution Help + This guide is intended as a supplement to help folks who may be new to coding, the GitHub ecosystem, or the collaboration process; it includes additional details to the process described in [CONTRIBUTING.md](../CONTRIBUTING.md). If you run into issues that aren't covered here, please feel free to add the steps you took and documentation you consulted to resolve them! It may be a big help to a future coder out there. Detailed instructions for tool installation and machine setup are outside the scope of this guide, but please feel free to reach out if you need assistance! - If you're unsure whether you have the right tools installed, you can confirm you have these installed by running the following in your terminal: `node --version`, `npm --version`, and `git --version` - - if you do not see a version number as output, you will need to install the relevant tool, see links in [CONTRIBUTING.md](../CONTRIBUTING.md) (note: `npm` is generally part of the `node` installation and does not need to be installed separately) + + - if you do not see a version number as output, you will need to install the relevant tool, see links in [CONTRIBUTING.md](../CONTRIBUTING.md) (note: `npm` is generally part of the `node` installation and does not need to be installed separately) - You can confirm your remotes are properly configured by running `git remote -v`; this should show your fork as the 'origin' repository and the parent as the 'upstream' repository, as in the following screenshot: - !["upstream remote example"](./upstream-remote-example.png) + !["upstream remote example"](./upstream-remote-example.png) - If you are not acquainted with `git`, see general setup and practices [in GitHub's docs](https://docs.github.com/en/get-started/quickstart/set-up-git). As mentioned above, a detailed Git tutorial is outside the scope of this guide, but feel free to reach out for assistance if you'd like to contribute but aren't sure how to get started. - Some notes on adding changes and making commits: - - Add changes to your branch with `git add -A` - - Commit changes with `git commit -m 'your commit message here'` - - Commit messages should describe your changes succinctly, for example, `git commit -m 'add contributor documentation'` + - Add changes to your branch with `git add -A` + - Commit changes with `git commit -m 'your commit message here'` + - Commit messages should describe your changes succinctly, for example, `git commit -m 'add contributor documentation'` diff --git a/index.html b/index.html index b059e65..10838ec 100644 --- a/index.html +++ b/index.html @@ -2,101 +2,117 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at https://mozilla.org/MPL/2.0/. --> - + - - + Bookbinder - - - + + + - - - + + + - +
-
-

Bookbinder JS

- A Javascript-based app for formatting PDFs for bookbinding-- a process called imposition. For more information, feature requests, or to - contribute, view the project's Github repository. - If you have issues with this version, you can try the old version instead, which only supports folio layouts. -
current version: %PACKAGE_VERSION%
+
+

Bookbinder JS

+ A Javascript-based app for formatting PDFs for bookbinding-- a process called imposition. + For more information, feature requests, or to contribute, view the project's + Github repository. If you have + issues with this version, you can try the + old version instead, which only supports folio + layouts. +
+ current version: %PACKAGE_VERSION% +
+
+
+
+

File Info

+ + + + While some PDF viewers display the first two pages as a spread, be aware the first + page, which is odd, will be on the right when printed. The first printed spread are + pages 2 & 3 of your PDF document. Evens always on the left, odds always on the + right. + + +
- -
-

File Info

- - - - While some PDF viewers display the first two pages as a spread, be aware the first page, which is odd, will be on the right when printed. The first printed spread are pages 2 & 3 of your PDF document. Evens always on the left, odds always on the right. - - - -
-
-

Source Manipulation

- - -
-
- -

-
- For books that read like a standard novel
- no rotation of pages applied -

-
- - - - -
+
+

Source Manipulation

+ + +
+
+ +

+
+ For books that read like a standard novel
+ no rotation of pages applied +

+
+ + + + +
+
- -
-

Printer

- - - - - - - - - x - (in Points) ℹ️ - +
+

Printer

+ + + + + + + + + + x + + (in Points) + ℹ️ + - - - ℹ️ - - - - ℹ️ - + + + ℹ️ + + + + ℹ️ + - NOTE: Not currently working for folios, sorry! Use the old version for now. - + + + NOTE: Not currently working for folios, sorry! Use the + old version + for now. +
+
+

Page Layout

+ + +
+ + + +
+
+
+ +
+ Folding instructions for quarto/octavo +

+ Folding these two layouts is slightly more complex than the simple single-fold + folio. Both of these have been designed so that the first fold is parallel to the + short edge of the paper - eg, if you hold the page in portrait orientation, you will + be folding the top edge to the bottom. One side of the page will have consecutive + numbers across this fold (ie, 3 and 30 on the top row, 2 and 31). Fold inward across + this line (so that 3 has been folded to lay against 2, and 30 against 31). The next + fold is along the other paper axis (parallel to the original long edge). For quarto, + you want to find the side with the numbers closest together, and fold inwards to + match them (this will be consecutive on the innermost sheet). For octavo, look at + the bottom-most row and find the consecutive pair, then fold inward. Octavo has one + last fold, for which you want to find the pair remaning that are closest, and fold + them together. +

+

+ Or, if you want to just memorize the order, once you find the first fold side, the + order is:
+ quarto - top to bottom, fold points away from you, left to right, fold points + towards you.
+ octavo - top to bottom, fold points away from you, left to right, fold points + away from you, top to bottom, fold points towards you. +

+
-
-

Page Layout

- - -
- - - -
- -
-
- -
- Folding instructions for quarto/octavo -

- Folding these two layouts is slightly more complex than the simple single-fold folio. - Both of these have been designed so that the first fold is parallel to the short edge of the paper - eg, if you hold the page in portrait orientation, you will be folding the top edge to the bottom. - One side of the page will have consecutive numbers across this fold (ie, 3 and 30 on the top row, 2 and 31). Fold inward across this line (so that 3 has been folded to lay against 2, and 30 against 31). - The next fold is along the other paper axis (parallel to the original long edge). For quarto, you want to find the side with the numbers closest together, and fold inwards to match them (this will be consecutive on the innermost sheet). - For octavo, look at the bottom-most row and find the consecutive pair, then fold inward. Octavo has one last fold, for which you want to find the pair remaning that are closest, and fold them together. -

- Or, if you want to just memorize the order, once you find the first fold side, the order is:
- quarto - top to bottom, fold points away from you, left to right, fold points towards you.
- octavo - top to bottom, fold points away from you, left to right, fold points away from you, top to bottom, fold points towards you. -

-
- - - - + + + + - - - + + + + - -
- White Space Manipulation. All values are in points, relative to original document.

1 point = 1/72 inch -
-
- Fore-Edge Margin: pt
- Binding Margin: pt
- Top Margin: pt
- Bottom Margin: pt
-
-
- - -
spine
-
-
- Paper/Layout Dimensions - layout:
- max page size:
-
-
- PDF Dimensions
- source:
- on page:
- scaling:
- offset:

- rotated:
-
- (all dimensions are in Points) + +
+ White Space Manipulation. All values are in points, relative to original document.

1 + point = 1/72 inch +
+
+ Fore-Edge Margin: + pt
+ Binding Margin: + pt
+ Top Margin: + pt
+ Bottom Margin: + pt
+
+
-
+ +
spine
+
+
+ Paper/Layout Dimensions + layout:
+ max page size: +
+
+
+ PDF Dimensions
+ source:
+ on page:
+ scaling:
+ offset: +
+
+
+
+ rotated:
+
+ (all dimensions are in Points) +
-
-
-
-
+
+
+
+
-
-

Signature Format

+
+

Signature Format

- - - - - - - length - ℹ️ - - - ℹ️ - -
- A note on calculating signature lengths -

- In this code, the length of a signature is the number of full-sized sheets of paper per signature. For a folio book, this means that the number of folds you punch through when sewing together a signature is the same as the number of sheets in the signature, as each sheet has one fold. For quarto, this will be two folds for each sheet - octavo, four; sextodecimo, eight. Calculate your signature lengths accordingly (in particular, you almost certainly don't want more than one sheet for sextodecimo). -

-
+ + + + + + + + + - length + ℹ️ + + + + + + ℹ️ + +
+ A note on calculating signature lengths +

+ In this code, the length of a signature is the number of full-sized sheets of paper + per signature. For a folio book, this means that the number of folds you punch + through when sewing together a signature is the same as the number of sheets in the + signature, as each sheet has one fold. For quarto, this will be two folds for each + sheet - octavo, four; sextodecimo, eight. Calculate your signature lengths + accordingly (in particular, you almost certainly don't want more than one sheet for + sextodecimo). +

+
-

Wacky Small Layouts

- These layouts contain one or more signature per printed page. They use only the "File Info" and "Printer" settings. The names are arbitrary and hold no standardized meaning. - Fore-Edge Padding:
pt
- - - -
- - -
- - - - -
- Instructions -

- Cut along the lines or cut just extra bit off the top (along exposed "top" of page) and then fold up along the dotted lines (if trimming, you can just trim off the cruft at the bottom-- tops need to be even for leveling purposes however). Outer margins intentionally under-marked so you can trim wherever feels right after folding & assembly. -

-

- Not the most paper efficient of layouts, but it's optimized for organization (if a signature is just one page, harder to botch the ordering) and many, smaller signatures make for rounder backs and large swell in my [six's] opinion (which helps w/ nice shoulders, if you're into that sort of thing) -

-
-
- - - - -
- Instructions -

-

- Cut the vertical center line. You can also cut the horizontal lines if you like, or just fold it up as detailed above. -

-
-
- - - - -
- Instructions -

- The classic no-cut, single page zine! Just fold it up right and you've got yourself a mutant non-standard folio making an 8 page little fellow-- all extra pages beyond 8 are discarded. For better folding instructions than I could possibly write, see: https://www.quarantinepubliclibrary.com/tutorial -

-
-
- - - - -
- Instructions -

- THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" (the non-typical flipping) -
- Remember to do a single-sheet test print if it's your first time to make sure duplex/mirroring is correct! -

- Cut out the rows. Fold up the folios like a zig-zag (two per face). Then fold the center of the text (smoosh the two matching page numbers together). -

- Note that when folding, it's important for the center of the folios to line up. That's what I measure against (less so fretting about folding exactly at the edge). Remember! Printer skew is a real thing! -

-
-
- - - - -
- Instructions -

-

- Cut along the cut lines. Fold up the 3 folio signatures, then the 4 folio. -

-
- - - - -
- Instructions -

- THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" (the non-typical flipping) -
- Remember to do a single-sheet test print if it's your first time to make sure duplex/mirroring is correct! -

- Cut out the rows. Fold up the folios like a zig-zag (two per face). Then fold the center of the text (smoosh the two matching page numbers together). -

- Note that when folding, it's important for the center of the folios to line up. That's what I measure against (less so fretting about folding exactly at the edge). Remember! Printer skew is a real thing! -

-
-
- - - -
- Instructions -

- THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" (the non-typical flipping) -
- Thankfully most small layouts fit on one page-- easy enough to do a test print and make sure you're duplexing correctly (if that's your route) -

-

- Cut out the rows. Fold up the folios like a big zig-zag (two per face). Then fold the first 2 folios into a signature and the last 3 folios into a signature. I often cut them apart only when I'm stitching things together. -

- Note that when folding, it's important for the center of the folios to line up. That's what I measure against (less so fretting about folding exactly at the edge). Remember! Printer skew is a real thing! -

-
-
-
-

Flyleafs

- - ℹ️ - -
-
-

Signature Info

- +

Wacky Small Layouts

+ These layouts contain one or more signature per printed page. They use only the "File + Info" and "Printer" settings. The names are arbitrary and hold no standardized + meaning. + Fore-Edge Padding:
+ pt
+ + + +
+ + +
+ + + + + +
+ Instructions +

+ Cut along the lines or cut just extra bit off the top (along exposed "top" of + page) and then fold up along the dotted lines (if trimming, you can just trim off + the cruft at the bottom-- tops need to be even for leveling purposes however). Outer + margins intentionally under-marked so you can trim wherever feels right after + folding & assembly. +

+

+ Not the most paper efficient of layouts, but it's optimized for organization (if a + signature is just one page, harder to botch the ordering) and many, smaller + signatures make for rounder backs and large swell in my [six's] opinion (which helps + w/ nice shoulders, if you're into that sort of thing) +

+
+
+ + + + + +
+ Instructions +

- +
-
- Total Pages: - Original PDF Pages: - -
-
- Total Sheets: - Number of Signatures: -
-
-
- Signature Arrangement: -
-
-
- -
-
- - - -
- -
- + Cut the vertical center line. You can also cut the horizontal lines if you like, or + just fold it up as detailed above. +

+ + + + + + + +
+ Instructions +

+ The classic no-cut, single page zine! Just fold it up right and you've got yourself + a mutant non-standard folio making an 8 page little fellow-- all extra pages beyond + 8 are discarded. For better folding instructions than I could possibly write, see: + https://www.quarantinepubliclibrary.com/tutorial +

+
+
+ + + + + +
+ Instructions +

+ + THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" + (the non-typical flipping) +
+ Remember to do a single-sheet test print if it's your first time to make sure + duplex/mirroring is correct! +

+

+ Cut out the rows. Fold up the folios like a zig-zag (two per face). Then fold the + center of the text (smoosh the two matching page numbers together). +

+

+ Note that when folding, it's important for the + center of the folios to line up. That's what I measure against (less so + fretting about folding exactly at the edge). Remember! Printer skew is a real thing! +

+
+
+ + + + + +
+ Instructions +

+

+ +
+ Cut along the cut lines. Fold up the 3 folio signatures, then the 4 folio. +

+
+ + + + + +
+ Instructions +

+ + THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" + (the non-typical flipping) +
+ Remember to do a single-sheet test print if it's your first time to make sure + duplex/mirroring is correct! +

+

+ Cut out the rows. Fold up the folios like a zig-zag (two per face). Then fold the + center of the text (smoosh the two matching page numbers together). +

+

+ Note that when folding, it's important for the + center of the folios to line up. That's what I measure against (less so + fretting about folding exactly at the edge). Remember! Printer skew is a real thing! +

+
+
+ + + + +
+ Instructions +

+ + THIS IS A LANDSCAPE LAYOUT. IT MUST BE PRINTED AND "flipped on the short side" + (the non-typical flipping) +
+ Thankfully most small layouts fit on one page-- easy enough to do a test print and + make sure you're duplexing correctly (if that's your route) +

+

+

+ +
+ Cut out the rows. Fold up the folios like a big zig-zag (two per face). Then fold + the first 2 folios into a signature and the last 3 folios into a signature. I often + cut them apart only when I'm stitching things together. +

+

+ Note that when folding, it's important for the + center of the folios to line up. That's what I measure against (less so + fretting about folding exactly at the edge). Remember! Printer skew is a real thing! +

+
+
+
+

Flyleafs

+ + + ℹ️ + +
+
+

Signature Info

+
+ +
+
+ Total Pages: + Original PDF Pages: +
+
+ Total Sheets: + Number of Signatures: +
+
+
Signature Arrangement:
+
+
+
+
+ + + +
+ +
+ diff --git a/package-lock.json b/package-lock.json index d9b9d80..0b20c7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bookbinder", - "version": "1.2.0", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bookbinder", - "version": "1.2.0", + "version": "1.3.4", "license": "MPL", "dependencies": { "file-saver": "^2.0.5", @@ -16,10 +16,21 @@ "zod": "^3.22.4" }, "devDependencies": { + "eslint": "^8.56.0", + "prettier": "^3.2.5", "vite": "^5.0.10", "vitest": "^1.0.4" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.9.tgz", @@ -350,6 +361,95 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -426,6 +526,41 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", @@ -625,6 +760,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitest/expect": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.0.4.tgz", @@ -812,6 +953,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -837,6 +987,52 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -854,6 +1050,22 @@ "optional": true, "peer": true }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -878,6 +1090,15 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -896,6 +1117,22 @@ "node": ">=4" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -908,6 +1145,24 @@ "node": "*" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -929,6 +1184,12 @@ "optional": true, "peer": true }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1035,9 +1296,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -1050,6 +1309,18 @@ "node": ">=0.4.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -1111,6 +1382,18 @@ "@esbuild/win32-x64": "0.19.9" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -1135,15 +1418,155 @@ "source-map": "~0.6.1" } }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "optional": true, - "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -1161,30 +1584,128 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, - "optional": true, - "peer": true + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } }, "node_modules/file-saver": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -1201,6 +1722,12 @@ "node": ">= 6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1211,16 +1738,78 @@ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": "*" + "node": ">=8" } }, "node_modules/html-encoding-sniffer": { @@ -1282,16 +1871,90 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -1311,6 +1974,18 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -1359,6 +2034,24 @@ } } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -1376,6 +2069,15 @@ "setimmediate": "^1.0.5" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1415,6 +2117,21 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1423,6 +2140,12 @@ "optional": true, "peer": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -1481,6 +2204,18 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -1516,6 +2251,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -1524,6 +2265,15 @@ "optional": true, "peer": true }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -1558,11 +2308,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -1571,6 +2375,24 @@ "optional": true, "peer": true }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1660,6 +2482,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1678,12 +2515,30 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -1698,6 +2553,40 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz", @@ -1726,6 +2615,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1834,6 +2746,30 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", @@ -1846,6 +2782,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -1873,6 +2821,12 @@ "node": ">=10" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/tinybench": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", @@ -1955,6 +2909,18 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ufo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", @@ -1979,6 +2945,15 @@ "node": ">= 4.0.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2391,6 +3366,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/ws": { "version": "7.5.7", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", diff --git a/package.json b/package.json index 731a8b9..408557a 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,25 @@ "dev": "vite --port 5173", "build": "vite build", "preview": "vite preview", - "test": "vitest" + "test": "vitest", + "lint": "npx eslint src", + "lint:fix": "npx eslint src --fix", + "prettier": "npx prettier --check .", + "prettier:fix": "npx prettier --write ." }, "author": "momijizukamori@gmail.com", "license": "MPL", "devDependencies": { + "eslint": "^8.56.0", + "prettier": "^3.2.5", "vite": "^5.0.10", + "vite-plugin-package-version": "^1.1.0", "vitest": "^1.0.4" }, "dependencies": { "file-saver": "^2.0.5", "jszip": "^3.7.1", "pdf-lib": "^1.16.0", - "zod": "^3.22.4", - "vite-plugin-package-version": "^1.1.0" + "zod": "^3.22.4" } } diff --git a/public/styles.css b/public/styles.css index 5f4f275..6873022 100644 --- a/public/styles.css +++ b/public/styles.css @@ -5,184 +5,180 @@ */ * { - --background: #e6e6e5; - --text: #292929; - --disabled-text: gray; - --link: #1616ad; + --background: #e6e6e5; + --text: #292929; + --disabled-text: gray; + --link: #1616ad; - box-sizing: border-box; + box-sizing: border-box; } -@media (prefers-color-scheme:dark) { - * { - --background: #292929; - --text: #e6e6e5; - --disabled-text: rgba(211, 211, 211, 0.5); - --link: skyblue; - } +@media (prefers-color-scheme: dark) { + * { + --background: #292929; + --text: #e6e6e5; + --disabled-text: rgba(211, 211, 211, 0.5); + --link: skyblue; + } } body { - font-family: Helvetica, Arial, sans-serif; - background-color: var(--background); - color: var(--text); + font-family: Helvetica, Arial, sans-serif; + background-color: var(--background); + color: var(--text); } -a:link, a:visited { - color: var(--link); +a:link, +a:visited { + color: var(--link); } *:disabled { - color: var(--disabled-text); + color: var(--disabled-text); } -h1, h2 { - margin-top: 0; +h1, +h2 { + margin-top: 0; } -body, .wrapper, #bookbinder { - display: flex; - flex-direction: column; - align-items: center; +body, +.wrapper, +#bookbinder { + display: flex; + flex-direction: column; + align-items: center; } button { - border: none; - border-radius: 2px; - padding: 4px; - text-align: center; - display: inline-block; - font-size: 16px; + border: none; + border-radius: 2px; + padding: 4px; + text-align: center; + display: inline-block; + font-size: 16px; } .large_btn { - height: 48px; - font-size: 24px; - + height: 48px; + font-size: 24px; } .medium_btn { - width: 90px; - height: 36px; - font-size: 12px; - + width: 90px; + height: 36px; + font-size: 12px; } .wrapper { - max-width: 500px; + max-width: 500px; } .section { - border: 1px solid var(--text); - padding: 0.5em; - margin: 0.5em auto; - width: 100%; + border: 1px solid var(--text); + padding: 0.5em; + margin: 0.5em auto; + width: 100%; } .row { - display: block; + display: block; } .hidden { - display: none; + display: none; } .instruction-indent { - margin-left: 25px; + margin-left: 25px; } .layout_margin_user_input { - width:225px; - margin-top:15px; - margin-bottom:15px; - text-align: right; + width: 225px; + margin-top: 15px; + margin-bottom: 15px; + text-align: right; } .layout_margin_user_input_field { - width: 4em; - margin-left: 10px; + width: 4em; + margin-left: 10px; } .layout_margin_description { - width:225px; - position: absolute; - margin-left: 250px; - font-size: smaller; + width: 225px; + position: absolute; + margin-left: 250px; + font-size: smaller; } #show_layout_info { - display: none; - margin-left: 10px; - margin-bottom: 20px; + display: none; + margin-left: 10px; + margin-bottom: 20px; } .layout_spine_label { - transform: rotate(-90deg); - position: absolute; - text-align: center; - padding: 0px; - height: 40px; - margin: 105px -123px; - width: 250px; - font-family: monospace; - letter-spacing: 14px; + transform: rotate(-90deg); + position: absolute; + text-align: center; + padding: 0px; + height: 40px; + margin: 105px -123px; + width: 250px; + font-family: monospace; + letter-spacing: 14px; } .grid_layout_page { - border: 1px solid purple; - width: 250px; - height: 250px; - margin-left: 0px; - background: #80008063; - pointer-events: none; + border: 1px solid purple; + width: 250px; + height: 250px; + margin-left: 0px; + background: #80008063; + pointer-events: none; } .pdf_layout_page { - border: 1px solid orange; - width: 150px; - height: 24px; - position: absolute; - margin: 0px; - padding: 0px; - background: #ffa50042; - pointer-events: none; + border: 1px solid orange; + width: 150px; + height: 24px; + position: absolute; + margin: 0px; + padding: 0px; + background: #ffa50042; + pointer-events: none; } .layout_margin_info { - position: absolute; - padding-left: 255px; - width: 480px; - font-size: small; -} -.stripes_paper{ - background: repeating-linear-gradient( - 45deg, - #606dbc, - #606dbc 10px, - #465298 10px, - #465298 20px - ); + position: absolute; + padding-left: 255px; + width: 480px; + font-size: small; +} +.stripes_paper { + background: repeating-linear-gradient(45deg, #606dbc, #606dbc 10px, #465298 10px, #465298 20px); } .stripes_pdf { - background: repeating-linear-gradient( - 133deg, - #ffeb3b3b, - #ffc1078c 10px, - #ff9800 10px, - #FF9800 15px - ); + background: repeating-linear-gradient( + 133deg, + #ffeb3b3b, + #ffc1078c 10px, + #ff9800 10px, + #ff9800 15px + ); } .stripes_sample_box { - width:20px; - height:20px; - display:inline-block; + width: 20px; + height: 20px; + display: inline-block; } .pdf_offset_dimensions_box { - padding-left: 10px; - display: inline-block; - vertical-align: top; + padding-left: 10px; + display: inline-block; + vertical-align: top; } .float_left { - float: left; + float: left; } .space_out { - display: flex; - width: 100%; - justify-content: space-between; + display: flex; + width: 100%; + justify-content: space-between; } diff --git a/src/book.js b/src/book.js index 3c9fc41..977e2e0 100644 --- a/src/book.js +++ b/src/book.js @@ -1,1046 +1,1152 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { PDFDocument, degrees, grayscale, rgb } from 'pdf-lib'; +import { PDFDocument, degrees, rgb } from 'pdf-lib'; import { saveAs } from 'file-saver'; import { Signatures } from './signatures.js'; import { PerfectBound } from './perfectbound.js'; import { WackyImposition } from './wacky_imposition.js'; import { PAGE_LAYOUTS, PAGE_SIZES, LINE_LEN } from './constants.js'; -import { updatePageLayoutInfo} from './utils/renderUtils.js'; +import { updatePageLayoutInfo } from './utils/renderUtils.js'; import JSZip from 'jszip'; import { loadConfiguration } from './utils/formUtils.js'; export class Book { - /** @param { import("./models/configuration.js").Configuration } configuration */ - constructor(configuration) { - this.inputpdf = null; // string with pdf filepath - this.password = null; // if necessary - - this.managedDoc = null; // original PDF with the pages rotated per source_rotation - use THIS for laying out pages - - this.signatureconfig = []; - - this.spineoffset = false; - - this.input = null; // opened pdf file - this.currentdoc = null; // uploaded PDF [Itext PDFReader object] untouched by source_rotation - use managedDoc for layout - this.pagecount = null; - this.cropbox = null; - - this.orderedpages = []; // ordered list of page numbers (consecutive) - this.rearrangedpages = []; // reordered list of page numbers (signatures etc.) - this.filelist = []; // list of ouput filenames and path - this.zip = null; - - this.update(configuration); + /** @param { import("./models/configuration.js").Configuration } configuration */ + constructor(configuration) { + this.inputpdf = null; // string with pdf filepath + this.password = null; // if necessary + + this.managedDoc = null; // original PDF with the pages rotated per source_rotation - use THIS for laying out pages + + this.signatureconfig = []; + + this.spineoffset = false; + + this.input = null; // opened pdf file + this.currentdoc = null; // uploaded PDF [Itext PDFReader object] untouched by source_rotation - use managedDoc for layout + this.pagecount = null; + this.cropbox = null; + + this.orderedpages = []; // ordered list of page numbers (consecutive) + this.rearrangedpages = []; // reordered list of page numbers (signatures etc.) + this.filelist = []; // list of ouput filenames and path + this.zip = null; + + this.update(configuration); + } + + /** @param { import("./models/configuration.js").Configuration } configuration */ + update(configuration) { + this.duplex = configuration.printerType === 'duplex'; + this.duplexrotate = configuration.rotatePage; + this.paper_rotation_90 = configuration.paperRotation90; + this.papersize = PAGE_SIZES[configuration.paperSize]; + if (configuration.paperRotation90) { + this.papersize = [this.papersize[1], this.papersize[0]]; } - /** @param { import("./models/configuration.js").Configuration } configuration */ - update(configuration) { - this.duplex = configuration.printerType === 'duplex'; - this.duplexrotate = configuration.rotatePage; - this.paper_rotation_90 = configuration.paperRotation90; - this.papersize = PAGE_SIZES[configuration.paperSize]; - if (configuration.paperRotation90) { - this.papersize = [this.papersize[1], this.papersize[0]] - } + this.source_rotation = configuration.sourceRotation; + this.print_file = configuration.printFile; + this.page_scaling = configuration.pageScaling; + this.page_positioning = configuration.pagePositioning; + this.flyleafs = configuration.flyleafs; + this.cropmarks = configuration.cropMarks; + this.pdfEdgeMarks = configuration.pdfEdgeMarks; + this.cutmarks = configuration.cutMarks; + this.format = configuration.sigFormat; + if (configuration.sigFormat === 'standardsig') { + this.sigsize = configuration.sigLength; + } + this.customsig = this.format === 'customsig'; + if (this.customsig) { + this.signatureconfig = configuration.customSigLength; + } - this.source_rotation = configuration.sourceRotation; - this.print_file = configuration.printFile; - this.page_scaling = configuration.pageScaling; - this.page_positioning = configuration.pagePositioning; - this.flyleafs = configuration.flyleafs; - this.cropmarks = configuration.cropMarks; - this.pdfEdgeMarks = configuration.pdfEdgeMarks; - this.cutmarks = configuration.cutMarks; - this.format = configuration.sigFormat; - if (configuration.sigFormat === "standardsig") { - this.sigsize = configuration.sigLength; - } - this.customsig = this.format === 'customsig'; - if (this.customsig) { - this.signatureconfig = configuration.customSigLength; + const pageLayout = PAGE_LAYOUTS[configuration.pageLayout]; + + this.page_layout = pageLayout; + this.per_sheet = pageLayout.per_sheet; + this.pack_pages = configuration.wackySpacing === 'wacky_pack'; + this.fore_edge_padding_pt = configuration.foreEdgePaddingPt; + + this.padding_pt = { + top: configuration.topEdgePaddingPt, + bottom: configuration.bottomEdgePaddingPt, + binding: configuration.bindingEdgePaddingPt, + fore_edge: configuration.mainForeEdgePaddingPt, + }; + } + + /** + * Populates [this.currentdoc] from user's file system + */ + async openpdf(file) { + this.inputpdf = file.name; + this.input = await file.arrayBuffer(); //fs.readFileSync(filepath); + this.currentdoc = await PDFDocument.load(this.input); + //TODO: handle pw-protected PDFs + const pages = this.currentdoc.getPages(); + this.cropbox = null; + + //FIXME: dumb hack because you can't embed blank pdf pages without errors. + pages.forEach((page) => { + if (!page.node.Contents()) { + page.drawLine({ + start: { x: 25, y: 75 }, + end: { x: 125, y: 175 }, + opacity: 0.0, + }); + } else { + if (!this.cropbox) { + const cropBox = page.getCropBox(); + const bleedBox = page.getBleedBox(); + const trimBox = page.getTrimBox(); + const artBox = page.getArtBox(); + console.log( + `\n\tCropBox [${cropBox}]`, + `\n\tBleedBox [${bleedBox}]`, + `\n\tTrimBox [${trimBox}]`, + `\n\tArtBox [${artBox}]` + ); + this.cropbox = page.getCropBox(); } + } + }); + } + + /** + * Populates [this.orderedpages] (array [0, 1, ... this.page_sheets * # of sheets]) + */ + createpagelist() { + this.pagecount = this.currentdoc.getPageCount(); + this.orderedpages = Array.from({ length: this.pagecount }, (x, i) => i); + + for (let i = 0; i < this.flyleafs; i++) { + this.orderedpages.unshift('b'); + this.orderedpages.unshift('b'); + + this.orderedpages.push('b'); + this.orderedpages.push('b'); + } - const pageLayout = PAGE_LAYOUTS[configuration.pageLayout] + // padding calculations if needed + const pagetotal = this.orderedpages.length; - this.page_layout = pageLayout; - this.per_sheet = pageLayout.per_sheet; - this.pack_pages = configuration.wackySpacing === 'wacky_pack'; - this.fore_edge_padding_pt = configuration.foreEdgePaddingPt + // calculate how many sheets of paper the output document needs + let sheets = Math.floor(pagetotal / this.per_sheet); - this.padding_pt = { - 'top' : configuration.topEdgePaddingPt, - 'bottom': configuration.bottomEdgePaddingPt, - 'binding': configuration.bindingEdgePaddingPt, - 'fore_edge': configuration.mainForeEdgePaddingPt, - }; + // pad out end of document if necessary + if (pagetotal % this.per_sheet > 0) { + sheets += 1; + const padding = sheets * this.per_sheet - pagetotal; + for (let i = 0; i < padding; i++) { + this.orderedpages.push('b'); + } } - - /** - * Populates [this.currentdoc] from user's file system - */ - async openpdf(file) { - this.inputpdf = file.name; - this.input = await file.arrayBuffer(); //fs.readFileSync(filepath); - this.currentdoc = await PDFDocument.load(this.input); - //TODO: handle pw-protected PDFs - const pages = this.currentdoc.getPages(); - this.cropbox = null; - - //FIXME: dumb hack because you can't embed blank pdf pages without errors. - pages.forEach(page => { - if (!page.node.Contents()){ - page.drawLine({ - start: { x: 25, y: 75 }, - end: { x: 125, y: 175 }, - opacity: 0.0, - }); - } else { - if (!this.cropbox) { - - const cropBox = page.getCropBox(); - const bleedBox = page.getBleedBox(); - const trimBox = page.getTrimBox(); - const artBox = page.getArtBox(); - console.log("\n\tCropBox [",cropBox,"] \n\tBleedBox [",bleedBox,"] \n\tTrimBox [",trimBox,"] \n\tArtBox [",artBox,"]"); - this.cropbox = page.getCropBox(); - } - } + console.log(`Calculated pagecount [${this.pagecount}] and ordered pages: ${this.orderedpages}`); + } + + /** + * Populates [this.managedDoc] with potentially rotated pages + * & populates [this.book] with the correct Book instance + */ + async createpages() { + this.createpagelist(); + let pages; + [this.managedDoc, pages] = await this.embedPagesInNewPdf(this.currentdoc); + + for (var i = 0; i < pages.length; ++i) { + var page = pages[i]; + var newPage = this.managedDoc.addPage(); + var rotate90cw = + this.source_rotation == '90cw' || + (this.source_rotation == 'out_binding' && i % 2 == 0) || + (this.source_rotation == 'in_binding' && i % 2 == 1); + var rotate90ccw = + this.source_rotation == '90ccw' || + (this.source_rotation == 'out_binding' && i % 2 == 1) || + (this.source_rotation == 'in_binding' && i % 2 == 0); + if (this.source_rotation == 'none') { + newPage.setSize(page.width, page.height); + newPage.drawPage(page); + } else if (rotate90ccw) { + newPage.setSize(page.height, page.width); + newPage.drawPage(page, { + x: page.height, + y: 0, + rotate: degrees(90), }); - + } else if (rotate90cw) { + newPage.setSize(page.height, page.width); + newPage.drawPage(page, { + x: 0, + y: page.width, + rotate: degrees(-90), + }); + } else { + var e = new Error("??? what sorta' layout you think you're going to get?"); + console.error(e); + throw e; + } + page.embed(); + this.cropbox = newPage.getCropBox(); } - /** - * Populates [this.orderedpages] (array [0, 1, ... this.page_sheets * # of sheets]) - */ - createpagelist() { - this.pagecount = this.currentdoc.getPageCount(); - this.orderedpages = Array.from({ length: this.pagecount }, (x, i) => i); - - for (let i = 0; i < this.flyleafs; i++) { - this.orderedpages.unshift('b'); - this.orderedpages.unshift('b'); - - this.orderedpages.push('b'); - this.orderedpages.push('b'); - } - - // padding calculations if needed - let pagetotal = this.orderedpages.length; - - // calculate how many sheets of paper the output document needs - let sheets = Math.floor(pagetotal / this.per_sheet); + console.log( + 'The updatedDoc doc has : ', + this.managedDoc.getPages(), + ' vs --- ', + this.managedDoc.getPageCount() + ); + + switch (this.format) { + case 'perfect': + this.book = new PerfectBound( + this.orderedpages, + this.duplex, + this.per_sheet, + this.duplexrotate + ); + this.rearrangedpages = [this.book.pagelistdetails]; + break; + case 'booklet': + // Booklets are a special case where sig size is the total book size + this.sigsize = Math.ceil(this.orderedpages.length / this.per_sheet); + /* falls through */ + case 'standardsig': + case 'customsig': + this.book = new Signatures( + this.orderedpages, + this.duplex, + this.sigsize, + this.per_sheet, + this.duplexrotate + ); - // pad out end of document if necessary - if (pagetotal % this.per_sheet > 0) { - sheets += 1; - let padding = (sheets * this.per_sheet) - pagetotal; - for (let i = 0; i < padding; i++) { - this.orderedpages.push('b'); - } + if (this.customsig) { + this.book.setsigconfig(this.signatureconfig); + } else { + this.book.createsigconfig(); } - console.log("Calculated pagecount [",this.pagecount,"] and ordered pages: ", this.orderedpages); + this.rearrangedpages = this.book.pagelistdetails; + break; + case 'a9_3_3_4': + case 'a10_6_10s': + case 'A7_2_16s': + case '1_3rd': + case '8_zine': + case 'a_3_6s': + case 'a_4_8s': + this.book = new WackyImposition( + this.orderedpages, + this.duplex, + this.format, + this.pack_pages + ); + break; } - /** - * Populates [this.managedDoc] with potentially rotated pages - * & populates [this.book] with the correct Book instance - */ - async createpages() { - this.createpagelist(); - let pages; - [this.managedDoc, pages] = await this.embedPagesInNewPdf(this.currentdoc); - - for (var i = 0; i < pages.length; ++i) { - var page = pages[i]; - var newPage = this.managedDoc.addPage(); - var rotate90cw = this.source_rotation == '90cw' - || (this.source_rotation == 'out_binding' && i % 2 == 0) - || (this.source_rotation == 'in_binding' && i % 2 == 1); - var rotate90ccw = this.source_rotation == '90ccw' - || (this.source_rotation == 'out_binding' && i % 2 == 1) - || (this.source_rotation == 'in_binding' && i % 2 == 0); - if (this.source_rotation == 'none') { - newPage.setSize(page.width, page.height); - newPage.drawPage(page); - } else if (rotate90ccw) { - newPage.setSize(page.height, page.width); - newPage.drawPage(page, { - x: page.height, - y: 0, - rotate: degrees(90), - }); - } else if (rotate90cw) { - newPage.setSize(page.height, page.width); - newPage.drawPage(page, { - x: 0, - y: page.width, - rotate: degrees(-90), - }); - } else { - var e = new Error("??? what sorta' layout you think you're going to get?"); - console.error(e); - throw e; - } - page.embed(); - this.cropbox = newPage.getCropBox(); - } - - console.log("The updatedDoc doc has : ", this.managedDoc.getPages(), " vs --- ", this.managedDoc.getPageCount()); - - switch(this.format) { - case 'perfect': - this.book = new PerfectBound(this.orderedpages, this.duplex, this.per_sheet, this.duplexrotate); - this.rearrangedpages = [this.book.pagelistdetails]; - break; - case 'booklet': - // Booklets are a special case where sig size is the total book size - this.sigsize = Math.ceil(this.orderedpages.length / this.per_sheet); - /* falls through */ - case 'standardsig': - case 'customsig': - this.book = new Signatures(this.orderedpages, this.duplex, this.sigsize, this.per_sheet, this.duplexrotate); - - if (this.customsig) { - this.book.setsigconfig(this.signatureconfig); - } else { - this.book.createsigconfig(); - } - this.rearrangedpages = this.book.pagelistdetails; - break; - case 'a9_3_3_4': - case 'a10_6_10s': - case 'A7_2_16s': - case '1_3rd': - case '8_zine': - case 'a_3_6s': - case 'a_4_8s': - this.book = new WackyImposition(this.orderedpages, this.duplex, this.format, this.pack_pages); - break; + console.log('Created pages for : ', this.book); + const dim = this.calculate_dimensions(); + + updatePageLayoutInfo({ + dimensions: dim, + book: this.book, + perSheet: this.per_sheet, + papersize: this.papersize, + cropbox: this.cropbox, + managedDoc: this.managedDoc, + positions: this.calculatelayout(), + }); + } + + /** + * Calls the appropriate builder based on [this.format] + * to generate PDF & populate Previewer + * @param isPreview - if it's true we only generate preview content, if it's not true... we still + * generate preview content AND a downloadable zip + */ + async createoutputfiles(isPreview) { + const previewFrame = document.getElementById('pdf'); + previewFrame.style.display = 'none'; + let resultPDF = null; + + // create a directory named after the input pdf and fill it with + // the signatures + this.zip = new JSZip(); + var origFileName = this.inputpdf.replace(/\s|,|\.pdf/, ''); + this.filename = origFileName; + + if ( + this.format == 'perfect' || + this.format == 'booklet' || + this.format == 'standardsig' || + this.format == 'customsig' + ) { + const generateAggregate = this.print_file != 'signatures'; + const generateSignatures = this.print_file != 'aggregated'; + const side1PageNumbers = new Set( + this.rearrangedpages.reduce((accumulator, currentValue) => { + return accumulator.concat(currentValue[0]); + }, []) + ); + const [pdf0PageNumbers, pdf1PageNumbers] = + !generateAggregate || this.duplex + ? [null, null] + : [ + Array.from(Array(this.managedDoc.getPageCount()).keys()).map((p) => { + return side1PageNumbers.has(p) ? p : 'b'; + }), + Array.from(Array(this.managedDoc.getPageCount()).keys()).map((p) => { + return !side1PageNumbers.has(p) ? p : 'b'; + }), + ]; + const [aggregatePdf0, embeddedPages0] = generateAggregate + ? await this.embedPagesInNewPdf(this.managedDoc, pdf0PageNumbers) + : [null, null]; + const [aggregatePdf1, embeddedPages1] = + generateAggregate && !this.duplex + ? await this.embedPagesInNewPdf(this.managedDoc, pdf1PageNumbers) + : [null, null]; + const forLoop = async () => { + for (let i = 0; i < this.rearrangedpages.length; i++) { + const signature = this.rearrangedpages[i]; + await this.createsignatures({ + embeddedPages: generateAggregate ? [embeddedPages0, embeddedPages1] : null, + aggregatePdfs: generateAggregate ? [aggregatePdf0, aggregatePdf1] : null, + pageIndexDetails: signature, + id: generateSignatures ? `signature${i}` : null, + isDuplex: this.duplex, + fileList: this.filelist, + }); } + }; + await forLoop(); - console.log("Created pages for : ",this.book); - let dim = this.calculate_dimensions(); - - updatePageLayoutInfo({ - dimensions: dim, - book: this.book, - perSheet: this.per_sheet, - papersize: this.papersize, - cropbox: this.cropbox, - managedDoc: this.managedDoc, - positions: this.calculatelayout() + if (aggregatePdf1 != null) { + await aggregatePdf1.save().then((pdfBytes) => { + if (!isPreview) this.zip.file('aggregate_side2.pdf', pdfBytes); }); + } + if (aggregatePdf0 != null) { + await aggregatePdf0.save().then((pdfBytes) => { + if (!isPreview) + this.zip.file(this.duplex ? 'aggregate_book.pdf' : 'aggregate_side1.pdf', pdfBytes); + }); + } + var rotationMetaInfo = + (this.paper_rotation_90 ? '_paperRotated' : '') + + (this.source_rotation == 'none' ? '' : `_${this.source_rotation}`); + this.filename = `${origFileName}${rotationMetaInfo}`; + resultPDF = aggregatePdf0; + } else if (this.format == 'a9_3_3_4') { + resultPDF = await this.buildSheets(this.filename, this.book.a9_3_3_4_builder()); + } else if (this.format == 'a10_6_10s') { + resultPDF = await this.buildSheets(this.filename, this.book.a10_6_10s_builder()); + } else if (this.format == 'a_4_8s') { + resultPDF = await this.buildSheets(this.filename, this.book.a_4_8s_builder()); + } else if (this.format == 'a_3_6s') { + resultPDF = await this.buildSheets(this.filename, this.book.a_3_6s_builder()); + } else if (this.format == 'A7_2_16s') { + resultPDF = await this.buildSheets(this.filename, this.book.a7_2_16s_builder()); + } else if (this.format == '1_3rd') { + resultPDF = await this.buildSheets(this.filename, this.book.page_1_3rd_builder()); + } else if (this.format == '8_zine') { + resultPDF = await this.buildSheets(this.filename, this.book.page_8_zine_builder()); } - - /** - * Calls the appropriate builder based on [this.format] - * to generate PDF & populate Previewer - * @param isPreview - if it's true we only generate preview content, if it's not true... we still - * generate preview content AND a downloadable zip - */ - async createoutputfiles(isPreview) { - let previewFrame = document.getElementById('pdf'); - previewFrame.style.display = 'none'; - let resultPDF = null; - - // create a directory named after the input pdf and fill it with - // the signatures - this.zip = new JSZip(); - var origFileName = this.inputpdf.replace(/\s|,|\.pdf/, ''); - this.filename = origFileName; - - if (this.format == 'perfect' || this.format == 'booklet' || this.format == 'standardsig' || this.format == 'customsig') { - const generateAggregate = this.print_file != "signatures"; - const generateSignatures = this.print_file != "aggregated"; - const filename = (this.format == 'standardsig' || this.format == 'customsig') ? 'signatures' : this.format; - const side1PageNumbers = new Set(this.rearrangedpages.reduce((accumulator, currentValue) => { return accumulator.concat(currentValue[0]); },[])); - const [pdf0PageNumbers, pdf1PageNumbers] = (!generateAggregate || this.duplex) ? [null, null] - : [ - Array.from(Array(this.managedDoc.getPageCount()).keys()).map( p => { return (side1PageNumbers.has(p) ? p : 'b');}), - Array.from(Array(this.managedDoc.getPageCount()).keys()).map( p => { return (!side1PageNumbers.has(p) ? p : 'b');}) - ]; - const [aggregatePdf0, embeddedPages0] = (generateAggregate) ? await this.embedPagesInNewPdf(this.managedDoc, pdf0PageNumbers) : [null, null]; - const [aggregatePdf1, embeddedPages1] = (generateAggregate && !this.duplex) ? await this.embedPagesInNewPdf(this.managedDoc, pdf1PageNumbers) : [null, null]; - const forLoop = async _ => { - for (let i = 0; i < this.rearrangedpages.length; i++) { - let signature = this.rearrangedpages[i]; - await this.createsignatures({ - embeddedPages: (generateAggregate) ? [embeddedPages0, embeddedPages1] : null, - aggregatePdfs: (generateAggregate) ? [aggregatePdf0, aggregatePdf1] : null, - pageIndexDetails: signature, - id: (generateSignatures) ? `signature${i}` : null, - isDuplex: this.duplex, - fileList: this.filelist - }); - } - }; - await forLoop(); - - if (aggregatePdf1 != null) { - await aggregatePdf1.save().then(pdfBytes => { - if (!isPreview) - this.zip.file('aggregate_side2.pdf', pdfBytes); - }); - } - if (aggregatePdf0 != null) { - await aggregatePdf0.save().then(pdfBytes => { - if (!isPreview) - this.zip.file((this.duplex) ? 'aggregate_book.pdf' : 'aggregate_side1.pdf', pdfBytes); - }); - } - var rotationMetaInfo = ((this.paper_rotation_90) ? "_paperRotated" : "") - + ((this.source_rotation == 'none') ? "" : `_${this.source_rotation}`); - this.filename = `${origFileName}${rotationMetaInfo}`; - resultPDF = aggregatePdf0; - } else if (this.format == 'a9_3_3_4') { - resultPDF = await this.buildSheets(this.filename, this.book.a9_3_3_4_builder()); - } else if (this.format == 'a10_6_10s') { - resultPDF = await this.buildSheets(this.filename, this.book.a10_6_10s_builder()); - } else if (this.format == 'a_4_8s') { - resultPDF = await this.buildSheets(this.filename, this.book.a_4_8s_builder()); - } else if (this.format == 'a_3_6s') { - resultPDF = await this.buildSheets(this.filename, this.book.a_3_6s_builder()); - } else if (this.format == 'A7_2_16s') { - resultPDF = await this.buildSheets(this.filename, this.book.a7_2_16s_builder()); - } else if (this.format == '1_3rd') { - resultPDF = await this.buildSheets(this.filename, this.book.page_1_3rd_builder()); - } else if (this.format == '8_zine') { - resultPDF = await this.buildSheets(this.filename, this.book.page_8_zine_builder()); - } - console.log("Attempting to generate preview for ",resultPDF); - - if (resultPDF != null) { - const pdfDataUri = await resultPDF.saveAsBase64({ dataUri: true }); - const viewerPrefs = resultPDF.catalog.getOrCreateViewerPreferences(); - viewerPrefs.setHideToolbar(false); - viewerPrefs.setHideMenubar(false); - viewerPrefs.setHideWindowUI(false); - viewerPrefs.setFitWindow(true); - viewerPrefs.setCenterWindow(true); - viewerPrefs.setDisplayDocTitle(true); - - previewFrame.style.width = `450px`; - let height = this.papersize[1] / this.papersize[0] * 500; - previewFrame.style.height = `${height}px`; - previewFrame.style.display = ''; - previewFrame.src = pdfDataUri; - } - - if (!isPreview) - return this.saveZip(); - else - return Promise.resolve(1); - } - - /** - * @return the aggregate file w/ all the original pages embedded, but nothing placed - */ - async create_base_aggregate_files() { - return aggregatePdf; + console.log('Attempting to generate preview for ', resultPDF); + + if (resultPDF != null) { + const pdfDataUri = await resultPDF.saveAsBase64({ dataUri: true }); + const viewerPrefs = resultPDF.catalog.getOrCreateViewerPreferences(); + viewerPrefs.setHideToolbar(false); + viewerPrefs.setHideMenubar(false); + viewerPrefs.setHideWindowUI(false); + viewerPrefs.setFitWindow(true); + viewerPrefs.setCenterWindow(true); + viewerPrefs.setDisplayDocTitle(true); + + previewFrame.style.width = `450px`; + const height = (this.papersize[1] / this.papersize[0]) * 500; + previewFrame.style.height = `${height}px`; + previewFrame.style.display = ''; + previewFrame.src = pdfDataUri; } - /** - * Generates a new PDF & embeds the prescribed pages of the source PDF into it - * - * @param pageNumbers - an array of page numbers. Ex: [1,5,6,7,8,'b',10] or null to embed all pages from source - * NOTE: re-construction behavior kicks in if there's 'b's in the list - * - * @return [newPdf with pages embedded, embedded page array] - */ - async embedPagesInNewPdf(sourcePdf, pageNumbers) { - const newPdf = await PDFDocument.create(); - const needsReSorting = pageNumbers != null && pageNumbers.includes("b"); - if (pageNumbers == null) { - pageNumbers = Array.from(Array(sourcePdf.getPageCount()).keys()); - } else { - pageNumbers = pageNumbers.filter( p => {return typeof p === 'number';}); - } - let embeddedPages = await newPdf.embedPdf(sourcePdf, pageNumbers); - // what a gnarly little hack. Letting this sit for now -- - // --- downstream code requires embeds to be in their 'correct' index possition - // but we want to only embed half the pages for the aggregate single sides - // thus we expand the embedded pages to allow those gaps to return. This is gross & dumb but whatever... - if (needsReSorting) { - embeddedPages = embeddedPages.reduce((acc, curVal, curI) => { - acc[pageNumbers[curI]] = curVal; - return acc; - },[]); - } - return [newPdf, embeddedPages]; + if (!isPreview) return this.saveZip(); + else return Promise.resolve(1); + } + + /** + * Generates a new PDF & embeds the prescribed pages of the source PDF into it + * + * @param pageNumbers - an array of page numbers. Ex: [1,5,6,7,8,'b',10] or null to embed all pages from source + * NOTE: re-construction behavior kicks in if there's 'b's in the list + * + * @return [newPdf with pages embedded, embedded page array] + */ + async embedPagesInNewPdf(sourcePdf, pageNumbers) { + const newPdf = await PDFDocument.create(); + const needsReSorting = pageNumbers != null && pageNumbers.includes('b'); + if (pageNumbers == null) { + pageNumbers = Array.from(Array(sourcePdf.getPageCount()).keys()); + } else { + pageNumbers = pageNumbers.filter((p) => { + return typeof p === 'number'; + }); } - - /** - * Part of the Classic (non-Wacky) flow. Called by [createsignatures]. - * (conditionally) populates the destPdf and (conditionally) generates the outname PDF - * - * @param config - object /w the following parameters: - * - outname : name of pdf added to ongoing zip file. Ex: 'signature1duplex.pdf' (or null if no signature file needed) - * - pageList : objects that contain 3 values: { isSigStart: boolean, isSigEnd: boolean, info: either the page number or 'b'} - * - back : is 'back' of page (boolean) - * - alt : alternate pages (boolean) - * - destPdf : PDF to write to, in addition to PDF created w/ `outname` (or null) - * - providedPages : pages already embedded in the `destPdf` to assemble in addition (or null) - * @return reference to the new PDF created - */ - async writepages(config) { - const printSignatures = config.outname != null; - const printAggregate = config.providedPages != null && config.destPdf != null; - const pagelist = config.pageList; - const back = config.back; - let filteredList = []; - let blankIndices = []; - pagelist.forEach((pageInfo, i) => { - if (pageInfo.info != 'b') { - filteredList.push(pageInfo.info); - } else { - blankIndices.push(i); - } - }); - const [outPDF, embeddedPages] = (printSignatures) ? await this.embedPagesInNewPdf(this.managedDoc, filteredList) : [null, null]; - - let destPdfPages = (printAggregate) ? filteredList.map( (pI) => { - return config.providedPages[pI]; - }) : null; - - if (printSignatures) - blankIndices.forEach(i => embeddedPages.splice(i, 0, 'b')); - if (printAggregate) - blankIndices.forEach(i => destPdfPages.splice(i, 0, 'b')); - - let block_start = 0; - const offset = this.per_sheet / 2; - let block_end = offset; - - let alt_folio = (this.per_sheet == 4 && back); - - let positions = this.calculatelayout(alt_folio); - - let side2flag = back; - - while (block_end <= pagelist.length) { - let sigDetails = config.pageList.slice(block_start, block_end) - if (printAggregate) { - this.draw_block_onto_page({ - outPDF: config.destPdf, - embeddedPages: destPdfPages, - block_start: block_start, - block_end: block_end, - papersize: this.papersize, - sigDetails: sigDetails, - positions: positions, - cropmarks: this.cropmarks, - pdfEdgeMarks: this.pdfEdgeMarks, - cutmarks: this.cutmarks, - alt: config.alt, - side2flag: side2flag - }); - } - if (printSignatures) { - side2flag = this.draw_block_onto_page({ - outPDF: outPDF, - embeddedPages: embeddedPages, - block_start: block_start, - block_end: block_end, - sigDetails: sigDetails, - papersize: this.papersize, - positions: positions, - cropmarks: this.cropmarks, - pdfEdgeMarks: this.pdfEdgeMarks, - cutmarks: this.cutmarks, - alt: config.alt, - side2flag: side2flag - }); - } - block_start += offset; - block_end += offset; - } - - if (printSignatures) { - await outPDF.save().then(pdfBytes => { this.zip.file(config.outname, pdfBytes); }); - } + let embeddedPages = await newPdf.embedPdf(sourcePdf, pageNumbers); + // what a gnarly little hack. Letting this sit for now -- + // --- downstream code requires embeds to be in their 'correct' index possition + // but we want to only embed half the pages for the aggregate single sides + // thus we expand the embedded pages to allow those gaps to return. This is gross & dumb but whatever... + if (needsReSorting) { + embeddedPages = embeddedPages.reduce((acc, curVal, curI) => { + acc[pageNumbers[curI]] = curVal; + return acc; + }, []); } - - draw_block_onto_page(config) { - const sigDetails = config.sigDetails; - const block_start = config.block_start; - const block_end = config.block_end; - const papersize = config.papersize; - const outPDF = config.outPDF; - const positions = config.positions; - const cropmarks = config.cropmarks; - const pdfEdgeMarks = config.pdfEdgeMarks; - const cutmarks = config.cutmarks; - const alt = config.alt; - let side2flag = config.side2flag; - - let block = config.embeddedPages.slice(block_start, block_end); - let currPage = outPDF.addPage([papersize[0], papersize[1]]); - - block.forEach((page, i) => { - if (page == 'b' || page === undefined) { - // blank page, move on. - } else { - let pos = positions[i]; - let rot = pos.rotation; - currPage.drawPage(page, { y: pos.y, x: pos.x, xScale: pos.sx, yScale: pos.sy, rotate: degrees(rot) }); - } + return [newPdf, embeddedPages]; + } + + /** + * Part of the Classic (non-Wacky) flow. Called by [createsignatures]. + * (conditionally) populates the destPdf and (conditionally) generates the outname PDF + * + * @param config - object /w the following parameters: + * - outname : name of pdf added to ongoing zip file. Ex: 'signature1duplex.pdf' (or null if no signature file needed) + * - pageList : objects that contain 3 values: { isSigStart: boolean, isSigEnd: boolean, info: either the page number or 'b'} + * - back : is 'back' of page (boolean) + * - alt : alternate pages (boolean) + * - destPdf : PDF to write to, in addition to PDF created w/ `outname` (or null) + * - providedPages : pages already embedded in the `destPdf` to assemble in addition (or null) + * @return reference to the new PDF created + */ + async writepages(config) { + const printSignatures = config.outname != null; + const printAggregate = config.providedPages != null && config.destPdf != null; + const pagelist = config.pageList; + const back = config.back; + const filteredList = []; + const blankIndices = []; + pagelist.forEach((pageInfo, i) => { + if (pageInfo.info != 'b') { + filteredList.push(pageInfo.info); + } else { + blankIndices.push(i); + } + }); + const [outPDF, embeddedPages] = printSignatures + ? await this.embedPagesInNewPdf(this.managedDoc, filteredList) + : [null, null]; + + const destPdfPages = printAggregate + ? filteredList.map((pI) => { + return config.providedPages[pI]; + }) + : null; + + if (printSignatures) blankIndices.forEach((i) => embeddedPages.splice(i, 0, 'b')); + if (printAggregate) blankIndices.forEach((i) => destPdfPages.splice(i, 0, 'b')); + + let block_start = 0; + const offset = this.per_sheet / 2; + let block_end = offset; + + const alt_folio = this.per_sheet == 4 && back; + + const positions = this.calculatelayout(alt_folio); + + let side2flag = back; + + while (block_end <= pagelist.length) { + const sigDetails = config.pageList.slice(block_start, block_end); + if (printAggregate) { + this.draw_block_onto_page({ + outPDF: config.destPdf, + embeddedPages: destPdfPages, + block_start: block_start, + block_end: block_end, + papersize: this.papersize, + sigDetails: sigDetails, + positions: positions, + cropmarks: this.cropmarks, + pdfEdgeMarks: this.pdfEdgeMarks, + cutmarks: this.cutmarks, + alt: config.alt, + side2flag: side2flag, }); - block.forEach((page, i) => { - if (sigDetails[i].isSigStart || sigDetails[i].isSigEnd) { - if (pdfEdgeMarks) { - this.draw_spine_marks(currPage, sigDetails[i], positions[i]) - } - } + } + if (printSignatures) { + side2flag = this.draw_block_onto_page({ + outPDF: outPDF, + embeddedPages: embeddedPages, + block_start: block_start, + block_end: block_end, + sigDetails: sigDetails, + papersize: this.papersize, + positions: positions, + cropmarks: this.cropmarks, + pdfEdgeMarks: this.pdfEdgeMarks, + cutmarks: this.cutmarks, + alt: config.alt, + side2flag: side2flag, }); - if (cropmarks) { - this.draw_cropmarks(currPage, side2flag); - } - if (cutmarks) { - this.draw_cutmarks(currPage); - } - if (alt) { - side2flag = !side2flag; - } - return side2flag; + } + block_start += offset; + block_end += offset; } - /* - * @param curPage - PDFPage - * @param sigDetails - object w/ {info (page # or 'b'), isSigStart (boolean), isSigEnd (boolean)} - * @param position - object w/ {rotation (degrees), sx, sy, x, y} - */ - draw_spine_marks(curPage, sigDetails, position) { - let w = 5; - if (position.rotation == 0) { - console.log(" --> draw this: ",{ - start: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] - w/2: position.spineMarkBottom[0] - w/2, - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] : position.spineMarkBottom[1] ) - }, - end: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] + w/2: position.spineMarkBottom[0]+ w/2, - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] : position.spineMarkBottom[1] ) - }, - thickness: 0.5, - color: rgb(0,0,0), - opacity: 1, - }) - curPage.drawLine({ - start: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] - w/2: position.spineMarkBottom[0] - w/2, - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] : position.spineMarkBottom[1] ) - }, - end: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] + w/2: position.spineMarkBottom[0]+ w/2, - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] : position.spineMarkBottom[1] ) - }, - thickness: 0.5, - color: rgb(0,0,0), - opacity: 1, - }) - } else { - curPage.drawLine({ - start: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] : position.spineMarkBottom[0], - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] - w/2: position.spineMarkBottom[1] ) - w/2 - }, - end: { - x: (sigDetails.isSigStart) ? position.spineMarkTop[0] : position.spineMarkBottom[0], - y: ((sigDetails.isSigStart) ? position.spineMarkTop[1] + w/2: position.spineMarkBottom[1] ) + w/2 - }, - thickness: 0.25, - color: rgb(0,0,0), - opacity: 1, - }) - } + if (printSignatures) { + await outPDF.save().then((pdfBytes) => { + this.zip.file(config.outname, pdfBytes); + }); } - - draw_cropmarks(currPage, side2flag) { - switch(this.per_sheet){ - case 32: - if (side2flag) { - if (this.duplexrotate){ - currPage.drawLine({ - start: {x: this.papersize[0] * 0.75, y: this.papersize[1] * 0.75 }, - end: {x: this.papersize[0] * 0.75, y: this.papersize[1] * 0.5 }, - opacity: 0.4, - dashArray: [1, 5] - });} else { - currPage.drawLine({ - start: {x: this.papersize[0] * 0.25, y: this.papersize[1] * 0.5 }, - end: {x: this.papersize[0] * 0.25, y: this.papersize[1] * 0.25 }, - opacity: 0.4, - dashArray: [1, 5] - }); - } - } - /* falls through */ - case 16: - if (side2flag) { - if (this.duplexrotate){ - currPage.drawLine({ - start: {x: 0, y: this.papersize[1] * 0.75 }, - end: {x: this.papersize[0] * 0.5, y: this.papersize[1] * 0.75 }, - opacity: 0.4, - dashArray: [3, 5] - });} else { - currPage.drawLine({ - start: {x: this.papersize[0] * 0.5, y: this.papersize[1] * 0.25 }, - end: {x: this.papersize[0], y: this.papersize[1] * 0.25 }, - opacity: 0.4, - dashArray: [3, 5] - }); - } - } - /* falls through */ - case 8: - if (side2flag) { - if (this.duplexrotate){ - currPage.drawLine({ - start: {x: this.papersize[0] * 0.5, y: 0 }, - end: { y: this.papersize[1] * 0.5, x: this.papersize[0] * 0.5 }, - opacity: 0.4, - dashArray: [5, 5] - });} else { - currPage.drawLine({ - start: {x: this.papersize[0] * 0.5, y: this.papersize[1] }, - end: { y: this.papersize[1] * 0.5, x: this.papersize[0] * 0.5 }, - opacity: 0.4, - dashArray: [5, 5] - }); - } - } - /* falls through */ - case 4: - if (!side2flag) { - currPage.drawLine({ - start: { x: 0, y: this.papersize[1] * 0.5 }, - end: { x: this.papersize[0], y: this.papersize[1] * 0.5 }, - opacity: 0.4, - dashArray: [10, 5] - }); - } - } - } - - draw_cutmarks(currPage) { - let lines = []; - switch(this.per_sheet){ - case 32: - lines = [...lines, - ...this.draw_hline(this.papersize[1] * 0.75, 0, this.papersize[0]), - ...this.draw_hline(this.papersize[1] * 0.25, 0, this.papersize[0]), - ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.75), - ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.25) - ]; - /* falls through */ - case 16: - lines = [...lines, - ...this.draw_vline(this.papersize[0] * 0.5, 0, this.papersize[1]), - ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.5) - ]; - /* falls through */ - case 8: - lines = [...lines, - ...this.draw_hline(this.papersize[1] * 0.5, 0, this.papersize[0]) - ]; - /* falls through */ - case 4: - } - - lines.forEach(line => { - currPage.drawLine({...line, opacity: 0.4}); - }); + } + + draw_block_onto_page(config) { + const sigDetails = config.sigDetails; + const block_start = config.block_start; + const block_end = config.block_end; + const papersize = config.papersize; + const outPDF = config.outPDF; + const positions = config.positions; + const cropmarks = config.cropmarks; + const pdfEdgeMarks = config.pdfEdgeMarks; + const cutmarks = config.cutmarks; + const alt = config.alt; + let side2flag = config.side2flag; + + const block = config.embeddedPages.slice(block_start, block_end); + const currPage = outPDF.addPage([papersize[0], papersize[1]]); + + block.forEach((page, i) => { + if (page == 'b' || page === undefined) { + // blank page, move on. + } else { + const pos = positions[i]; + const rot = pos.rotation; + currPage.drawPage(page, { + y: pos.y, + x: pos.x, + xScale: pos.sx, + yScale: pos.sy, + rotate: degrees(rot), + }); + } + }); + block.forEach((page, i) => { + if (sigDetails[i].isSigStart || sigDetails[i].isSigEnd) { + if (pdfEdgeMarks) { + this.draw_spine_marks(currPage, sigDetails[i], positions[i]); + } + } + }); + if (cropmarks) { + this.draw_cropmarks(currPage, side2flag); } - - draw_vline(x, ystart, yend){ - return [{start: {x: x, y: ystart}, end: {x: x, y: ystart + LINE_LEN}}, {start: {x: x, y: yend - LINE_LEN}, end: {x: x, y: yend}}]; + if (cutmarks) { + this.draw_cutmarks(currPage); } - - draw_hline(y, xstart, xend){ - return [{start: {x: xstart, y: y}, end: {x: xstart + LINE_LEN, y: y}}, {start: {x: xend - LINE_LEN, y: y}, end: {x: xend, y: y}}]; + if (alt) { + side2flag = !side2flag; } - - draw_cross(x, y) { - return [{start: {x: x - LINE_LEN, y: y}, end: {x: x + LINE_LEN, y: y}}, {start: {x: x, y: y - LINE_LEN}, end: {x: x, y: y + LINE_LEN}}]; + return side2flag; + } + + /* + * @param curPage - PDFPage + * @param sigDetails - object w/ {info (page # or 'b'), isSigStart (boolean), isSigEnd (boolean)} + * @param position - object w/ {rotation (degrees), sx, sy, x, y} + */ + draw_spine_marks(curPage, sigDetails, position) { + const w = 5; + if (position.rotation == 0) { + const start = { + x: sigDetails.isSigStart + ? position.spineMarkTop[0] - w / 2 + : position.spineMarkBottom[0] - w / 2, + y: sigDetails.isSigStart ? position.spineMarkTop[1] : position.spineMarkBottom[1], + }; + + const end = { + x: sigDetails.isSigStart + ? position.spineMarkTop[0] + w / 2 + : position.spineMarkBottom[0] + w / 2, + y: sigDetails.isSigStart ? position.spineMarkTop[1] : position.spineMarkBottom[1], + }; + const drawOpts = { + start, + end, + thickness: 0.5, + color: rgb(0, 0, 0), + opacity: 1, + }; + console.log(' --> draw this: ', drawOpts); + curPage.drawLine(drawOpts); + } else { + curPage.drawLine({ + start: { + x: sigDetails.isSigStart ? position.spineMarkTop[0] : position.spineMarkBottom[0], + y: + (sigDetails.isSigStart + ? position.spineMarkTop[1] - w / 2 + : position.spineMarkBottom[1]) - + w / 2, + }, + end: { + x: sigDetails.isSigStart ? position.spineMarkTop[0] : position.spineMarkBottom[0], + y: + (sigDetails.isSigStart + ? position.spineMarkTop[1] + w / 2 + : position.spineMarkBottom[1]) + + w / 2, + }, + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1, + }); } - - /** - * Looks at [this.cropbox] and [this.padding_pt] and [this.papersize] and [this.page_layout] and [this.page_scaling] - * in order to calculate the information needed to render a PDF page within a layout cell. It provides several functions - * in the return object that calculate the positioning and scaling needed when provided the rotation information. - * - * When calculating 'x' and 'y' values, those are relative to a laid out PDF page, not necessarily paper sheet x & y - * - * @return the object: { - * layoutCell: 2 dimensional array of the largest possible space the PDF page could take within the layout (and not overflow) - * rawPdfSize: 2 dimensional array of dimensions for the PDF (pre scaled) - * pdfSize: 2 dimensional array of dimensions for the PDF page + margins (pre scaled) - * pdfScale: 2 dimensional array of scaling factors for the raw PDF so it fits in layoutCell (w/ margins) - * padding: object containing the already scaled padding. Keys are: fore_edge, binding, top, bottom - * xForeEdgeShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * xBindingShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * xPdfWidthFunc: requires the page rotation, in degrees. In pts, already scaled. - * yPdfHeightFunc: requires the page rotation, in degrees. In pts, already scaled. - * yTopShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * yBottomShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * } - */ - calculate_dimensions() { - let onlyPos = function(v) { return (v > 0) ? v : 0; }; - let onlyNeg = function(v) { return (v < 0) ? v : 0; }; - // PDF + margins (positive) - let pagex = this.cropbox.width + onlyPos(this.padding_pt.binding) + onlyPos(this.padding_pt.fore_edge); - let pagey = this.cropbox.height + onlyPos(this.padding_pt.top) + onlyPos(this.padding_pt.bottom); - - let layout = this.page_layout; - - // Calculate the size of each page box on the sheet - let finalx = this.papersize[0] / layout.cols; - let finaly = this.papersize[1] / layout.rows; - - - // if pages are rotated a quarter-turn in this layout, we need to swap the width and height measurements - if (layout.landscape) { - let temp = finalx; - finalx = finaly; - finaly = temp; + } + + draw_cropmarks(currPage, side2flag) { + switch (this.per_sheet) { + case 32: + if (side2flag) { + if (this.duplexrotate) { + currPage.drawLine({ + start: { x: this.papersize[0] * 0.75, y: this.papersize[1] * 0.75 }, + end: { x: this.papersize[0] * 0.75, y: this.papersize[1] * 0.5 }, + opacity: 0.4, + dashArray: [1, 5], + }); + } else { + currPage.drawLine({ + start: { x: this.papersize[0] * 0.25, y: this.papersize[1] * 0.5 }, + end: { x: this.papersize[0] * 0.25, y: this.papersize[1] * 0.25 }, + opacity: 0.4, + dashArray: [1, 5], + }); + } } - - let sx = 1; - let sy = 1; - - // The page_scaling options are: 'lockratio', 'stretch', 'centered' - if (this.page_scaling == 'lockratio') { - let scale = Math.min(finalx/pagex, finaly/pagey); - sx = scale; - sy = scale; - } else if (this.page_scaling == 'stretch') { - sx = finalx / pagex; - sy = finaly / pagey; - } // else = centered retains 1 x 1 - - let padding = { - 'fore_edge' : this.padding_pt.fore_edge * sx, - 'binding' : this.padding_pt.binding * sx, - 'bottom' : this.padding_pt.bottom * sy, - 'top' : this.padding_pt.top * sy - }; - - // page_positioning has 2 options: centered, binding_alinged - let positioning = this.page_positioning; - - let xForeEdgeShiftFunc = function() { - // amount to inset by, relative to fore edge, on left side of book - let xgap = finalx - pagex * sx; - return padding.fore_edge + ((positioning == 'centered' )? xgap/2 : xgap); - }; - let xBindingShiftFunc = function() { - // amount to inset by, relative to binding, on right side of book - let xgap = finalx - pagex * sx; - return padding.binding + ((positioning == 'centered' )? xgap/2 : 0); - }; - let yTopShiftFunc = function() { - let ygap = finaly - pagey * sy; - return padding.top + ygap/2 ; - }; - let yBottomShiftFunc = function() { - let ygap = finaly - pagey * sy; - return padding.bottom + ygap/2 ; - }; - let xPdfWidthFunc = function() { - return pagex * sx - padding.fore_edge - padding.binding; - }; - let yPdfHeightFunc = function() { - return pagey * sy - padding.top - padding.bottom; - }; - return { - layout: layout, - rawPdfSize: [this.cropbox.width, this.cropbox.height], - pdfScale: [sx, sy], - pdfSize: [pagex, pagey], - layoutCell: [finalx, finaly], - padding: padding, - - xForeEdgeShiftFunc: xForeEdgeShiftFunc, - xBindingShiftFunc: xBindingShiftFunc, - xPdfWidthFunc: xPdfWidthFunc, - yPdfHeightFunc: yPdfHeightFunc, - yTopShiftFunc: yTopShiftFunc, - yBottomShiftFunc: yBottomShiftFunc, - - positioning: positioning - }; - } - - /** - * When considering page size, don't forget to take into account - * this.padding_pt's ['top','bottom','binding','fore_edge'] values - * - * @return an array of objects in the form {rotation: col, sx: sx, sy: sy, x: x, y: y} - */ - calculatelayout(alt_folio){ - // vampire - let l = this.calculate_dimensions(); - let cellWidth = l.layoutCell[0]; - let cellHeight = l.layoutCell[1]; - let positions = []; - - l.layout.rotations.forEach((row, i) => { - row.forEach((col, j) => { - let xForeEdgeShift = l.xForeEdgeShiftFunc(); - let xBindingShift = l.xBindingShiftFunc(); - let yTopShift = l.yTopShiftFunc(); - let yBottomShift = l.yBottomShiftFunc(); - - let isLeftPage = j % 2 == 0; //page on 'left' side of open book - let x = (j * cellWidth) + ((isLeftPage) ? xForeEdgeShift : xBindingShift); - let y = (i * cellHeight) + yBottomShift; - let spineMarkTop = [(j * cellWidth), ((i + 1) * cellHeight) - yTopShift] - let spineMarkBottom = [((j+1) * cellWidth), (i * cellHeight) + yBottomShift] - - if (col == -180) { // upside-down page - isLeftPage = j % 2 == 1; //page on 'left' (right side on screen) - y = (i + 1) * cellHeight - yBottomShift; - x = (j + 1) * cellWidth - ((isLeftPage) ? xForeEdgeShift : xBindingShift); - spineMarkTop = [(j + 1) * cellWidth, (i + 1) * cellHeight]; - spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight]; - - } else if (col == 90) { // 'top' of page is on left, right side of screen - isLeftPage = i % 2 == 0; // page is on 'left' (top side of screen) - x = (1 + j) * cellHeight - yBottomShift; - y = i * cellWidth + ((isLeftPage) ? xBindingShift : xForeEdgeShift); - spineMarkTop = [(1 + j) * cellHeight, i * cellWidth]; - spineMarkBottom = [j * cellHeight, i * cellWidth]; - - } else if (col == -90) { // 'top' of page is on the right, left sight of screen - isLeftPage = i % 2 == 1; // page is on 'left' (bottom side of screen) - x = j * cellHeight + yBottomShift; - y = (1+i) * cellWidth - ((isLeftPage) ? xForeEdgeShift : xBindingShift); - spineMarkTop = [(j+1) * cellHeight - yTopShift, (isLeftPage ? i : i+1) * cellWidth]; - spineMarkBottom = [j * cellHeight + yBottomShift, (isLeftPage ? i : i+1) * cellWidth]; - } - - console.log(">> (", i, ",",j,")[",col,"] : [",x,", ",y,"] :: [xForeEdgeShift: ",xForeEdgeShift,"][xBindingShift: ",xBindingShift,"]"); - positions.push({rotation: col, sx: l.pdfScale[0], sy: l.pdfScale[1], x: x, y: y, - spineMarkTop: spineMarkTop, - spineMarkBottom: spineMarkBottom, - isLeftPage: isLeftPage - }); + /* falls through */ + case 16: + if (side2flag) { + if (this.duplexrotate) { + currPage.drawLine({ + start: { x: 0, y: this.papersize[1] * 0.75 }, + end: { x: this.papersize[0] * 0.5, y: this.papersize[1] * 0.75 }, + opacity: 0.4, + dashArray: [3, 5], }); - }); - console.log("And in the end of it all, (calculatelayout) we get: ",positions); - return positions; - } - - /** - * PDF builder base function for Classic (non-Wacky) layouts. Called by [createoutputfiles] - * - * @param config - object w/ the following parameters: - * - pageIndexDetails : a nested list of objects. Each object: {info: page # or 'b', isSigStart: boolean, isSigEnd: boolean} ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled). Ex: [[{info: 3, isSigStart: true, isSigend: false},{info: 4, isSigStart: false, isSigend: true}]] - * - aggregatePdfs : list of destination PDF(s_ for aggregated content ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) - * - embeddedPages : list of lists of embedded pages from source document ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) - * - id : string dentifier for signature file name (null if no signature files to be generated) - * - isDuplex : boolean - * - fileList : list of filenames for sig filename to be added to (modifies list) - */ - async createsignatures(config) { - const printAggregate = config.aggregatePdfs != null; - const printSignatures = config.id != null; - const pages = config.pageIndexDetails; - // duplex printers print both sides of the sheet, - if (config.isDuplex) { - let outduplex = (printSignatures) ? config.id + 'duplex' + '.pdf' : null; - await this.writepages({ - outname: outduplex, - pageList: pages[0], - back: false, - alt: true, - destPdf: (printAggregate) ? config.aggregatePdfs[0] : null, - providedPages: (printAggregate) ? config.embeddedPages[0] : null + } else { + currPage.drawLine({ + start: { x: this.papersize[0] * 0.5, y: this.papersize[1] * 0.25 }, + end: { x: this.papersize[0], y: this.papersize[1] * 0.25 }, + opacity: 0.4, + dashArray: [3, 5], }); - if (printSignatures) { - config.fileList.push(outduplex); - } - } else { - // for non-duplex printers we have two files, print the first, flip - // the sheets over, then print the second - let outname1 = (printSignatures) ? config.id + 'side1.pdf' : null; - let outname2 = (printSignatures) ? config.id + 'side2.pdf' : null; - - await this.writepages({ - outname: outname1, - pageList: pages[0], - back: false, - alt: false, - destPdf: (printAggregate) ? config.aggregatePdfs[0] : null, - providedPages: (printAggregate) ? config.embeddedPages[0] : null + } + } + /* falls through */ + case 8: + if (side2flag) { + if (this.duplexrotate) { + currPage.drawLine({ + start: { x: this.papersize[0] * 0.5, y: 0 }, + end: { y: this.papersize[1] * 0.5, x: this.papersize[0] * 0.5 }, + opacity: 0.4, + dashArray: [5, 5], }); - await this.writepages({ - outname: outname2, - pageList: pages[1], - back: true, - alt: false, - destPdf: (printAggregate) ? config.aggregatePdfs[1] : null, - providedPages: (printAggregate) ? config.embeddedPages[1] : null + } else { + currPage.drawLine({ + start: { x: this.papersize[0] * 0.5, y: this.papersize[1] }, + end: { y: this.papersize[1] * 0.5, x: this.papersize[0] * 0.5 }, + opacity: 0.4, + dashArray: [5, 5], }); - if (printSignatures) { - config.fileList.push(outname1); - config.fileList.push(outname2); - } + } + } + /* falls through */ + case 4: + if (!side2flag) { + currPage.drawLine({ + start: { x: 0, y: this.papersize[1] * 0.5 }, + end: { x: this.papersize[0], y: this.papersize[1] * 0.5 }, + opacity: 0.4, + dashArray: [10, 5], + }); } - console.log("After creating signatures, our filelist looks like: ",this.filelist); } - - bundleSettings() { - const currentConfig = loadConfiguration(); - const settings = `Imposer settings: ${JSON.stringify(currentConfig, null, 2)}` - + '\n\n' - + `Link to the imposer with these settings: ${window.location.href}` - this.zip?.file("settings.txt", settings); + } + + draw_cutmarks(currPage) { + let lines = []; + switch (this.per_sheet) { + case 32: + lines = [ + ...lines, + ...this.draw_hline(this.papersize[1] * 0.75, 0, this.papersize[0]), + ...this.draw_hline(this.papersize[1] * 0.25, 0, this.papersize[0]), + ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.75), + ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.25), + ]; + /* falls through */ + case 16: + lines = [ + ...lines, + ...this.draw_vline(this.papersize[0] * 0.5, 0, this.papersize[1]), + ...this.draw_cross(this.papersize[0] * 0.5, this.papersize[1] * 0.5), + ]; + /* falls through */ + case 8: + lines = [...lines, ...this.draw_hline(this.papersize[1] * 0.5, 0, this.papersize[0])]; + /* falls through */ + case 4: } - saveZip() { - console.log("Saving zip... ") - this.bundleSettings(); - return this.zip.generateAsync({ type: "blob" }) - .then(blob => { - console.log(" calling saveAs on ", this.filename); - saveAs(blob, this.filename + ".zip"); - }); + lines.forEach((line) => { + currPage.drawLine({ ...line, opacity: 0.4 }); + }); + } + + draw_vline(x, ystart, yend) { + return [ + { start: { x: x, y: ystart }, end: { x: x, y: ystart + LINE_LEN } }, + { start: { x: x, y: yend - LINE_LEN }, end: { x: x, y: yend } }, + ]; + } + + draw_hline(y, xstart, xend) { + return [ + { start: { x: xstart, y: y }, end: { x: xstart + LINE_LEN, y: y } }, + { start: { x: xend - LINE_LEN, y: y }, end: { x: xend, y: y } }, + ]; + } + + draw_cross(x, y) { + return [ + { start: { x: x - LINE_LEN, y: y }, end: { x: x + LINE_LEN, y: y } }, + { start: { x: x, y: y - LINE_LEN }, end: { x: x, y: y + LINE_LEN } }, + ]; + } + + /** + * Looks at [this.cropbox] and [this.padding_pt] and [this.papersize] and [this.page_layout] and [this.page_scaling] + * in order to calculate the information needed to render a PDF page within a layout cell. It provides several functions + * in the return object that calculate the positioning and scaling needed when provided the rotation information. + * + * When calculating 'x' and 'y' values, those are relative to a laid out PDF page, not necessarily paper sheet x & y + * + * @return the object: { + * layoutCell: 2 dimensional array of the largest possible space the PDF page could take within the layout (and not overflow) + * rawPdfSize: 2 dimensional array of dimensions for the PDF (pre scaled) + * pdfSize: 2 dimensional array of dimensions for the PDF page + margins (pre scaled) + * pdfScale: 2 dimensional array of scaling factors for the raw PDF so it fits in layoutCell (w/ margins) + * padding: object containing the already scaled padding. Keys are: fore_edge, binding, top, bottom + * xForeEdgeShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * xBindingShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * xPdfWidthFunc: requires the page rotation, in degrees. In pts, already scaled. + * yPdfHeightFunc: requires the page rotation, in degrees. In pts, already scaled. + * yTopShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * yBottomShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * } + */ + calculate_dimensions() { + const onlyPos = function (v) { + return v > 0 ? v : 0; + }; + // const onlyNeg = function (v) { + // return v < 0 ? v : 0; + // }; + // PDF + margins (positive) + const pagex = + this.cropbox.width + onlyPos(this.padding_pt.binding) + onlyPos(this.padding_pt.fore_edge); + const pagey = + this.cropbox.height + onlyPos(this.padding_pt.top) + onlyPos(this.padding_pt.bottom); + + const layout = this.page_layout; + + // Calculate the size of each page box on the sheet + let finalx = this.papersize[0] / layout.cols; + let finaly = this.papersize[1] / layout.rows; + + // if pages are rotated a quarter-turn in this layout, we need to swap the width and height measurements + if (layout.landscape) { + const temp = finalx; + finalx = finaly; + finaly = temp; } - /** - * @param id - base for the final PDF name - * @param builder - object to help construct this configuration. Object definition: { - * sheetMaker: function that takes the page count as a param and returns an array of sheets. A sheet is { - * num: page number from original doc, - * isBlank: true renders it blank-- will override any `num` included, - * vFlip: true if rendered upside down (180 rotation) - * }, - * lineMaker: function that makes a function that generates trim lines for the PDF, - * isLandscape: true if we need to have largest dimension be width, - * fileNameMod: string to affix to exported file name (contains no buffer begin/end characters) - * isPacked: boolean - true if white spaces goes on the outside, false if white space goes everywhere (non-binding edge) - * } - */ - async buildSheets(id, builder) { - let sheets = builder.sheetMaker(this.pagecount); - let lineMaker = builder.lineMaker(); - console.log("Working with the sheet descritpion: ", sheets); - const outPDF = await PDFDocument.create(); - const outPDF_back = await PDFDocument.create(); - - for (let i=0; i < sheets.length; ++i ) { - let isFront = i % 2 == 0; - let isFirst = i < 2; - console.log("Trying to write ", sheets[i]); - let targetPDF = (this.duplex || isFront) ? outPDF : outPDF_back; - await this.write_single_page(targetPDF, builder.isLandscape, isFront, isFirst, sheets[i], lineMaker); + let sx = 1; + let sy = 1; + + // The page_scaling options are: 'lockratio', 'stretch', 'centered' + if (this.page_scaling == 'lockratio') { + const scale = Math.min(finalx / pagex, finaly / pagey); + sx = scale; + sy = scale; + } else if (this.page_scaling == 'stretch') { + sx = finalx / pagex; + sy = finaly / pagey; + } // else = centered retains 1 x 1 + + const padding = { + fore_edge: this.padding_pt.fore_edge * sx, + binding: this.padding_pt.binding * sx, + bottom: this.padding_pt.bottom * sy, + top: this.padding_pt.top * sy, + }; + + // page_positioning has 2 options: centered, binding_alinged + const positioning = this.page_positioning; + + const xForeEdgeShiftFunc = function () { + // amount to inset by, relative to fore edge, on left side of book + const xgap = finalx - pagex * sx; + return padding.fore_edge + (positioning == 'centered' ? xgap / 2 : xgap); + }; + const xBindingShiftFunc = function () { + // amount to inset by, relative to binding, on right side of book + const xgap = finalx - pagex * sx; + return padding.binding + (positioning == 'centered' ? xgap / 2 : 0); + }; + const yTopShiftFunc = function () { + const ygap = finaly - pagey * sy; + return padding.top + ygap / 2; + }; + const yBottomShiftFunc = function () { + const ygap = finaly - pagey * sy; + return padding.bottom + ygap / 2; + }; + const xPdfWidthFunc = function () { + return pagex * sx - padding.fore_edge - padding.binding; + }; + const yPdfHeightFunc = function () { + return pagey * sy - padding.top - padding.bottom; + }; + return { + layout: layout, + rawPdfSize: [this.cropbox.width, this.cropbox.height], + pdfScale: [sx, sy], + pdfSize: [pagex, pagey], + layoutCell: [finalx, finaly], + padding: padding, + + xForeEdgeShiftFunc: xForeEdgeShiftFunc, + xBindingShiftFunc: xBindingShiftFunc, + xPdfWidthFunc: xPdfWidthFunc, + yPdfHeightFunc: yPdfHeightFunc, + yTopShiftFunc: yTopShiftFunc, + yBottomShiftFunc: yBottomShiftFunc, + + positioning: positioning, + }; + } + + /** + * When considering page size, don't forget to take into account + * this.padding_pt's ['top','bottom','binding','fore_edge'] values + * + * @return an array of objects in the form {rotation: col, sx: sx, sy: sy, x: x, y: y} + */ + calculatelayout() { + // vampire + const l = this.calculate_dimensions(); + const cellWidth = l.layoutCell[0]; + const cellHeight = l.layoutCell[1]; + const positions = []; + + l.layout.rotations.forEach((row, i) => { + row.forEach((col, j) => { + const xForeEdgeShift = l.xForeEdgeShiftFunc(); + const xBindingShift = l.xBindingShiftFunc(); + const yTopShift = l.yTopShiftFunc(); + const yBottomShift = l.yBottomShiftFunc(); + + let isLeftPage = j % 2 == 0; //page on 'left' side of open book + let x = j * cellWidth + (isLeftPage ? xForeEdgeShift : xBindingShift); + let y = i * cellHeight + yBottomShift; + let spineMarkTop = [j * cellWidth, (i + 1) * cellHeight - yTopShift]; + let spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight + yBottomShift]; + + if (col == -180) { + // upside-down page + isLeftPage = j % 2 == 1; //page on 'left' (right side on screen) + y = (i + 1) * cellHeight - yBottomShift; + x = (j + 1) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); + spineMarkTop = [(j + 1) * cellWidth, (i + 1) * cellHeight]; + spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight]; + } else if (col == 90) { + // 'top' of page is on left, right side of screen + isLeftPage = i % 2 == 0; // page is on 'left' (top side of screen) + x = (1 + j) * cellHeight - yBottomShift; + y = i * cellWidth + (isLeftPage ? xBindingShift : xForeEdgeShift); + spineMarkTop = [(1 + j) * cellHeight, i * cellWidth]; + spineMarkBottom = [j * cellHeight, i * cellWidth]; + } else if (col == -90) { + // 'top' of page is on the right, left sight of screen + isLeftPage = i % 2 == 1; // page is on 'left' (bottom side of screen) + x = j * cellHeight + yBottomShift; + y = (1 + i) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); + spineMarkTop = [(j + 1) * cellHeight - yTopShift, (isLeftPage ? i : i + 1) * cellWidth]; + spineMarkBottom = [j * cellHeight + yBottomShift, (isLeftPage ? i : i + 1) * cellWidth]; } - { - console.log("Trying to save to PDF ", builder.fileNameMod, " w/ packing : ", this.pack_pages); - let fileName = id + "_" + builder.fileNameMod + ( this.duplex ? '' : '_fronts') +'.pdf'; - await outPDF.save().then(pdfBytes => { - console.log("Calling zip.file on ", fileName); - this.zip.file(fileName, pdfBytes); - }); - this.filelist.push(fileName); - } - if (!this.duplex) { - console.log("Trying to save to PDF (back pages)"); - let fileName = id + "_" + builder.fileNameMod + '_backs.pdf'; - await outPDF_back.save().then(pdfBytes => { - console.log("Calling zip.file on ", fileName); - this.zip.file(fileName, pdfBytes); - }); - this.filelist.push(fileName); - } - console.log("buildSheets complete"); - return outPDF; - } - /** - * Spits out a document of specificed `papersize` dimensions. - * The 2 dimensional pagelist determines the size of the rendered pages. - * The height of each rendered page is `papersize[1] / pagelist.length`. - * The width of each rendered page is `papersize[0] / pagelist[x].length`. - * - * @param outPDF - the PDFDocument document we're appending a page to - * @param isLandscape - true if we need to have largest dimension be width - * @param isFront - true if front of page - * @param isFirst - true if this is the first (front/back) pair of sheets - * @param pagelist - a 2 dimensional array. Outer array is rows, nested array page objects. Object definition: { - * num: page number from original doc, - * isBlank: true renders it blank-- will override any `num` included, - * vFlip: true if rendered upside down (180 rotation) - * } - * @param lineMaker - a function called to generate list of lines as described by PDF-lib.js's `PDFPageDrawLineOptions` object. - * Function takes as parameters: - * @return - */ - async write_single_page(outPDF, isLandscape, isFront, isFirst, pagelist, lineMaker) { - let filteredList = []; - console.log(pagelist); - pagelist = pagelist.filter( r => { // need second sheet to remain small even if there's room to expand - return !isFirst || r.filter(c => {return c.isBlank == false;}).length > 0; + console.log( + `>> (${i},${j})[${col}] : [${x},${y}] :: [xForeEdgeShift: ${xForeEdgeShift}][xBindingShift: ${xBindingShift}]` + ); + positions.push({ + rotation: col, + sx: l.pdfScale[0], + sy: l.pdfScale[1], + x: x, + y: y, + spineMarkTop: spineMarkTop, + spineMarkBottom: spineMarkBottom, + isLeftPage: isLeftPage, }); - console.log(pagelist); - console.log("Hitting that write_single_page : isPacked[",this.pack_pages,"] || (front ",isFront,"/ first ",isFirst,") [",pagelist.length,",",pagelist[0].length,"]"); - pagelist.forEach(row => { row.forEach( page => { if (!page.isBlank) filteredList.push(page.num); }); }); - if (filteredList.length == 0) { - console.warn("All the pages are empty! : ",pagelist); - return; - } - let embeddedPages = await outPDF.embedPdf(this.managedDoc, filteredList); - // TODO : make sure the max dimen is correct here... - let papersize = isLandscape ? [this.papersize[1], this.papersize[0]] : [this.papersize[0], this.papersize[1]]; - let curPage = outPDF.addPage(papersize); - let sourcePage = embeddedPages.slice(0, 1)[0]; - let pageHeight = papersize[1] / pagelist.length; - let pageWidth = papersize[0] / pagelist[0].length; - let heightRatio = pageHeight / sourcePage.height; - let widthRatio = pageWidth / (sourcePage.width + this.fore_edge_padding_pt); - let pageScale = Math.min(heightRatio, widthRatio); - let vGap = papersize[1] - (sourcePage.height * pageScale * pagelist.length); - let topGap = (this.pack_pages) ? vGap / 2.0 : vGap / (pagelist.length * 2); - let hGap = papersize[0] - ((sourcePage.width + this.fore_edge_padding_pt) * pageScale * pagelist[0].length); - let leftGap = (this.pack_pages) ? hGap / 2.0 : (hGap / pagelist[0].length) ; - let printPageWidth = pageScale * sourcePage.width; - let printPageHeight = pageScale * sourcePage.height; - let printedForeEdgeGutter = pageScale * this.fore_edge_padding_pt; - for (let row=0; row < pagelist.length; ++row ) { - let y = sourcePage.height * pageScale * row; - for (let i=0; i < pagelist[row].length; ++i) { - let x = (sourcePage.width + this.fore_edge_padding_pt) * pageScale * i + (this.fore_edge_padding_pt * (i + 1)%2); - let pageInfo = pagelist[row][i]; - if (pageInfo.isBlank) - continue; - let origPage = embeddedPages[filteredList.indexOf(pageInfo.num)]; - let hOffset = (this.pack_pages) ? leftGap : (1 + i - i % 2) * leftGap; - let vOffset = (this.pack_pages) ? topGap : topGap + (2 * topGap * row); - let positioning = { - x: x + hOffset + (pageInfo.vFlip ? printPageWidth : 0) + printedForeEdgeGutter * ((i + 1)%2), - y: y + vOffset + (pageInfo.vFlip ? printPageHeight : 0), - width: printPageWidth , - height: printPageHeight, - rotate: pageInfo.vFlip ? degrees(180) : degrees(0) - }; - //console.log(" [",row,",",i,"] Given page info ", pageInfo, " now embedding at ", positioning); - curPage.drawPage(origPage, positioning); - } - } - lineMaker({ - isFront: isFront, - gap: [leftGap, topGap], - renderPageSize: [printPageWidth + printedForeEdgeGutter, printPageHeight], - paperSize: papersize, - isPacked: this.pack_pages - }).forEach( line => { curPage.drawLine(line);}); + }); + }); + console.log('And in the end of it all, (calculatelayout) we get: ', positions); + return positions; + } + + /** + * PDF builder base function for Classic (non-Wacky) layouts. Called by [createoutputfiles] + * + * @param config - object w/ the following parameters: + * - pageIndexDetails : a nested list of objects. Each object: {info: page # or 'b', isSigStart: boolean, isSigEnd: boolean} ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled). Ex: [[{info: 3, isSigStart: true, isSigend: false},{info: 4, isSigStart: false, isSigend: true}]] + * - aggregatePdfs : list of destination PDF(s_ for aggregated content ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) + * - embeddedPages : list of lists of embedded pages from source document ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) + * - id : string dentifier for signature file name (null if no signature files to be generated) + * - isDuplex : boolean + * - fileList : list of filenames for sig filename to be added to (modifies list) + */ + async createsignatures(config) { + const printAggregate = config.aggregatePdfs != null; + const printSignatures = config.id != null; + const pages = config.pageIndexDetails; + // duplex printers print both sides of the sheet, + if (config.isDuplex) { + const outduplex = printSignatures ? `${config.id}duplex.pdf` : null; + await this.writepages({ + outname: outduplex, + pageList: pages[0], + back: false, + alt: true, + destPdf: printAggregate ? config.aggregatePdfs[0] : null, + providedPages: printAggregate ? config.embeddedPages[0] : null, + }); + if (printSignatures) { + config.fileList.push(outduplex); + } + } else { + // for non-duplex printers we have two files, print the first, flip + // the sheets over, then print the second + const outname1 = printSignatures ? `${config.id}side1.pdf` : null; + const outname2 = printSignatures ? `${config.id}side2.pdf` : null; + + await this.writepages({ + outname: outname1, + pageList: pages[0], + back: false, + alt: false, + destPdf: printAggregate ? config.aggregatePdfs[0] : null, + providedPages: printAggregate ? config.embeddedPages[0] : null, + }); + await this.writepages({ + outname: outname2, + pageList: pages[1], + back: true, + alt: false, + destPdf: printAggregate ? config.aggregatePdfs[1] : null, + providedPages: printAggregate ? config.embeddedPages[1] : null, + }); + if (printSignatures) { + config.fileList.push(outname1); + config.fileList.push(outname2); + } } - + console.log('After creating signatures, our filelist looks like: ', this.filelist); + } + + bundleSettings() { + const currentConfig = loadConfiguration(); + const settings = + `Imposer settings: ${JSON.stringify(currentConfig, null, 2)}` + + '\n\n' + + `Link to the imposer with these settings: ${window.location.href}`; + this.zip?.file('settings.txt', settings); + } + + saveZip() { + console.log('Saving zip... '); + this.bundleSettings(); + return this.zip.generateAsync({ type: 'blob' }).then((blob) => { + console.log(' calling saveAs on ', this.filename); + saveAs(blob, `${this.filename}.zip`); + }); + } + + /** + * @param id - base for the final PDF name + * @param builder - object to help construct this configuration. Object definition: { + * sheetMaker: function that takes the page count as a param and returns an array of sheets. A sheet is { + * num: page number from original doc, + * isBlank: true renders it blank-- will override any `num` included, + * vFlip: true if rendered upside down (180 rotation) + * }, + * lineMaker: function that makes a function that generates trim lines for the PDF, + * isLandscape: true if we need to have largest dimension be width, + * fileNameMod: string to affix to exported file name (contains no buffer begin/end characters) + * isPacked: boolean - true if white spaces goes on the outside, false if white space goes everywhere (non-binding edge) + * } + */ + async buildSheets(id, builder) { + const sheets = builder.sheetMaker(this.pagecount); + const lineMaker = builder.lineMaker(); + console.log('Working with the sheet descritpion: ', sheets); + const outPDF = await PDFDocument.create(); + const outPDF_back = await PDFDocument.create(); + + for (let i = 0; i < sheets.length; ++i) { + const isFront = i % 2 == 0; + const isFirst = i < 2; + console.log('Trying to write ', sheets[i]); + const targetPDF = this.duplex || isFront ? outPDF : outPDF_back; + await this.write_single_page( + targetPDF, + builder.isLandscape, + isFront, + isFirst, + sheets[i], + lineMaker + ); + } + { + console.log(`Trying to save to PDF ${builder.fileNameMod} w/ packing : ${this.pack_pages}`); + const fileName = `${id}_${builder.fileNameMod}${this.duplex ? '' : '_fronts'}.pdf`; + await outPDF.save().then((pdfBytes) => { + console.log('Calling zip.file on ', fileName); + this.zip.file(fileName, pdfBytes); + }); + this.filelist.push(fileName); + } + if (!this.duplex) { + console.log('Trying to save to PDF (back pages)'); + const fileName = `${id}_${builder.fileNameMod}_backs.pdf`; + await outPDF_back.save().then((pdfBytes) => { + console.log('Calling zip.file on ', fileName); + this.zip.file(fileName, pdfBytes); + }); + this.filelist.push(fileName); + } + console.log('buildSheets complete'); + return outPDF; + } + + /** + * Spits out a document of specificed `papersize` dimensions. + * The 2 dimensional pagelist determines the size of the rendered pages. + * The height of each rendered page is `papersize[1] / pagelist.length`. + * The width of each rendered page is `papersize[0] / pagelist[x].length`. + * + * @param outPDF - the PDFDocument document we're appending a page to + * @param isLandscape - true if we need to have largest dimension be width + * @param isFront - true if front of page + * @param isFirst - true if this is the first (front/back) pair of sheets + * @param pagelist - a 2 dimensional array. Outer array is rows, nested array page objects. Object definition: { + * num: page number from original doc, + * isBlank: true renders it blank-- will override any `num` included, + * vFlip: true if rendered upside down (180 rotation) + * } + * @param lineMaker - a function called to generate list of lines as described by PDF-lib.js's `PDFPageDrawLineOptions` object. + * Function takes as parameters: + * @return + */ + async write_single_page(outPDF, isLandscape, isFront, isFirst, pagelist, lineMaker) { + const filteredList = []; + console.log(pagelist); + pagelist = pagelist.filter((r) => { + // need second sheet to remain small even if there's room to expand + return ( + !isFirst || + r.filter((c) => { + return c.isBlank == false; + }).length > 0 + ); + }); + console.log(pagelist); + console.log( + `Hitting that write_single_page : isPacked[${this.pack_pages}] || (front ${isFront}/ first ${isFirst}) [${pagelist.length},${pagelist[0].length}]` + ); + pagelist.forEach((row) => { + row.forEach((page) => { + if (!page.isBlank) filteredList.push(page.num); + }); + }); + if (filteredList.length == 0) { + console.warn('All the pages are empty! : ', pagelist); + return; + } + const embeddedPages = await outPDF.embedPdf(this.managedDoc, filteredList); + // TODO : make sure the max dimen is correct here... + const papersize = isLandscape + ? [this.papersize[1], this.papersize[0]] + : [this.papersize[0], this.papersize[1]]; + const curPage = outPDF.addPage(papersize); + const sourcePage = embeddedPages.slice(0, 1)[0]; + const pageHeight = papersize[1] / pagelist.length; + const pageWidth = papersize[0] / pagelist[0].length; + const heightRatio = pageHeight / sourcePage.height; + const widthRatio = pageWidth / (sourcePage.width + this.fore_edge_padding_pt); + const pageScale = Math.min(heightRatio, widthRatio); + const vGap = papersize[1] - sourcePage.height * pageScale * pagelist.length; + const topGap = this.pack_pages ? vGap / 2.0 : vGap / (pagelist.length * 2); + const hGap = + papersize[0] - + (sourcePage.width + this.fore_edge_padding_pt) * pageScale * pagelist[0].length; + const leftGap = this.pack_pages ? hGap / 2.0 : hGap / pagelist[0].length; + const printPageWidth = pageScale * sourcePage.width; + const printPageHeight = pageScale * sourcePage.height; + const printedForeEdgeGutter = pageScale * this.fore_edge_padding_pt; + for (let row = 0; row < pagelist.length; ++row) { + const y = sourcePage.height * pageScale * row; + for (let i = 0; i < pagelist[row].length; ++i) { + const x = + (sourcePage.width + this.fore_edge_padding_pt) * pageScale * i + + ((this.fore_edge_padding_pt * (i + 1)) % 2); + const pageInfo = pagelist[row][i]; + if (pageInfo.isBlank) continue; + const origPage = embeddedPages[filteredList.indexOf(pageInfo.num)]; + const hOffset = this.pack_pages ? leftGap : (1 + i - (i % 2)) * leftGap; + const vOffset = this.pack_pages ? topGap : topGap + 2 * topGap * row; + const positioning = { + x: + x + + hOffset + + (pageInfo.vFlip ? printPageWidth : 0) + + printedForeEdgeGutter * ((i + 1) % 2), + y: y + vOffset + (pageInfo.vFlip ? printPageHeight : 0), + width: printPageWidth, + height: printPageHeight, + rotate: pageInfo.vFlip ? degrees(180) : degrees(0), + }; + //console.log(" [",row,",",i,"] Given page info ", pageInfo, " now embedding at ", positioning); + curPage.drawPage(origPage, positioning); + } + } + lineMaker({ + isFront: isFront, + gap: [leftGap, topGap], + renderPageSize: [printPageWidth + printedForeEdgeGutter, printPageHeight], + paperSize: papersize, + isPacked: this.pack_pages, + }).forEach((line) => { + curPage.drawLine(line); + }); + } } diff --git a/src/book.test.js b/src/book.test.js index d4aa67c..e20563e 100644 --- a/src/book.test.js +++ b/src/book.test.js @@ -1,67 +1,67 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; -import { Book } from "./book"; -import { schema } from "./models/configuration"; +import { Book } from './book'; +import { schema } from './models/configuration'; -describe("Book model", () => { - // TODO confirm that this is what a newly created book (that is, without any settings changed) should look like; I've copied the result from how it currently works assuming it's working as-intended - const defaultBook = { - inputpdf: null, - password: null, - duplex: true, - duplexrotate: false, - papersize: [595, 842], - flyleafs: 1, - spineoffset: false, - format: "standardsig", - sigsize: 4, - customsig: false, - input: null, - currentdoc: null, - pagecount: null, - cropbox: null, - orderedpages: [], - rearrangedpages: [], - filelist: [], - zip: null, - page_layout: { - rotations: [[-90], [-90]], - landscape: true, - rows: 2, - cols: 1, - per_sheet: 4, - }, - per_sheet: 4, - cropmarks: false, - pdfEdgeMarks: false, - cutmarks: false, - fore_edge_padding_pt: 0, - pack_pages: true, - padding_pt: { - binding: 0, - bottom: 0, - fore_edge: 0, - top: 0, - }, - managedDoc: null, - page_positioning: "centered", - page_scaling: "lockratio", - paper_rotation_90: false, - source_rotation: "none", - print_file: "both", - signatureconfig: [], - }; +describe('Book model', () => { + // TODO confirm that this is what a newly created book (that is, without any settings changed) should look like; I've copied the result from how it currently works assuming it's working as-intended + const defaultBook = { + inputpdf: null, + password: null, + duplex: true, + duplexrotate: false, + papersize: [595, 842], + flyleafs: 1, + spineoffset: false, + format: 'standardsig', + sigsize: 4, + customsig: false, + input: null, + currentdoc: null, + pagecount: null, + cropbox: null, + orderedpages: [], + rearrangedpages: [], + filelist: [], + zip: null, + page_layout: { + rotations: [[-90], [-90]], + landscape: true, + rows: 2, + cols: 1, + per_sheet: 4, + }, + per_sheet: 4, + cropmarks: false, + pdfEdgeMarks: false, + cutmarks: false, + fore_edge_padding_pt: 0, + pack_pages: true, + padding_pt: { + binding: 0, + bottom: 0, + fore_edge: 0, + top: 0, + }, + managedDoc: null, + page_positioning: 'centered', + page_scaling: 'lockratio', + paper_rotation_90: false, + source_rotation: 'none', + print_file: 'both', + signatureconfig: [], + }; - it("returns a new Book", () => { - const defaultConfiguration = schema.parse({}); - const expected = defaultBook; - const actual = new Book(defaultConfiguration); - expect(actual).toEqual(expected); - }); - // TODO test update - // TODO test openPDF - // TODO test createoutputfiles - // TODO test createPages + it('returns a new Book', () => { + const defaultConfiguration = schema.parse({}); + const expected = defaultBook; + const actual = new Book(defaultConfiguration); + expect(actual).toEqual(expected); + }); + // TODO test update + // TODO test openPDF + // TODO test createoutputfiles + // TODO test createPages }); diff --git a/src/constants.js b/src/constants.js index 3fe8435..492fe43 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,179 +1,179 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. /** units in "pt" */ export const PAGE_SIZES = { - LETTER: [612, 792], - NOTE: [540, 720], - LEGAL: [612, 1008], - TABLOID: [792, 1224], - EXECUTIVE: [522, 756], - POSTCARD: [283, 416], - A0: [2384, 3370], - A1: [1684, 2384], - A3: [842, 1191], - A4: [595, 842], - A5: [420, 595], - A6: [297, 420], - A7: [210, 297], - A8: [148, 210], - A9: [105, 148], - B0: [2834, 4008], - B1: [2004, 2834], - B2: [1417, 2004], - B3: [1000, 1417], - B4: [708, 1000], - B5: [498, 708], - B6: [354, 498], - B7: [249, 354], - B8: [175, 249], - B9: [124, 175], - B10: [87, 124], - ARCH_E: [2592, 3456], - ARCH_C: [1296, 1728], - ARCH_B: [864, 1296], - ARCH_A: [648, 864], - FLSA: [612, 936], - FLSE: [648, 936], - HALFLETTER: [396, 612], - _11X17: [792, 1224], - ID_1: [242.65, 153], - ID_2: [297, 210], - ID_3: [354, 249], - LEDGER: [1224, 792], - CROWN_QUARTO: [535, 697], - LARGE_CROWN_QUARTO: [569, 731], - DEMY_QUARTO: [620, 782], - ROYAL_QUARTO: [671, 884], - CROWN_OCTAVO: [348, 527], - LARGE_CROWN_OCTAVO: [365, 561], - DEMY_OCTAVO: [391, 612], - ROYAL_OCTAVO: [442, 663], - SMALL_PAPERBACK: [314, 504], - PENGUIN_SMALL_PAPERBACK: [314, 513], - PENGUIN_LARGE_PAPERBACK: [365, 561], + LETTER: [612, 792], + NOTE: [540, 720], + LEGAL: [612, 1008], + TABLOID: [792, 1224], + EXECUTIVE: [522, 756], + POSTCARD: [283, 416], + A0: [2384, 3370], + A1: [1684, 2384], + A3: [842, 1191], + A4: [595, 842], + A5: [420, 595], + A6: [297, 420], + A7: [210, 297], + A8: [148, 210], + A9: [105, 148], + B0: [2834, 4008], + B1: [2004, 2834], + B2: [1417, 2004], + B3: [1000, 1417], + B4: [708, 1000], + B5: [498, 708], + B6: [354, 498], + B7: [249, 354], + B8: [175, 249], + B9: [124, 175], + B10: [87, 124], + ARCH_E: [2592, 3456], + ARCH_C: [1296, 1728], + ARCH_B: [864, 1296], + ARCH_A: [648, 864], + FLSA: [612, 936], + FLSE: [648, 936], + HALFLETTER: [396, 612], + _11X17: [792, 1224], + ID_1: [242.65, 153], + ID_2: [297, 210], + ID_3: [354, 249], + LEDGER: [1224, 792], + CROWN_QUARTO: [535, 697], + LARGE_CROWN_QUARTO: [569, 731], + DEMY_QUARTO: [620, 782], + ROYAL_QUARTO: [671, 884], + CROWN_OCTAVO: [348, 527], + LARGE_CROWN_OCTAVO: [365, 561], + DEMY_OCTAVO: [391, 612], + ROYAL_OCTAVO: [442, 663], + SMALL_PAPERBACK: [314, 504], + PENGUIN_SMALL_PAPERBACK: [314, 513], + PENGUIN_LARGE_PAPERBACK: [365, 561], }; export const TARGET_BOOK_SIZE = { - standard: [314.5, 502.0], - large: [368.5, 558.5], + standard: [314.5, 502.0], + large: [368.5, 558.5], }; export const LINE_LEN = 18; export const PAGE_LAYOUTS = { - /* + /* Pages in these layouts are assumed to be already reordered. Layout should go left to right, top to bottom. Values are the degree of rotation from a portrait offset needed to re-impose this on a portrait-oriented page, and should only need to be specified for one side. */ - folio: { - rotations: [[-90], [-90]], - landscape: true, - rows: 2, - cols: 1, - per_sheet: 4, - }, - folio_alt: { - rotations: [[90], [90]], - landscape: true, - rows: 2, - cols: 1, - per_sheet: 4, - }, - quarto: { - rotations: [ - [0, 0], - [-180, -180], - ], - landscape: false, - rows: 2, - cols: 2, - per_sheet: 8, - }, - octavo: { - rotations: [ - [-90, 90], - [-90, 90], - [-90, 90], - [-90, 90], - ], - landscape: true, - rows: 4, - cols: 2, - per_sheet: 16, - }, - sextodecimo: { - rotations: [ - [0, 0, 0, 0], - [-180, -180, -180, -180], - [0, 0, 0, 0], - [-180, -180, -180, -180], - ], - landscape: false, - rows: 4, - cols: 4, - per_sheet: 32, - }, + folio: { + rotations: [[-90], [-90]], + landscape: true, + rows: 2, + cols: 1, + per_sheet: 4, + }, + folio_alt: { + rotations: [[90], [90]], + landscape: true, + rows: 2, + cols: 1, + per_sheet: 4, + }, + quarto: { + rotations: [ + [0, 0], + [-180, -180], + ], + landscape: false, + rows: 2, + cols: 2, + per_sheet: 8, + }, + octavo: { + rotations: [ + [-90, 90], + [-90, 90], + [-90, 90], + [-90, 90], + ], + landscape: true, + rows: 4, + cols: 2, + per_sheet: 16, + }, + sextodecimo: { + rotations: [ + [0, 0, 0, 0], + [-180, -180, -180, -180], + [0, 0, 0, 0], + [-180, -180, -180, -180], + ], + landscape: false, + rows: 4, + cols: 4, + per_sheet: 32, + }, }; export const BOOKLET_LAYOUTS = { - /* + /* For page layouts: pages are 1-indexed for sanity reasons, and the order for the back list must be reversed 'front' will be the side that ends up with consecutive pagenumbers on the innermost fold, by convention. page numbers should be listed from left to right, top to bottom, starting in the top left. */ - 4: { - front: [3, 2], - back: [1, 4], - rotate: [4, 1], - }, - 8: { - front: [6, 3, 7, 2], - back: [8, 1, 5, 4], - rotate: [4, 5, 1, 8], - }, - 16: { - front: [3, 6, 14, 11, 15, 10, 2, 7], - back: [1, 8, 16, 9, 13, 12, 4, 5], - rotate: [5, 4, 12, 13, 9, 16, 8, 1], - }, - 32: { - front: [30, 3, 6, 27, 19, 14, 11, 22, 18, 15, 10, 23, 31, 2, 7, 26], - back: [32, 1, 8, 25, 17, 16, 9, 24, 20, 13, 12, 21, 29, 4, 5, 28], - rotate: [28, 5, 4, 29, 21, 12, 13, 20, 24, 9, 16, 17, 25, 8, 1, 32], - }, + 4: { + front: [3, 2], + back: [1, 4], + rotate: [4, 1], + }, + 8: { + front: [6, 3, 7, 2], + back: [8, 1, 5, 4], + rotate: [4, 5, 1, 8], + }, + 16: { + front: [3, 6, 14, 11, 15, 10, 2, 7], + back: [1, 8, 16, 9, 13, 12, 4, 5], + rotate: [5, 4, 12, 13, 9, 16, 8, 1], + }, + 32: { + front: [30, 3, 6, 27, 19, 14, 11, 22, 18, 15, 10, 23, 31, 2, 7, 26], + back: [32, 1, 8, 25, 17, 16, 9, 24, 20, 13, 12, 21, 29, 4, 5, 28], + rotate: [28, 5, 4, 29, 21, 12, 13, 20, 24, 9, 16, 17, 25, 8, 1, 32], + }, }; export const PERFECTBOUND_LAYOUTS = { - /* + /* For page layouts: pages are 1-indexed for sanity reasons, and the order for the back list must be reversed 'front' will be the side that ends up with consecutive pagenumbers on the innermost fold, by convention. page numbers should be listed from left to right, top to bottom, starting in the top left. */ - 4: { - front: [3, 2], - back: [1, 4], - rotate: [4, 1], - }, - 8: { - front: [4, 1, 7, 6], - back: [8, 5, 3, 2], - rotate: [2, 3, 5, 8], - }, - 16: { - front: [5, 10, 8, 11, 3, 16, 2, 13], - back: [1, 14, 4, 15, 7, 12, 6, 9], - rotate: [9, 6, 12, 7, 15, 4, 14, 1], - }, - 32: { - front: [8, 5, 10, 11, 27, 26, 21, 24, 32, 29, 18, 19, 3, 2, 13, 16], - back: [4, 1, 14, 15, 31, 30, 17, 20, 28, 25, 22, 23, 7, 6, 9, 12], - rotate: [12, 9, 6, 7, 23, 22, 25, 28, 20, 17, 30, 31, 15, 14, 1, 4], - }, + 4: { + front: [3, 2], + back: [1, 4], + rotate: [4, 1], + }, + 8: { + front: [4, 1, 7, 6], + back: [8, 5, 3, 2], + rotate: [2, 3, 5, 8], + }, + 16: { + front: [5, 10, 8, 11, 3, 16, 2, 13], + back: [1, 14, 4, 15, 7, 12, 6, 9], + rotate: [9, 6, 12, 7, 15, 4, 14, 1], + }, + 32: { + front: [8, 5, 10, 11, 27, 26, 21, 24, 32, 29, 18, 19, 3, 2, 13, 16], + back: [4, 1, 14, 15, 31, 30, 17, 20, 28, 25, 22, 23, 7, 6, 9, 12], + rotate: [12, 9, 6, 7, 23, 22, 25, 28, 20, 17, 30, 31, 15, 14, 1, 4], + }, }; diff --git a/src/constants.test.js b/src/constants.test.js index ef7f354..4d98ead 100644 --- a/src/constants.test.js +++ b/src/constants.test.js @@ -1,83 +1,71 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; import { PERFECTBOUND_LAYOUTS, BOOKLET_LAYOUTS, PAGE_LAYOUTS } from './constants'; function numSort(a, b) { - return a - b; - } - + return a - b; +} describe('Tests of the perfectbound layout constants', () => { - Object.keys(PERFECTBOUND_LAYOUTS).forEach( (key) => { - const {front, back, rotate} = PERFECTBOUND_LAYOUTS[key]; - it(`has the same pages on the back regardless of rotation for ${key}-per-sheet layouts`, () => { - const back_sorted = back.sort(numSort); - const back_rotated = rotate.sort(numSort); - expect(back_sorted).toEqual(back_rotated); - - }); - - it(`accounts for all pages in ${key}-per-sheet layouts`, () => { - let both = front.concat(back); - both.sort(numSort); - const expected = Array.from({length: key}, (x, i) => i + 1); - expect(both).toEqual(expected); - - }); -}); - + Object.keys(PERFECTBOUND_LAYOUTS).forEach((key) => { + const { front, back, rotate } = PERFECTBOUND_LAYOUTS[key]; + it(`has the same pages on the back regardless of rotation for ${key}-per-sheet layouts`, () => { + const back_sorted = back.sort(numSort); + const back_rotated = rotate.sort(numSort); + expect(back_sorted).toEqual(back_rotated); + }); + + it(`accounts for all pages in ${key}-per-sheet layouts`, () => { + const both = front.concat(back); + both.sort(numSort); + const expected = Array.from({ length: key }, (x, i) => i + 1); + expect(both).toEqual(expected); + }); + }); }); - describe('Tests of the booklet layout constants', () => { - Object.keys(BOOKLET_LAYOUTS).forEach( (key) => { - const {front, back, rotate} = BOOKLET_LAYOUTS[key]; - it(`has the same pages on the back regardless of rotation for ${key}-per-sheet layouts`, () => { - const back_sorted = back.sort(numSort); - const back_rotated = rotate.sort(numSort); - expect(back_sorted).toEqual(back_rotated); - - }); - - it(`accounts for all pages in ${key}-per-sheet layouts`, () => { - let both = front.concat(back); - both.sort(numSort); - const expected = Array.from({length: key}, (x, i) => i + 1); - expect(both).toEqual(expected); - - }); -}); - + Object.keys(BOOKLET_LAYOUTS).forEach((key) => { + const { front, back, rotate } = BOOKLET_LAYOUTS[key]; + it(`has the same pages on the back regardless of rotation for ${key}-per-sheet layouts`, () => { + const back_sorted = back.sort(numSort); + const back_rotated = rotate.sort(numSort); + expect(back_sorted).toEqual(back_rotated); + }); + + it(`accounts for all pages in ${key}-per-sheet layouts`, () => { + const both = front.concat(back); + both.sort(numSort); + const expected = Array.from({ length: key }, (x, i) => i + 1); + expect(both).toEqual(expected); + }); + }); }); describe('Tests of the page layout constants', () => { - Object.keys(PAGE_LAYOUTS).forEach( (key) => { - const {rotations, rows, cols, per_sheet} = PAGE_LAYOUTS[key]; - it(`has the correct number of rows and columns ${key} layouts`, () => { - const row_cols = rows * cols - // rows and cols are per-side, while per_sheet is both sides - expect(per_sheet).toEqual(row_cols * 2); - - }); - - it(`accounts for all rows in ${key} layout rotations`, () => { - expect(rows).toEqual(rotations.length); - - }); - - it(`accounts for all columns in ${key} layout rotations`, () => { - let col_counts = []; - rotations.forEach(row => { - col_counts.push(row.length); - }); - const col_array = new Array(rows).fill(cols); - expect(col_counts).toEqual(col_array); - - }); + Object.keys(PAGE_LAYOUTS).forEach((key) => { + const { rotations, rows, cols, per_sheet } = PAGE_LAYOUTS[key]; + it(`has the correct number of rows and columns ${key} layouts`, () => { + const row_cols = rows * cols; + // rows and cols are per-side, while per_sheet is both sides + expect(per_sheet).toEqual(row_cols * 2); + }); + + it(`accounts for all rows in ${key} layout rotations`, () => { + expect(rows).toEqual(rotations.length); + }); + + it(`accounts for all columns in ${key} layout rotations`, () => { + const col_counts = []; + rotations.forEach((row) => { + col_counts.push(row.length); + }); + const col_array = new Array(rows).fill(cols); + expect(col_counts).toEqual(col_array); + }); + }); }); - -}); \ No newline at end of file diff --git a/src/main.js b/src/main.js index b1d1217..5a1b15c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,56 +1,56 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { Book } from './book.js'; import { loadForm } from './utils/formUtils.js'; import { handleFileChange, handleInputChange } from './utils/changeHandlers.js'; -import { handleGenerateClick, handlePreviewClick, handleResetSettingsClick } from './utils/clickHandlers.js'; +import { + handleGenerateClick, + handlePreviewClick, + handleResetSettingsClick, +} from './utils/clickHandlers.js'; import { renderPaperSelectOptions } from './utils/renderUtils.js'; window.addEventListener('DOMContentLoaded', () => { - // render dynamic content - renderPaperSelectOptions(); - const configuration = loadForm(); + // render dynamic content + renderPaperSelectOptions(); + const configuration = loadForm(); - // grab DOM elements - const generate = document.getElementById('generate'); - const preview = document.getElementById('preview'); - const resetSettings = document.getElementById('reset_settings'); - const bookbinderForm = document.getElementById('bookbinder'); - const fileInput = document.getElementById('input_file'); - const inputs = document.querySelectorAll('input, select'); - const sourceRotation = document.getElementById('source_rotation'); - const sourceRotationExamples = Array.from(document.getElementsByClassName('source_rotation_example')); + // grab DOM elements + const generate = document.getElementById('generate'); + const preview = document.getElementById('preview'); + const resetSettings = document.getElementById('reset_settings'); + const bookbinderForm = document.getElementById('bookbinder'); + const fileInput = document.getElementById('input_file'); + const inputs = document.querySelectorAll('input, select'); + const sourceRotation = document.getElementById('source_rotation'); + const sourceRotationExamples = Array.from( + document.getElementsByClassName('source_rotation_example') + ); - // spin up a book to pass to listeners - const book = new Book(configuration); + // spin up a book to pass to listeners + const book = new Book(configuration); - // add event listeners to grabbed elements - inputs.forEach((input) => { - input.addEventListener('change', () => - handleInputChange(book, bookbinderForm) - ); - }); - fileInput.addEventListener('change', (e) => { - handleFileChange(e, book); - generate.removeAttribute('disabled'); - preview.removeAttribute('disabled'); - }); - generate.addEventListener('click', () => - handleGenerateClick(generate, book) - ); - preview.addEventListener('click', () => - handlePreviewClick(preview, book) - ); - resetSettings.addEventListener('click', () => { - console.log('Resetting settings...'); - handleResetSettingsClick(book); - }); - sourceRotation.addEventListener('change', (e) => { - const selectedValue = e.target.value + '_example'; - sourceRotationExamples.forEach((example) => { - example.style.display = (example.id === selectedValue ? 'block' : 'none'); - }); + // add event listeners to grabbed elements + inputs.forEach((input) => { + input.addEventListener('change', () => handleInputChange(book, bookbinderForm)); + }); + fileInput.addEventListener('change', (e) => { + handleFileChange(e, book); + generate.removeAttribute('disabled'); + preview.removeAttribute('disabled'); + }); + generate.addEventListener('click', () => handleGenerateClick(generate, book)); + preview.addEventListener('click', () => handlePreviewClick(preview, book)); + resetSettings.addEventListener('click', () => { + console.log('Resetting settings...'); + handleResetSettingsClick(book); + }); + sourceRotation.addEventListener('change', (e) => { + const selectedValue = `${e.target.value}_example`; + sourceRotationExamples.forEach((example) => { + example.style.display = example.id === selectedValue ? 'block' : 'none'; }); + }); }); diff --git a/src/models/configuration.js b/src/models/configuration.js index d96465f..27f370c 100644 --- a/src/models/configuration.js +++ b/src/models/configuration.js @@ -1,85 +1,101 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { ZodType, z } from "zod"; -import { PAGE_SIZES } from "../constants"; +import { z } from 'zod'; +import { PAGE_SIZES } from '../constants'; const commaSeparatedNumberList = z - .union([z.string(), z.array(z.number())]) - .transform((val) => { - if (Array.isArray(val)) { - return val; - } + .union([z.string(), z.array(z.number())]) + .transform((val) => { + if (Array.isArray(val)) { + return val; + } - return val.split(/, */); - }) - .pipe(z.array(z.coerce.number())); + return val.split(/, */); + }) + .pipe(z.array(z.coerce.number())); const coercedBoolean = z - .union([ - z - .string() - .toLowerCase() - .pipe(z.enum(["true", "false", "on"])), - z.boolean(), - ]) - .transform((val) => val === "true" || val === "on" || val === true); + .union([ + z + .string() + .toLowerCase() + .pipe(z.enum(['true', 'false', 'on'])), + z.boolean(), + ]) + .transform((val) => val === 'true' || val === 'on' || val === true); /** * @type {(schema: ZodType) => ZodType} */ const urlSafe = (schema) => schema.optional().catch(() => undefined); -const sourceRotation = urlSafe(z.enum(["none", "90cw", "90ccw", "out_binding", "in_binding"])).default("none"); +const sourceRotation = urlSafe( + z.enum(['none', '90cw', '90ccw', 'out_binding', 'in_binding']) +).default('none'); /** @type { keyof typeof import("../constants").PAGE_SIZES } */ const availablePaperSizes = Object.keys(PAGE_SIZES); -const paperSize = urlSafe(z.enum([...availablePaperSizes, "CUSTOM"])).default("A4"); +const paperSize = urlSafe(z.enum([...availablePaperSizes, 'CUSTOM'])).default('A4'); -const paperSizeUnit = urlSafe(z.enum(["pt", "in", "cm"])).default("pt"); +const paperSizeUnit = urlSafe(z.enum(['pt', 'in', 'cm'])).default('pt'); -const printerType = urlSafe(z.enum(["single", "duplex"])).default("duplex"); +const printerType = urlSafe(z.enum(['single', 'duplex'])).default('duplex'); -const pageLayout = urlSafe(z.enum(["folio", "quarto", "octavo", "sextodecimo"])).default("folio"); +const pageLayout = urlSafe(z.enum(['folio', 'quarto', 'octavo', 'sextodecimo'])).default('folio'); -const pageScaling = urlSafe(z.enum(["centered", "lockratio", "stretch"])).default("lockratio"); +const pageScaling = urlSafe(z.enum(['centered', 'lockratio', 'stretch'])).default('lockratio'); -const pagePositioning = urlSafe(z.enum(["centered", "binding_aligned"])).default("centered"); +const pagePositioning = urlSafe(z.enum(['centered', 'binding_aligned'])).default('centered'); -const sigFormat = urlSafe(z.enum(["booklet", "perfect", "standardsig", "customsig", "1_3rd", "A7_2_16s", "8_zine", "a_3_6s", "a9_3_3_4", "a_4_8s", "a10_6_10s"])).default("standardsig"); +const sigFormat = urlSafe( + z.enum([ + 'booklet', + 'perfect', + 'standardsig', + 'customsig', + '1_3rd', + 'A7_2_16s', + '8_zine', + 'a_3_6s', + 'a9_3_3_4', + 'a_4_8s', + 'a10_6_10s', + ]) +).default('standardsig'); -const wackySpacing = urlSafe(z.enum(["wacky_pack", "wacky_gap"])).default("wacky_pack"); +const wackySpacing = urlSafe(z.enum(['wacky_pack', 'wacky_gap'])).default('wacky_pack'); -const printFile = urlSafe(z.enum(["aggregated", "signatures", "both"])).default("both"); +const printFile = urlSafe(z.enum(['aggregated', 'signatures', 'both'])).default('both'); export const schema = z.object({ - printFile, - sourceRotation, - rotatePage: urlSafe(coercedBoolean).default(false), - paperSize, - paperSizeUnit, - printerType, - paperRotation90: urlSafe(coercedBoolean).default(false), - pageLayout, - cropMarks: urlSafe(coercedBoolean).default(false), - cutMarks: urlSafe(coercedBoolean).default(false), - pdfEdgeMarks: urlSafe(coercedBoolean).default(false), - pageScaling, - pagePositioning, - mainForeEdgePaddingPt: urlSafe(z.coerce.number()).default(0), - bindingEdgePaddingPt: urlSafe(z.coerce.number()).default(0), - topEdgePaddingPt: urlSafe(z.coerce.number()).default(0), - bottomEdgePaddingPt: urlSafe(z.coerce.number()).default(0), - sigFormat, - sigLength: urlSafe(z.coerce.number()).default(4), // Specific to standard - customSigLength: urlSafe(commaSeparatedNumberList).default([]), // Specific to custom. - foreEdgePaddingPt: urlSafe(z.coerce.number()).default(0), // specific to wacky small - wackySpacing, // specific to wacky small - flyleafs: urlSafe(z.coerce.number()).default(1), - paperSizeCustomWidth: urlSafe(z.coerce.number()), - paperSizeCustomHeight: urlSafe(z.coerce.number()), + printFile, + sourceRotation, + rotatePage: urlSafe(coercedBoolean).default(false), + paperSize, + paperSizeUnit, + printerType, + paperRotation90: urlSafe(coercedBoolean).default(false), + pageLayout, + cropMarks: urlSafe(coercedBoolean).default(false), + cutMarks: urlSafe(coercedBoolean).default(false), + pdfEdgeMarks: urlSafe(coercedBoolean).default(false), + pageScaling, + pagePositioning, + mainForeEdgePaddingPt: urlSafe(z.coerce.number()).default(0), + bindingEdgePaddingPt: urlSafe(z.coerce.number()).default(0), + topEdgePaddingPt: urlSafe(z.coerce.number()).default(0), + bottomEdgePaddingPt: urlSafe(z.coerce.number()).default(0), + sigFormat, + sigLength: urlSafe(z.coerce.number()).default(4), // Specific to standard + customSigLength: urlSafe(commaSeparatedNumberList).default([]), // Specific to custom. + foreEdgePaddingPt: urlSafe(z.coerce.number()).default(0), // specific to wacky small + wackySpacing, // specific to wacky small + flyleafs: urlSafe(z.coerce.number()).default(1), + paperSizeCustomWidth: urlSafe(z.coerce.number()), + paperSizeCustomHeight: urlSafe(z.coerce.number()), }); /** @typedef {z.infer} Configuration */ diff --git a/src/perfectbound.js b/src/perfectbound.js index 89f6a01..36005f0 100644 --- a/src/perfectbound.js +++ b/src/perfectbound.js @@ -1,62 +1,59 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { PERFECTBOUND_LAYOUTS } from './constants'; export class PerfectBound { - - // Duplex printer pages are arranged in a 4-1-2-3 pattern. - // Single-sided pages are arranged in two sets, 4-1 and 2-3. - // After printing two to a page, each sheet is folded in half - // and all the sheets collated into a block for gluing. - // this.pagelist holds the rearranged index numbers that the - // book class uses to create a finished document - constructor(pages, duplex, per_sheet, duplexrotate) { - this.duplex=duplex; - this.per_sheet = per_sheet || 4; // pages per sheet - default is 4. - this.duplexrotate = duplexrotate || false; - - this.sheets = Math.ceil(pages.length/per_sheet); - this.sigconfig=['N/A']; - - const {front, rotate, back} = PERFECTBOUND_LAYOUTS[per_sheet]; - const front_config = front; - const back_config = duplexrotate ? rotate : back; - - this.pagelistdetails = duplex ? [[]] : [[], []]; - - // Pad the page list with blanks if necessary - const totalpages = this.sheets * per_sheet; - if (totalpages > pages.length) { - const diff = totalpages - pages.length; - let blanks = new Array(diff).fill('b'); - this.inputpagelist.push(...blanks); - } - - for (let i=0; i < pages.length; i=i+per_sheet ) { - const block = pages.slice(i, i + per_sheet); - - front_config.forEach((pnum) => { - let page = block[pnum - 1]; //page layouts are 1-indexed, not 0-index - this.pagelistdetails[0].push({ - info: page, - isSigStart: true, - isSigEnd: true - }); - }); - - const backlist = this.duplex ? 0 : 1; - - back_config.forEach((pnum) => { - let page = block[pnum - 1]; - this.pagelistdetails[backlist].push({ - info: page, - isSigStart: true, - isSigEnd: true - }); - }); - - } + // Duplex printer pages are arranged in a 4-1-2-3 pattern. + // Single-sided pages are arranged in two sets, 4-1 and 2-3. + // After printing two to a page, each sheet is folded in half + // and all the sheets collated into a block for gluing. + // this.pagelist holds the rearranged index numbers that the + // book class uses to create a finished document + constructor(pages, duplex, per_sheet, duplexrotate) { + this.duplex = duplex; + this.per_sheet = per_sheet || 4; // pages per sheet - default is 4. + this.duplexrotate = duplexrotate || false; + + this.sheets = Math.ceil(pages.length / per_sheet); + this.sigconfig = ['N/A']; + + const { front, rotate, back } = PERFECTBOUND_LAYOUTS[per_sheet]; + const front_config = front; + const back_config = duplexrotate ? rotate : back; + + this.pagelistdetails = duplex ? [[]] : [[], []]; + + // Pad the page list with blanks if necessary + const totalpages = this.sheets * per_sheet; + if (totalpages > pages.length) { + const diff = totalpages - pages.length; + const blanks = new Array(diff).fill('b'); + this.inputpagelist.push(...blanks); } -} \ No newline at end of file + for (let i = 0; i < pages.length; i = i + per_sheet) { + const block = pages.slice(i, i + per_sheet); + + front_config.forEach((pnum) => { + const page = block[pnum - 1]; //page layouts are 1-indexed, not 0-index + this.pagelistdetails[0].push({ + info: page, + isSigStart: true, + isSigEnd: true, + }); + }); + + const backlist = this.duplex ? 0 : 1; + + back_config.forEach((pnum) => { + const page = block[pnum - 1]; + this.pagelistdetails[backlist].push({ + info: page, + isSigStart: true, + isSigEnd: true, + }); + }); + } + } +} diff --git a/src/perfectbound.test.js b/src/perfectbound.test.js index ee0bc35..0687464 100644 --- a/src/perfectbound.test.js +++ b/src/perfectbound.test.js @@ -1,69 +1,77 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; import { PerfectBound } from './perfectbound'; const testPages = []; -const testPagesEven = Array.from({length: 32}, (x, i) => i + 1); -const generatePageInfo = (page) => {return {info: page, isSigEnd: true, isSigStart: true}}; +const testPagesEven = Array.from({ length: 32 }, (x, i) => i + 1); +const generatePageInfo = (page) => { + return { info: page, isSigEnd: true, isSigStart: true }; +}; describe('PerfectBound model', () => { - it('returns config for a perfectbound booklet based on params (default)', () => { - const testDuplex = true; - const expected = { - duplex: true, - sheets: 0, - per_sheet: 4, - duplexrotate: true, - sigconfig: ['N/A'], - pagelistdetails: [[]], - }; - const actual = new PerfectBound(testPages, testDuplex, 4, true); - expect(actual).toEqual(expected); - }); + it('returns config for a perfectbound booklet based on params (default)', () => { + const testDuplex = true; + const expected = { + duplex: true, + sheets: 0, + per_sheet: 4, + duplexrotate: true, + sigconfig: ['N/A'], + pagelistdetails: [[]], + }; + const actual = new PerfectBound(testPages, testDuplex, 4, true); + expect(actual).toEqual(expected); + }); - it('returns config for a perfectbound booklet based on params (no duplex)', () => { - const testDuplex = false; - const expected = { - duplex: false, - duplexrotate: false, - sheets: 0, - per_sheet: 4, - sigconfig: ['N/A'], - pagelistdetails: [[], []], - }; - const actual = new PerfectBound(testPages, testDuplex, 4, false); - expect(actual).toEqual(expected); - }); + it('returns config for a perfectbound booklet based on params (no duplex)', () => { + const testDuplex = false; + const expected = { + duplex: false, + duplexrotate: false, + sheets: 0, + per_sheet: 4, + sigconfig: ['N/A'], + pagelistdetails: [[], []], + }; + const actual = new PerfectBound(testPages, testDuplex, 4, false); + expect(actual).toEqual(expected); + }); - it('correctly orders folio pages with no duplex', () => { - const testDuplex = false; - const expected = { - duplex: false, - duplexrotate: false, - sheets: 8, - per_sheet: 4, - sigconfig: ['N/A'], - pagelistdetails: [[3,2,7,6,11,10,15,14,19,18,23,22,27,26,31,30].map(generatePageInfo), [1,4,5,8,9,12,13,16,17,20,21,24,25,28,29,32].map(generatePageInfo)], - }; - const actual = new PerfectBound(testPagesEven, testDuplex, 4, false); - expect(actual).toEqual(expected); - }); + it('correctly orders folio pages with no duplex', () => { + const testDuplex = false; + const expected = { + duplex: false, + duplexrotate: false, + sheets: 8, + per_sheet: 4, + sigconfig: ['N/A'], + pagelistdetails: [ + [3, 2, 7, 6, 11, 10, 15, 14, 19, 18, 23, 22, 27, 26, 31, 30].map(generatePageInfo), + [1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21, 24, 25, 28, 29, 32].map(generatePageInfo), + ], + }; + const actual = new PerfectBound(testPagesEven, testDuplex, 4, false); + expect(actual).toEqual(expected); + }); - it('correctly orders quarto pages with no duplex', () => { - const testDuplex = false; - const expected = { - duplex: false, - duplexrotate: false, - sheets: 4, - per_sheet: 8, - sigconfig: ['N/A'], - pagelistdetails: [[4,1,7,6,12,9,15,14,20,17,23,22,28,25,31,30].map(generatePageInfo), [8,5,3,2,16,13,11,10,24,21,19,18,32,29,27,26].map(generatePageInfo)], - }; - const actual = new PerfectBound(testPagesEven, testDuplex, 8, false); - expect(actual).toEqual(expected); - }); + it('correctly orders quarto pages with no duplex', () => { + const testDuplex = false; + const expected = { + duplex: false, + duplexrotate: false, + sheets: 4, + per_sheet: 8, + sigconfig: ['N/A'], + pagelistdetails: [ + [4, 1, 7, 6, 12, 9, 15, 14, 20, 17, 23, 22, 28, 25, 31, 30].map(generatePageInfo), + [8, 5, 3, 2, 16, 13, 11, 10, 24, 21, 19, 18, 32, 29, 27, 26].map(generatePageInfo), + ], + }; + const actual = new PerfectBound(testPagesEven, testDuplex, 8, false); + expect(actual).toEqual(expected); + }); }); diff --git a/src/signatures.js b/src/signatures.js index 1a15123..21e5da8 100644 --- a/src/signatures.js +++ b/src/signatures.js @@ -1,172 +1,166 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { BOOKLET_LAYOUTS } from './constants'; export class Signatures { + // Takes a list of pagenumbers, splits them evenly, then rearranges the pages in each chunk. - // Takes a list of pagenumbers, splits them evenly, then rearranges the pages in each chunk. + constructor(pages, duplex, sigsize, per_sheet, duplexrotate) { + this.sigsize = sigsize; + this.duplex = duplex; + this.inputpagelist = pages; + this.per_sheet = per_sheet || 4; // pages per sheet - default is 4. + this.duplexrotate = duplexrotate || false; - constructor(pages, duplex, sigsize, per_sheet, duplexrotate) { - this.sigsize = sigsize; - this.duplex = duplex; - this.inputpagelist = pages; - this.per_sheet = per_sheet || 4; // pages per sheet - default is 4. - this.duplexrotate = duplexrotate || false; + this.pagelistdetails = []; - this.pagelistdetails = []; + this.sheets = Math.ceil(pages.length / this.per_sheet); - this.sheets = Math.ceil(pages.length / this.per_sheet); + this.sigconfig = []; + this.signaturepagelists = []; + } + setsigconfig(config) { + this.sigconfig = config; - this.sigconfig = []; - this.signaturepagelists = []; + const targetlength = this.inputpagelist.length; - } - setsigconfig(config) { + // calculatelength given by multiplying config values by pages per sheet + // and ensuring padding if longer than this.inputlist + let total = 0; - this.sigconfig = config; + this.sigconfig.forEach((num) => (total += num * this.per_sheet)); - let targetlength = this.inputpagelist.length; - - // calculatelength given by multiplying config values by pages per sheet - // and ensuring padding if longer than this.inputlist - let total = 0; - - this.sigconfig.forEach(num => total += num * this.per_sheet); - - if (total > targetlength) { - - let diff = total - targetlength; - let blanks = new Array(diff).fill('b'); - this.inputpagelist.push(...blanks); - } - this.pagelistdetails = []; - this.signaturepagelists = []; - - this.splitpagelist(); - } - createsigconfig() { - - this.sigconfig = this.generatesignatureindex(); - this.pagelistdetails = []; - this.signaturepagelists = []; - - this.splitpagelist(); - } - - splitpagelist() { - let point = 0; - let splitpoints = [0]; - - // calculate the points at which to split the document - this.sigconfig.forEach(number => { - point = point + (number * this.per_sheet); - splitpoints.push(point); - }); - - - for (let i = 0; i < this.sigconfig.length; i++) { - let start = splitpoints[i]; - let end = splitpoints[i + 1]; - - let pagerange = this.inputpagelist.slice(start, end); // .reverse(); - this.signaturepagelists.push(pagerange); - } - - let newsigs = []; - - // Use the booklet class for each signature - this.signaturepagelists.forEach(pagerange => { - let pagelistdetails = this.booklet(pagerange, this.duplex, this.per_sheet, this.duplexrotate); - newsigs.push(pagelistdetails); - }); - - this.pagelistdetails = newsigs; - } - generatesignatureindex() { - - let preliminarytotal = Math.floor(this.sheets / this.sigsize); - let modulus = this.sheets % this.sigsize; - let signaturetotal = preliminarytotal; - let flag = false; - let result = []; - - if (modulus > 0) { - - // need an extra signature - signaturetotal += 1; - flag = true; - } - - - // calculate how many signatures are the full size and how many are one sheet short. - let factor = signaturetotal - (this.sigsize - 1); - factor += (modulus - 1); - - for(let i = 0; i < signaturetotal; i++) { - - if (i >= factor && flag) { - result.push(this.sigsize - 1); - } else { - result.push(this.sigsize); - } - } + if (total > targetlength) { + const diff = total - targetlength; + const blanks = new Array(diff).fill('b'); + this.inputpagelist.push(...blanks); + } + this.pagelistdetails = []; + this.signaturepagelists = []; + + this.splitpagelist(); + } + createsigconfig() { + this.sigconfig = this.generatesignatureindex(); + this.pagelistdetails = []; + this.signaturepagelists = []; + + this.splitpagelist(); + } + + splitpagelist() { + let point = 0; + const splitpoints = [0]; + + // calculate the points at which to split the document + this.sigconfig.forEach((number) => { + point = point + number * this.per_sheet; + splitpoints.push(point); + }); + + for (let i = 0; i < this.sigconfig.length; i++) { + const start = splitpoints[i]; + const end = splitpoints[i + 1]; + + const pagerange = this.inputpagelist.slice(start, end); // .reverse(); + this.signaturepagelists.push(pagerange); + } - return result; - } + const newsigs = []; + + // Use the booklet class for each signature + this.signaturepagelists.forEach((pagerange) => { + const pagelistdetails = this.booklet( + pagerange, + this.duplex, + this.per_sheet, + this.duplexrotate + ); + newsigs.push(pagelistdetails); + }); + + this.pagelistdetails = newsigs; + } + generatesignatureindex() { + const preliminarytotal = Math.floor(this.sheets / this.sigsize); + const modulus = this.sheets % this.sigsize; + let signaturetotal = preliminarytotal; + let flag = false; + const result = []; + + if (modulus > 0) { + // need an extra signature + signaturetotal += 1; + flag = true; + } - booklet(pages, duplex, per_sheet, duplexrotate) { - let pagelistdetails = duplex ? [[]] : [[], []]; - let sheets = 1; - const {front, rotate, back} = BOOKLET_LAYOUTS[per_sheet]; + // calculate how many signatures are the full size and how many are one sheet short. + let factor = signaturetotal - (this.sigsize - 1); + factor += modulus - 1; - let center = pages.length / 2; // because of zero indexing, this is actually the first page after the center fold - const pageblock = per_sheet / 2; // number of pages before and after the center fold, per sheet - const front_config = front; - const back_config = duplexrotate ? rotate : back; + for (let i = 0; i < signaturetotal; i++) { + if (i >= factor && flag) { + result.push(this.sigsize - 1); + } else { + result.push(this.sigsize); + } + } - // The way the code works: we start with the innermost sheet of the signature (the only one with consecutive page numbers) - // We then grab the the sections of pages that come on either side and reorder according to predefined page layout - - let front_start = center - pageblock; - let front_end = center; - let back_start = center; - let back_end = center + pageblock; - - while (front_start >= 0 && back_end <= pages.length) { - let front_block = pages.slice(front_start, front_end); - let back_block = pages.slice(back_start, back_end); - - let block = [...front_block, ...back_block]; - - front_config.forEach((pnum) => { - let page = block[pnum - 1]; //page layouts are 1-indexed, not 0-index - pagelistdetails[0].push({ - info: page, - isSigStart:front_start == 0 && pnum == 1, - isSigEnd: front_start == 0 && pnum == block.length - }); - }); - - const backlist = this.duplex ? 0 : 1; - - back_config.forEach((pnum) => { - let page = block[pnum - 1]; - pagelistdetails[backlist].push({ - info: page, - isSigStart:front_start == 0 && pnum == 1, - isSigEnd: front_start == 0 && pnum == block.length - }); - }); - - // Update all our counters - front_start -= pageblock; - front_end -= pageblock; - back_start += pageblock; - back_end += pageblock; - } - - return pagelistdetails; + return result; + } + + booklet(pages, duplex, per_sheet, duplexrotate) { + const pagelistdetails = duplex ? [[]] : [[], []]; + const { front, rotate, back } = BOOKLET_LAYOUTS[per_sheet]; + + const center = pages.length / 2; // because of zero indexing, this is actually the first page after the center fold + const pageblock = per_sheet / 2; // number of pages before and after the center fold, per sheet + const front_config = front; + const back_config = duplexrotate ? rotate : back; + + // The way the code works: we start with the innermost sheet of the signature (the only one with consecutive page numbers) + // We then grab the the sections of pages that come on either side and reorder according to predefined page layout + + let front_start = center - pageblock; + let front_end = center; + let back_start = center; + let back_end = center + pageblock; + + while (front_start >= 0 && back_end <= pages.length) { + const front_block = pages.slice(front_start, front_end); + const back_block = pages.slice(back_start, back_end); + + const block = [...front_block, ...back_block]; + + front_config.forEach((pnum) => { + const page = block[pnum - 1]; //page layouts are 1-indexed, not 0-index + pagelistdetails[0].push({ + info: page, + isSigStart: front_start == 0 && pnum == 1, + isSigEnd: front_start == 0 && pnum == block.length, + }); + }); + + const backlist = this.duplex ? 0 : 1; + + back_config.forEach((pnum) => { + const page = block[pnum - 1]; + pagelistdetails[backlist].push({ + info: page, + isSigStart: front_start == 0 && pnum == 1, + isSigEnd: front_start == 0 && pnum == block.length, + }); + }); + + // Update all our counters + front_start -= pageblock; + front_end -= pageblock; + back_start += pageblock; + back_end += pageblock; } -} \ No newline at end of file + + return pagelistdetails; + } +} diff --git a/src/signatures.test.js b/src/signatures.test.js index db0310e..0e1e39c 100644 --- a/src/signatures.test.js +++ b/src/signatures.test.js @@ -1,37 +1,37 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; import { Signatures } from './signatures'; describe('Signatures model', () => { - it('returns a new Signatures instance', () => { - const testPages = []; - const testDuplex = false; - const testSigSize = 4; - const testPerSheet = 8; - const testDuplexRotate = true; - const expected = { - sigsize: 4, - duplex: false, - inputpagelist: [], - per_sheet: 8, - duplexrotate: true, - pagelistdetails: [], - sheets: 0, - sigconfig: [], - signaturepagelists: [], - }; - const actual = new Signatures( - testPages, - testDuplex, - testSigSize, - testPerSheet, - testDuplexRotate - ); - expect(actual).toEqual(expected); - }); - // TODO add tests with actual pages + it('returns a new Signatures instance', () => { + const testPages = []; + const testDuplex = false; + const testSigSize = 4; + const testPerSheet = 8; + const testDuplexRotate = true; + const expected = { + sigsize: 4, + duplex: false, + inputpagelist: [], + per_sheet: 8, + duplexrotate: true, + pagelistdetails: [], + sheets: 0, + sigconfig: [], + signaturepagelists: [], + }; + const actual = new Signatures( + testPages, + testDuplex, + testSigSize, + testPerSheet, + testDuplexRotate + ); + expect(actual).toEqual(expected); + }); + // TODO add tests with actual pages }); diff --git a/src/utils/changeHandlers.js b/src/utils/changeHandlers.js index 8d7491a..8c774a4 100644 --- a/src/utils/changeHandlers.js +++ b/src/utils/changeHandlers.js @@ -1,25 +1,25 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { saveForm, updateRenderedForm } from "./formUtils"; -import { updatePaperSelectOptionsUnits, updateAddOrRemoveCustomPaperOption} from './renderUtils'; +import { saveForm, updateRenderedForm } from './formUtils'; +import { updatePaperSelectOptionsUnits, updateAddOrRemoveCustomPaperOption } from './renderUtils'; export function handleInputChange(book, bookbinderForm) { - const formData = new FormData(bookbinderForm); - const updatedConfiguration = saveForm(formData); - book.update(updatedConfiguration); - updateAddOrRemoveCustomPaperOption() - updatePaperSelectOptionsUnits() // make sure this goes AFTER the Custom update! - if (book.inputpdf) { - updateRenderedForm(book); - } + const formData = new FormData(bookbinderForm); + const updatedConfiguration = saveForm(formData); + book.update(updatedConfiguration); + updateAddOrRemoveCustomPaperOption(); + updatePaperSelectOptionsUnits(); // make sure this goes AFTER the Custom update! + if (book.inputpdf) { + updateRenderedForm(book); + } } export function handleFileChange(e, book) { - const fileList = e.target.files; - if (fileList.length > 0) { - const updated = book.openpdf(fileList[0]); - updated.then(() => updateRenderedForm(book)); - } + const fileList = e.target.files; + if (fileList.length > 0) { + const updated = book.openpdf(fileList[0]); + updated.then(() => updateRenderedForm(book)); + } } diff --git a/src/utils/clickHandlers.js b/src/utils/clickHandlers.js index 2d515d3..9964798 100644 --- a/src/utils/clickHandlers.js +++ b/src/utils/clickHandlers.js @@ -1,50 +1,50 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { resetForm } from "./formUtils"; -import { updateAddOrRemoveCustomPaperOption, updatePaperSelectOptionsUnits } from "./renderUtils"; +import { resetForm } from './formUtils'; +import { updateAddOrRemoveCustomPaperOption, updatePaperSelectOptionsUnits } from './renderUtils'; export function handleGenerateClick(generateEl, book) { - generateEl.setAttribute('disabled', true); - generateEl.style.fontSize = "13px"; - generateEl.innerText = 'Generating, this may take a little while...'; - console.log('The whole Book model:', book); - const result = book.createoutputfiles(false); - result - .then((_) => { - console.log('Generated result!'); - }) - .catch((error) => { - console.error(error); - }) - .finally((_) => { - generateEl.removeAttribute('disabled'); - generateEl.style.fontSize = "24px"; - generateEl.innerText = 'Generate PDF Output'; - }); + generateEl.setAttribute('disabled', true); + generateEl.style.fontSize = '13px'; + generateEl.innerText = 'Generating, this may take a little while...'; + console.log('The whole Book model:', book); + const result = book.createoutputfiles(false); + result + .then(() => { + console.log('Generated result!'); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + generateEl.removeAttribute('disabled'); + generateEl.style.fontSize = '24px'; + generateEl.innerText = 'Generate PDF Output'; + }); } export function handlePreviewClick(previewEl, book) { - previewEl.setAttribute('disabled', true); - previewEl.innerText = 'Generating Preview...'; - const result = book.createoutputfiles(true); - result - .then((_) => { - console.log('Preview result!'); - }) - .catch((error) => { - console.error(error); - }) - .finally((_) => { - previewEl.removeAttribute('disabled'); - previewEl.innerText = 'Preview Output'; - }); + previewEl.setAttribute('disabled', true); + previewEl.innerText = 'Generating Preview...'; + const result = book.createoutputfiles(true); + result + .then(() => { + console.log('Preview result!'); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + previewEl.removeAttribute('disabled'); + previewEl.innerText = 'Preview Output'; + }); } export function handleResetSettingsClick(book) { - const defaultConfiguration = resetForm(); - book.update(defaultConfiguration); - updateAddOrRemoveCustomPaperOption(); - updatePaperSelectOptionsUnits(); + const defaultConfiguration = resetForm(); + book.update(defaultConfiguration); + updateAddOrRemoveCustomPaperOption(); + updatePaperSelectOptionsUnits(); } diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js index 8853706..a477b5b 100644 --- a/src/utils/formUtils.js +++ b/src/utils/formUtils.js @@ -1,11 +1,11 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { schema } from "../models/configuration"; -import { clearLocalSettings, getLocalSettings, setLocalSettings } from "./localStorageUtils"; -import { renderFormFromSettings, renderInfoBox, renderPageCount, renderWacky } from "./renderUtils"; -import { clearUrlParams, setUrlParams, toUrlParams, updateWindowLocation } from "./uri"; +import { schema } from '../models/configuration'; +import { clearLocalSettings, getLocalSettings, setLocalSettings } from './localStorageUtils'; +import { renderFormFromSettings, renderInfoBox, renderPageCount, renderWacky } from './renderUtils'; +import { clearUrlParams, setUrlParams, toUrlParams, updateWindowLocation } from './uri'; /** * Parses a Form into a common Configiration. @@ -13,41 +13,41 @@ import { clearUrlParams, setUrlParams, toUrlParams, updateWindowLocation } from * @returns { import("../models/configuration").Configuration } The configuration */ const fromFormToConfiguration = (form) => - schema.parse({ - sourceRotation: form.get("source_rotation"), - paperSize: form.get("paper_size"), - paperSizeUnit: form.get("paper_size_unit"), - printerType: form.get("printer_type"), - rotatePage: form.has("rotate_page"), - paperRotation90: form.has("paper_rotation_90"), - pageLayout: form.get("pagelayout"), - cropMarks: form.has("cropmarks"), - pdfEdgeMarks: form.has("pdf_edge_marks"), - cutMarks: form.has("cutmarks"), - pageScaling: form.get("page_scaling"), - pagePositioning: form.get("page_positioning"), - mainForeEdgePaddingPt: form.get("main_fore_edge_padding_pt"), - bindingEdgePaddingPt: form.get("binding_edge_padding_pt"), - topEdgePaddingPt: form.get("top_edge_padding_pt"), - bottomEdgePaddingPt: form.get("bottom_edge_padding_pt"), - sigFormat: form.get("sig_format"), - sigLength: form.get("sig_length"), - customSigLength: form.get("custom_sig"), - foreEdgePaddingPt: form.get("fore_edge_padding_pt"), - wackySpacing: form.get("wacky_spacing"), - fileDownload: form.get("file_download"), - printFile: form.get("print_file"), - flyleafs: form.get("flyleafs"), - paperSizeCustomWidth: form.get("paper_size_custom_width"), - paperSizeCustomHeight: form.get("paper_size_custom_height"), - }); + schema.parse({ + sourceRotation: form.get('source_rotation'), + paperSize: form.get('paper_size'), + paperSizeUnit: form.get('paper_size_unit'), + printerType: form.get('printer_type'), + rotatePage: form.has('rotate_page'), + paperRotation90: form.has('paper_rotation_90'), + pageLayout: form.get('pagelayout'), + cropMarks: form.has('cropmarks'), + pdfEdgeMarks: form.has('pdf_edge_marks'), + cutMarks: form.has('cutmarks'), + pageScaling: form.get('page_scaling'), + pagePositioning: form.get('page_positioning'), + mainForeEdgePaddingPt: form.get('main_fore_edge_padding_pt'), + bindingEdgePaddingPt: form.get('binding_edge_padding_pt'), + topEdgePaddingPt: form.get('top_edge_padding_pt'), + bottomEdgePaddingPt: form.get('bottom_edge_padding_pt'), + sigFormat: form.get('sig_format'), + sigLength: form.get('sig_length'), + customSigLength: form.get('custom_sig'), + foreEdgePaddingPt: form.get('fore_edge_padding_pt'), + wackySpacing: form.get('wacky_spacing'), + fileDownload: form.get('file_download'), + printFile: form.get('print_file'), + flyleafs: form.get('flyleafs'), + paperSizeCustomWidth: form.get('paper_size_custom_width'), + paperSizeCustomHeight: form.get('paper_size_custom_height'), + }); /** * Sets the configuration to the URL. * @param { import("../models/configuration").Configuration } configuration The configuration to set */ const setConfigurationToUrl = (configuration) => { - updateWindowLocation(setUrlParams(window.location.href, configuration)); + updateWindowLocation(setUrlParams(window.location.href, configuration)); }; /** @@ -55,14 +55,14 @@ const setConfigurationToUrl = (configuration) => { * @returns { import("../models/configuration").Configuration } The configuration */ export const loadConfiguration = () => { - const urlParams = toUrlParams(window.location.href); - const hasUrlParams = Object.keys(urlParams).length > 0; + const urlParams = toUrlParams(window.location.href); + const hasUrlParams = Object.keys(urlParams).length > 0; - const localSettings = hasUrlParams ? urlParams : getLocalSettings(); + const localSettings = hasUrlParams ? urlParams : getLocalSettings(); - const configuration = schema.parse(localSettings); - setConfigurationToUrl(configuration); - return configuration; + const configuration = schema.parse(localSettings); + setConfigurationToUrl(configuration); + return configuration; }; /** @@ -70,12 +70,12 @@ export const loadConfiguration = () => { * @param { import("../book").Book } book The book to update the form from */ export function updateRenderedForm(book) { - book.createpages().then(() => { - console.log("... pages created"); - renderPageCount(book); - renderInfoBox(book); - renderWacky(); - }); + book.createpages().then(() => { + console.log('... pages created'); + renderPageCount(book); + renderInfoBox(book); + renderWacky(); + }); } /** @@ -84,10 +84,10 @@ export function updateRenderedForm(book) { * @returns { import("../models/configuration").Configuration } The updated configuration set */ export function saveForm(form) { - const configuration = fromFormToConfiguration(form); - setLocalSettings(configuration); - setConfigurationToUrl(configuration); - return configuration; + const configuration = fromFormToConfiguration(form); + setLocalSettings(configuration); + setConfigurationToUrl(configuration); + return configuration; } /** @@ -95,16 +95,16 @@ export function saveForm(form) { * @returns { import("../models/configuration").Configuration } The configuration last saved, or a default configuration set */ export function loadForm() { - const configuration = loadConfiguration(); - renderFormFromSettings(configuration); - return configuration; + const configuration = loadConfiguration(); + renderFormFromSettings(configuration); + return configuration; } /** * Resets the form to the default configuration. */ export const resetForm = () => { - clearLocalSettings(); - updateWindowLocation(clearUrlParams(window.location.href)); - return loadForm(); + clearLocalSettings(); + updateWindowLocation(clearUrlParams(window.location.href)); + return loadForm(); }; diff --git a/src/utils/localStorageUtils.js b/src/utils/localStorageUtils.js index b5dd58f..4ca6880 100644 --- a/src/utils/localStorageUtils.js +++ b/src/utils/localStorageUtils.js @@ -1,23 +1,23 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. const STORAGE_KEY = 'bookbinderSettings'; export function getLocalSettings() { - const emptySettings = {}; - const localSettings = JSON.parse(localStorage.getItem(STORAGE_KEY)); - if (!localSettings) { - setLocalSettings(emptySettings); - return getLocalSettings(); - } - return localSettings; + const emptySettings = {}; + const localSettings = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (!localSettings) { + setLocalSettings(emptySettings); + return getLocalSettings(); + } + return localSettings; } export function setLocalSettings(newSettings) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings)); } export function clearLocalSettings() { - localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(STORAGE_KEY); } diff --git a/src/utils/localStorageUtils.test.js b/src/utils/localStorageUtils.test.js index 6d81f5a..99122d2 100644 --- a/src/utils/localStorageUtils.test.js +++ b/src/utils/localStorageUtils.test.js @@ -1,55 +1,51 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; -import { - getLocalSettings, - setLocalSettings, - clearLocalSettings, -} from './localStorageUtils'; +import { getLocalSettings, setLocalSettings, clearLocalSettings } from './localStorageUtils'; describe('local storage utils', () => { - const mockSettings = { - duplex: false, - format: 'standardsig', - sigsize: 8, - lockratio: true, - papersize: 'A4', - pagelayout: 'octavo', - }; + const mockSettings = { + duplex: false, + format: 'standardsig', + sigsize: 8, + lockratio: true, + papersize: 'A4', + pagelayout: 'octavo', + }; - it('gets empty settings from local storage if none have been set', () => { - try { - const expected = {}; - const actual = getLocalSettings(); - expect(actual).toEqual(expected); - } catch (error) { - expect(error).toBeFalsy(); - } - }); + it('gets empty settings from local storage if none have been set', () => { + try { + const expected = {}; + const actual = getLocalSettings(); + expect(actual).toEqual(expected); + } catch (error) { + expect(error).toBeFalsy(); + } + }); - it('sets local settings and gets those settings', () => { - try { - const expected = mockSettings; - setLocalSettings(mockSettings); - const actual = getLocalSettings(); - expect(actual).toEqual(expected); - } catch (error) { - expect(error).toBeFalsy(); - } - }); + it('sets local settings and gets those settings', () => { + try { + const expected = mockSettings; + setLocalSettings(mockSettings); + const actual = getLocalSettings(); + expect(actual).toEqual(expected); + } catch (error) { + expect(error).toBeFalsy(); + } + }); - it('removes local settings', () => { - try { - const expected = {}; - setLocalSettings(mockSettings); - clearLocalSettings(); - const actual = getLocalSettings(); - expect(actual).toEqual(expected); - } catch (error) { - expect(error).toBeFalsy(); - } - }); + it('removes local settings', () => { + try { + const expected = {}; + setLocalSettings(mockSettings); + clearLocalSettings(); + const actual = getLocalSettings(); + expect(actual).toEqual(expected); + } catch (error) { + expect(error).toBeFalsy(); + } + }); }); diff --git a/src/utils/renderUtils.js b/src/utils/renderUtils.js index efebef4..54c2e6a 100644 --- a/src/utils/renderUtils.js +++ b/src/utils/renderUtils.js @@ -1,32 +1,31 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { PAGE_SIZES } from '../constants'; export function renderPageCount(book) { - const pageCount = document.getElementById('page_count'); - pageCount.innerText = book.pagecount; + const pageCount = document.getElementById('page_count'); + pageCount.innerText = book.pagecount; } export function renderInfoBox(book) { - const totalSheets = document.getElementById('total_sheets'); - const sigCount = document.getElementById('sig_count'); - const sigArrange = document.getElementById('sig_arrange'); - const totalPages = document.getElementById('total_pages'); - - if (book.book == null || book.book == undefined) - return - - const outputPages = book.book.pagelistdetails.reduce((acc, list) => { - list.forEach((sublist) => (acc += (sublist.length ? sublist.length : 1))); - return acc; - }, 0); - - totalSheets.innerText = book.book.sheets; - sigCount.innerText = book.book.sigconfig.length; - sigArrange.innerText = book.book.sigconfig.join(', '); - totalPages.innerText = outputPages; + const totalSheets = document.getElementById('total_sheets'); + const sigCount = document.getElementById('sig_count'); + const sigArrange = document.getElementById('sig_arrange'); + const totalPages = document.getElementById('total_pages'); + + if (book.book == null || book.book == undefined) return; + + const outputPages = book.book.pagelistdetails.reduce((acc, list) => { + list.forEach((sublist) => (acc += sublist.length ? sublist.length : 1)); + return acc; + }, 0); + + totalSheets.innerText = book.book.sheets; + sigCount.innerText = book.book.sigconfig.length; + sigArrange.innerText = book.book.sigconfig.join(', '); + totalPages.innerText = outputPages; } /** @@ -34,188 +33,221 @@ export function renderInfoBox(book) { * */ export function updatePageLayoutInfo(info) { - document.getElementById("show_layout_info").style.display = 'block' - console.log("So much info from updatePageLayoutInfo: ", info) - let needsRotation = info.dimensions.layout.rotations[0] == -90 || info.dimensions.layout.rotations[0] == 90 || info.dimensions.layout.rotations[0][0] == -90 || info.dimensions.layout.rotations[0][0] == 90 + document.getElementById('show_layout_info').style.display = 'block'; + console.log('So much info from updatePageLayoutInfo: ', info); + const needsRotation = + info.dimensions.layout.rotations[0] == -90 || + info.dimensions.layout.rotations[0] == 90 || + info.dimensions.layout.rotations[0][0] == -90 || + info.dimensions.layout.rotations[0][0] == 90; // Paper size: ${info.papersize[0]}, ${info.papersize[1]}
- let paperDims = info.dimensions.layoutCell - let pdfDims = [info.dimensions.xPdfWidthFunc(), info.dimensions.yPdfHeightFunc()] - - let scale = Math.min(Math.min(250/paperDims[0], 250/paperDims[1]), Math.min(250/pdfDims[0], 250/pdfDims[1])) - - let dims = [paperDims[0] * scale, paperDims[1] * scale] - let displayDiv = document.getElementById("grid_layout_preview") // blue box - displayDiv.style.width = `${dims[0]}px` - displayDiv.style.height = `${dims[1]}px` - - dims = [pdfDims[0] * scale, pdfDims[1] * scale] - displayDiv = document.getElementById("pdf_on_page_layout_preview") // orange box - displayDiv.style.width = `${dims[0]}px` - displayDiv.style.height = `${dims[1]}px` - - dims = [info.dimensions.xBindingShiftFunc() * scale, info.dimensions.yTopShiftFunc() * scale] - displayDiv.style.margin = `${dims[1]}px ${dims[0]}px` - - document.getElementById("page_grid_layout").innerText = `${info.dimensions.layout.rows} rows x ${info.dimensions.layout.cols} cols` - document.getElementById("page_grid_dimensions").innerText = `${paperDims[0]}, ${paperDims[1]}` - document.getElementById("pdf_source_dimensions").innerText = `${info.cropbox.width}, ${info.cropbox.height}` - document.getElementById("pdf_page_dimensions").innerText = `${pdfDims[0].toFixed(2)}, ${pdfDims[1].toFixed(2)}` - document.getElementById("pdf_offset_dimensions").innerHTML = ` + const paperDims = info.dimensions.layoutCell; + const pdfDims = [info.dimensions.xPdfWidthFunc(), info.dimensions.yPdfHeightFunc()]; + + const scale = Math.min( + Math.min(250 / paperDims[0], 250 / paperDims[1]), + Math.min(250 / pdfDims[0], 250 / pdfDims[1]) + ); + + let dims = [paperDims[0] * scale, paperDims[1] * scale]; + let displayDiv = document.getElementById('grid_layout_preview'); // blue box + displayDiv.style.width = `${dims[0]}px`; + displayDiv.style.height = `${dims[1]}px`; + + dims = [pdfDims[0] * scale, pdfDims[1] * scale]; + displayDiv = document.getElementById('pdf_on_page_layout_preview'); // orange box + displayDiv.style.width = `${dims[0]}px`; + displayDiv.style.height = `${dims[1]}px`; + + dims = [info.dimensions.xBindingShiftFunc() * scale, info.dimensions.yTopShiftFunc() * scale]; + displayDiv.style.margin = `${dims[1]}px ${dims[0]}px`; + + document.getElementById('page_grid_layout').innerText = + `${info.dimensions.layout.rows} rows x ${info.dimensions.layout.cols} cols`; + document.getElementById('page_grid_dimensions').innerText = `${paperDims[0]}, ${paperDims[1]}`; + document.getElementById('pdf_source_dimensions').innerText = + `${info.cropbox.width}, ${info.cropbox.height}`; + document.getElementById('pdf_page_dimensions').innerText = + `${pdfDims[0].toFixed(2)}, ${pdfDims[1].toFixed(2)}`; + document.getElementById('pdf_offset_dimensions').innerHTML = ` ${info.dimensions.xBindingShiftFunc().toFixed(2)} from spine
${info.dimensions.yTopShiftFunc().toFixed(2)} from top
${info.dimensions.xForeEdgeShiftFunc().toFixed(2)} from fore edge
${info.dimensions.yBottomShiftFunc().toFixed(2)} from bottom - ` - document.getElementById("pdf_scale_dimensions").innerText = `${info.dimensions.pdfScale[0].toFixed(2)}, ${info.dimensions.pdfScale[1].toFixed(2)}` - document.getElementById("pdf_page_rotation_info").innerText = `${needsRotation}` + `; + document.getElementById('pdf_scale_dimensions').innerText = + `${info.dimensions.pdfScale[0].toFixed(2)}, ${info.dimensions.pdfScale[1].toFixed(2)}`; + document.getElementById('pdf_page_rotation_info').innerText = `${needsRotation}`; } /** * Expects a data object describing the overall page layout info: * */ export function updatePaperSelectOptionsUnits() { - const paperList = document.getElementById('paper_size'); - const paperListUnit = document.getElementById('paper_size_unit').value - for(let option of Array.from(paperList.children)) { - let paperName = option.value - let paper = PAGE_SIZES[paperName] - let label = `${paperName} (${paper[0]} x ${paper[1]} pt)` - // conversion values from Google default converter - if (paperListUnit == 'in') { - label = `${paperName} (${(paper[0] * 0.0138889).toFixed(1)} x ${(paper[1] * 0.0138889).toFixed(1)} inches)` - } else if (paperListUnit == 'cm') { - label= `${paperName} (${(paper[0] * 0.0352778).toFixed(2)} x ${(paper[1] * 0.0352778).toFixed(2)} cm)` - } - option.setAttribute('label', label); + const paperList = document.getElementById('paper_size'); + const paperListUnit = document.getElementById('paper_size_unit').value; + for (const option of Array.from(paperList.children)) { + const paperName = option.value; + const paper = PAGE_SIZES[paperName]; + let label = `${paperName} (${paper[0]} x ${paper[1]} pt)`; + // conversion values from Google default converter + if (paperListUnit == 'in') { + label = `${paperName} (${(paper[0] * 0.0138889).toFixed(1)} x ${(paper[1] * 0.0138889).toFixed(1)} inches)`; + } else if (paperListUnit == 'cm') { + label = `${paperName} (${(paper[0] * 0.0352778).toFixed(2)} x ${(paper[1] * 0.0352778).toFixed(2)} cm)`; } + option.setAttribute('label', label); + } } export function updateAddOrRemoveCustomPaperOption() { - const width = document.getElementById('paper_size_custom_width').value; - const height = document.getElementById('paper_size_custom_height').value; - const paperList = document.getElementById('paper_size'); - const validDimensions = width.length > 0 && height.length > 0 && !isNaN(width) && !isNaN(height) - const customOpt = paperList.children.namedItem("CUSTOM") - if (customOpt == null && !validDimensions) { - // No custom option, no valid custom settings - } else if (customOpt != null && !validDimensions) { - // Need to remove custom option because no valid custom settings - customOpt.remove() - } else if (customOpt == null) { - // no option but valid settings! - let opt = document.createElement('option'); - opt.setAttribute('value', 'CUSTOM'); - opt.setAttribute('name', 'CUSTOM'); - PAGE_SIZES['CUSTOM'] = [Number(width), Number(height)] - paperList.appendChild(opt); - } else { - // valid option, valid dimensions -- lets just make sure they're up to date - PAGE_SIZES['CUSTOM'] = [Number(width), Number(height)] - } - - + const width = document.getElementById('paper_size_custom_width').value; + const height = document.getElementById('paper_size_custom_height').value; + const paperList = document.getElementById('paper_size'); + const validDimensions = width.length > 0 && height.length > 0 && !isNaN(width) && !isNaN(height); + const customOpt = paperList.children.namedItem('CUSTOM'); + if (customOpt == null && !validDimensions) { + // No custom option, no valid custom settings + } else if (customOpt != null && !validDimensions) { + // Need to remove custom option because no valid custom settings + customOpt.remove(); + } else if (customOpt == null) { + // no option but valid settings! + const opt = document.createElement('option'); + opt.setAttribute('value', 'CUSTOM'); + opt.setAttribute('name', 'CUSTOM'); + PAGE_SIZES.CUSTOM = [Number(width), Number(height)]; + paperList.appendChild(opt); + } else { + // valid option, valid dimensions -- lets just make sure they're up to date + PAGE_SIZES.CUSTOM = [Number(width), Number(height)]; + } } export function renderPaperSelectOptions() { - const paperList = document.getElementById('paper_size'); - let maxLength = Math.max(Object.keys(PAGE_SIZES).map( (s) => { s.length})) - Object.keys(PAGE_SIZES).forEach((key) => { - let opt = document.createElement('option'); - opt.setAttribute('value', key); - opt.setAttribute('name', key); - if (key == 'A4') { - opt.setAttribute('selected', true); - } - opt.innerText = key; - paperList.appendChild(opt); - }); - updatePaperSelectOptionsUnits() + const paperList = document.getElementById('paper_size'); + // const maxLength = Math.max( + // Object.keys(PAGE_SIZES).map((s) => { + // s.length; + // }) + // ); + Object.keys(PAGE_SIZES).forEach((key) => { + const opt = document.createElement('option'); + opt.setAttribute('value', key); + opt.setAttribute('name', key); + if (key == 'A4') { + opt.setAttribute('selected', true); + } + opt.innerText = key; + paperList.appendChild(opt); + }); + updatePaperSelectOptionsUnits(); } export function renderWacky() { - const isWacky = - document.getElementById('a9_3_3_4').checked || - document.getElementById('a10_6_10s').checked || - document.getElementById('a_3_6s').checked || - document.getElementById('a_4_8s').checked || - document.getElementById('A7_2_16s').checked || - document.getElementById('1_3rd').checked; - console.log('Is a wacky layout? ', isWacky); - document - .getElementById('book_size') - .querySelectorAll('input') - .forEach((x) => { - x.disabled = isWacky; - }); - document.getElementById('book_size').style.opacity = isWacky ? 0.3 : 1.0; + const isWacky = + document.getElementById('a9_3_3_4').checked || + document.getElementById('a10_6_10s').checked || + document.getElementById('a_3_6s').checked || + document.getElementById('a_4_8s').checked || + document.getElementById('A7_2_16s').checked || + document.getElementById('1_3rd').checked; + console.log('Is a wacky layout? ', isWacky); + document + .getElementById('book_size') + .querySelectorAll('input') + .forEach((x) => { + x.disabled = isWacky; + }); + document.getElementById('book_size').style.opacity = isWacky ? 0.3 : 1.0; } /** @param { import("../models/configuration").Configuration } configuration */ export function renderFormFromSettings(configuration) { - // Clear all checked attributes - document.querySelectorAll("[type=radio]").forEach((e) => { e.checked = false }); - document.querySelectorAll("[type=checkbox]").forEach((e) => { e.checked = false }); - - // Set checkboxes - if (configuration.paperRotation90) { - document.querySelector("input[name='paper_rotation_90']").checked = true; - } - - if (configuration.rotatePage) { - document.querySelector("input[name='rotate_page']").checked = true; - } - - if (configuration.cropMarks) { - document.querySelector("input[name='cropmarks']").checked = true; - } - - if (configuration.pdfEdgeMarks) { - document.querySelector("input[name='pdf_edge_marks']").checked = true; - } - - if (configuration.cutMarks) { - document.querySelector("input[name='cutmarks']").checked = true; - } - - // Set radio options - document.querySelector(`input[name="sig_format"][value="${configuration.sigFormat}"]`).checked = true; - document.querySelector(`input[name="wacky_spacing"][value="${configuration.wackySpacing}"]`).checked = true; - - // Set freeform inputs - document.querySelector('input[name="main_fore_edge_padding_pt"]').value = configuration.mainForeEdgePaddingPt; - document.querySelector('input[name="binding_edge_padding_pt"]').value = configuration.bindingEdgePaddingPt; - document.querySelector('input[name="top_edge_padding_pt"]').value = configuration.topEdgePaddingPt; - document.querySelector('input[name="bottom_edge_padding_pt"]').value = configuration.bottomEdgePaddingPt; - document.querySelector('input[name="fore_edge_padding_pt"]').value = configuration.foreEdgePaddingPt; - document.querySelector('input[name="flyleafs"]').value = configuration.flyleafs; - - // Set select options - document.querySelector('select[name="source_rotation"]').value = configuration.sourceRotation; - document.querySelector('select[name="pagelayout"]').value = configuration.pageLayout; - document.querySelector('select[name="page_scaling"]').value = configuration.pageScaling; - document.querySelector('select[name="page_positioning"]').value = configuration.pagePositioning; - document.querySelector('select[name="print_file"]').value = configuration.printFile; - document.querySelector('select[name="paper_size"]').value = configuration.paperSize; - document.querySelector('select[name="paper_size_unit"]').value = configuration.paperSizeUnit; - document.querySelector('select[name="printer_type"]').value = configuration.printerType; - - // Set options which are not always present - if (configuration.paperSize === "CUSTOM" && configuration.paperSizeCustomHeight !== undefined && configuration.paperSizeCustomWidth !== undefined) { - document.querySelector('input[name="paper_size_custom_height"]').value = configuration.paperSizeCustomHeight; - document.querySelector('input[name="paper_size_custom_width"]').value = configuration.paperSizeCustomWidth; - updateAddOrRemoveCustomPaperOption(); - updatePaperSelectOptionsUnits(); - document.querySelector('select[name="paper_size"]').value = "CUSTOM"; - } - - if (configuration.sigFormat == "customsig") { - document.querySelector("input[name='custom_sig']").value = configuration.customSigLength; - } else { - document.querySelector("input[name='sig_length']").value = configuration.sigLength; - } - - // Hide and show elements based on configuration - let sourceRotationExamples = Array.from(document.getElementsByClassName('source_rotation_example')); - const selectedValue = configuration.sourceRotation + '_example'; - sourceRotationExamples.forEach((example) => { - example.style.display = (example.id === selectedValue ? 'block' : 'none'); - }); + // Clear all checked attributes + document.querySelectorAll('[type=radio]').forEach((e) => { + e.checked = false; + }); + document.querySelectorAll('[type=checkbox]').forEach((e) => { + e.checked = false; + }); + + // Set checkboxes + if (configuration.paperRotation90) { + document.querySelector("input[name='paper_rotation_90']").checked = true; + } + + if (configuration.rotatePage) { + document.querySelector("input[name='rotate_page']").checked = true; + } + + if (configuration.cropMarks) { + document.querySelector("input[name='cropmarks']").checked = true; + } + + if (configuration.pdfEdgeMarks) { + document.querySelector("input[name='pdf_edge_marks']").checked = true; + } + + if (configuration.cutMarks) { + document.querySelector("input[name='cutmarks']").checked = true; + } + + // Set radio options + document.querySelector(`input[name="sig_format"][value="${configuration.sigFormat}"]`).checked = + true; + document.querySelector( + `input[name="wacky_spacing"][value="${configuration.wackySpacing}"]` + ).checked = true; + + // Set freeform inputs + document.querySelector('input[name="main_fore_edge_padding_pt"]').value = + configuration.mainForeEdgePaddingPt; + document.querySelector('input[name="binding_edge_padding_pt"]').value = + configuration.bindingEdgePaddingPt; + document.querySelector('input[name="top_edge_padding_pt"]').value = + configuration.topEdgePaddingPt; + document.querySelector('input[name="bottom_edge_padding_pt"]').value = + configuration.bottomEdgePaddingPt; + document.querySelector('input[name="fore_edge_padding_pt"]').value = + configuration.foreEdgePaddingPt; + document.querySelector('input[name="flyleafs"]').value = configuration.flyleafs; + + // Set select options + document.querySelector('select[name="source_rotation"]').value = configuration.sourceRotation; + document.querySelector('select[name="pagelayout"]').value = configuration.pageLayout; + document.querySelector('select[name="page_scaling"]').value = configuration.pageScaling; + document.querySelector('select[name="page_positioning"]').value = configuration.pagePositioning; + document.querySelector('select[name="print_file"]').value = configuration.printFile; + document.querySelector('select[name="paper_size"]').value = configuration.paperSize; + document.querySelector('select[name="paper_size_unit"]').value = configuration.paperSizeUnit; + document.querySelector('select[name="printer_type"]').value = configuration.printerType; + + // Set options which are not always present + if ( + configuration.paperSize === 'CUSTOM' && + configuration.paperSizeCustomHeight !== undefined && + configuration.paperSizeCustomWidth !== undefined + ) { + document.querySelector('input[name="paper_size_custom_height"]').value = + configuration.paperSizeCustomHeight; + document.querySelector('input[name="paper_size_custom_width"]').value = + configuration.paperSizeCustomWidth; + updateAddOrRemoveCustomPaperOption(); + updatePaperSelectOptionsUnits(); + document.querySelector('select[name="paper_size"]').value = 'CUSTOM'; + } + + if (configuration.sigFormat == 'customsig') { + document.querySelector("input[name='custom_sig']").value = configuration.customSigLength; + } else { + document.querySelector("input[name='sig_length']").value = configuration.sigLength; + } + + // Hide and show elements based on configuration + const sourceRotationExamples = Array.from( + document.getElementsByClassName('source_rotation_example') + ); + const selectedValue = `${configuration.sourceRotation}_example`; + sourceRotationExamples.forEach((example) => { + example.style.display = example.id === selectedValue ? 'block' : 'none'; + }); } diff --git a/src/utils/uri.js b/src/utils/uri.js index 899470c..51092f6 100644 --- a/src/utils/uri.js +++ b/src/utils/uri.js @@ -1,6 +1,6 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Gets parameters from a URL. @@ -8,8 +8,8 @@ * @returns { Record } The URL parameters */ export const toUrlParams = (url) => { - const params = new URL(url).searchParams.entries(); - return Object.fromEntries(params); + const params = new URL(url).searchParams.entries(); + return Object.fromEntries(params); }; /** @@ -19,17 +19,17 @@ export const toUrlParams = (url) => { * @returns { string } A new URL string with the params set */ export const setUrlParams = (url, params) => { - const urlRepresentation = new URL(url); + const urlRepresentation = new URL(url); - for (const [key, value] of Object.entries(params)) { - if (value === null || value === undefined) { - continue; - } - - urlRepresentation.searchParams.set(key, String(value)); + for (const [key, value] of Object.entries(params)) { + if (value === null || value === undefined) { + continue; } - return urlRepresentation.toString(); + urlRepresentation.searchParams.set(key, String(value)); + } + + return urlRepresentation.toString(); }; /** @@ -38,9 +38,9 @@ export const setUrlParams = (url, params) => { * @returns { string } A new URL object with no params */ export const clearUrlParams = (url) => { - const urlRepresentation = new URL(url); - urlRepresentation.search = ""; - return urlRepresentation.toString(); + const urlRepresentation = new URL(url); + urlRepresentation.search = ''; + return urlRepresentation.toString(); }; /** @@ -48,5 +48,5 @@ export const clearUrlParams = (url) => { * @param { string } url The URL to update the location to */ export const updateWindowLocation = (url) => { - window.history.pushState({}, "", url.toString()); + window.history.pushState({}, '', url.toString()); }; diff --git a/src/wacky_imposition.js b/src/wacky_imposition.js index cc21632..61e0fb5 100644 --- a/src/wacky_imposition.js +++ b/src/wacky_imposition.js @@ -1,626 +1,731 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Dumping grounds for weird foldy single-sheet imposition. Different flow from how the other books work. - * - * The builder functions are the entry points to the different layouts. + * + * The builder functions are the entry points to the different layouts. */ -export class WackyImposition{ - constructor(pages, duplex,format, isPacked) { - this.duplex=duplex; - this.sigconfig=[] // sig_count looks at the length of this array, sig_arrange joins them together with a ,; - this.pagelistdetails = [[]]; - this.isPacked = isPacked; - console.log("Constructor sees ", pages) - // for UI estimates - this.sheets = Math.ceil(pages.length / 20.0); - console.log("... spits out pageList ", this.pagelist) - if (format == "a9_3_3_4") { - this.sheets = Math.ceil(pages.length/40.0); - this.sigconfig = Array(Math.ceil(pages.length/40.0) * 3) - } else if (format == "a10_6_10s") { - this.sheets = Math.ceil(pages.length/120.0); - this.sigconfig = Array(Math.ceil(pages.length/20.0) * 2) - } else if (format == "a_4_8s") { - this.sheets = Math.ceil(pages.length/64.0); - this.sigconfig = Array(Math.ceil(pages.length/16.0) * 2) - } else if (format == "a_3_6s") { - this.sheets = Math.ceil(pages.length/36.0); - this.sigconfig = Array(Math.ceil(pages.length/12)) - } else if (format == "A7_2_16s") { - this.sheets = Math.ceil(pages.length/32.0); - this.sigconfig = Array(this.sheets * 2) - } else if (format == "8_zine") { - this.sheets = 1; - this.sigconfig = [1]; - } - } - - page_8_zine_builder() { - return { - sheetMaker: this.build_8_zine_sheetList.bind(this), - lineMaker: this.build_8_zine_lineFunction.bind(this), - isLandscape: false, - fileNameMod: "8_zine" + ((this.isPacked) ? "_packed" : "_spread") - } - } - - page_1_3rd_builder() { - return { - sheetMaker: this.build_1_3rd_sheetList.bind(this), - lineMaker: this.build_1_3rd_lineFunction.bind(this), - isLandscape: false, - fileNameMod: "one_third" + ((this.isPacked) ? "_packed" : "_spread") - } - } - - a9_3_3_4_builder() { - return { - sheetMaker: this.build_3_3_4_sheetList.bind(this), - lineMaker: this.build_3_3_4_lineFunction.bind(this), - isLandscape: false, - fileNameMod: "little"+ ((this.isPacked) ? "_packed" : "_spread") - } - } - - a10_6_10s_builder() { - return { - sheetMaker: this.build_6_10s_sheetList.bind(this), - lineMaker: this.build_6_10s_lineFunction.bind(this), - isLandscape: true, - fileNameMod: "mini"+ ((this.isPacked) ? "_packed" : "_spread") - } - } - - a_3_6s_builder() { - return { - sheetMaker: this.build_3_6s_sheetList.bind(this), - lineMaker: this.build_strip_lineFunction.bind(this, 3, 6), - isLandscape: true, - fileNameMod: "3_by_6"+ ((this.isPacked) ? "_packed" : "_spread") - } - } - - a_4_8s_builder() { - return { - sheetMaker: this.build_4_8s_sheetList.bind(this), - lineMaker: this.build_strip_lineFunction.bind(this, 4, 8), - isLandscape: true, - fileNameMod: "4_by_8"+ ((this.isPacked) ? "_packed" : "_spread") - } - } - - a7_2_16s_builder() { - return { - sheetMaker: this.build_2_16s_sheetList.bind(this), - lineMaker: this.build_2_16s_lineFunction.bind(this), - isLandscape: false, - fileNameMod: "4_by_4_two_signatures"+ ((this.isPacked) ? "_packed" : "_spread") - } - } - - - // ---------------- the real guts of the layout - - - /** - * It's an 8 page zine. Same page count every time.... - * - * @param pageCount - total pages in document (to add blanks if < 8) - * @return an array of a single sheets. It's just one printed page (face) - * The sheet is an array of rows, containing a list of page objects - */ - build_8_zine_sheetList(pageCount) { - let p = this.page; - let f = this.flipPage; - return [[ - this.auditForBlanks([ p(7),p(0),p(1),p(2) ], pageCount), - this.auditForBlanks([ f(6),f(5),f(4),f(3) ], pageCount) - ]]; - } - - /** - * Produces a 3 folio signature per sheet - * @param pageCount - total pages in document - * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. - * Each sheet is an array of rows, containing a list of page objects - */ - build_1_3rd_sheetList(pageCount) { - let p = this.page; - let f = this.flipPage; - let sheets = []; - let sheetCount = Math.ceil(pageCount / 12.0); - console.log("Building the 1/3rd page layout. Given ",pageCount," page count, there will be ",sheetCount," sheets..."); - for (let sheet=0; sheet < sheetCount; ++sheet ) { - let i = sheet * 12 - 1; - let front = [ - this.auditForBlanks([p(i+8), p(i+5)], pageCount), - this.auditForBlanks([f(i+9), f(i+4)], pageCount), - this.auditForBlanks([p(i+12), p(i+1)], pageCount), - ]; - let back = [ - this.auditForBlanks([p(i+6), p(i+7)], pageCount), - this.auditForBlanks([f(i+3), f(i+10)], pageCount), - this.auditForBlanks([p(i+2), p(i+11)], pageCount), - ] - sheets.push(front); - sheets.push(back); - } - return sheets; - } - - /** - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_1_3rd_lineFunction() { - return info => { - let vGap = row => { return (info.isPacked) ? info.gap[1] : info.gap[1] * row} - let hGap = col => { return (info.isPacked) ? info.gap[0] : info.gap[0] * col} - let foldMarks = []; - [0,1,2,3].forEach( row => { - [0,1,2].forEach( page => { - foldMarks = foldMarks.concat(this.crosshairMark( - hGap(page) + info.renderPageSize[0] * page, - vGap(row) + info.renderPageSize[1] * row, - 5 - )); - }); - }); - return [ - this.foldHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1]), - this.foldHorizontal(info.paperSize[0],vGap(2) + info.renderPageSize[1] * 2), - this.cutHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), - ] - .concat(foldMarks); - }; - } - - /** - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_8_zine_lineFunction() { - return info => { - return []; - }; - } - - /** - * Produces two 3 folio foldup signatures and a 4 folio foldup signature. - * 5 rows, 4 pages across. 1-12 / 13-24 / 25 - 40 - * @param pageCount - total pages in document - * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. - * Each sheet is an array of rows, containing a list of page objects - */ - build_3_3_4_sheetList(pageCount) { - let p = this.page; - let f = this.flipPage; - let sheets = []; - let sheetCount = Math.ceil(pageCount / 40.0); - console.log("Building the 3/3/4 pages. Given ",pageCount," page count, there will be ",sheetCount," sheets..."); - for (let sheet=0; sheet < sheetCount; ++sheet ) { - let i = sheet * 40 - 1; - let frontThrees = [ - this.auditForBlanks([f(i+5), f(i+8), f(i+17), f(i+20)], pageCount), - this.auditForBlanks([p(i+4), p(i+9), p(i+16), p(i+21)], pageCount), - this.auditForBlanks([f(i+1), f(i+12), f(i+13), f(i+24)], pageCount), - ]; - let frontFour = [ - this.auditForBlanks([p(i+36), p(i+29), p(i+28), p(i+37)], pageCount), - this.auditForBlanks([f(i+33), f(i+32), f(i+25), f(i+40)], pageCount), - ] - let backThrees = [ - this.auditForBlanks([f(i+19), f(i+18), f(i+7), f(i+6)], pageCount), - this.auditForBlanks([p(i+22), p(i+15), p(i+10), p(i+3)], pageCount), - this.auditForBlanks([f(i+23), f(i+14), f(i+11), f(i+2)], pageCount), - ]; - let backFour = [ - this.auditForBlanks([p(i+38), p(i+27), p(i+30), p(i+35)], pageCount), - this.auditForBlanks([f(i+39), f(i+26), f(i+31), f(i+34)], pageCount), - ] - console.log("Sheet ",sheet," has front 3s ",frontThrees," and front 4 ", frontFour," w/ back 3s ",backThrees," and back 4", backFour) - sheets.push(frontFour.concat(frontThrees)); - sheets.push(backFour.concat(backThrees)); - } - return sheets; - } - - /** - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_3_3_4_lineFunction() { - return info => { - let vGap = row => { return (info.isPacked) ? info.gap[1] : info.gap[1] * row}; - let hGap = col => { return (info.isPacked) ? info.gap[0] : info.gap[0] * col}; - let cutBetweenTheThrees = { - start: { x: hGap(2) + 2 * info.renderPageSize[0], y: info.renderPageSize[1] * 2 + info.gap[1] }, - end: { x: hGap(2) + 2 * info.renderPageSize[0], y: info.paperSize[1] }, - thickness: 0.25, - opacity: 0.4, - }; - let foldBetweenTheFours = { - start: { x: hGap(2) + 2 * info.renderPageSize[0], y: info.renderPageSize[1] * 2 + info.gap[1] }, - end: { x: hGap(2) + 2 * info.renderPageSize[0], y: 0 }, - thickness: 0.5, - opacity: 0.4, - dashArray: [2, 5] - }; - let foldMarks = []; - - [0,1,2,3,4,5].forEach( row => { - [0,1,2,3,4].forEach( page => { - foldMarks = foldMarks.concat(this.crosshairMark( - hGap(page) + info.renderPageSize[0] * page, - vGap(row) + info.renderPageSize[1] * row, - 5 - )); - }); - }); - return [ - this.foldHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1]), - this.cutHorizontal(info.paperSize[0], vGap(2) + info.renderPageSize[1] * 2), - this.foldHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), - this.foldHorizontal(info.paperSize[0], vGap(4) + info.renderPageSize[1] * 4), - this.cutVertical(info.paperSize[1], hGap(0)), - this.cutVertical(info.paperSize[1], hGap(4) + info.renderPageSize[0] * 4), - cutBetweenTheThrees, - foldBetweenTheFours - ].concat(foldMarks); - }; - } - - /** - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_6_10s_lineFunction() { - return info => { - let cutOffset = (!this.duplex || info.isFront) ? 4 : 6; - let vGap = row => { return (info.isPacked) ? info.gap[1] : info.gap[1] * (2 * row); }; - let hGap = col => { return (info.isPacked) ? info.gap[0] : col * info.gap[0]}; - let baseCuts = [ - //this.cutHorizontal(info.paperSize[0], vGap(0)), - this.cutHorizontal(info.paperSize[0], vGap(0) + info.renderPageSize[1] * 0), - this.cutHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1] * 1), - this.cutHorizontal(info.paperSize[0], vGap(2) + info.renderPageSize[1] * 2), - this.cutHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), - this.cutHorizontal(info.paperSize[0], vGap(4) + info.renderPageSize[1] * 4), - this.cutHorizontal(info.paperSize[0], vGap(5) + info.renderPageSize[1] * 5), - this.cutHorizontal(info.paperSize[0], vGap(6) + info.renderPageSize[1] * 6), - - // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 2), - this.cutVertical(info.paperSize[1], hGap(cutOffset) + info.renderPageSize[0] * cutOffset), - // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 6), - // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 8), - this.cutVertical(info.paperSize[1], hGap(0)), - this.cutVertical(info.paperSize[1], hGap(10) + info.renderPageSize[0] * 10), - ]; - let foldMarks = []; - [0,1,2,3,4,5,6].forEach( row => { - [...Array(10).keys()].forEach( page => { - foldMarks = foldMarks.concat(this.crosshairMark( - hGap(page) + info.renderPageSize[0] * page, - vGap(row) + info.renderPageSize[1] * row, - 5 - )); - }); - }); - console.log("Providing lines: \nbase cuts: ",baseCuts,"\nfold marks: ",foldMarks,"\ntotal: ",baseCuts.concat(foldMarks)) - return baseCuts.concat(foldMarks) - }; - } - - build_3_6s_sheetList(pageCount) { - let page = this.page; - let frontFunc = i => {return [page(i+2),page(i+11),page(i+10),page(i+3),page(i+6),page(i+7)];}; - let backFunc = i => {return [page(i+8),page(i+5),page(i+4),page(i+9),page(i+12),page(i+1)];}; - return this.build_strips_sheetList(3, 3, pageCount, frontFunc, backFunc); - } - build_4_8s_sheetList(pageCount) { - let page = this.page; - let frontFunc = i => {return [page(i+2),page(i+15),page(i+14),page(i+3),page(i+6),page(i+11),page(i+10),page(i+7)];}; - let backFunc = i => {return [page(i+8),page(i+9),page(i+12),page(i+5),page(i+4),page(i+13),page(i+16),page(i+1)];}; - return this.build_strips_sheetList(4, 4, pageCount, frontFunc, backFunc); - } - build_6_10s_sheetList(pageCount) { - let page = this.page; - let frontFunc = i => {return [page(i+6),page(i+3),page(i+2),page(i+7),page(i+10),page(i+19),page(i+18),page(i+11),page(i+14),page(i+15)];}; - let backFunc = i => {return [page(i+16),page(i+13),page(i+12),page(i+17),page(i+20),page(i+9),page(i+8),page(i+1),page(i+4),page(i+5)];}; - return this.build_strips_sheetList(6, 5, pageCount, frontFunc, backFunc); - } - - /** - * @param rows - number of rows for page - * @param folioPerRow - number of folios per row (not pages!) - * @param pageCount - total pages in document - * @param frontPageFunc - function (takes page number i to start (0)) that lays out the front pages as an array of pages - * @param backPageFunc - function (takes page number i to start (0)) that lays out the back pages as an array of pages - * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. - * Each sheet is an array of rows, containing a list of page objects - */ - build_strips_sheetList(rows, folioPerRow, pageCount, frontPageFunc, backPageFunc) { - let fronts = [] - let backs = [] - let page = this.page; - let blank = this.blankPage; - let totalPagesPerRow = folioPerRow * 4; - let rowCount = Math.ceil(pageCount / totalPagesPerRow); - console.log("Building the ",rows," rows of ",(folioPerRow * 2)," pages. Given ",pageCount," page count, there will be ",rowCount," rows..."); - for (let row=0; row < rowCount; ++row ) { - let i = row * totalPagesPerRow - 1; - let front = frontPageFunc(i); - let back = backPageFunc(i); - if (!this.duplex) { - console.log("in duplex mode, reversing") - back.reverse(); - } - fronts.push(this.auditForBlanks(front, pageCount)); - backs.push(this.auditForBlanks(back, pageCount)); - console.log(" -> adding front ",fronts[fronts.length - 1], " and back ", backs[fronts.length-1]) - } - let sheets = []; - for (let row=0; row < rowCount; ++row ) { - let sheet = Math.floor(row/rows); - if (row % rows == 0 ) { - sheets[sheet*2] = []; - sheets[sheet*2 + 1] = []; - console.log(sheets) - } - sheets[sheet*2].unshift(fronts[row]); - sheets[sheet*2 + 1].unshift(backs[row]); - console.log(" -> row ",row," => sheet ", sheet, " grabs front ",fronts[row]," and back ",backs[row]) - } - if (sheets[sheets.length - 1].length < rows){ - let rowOfBlanks = new Array(folioPerRow * 2) - for(let j = 0; j < rowOfBlanks.length; ++j) { rowOfBlanks[j] = blank()} - console.log("I present you my blanks! [for ",folioPerRow,"] : ", rowOfBlanks) - for (let filler = 0; filler < rows - rowCount % rows; ++filler) { - let sheet = Math.floor(rowCount/rows); - if (filler % 2 == 0) { - sheets[sheet*2].unshift(rowOfBlanks); - sheets[sheet*2 + 1].unshift(rowOfBlanks); - } else { - sheets[sheet*2].push(rowOfBlanks); - sheets[sheet*2 + 1].push(rowOfBlanks); - } - } - } - for (let i=0;i < sheets.length; ++i){ - if (i % 2 == 1 && !this.duplex) { // stupid "flip on short edge" rotation plans... - sheets[i].reverse(); - sheets[i].forEach( row => { - row.forEach( page => { - this.rotate180(page); - }); - }); - } - } - return sheets; - } - - /** - * @param rowCount - how many rows expected to be cut out - * @param colCount - how many column fold lines - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_strip_lineFunction(rowCount, colCount) { - return info => { - let vGap = row => { return (info.isPacked) ? info.gap[1] : info.gap[1] * (2 * row); }; - let hGap = col => { return (info.isPacked) ? info.gap[0] : col * info.gap[0]}; - let baseCuts = [ - this.cutVertical(info.paperSize[1], hGap(0)), - this.cutVertical(info.paperSize[1], hGap(colCount) + info.renderPageSize[0] * colCount), - ]; - for (let i = 0; i <= rowCount; ++i) { - baseCuts.push(this.cutHorizontal(info.paperSize[0], vGap(i) + info.renderPageSize[1] * i)); - } - let foldMarks = []; - [...Array(rowCount).keys()].forEach( row => { - [...Array(colCount).keys()].forEach( page => { - foldMarks = foldMarks.concat(this.crosshairMark( - hGap(page) + info.renderPageSize[0] * page, - vGap(row) + info.renderPageSize[1] * row, - 5 - )); - }); - }); - console.log("Providing lines: \nbase cuts: ",baseCuts,"\nfold marks: ",foldMarks,"\ntotal: ",baseCuts.concat(foldMarks)) - return baseCuts.concat(foldMarks) - }; - } - - /** - * @param pageCount - total pages in document - * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. - * Each sheet is an array of rows, containing a list of page objects - */ - build_2_16s_sheetList(pageCount) { - let p = this.page; - let f = this.flipPage; - let sheets = []; - let sheetCount = Math.ceil(pageCount / 32.0); - console.log("Building the 32 pages. Given ",pageCount," page count, there will be ",sheetCount," sheets..."); - for (let sheet=0; sheet < sheetCount; ++sheet ) { - let i = sheet * 32 - 1; - let front = [ - this.auditForBlanks([p(i+8), p(i+9), p(i+24), p(i+25)], pageCount), - this.auditForBlanks([f(i+1), f(i+16), f(i+17), f(i+32)], pageCount), - this.auditForBlanks([p(i+4), p(i+13), p(i+20), p(i+29)], pageCount), - this.auditForBlanks([f(i+5), f(i+12), f(i+21), f(i+28)], pageCount), - ]; - let back = [ - this.auditForBlanks([p(i+26), p(i+23), p(i+10), p(i+7)], pageCount), - this.auditForBlanks([f(i+31), f(i+18), f(i+15), f(i+2)], pageCount), - this.auditForBlanks([p(i+30), p(i+19), p(i+14), p(i+3)], pageCount), - this.auditForBlanks([f(i+27), f(i+22), f(i+11), f(i+6)], pageCount), - ] - sheets.push(front); - sheets.push(back); - } - return sheets - } - - /** - * @return a FUNCTION. The function takes as it's parameter: - * Object definition: { - * gap: [leftGap, topGap], - * renderPageSize: [width, height], - * paperSize: [width, height], - * isFront: boolean, - * isPacked: boolean - * } - * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object - */ - build_2_16s_lineFunction() { - return info => { - let vGap = row => { return (info.isPacked) ? info.gap[1] : info.gap[1] * (2 * row); }; - let hGap = col => { return (info.isPacked) ? info.gap[0] : col * info.gap[0]}; - let foldMarks = []; - [0,1,2,3,4].forEach( row => { - [0,1,2,3,4].forEach( page => { - foldMarks = foldMarks.concat(this.crosshairMark( - hGap(page) + info.renderPageSize[0] * page, - vGap(row) + info.renderPageSize[1] * row, - 5 - )); - }); - }); - return [ - {...(this.foldHorizontal(info.paperSize[0], info.paperSize[1]/2, 0.)), opacity: 0.4}, - this.cutVertical(info.paperSize[1], info.paperSize[0]/2), - this.cutVertical(info.paperSize[1], info.paperSize[0] - info.gap[0]), - this.cutVertical(info.paperSize[1], info.gap[0]), - ].concat(foldMarks); - }; - } - - // ---------------- drawing lines helpers - - foldHorizontal(paperWidth, y) { - return { - start: { x: 0, y: y }, - end: { x: paperWidth, y: y}, - thickness: 0.25, - opacity: 0.4, - dashArray: [2, 5] - } - } - foldVertical(paperHeight, x) { - return { - start: { x: x, y: 0 }, - end: { x: x, y: paperHeight}, - thickness: 0.25, - opacity: 0.4, - dashArray: [2, 5] - } - } - cutHorizontal(paperWidth, y) { - return { - start: { x: 0, y: y }, - end: { x: paperWidth, y: y}, - thickness: 0.5, - opacity: 0.4 - } - } - cutVertical(paperHeight, x) { - return { - start: { x: x, y: 0 }, - end: { x: x, y: paperHeight}, - thickness: 0.5, - opacity: 0.4 - } - } - crosshairMark(x, y, size) { - return [ - { - start: { x: x - size/2, y: y }, - end: { x: x + size/2, y: y}, - thickness: 0.5, - opacity: 0.4 +export class WackyImposition { + constructor(pages, duplex, format, isPacked) { + this.duplex = duplex; + this.sigconfig = []; // sig_count looks at the length of this array, sig_arrange joins them together with a ,; + this.pagelistdetails = [[]]; + this.isPacked = isPacked; + console.log(`Constructor sees ${pages}`); + // for UI estimates + this.sheets = Math.ceil(pages.length / 20.0); + console.log(`... spits out pageList ${this.pagelist}`); + if (format == 'a9_3_3_4') { + this.sheets = Math.ceil(pages.length / 40.0); + this.sigconfig = Array(Math.ceil(pages.length / 40.0) * 3); + } else if (format == 'a10_6_10s') { + this.sheets = Math.ceil(pages.length / 120.0); + this.sigconfig = Array(Math.ceil(pages.length / 20.0) * 2); + } else if (format == 'a_4_8s') { + this.sheets = Math.ceil(pages.length / 64.0); + this.sigconfig = Array(Math.ceil(pages.length / 16.0) * 2); + } else if (format == 'a_3_6s') { + this.sheets = Math.ceil(pages.length / 36.0); + this.sigconfig = Array(Math.ceil(pages.length / 12)); + } else if (format == 'A7_2_16s') { + this.sheets = Math.ceil(pages.length / 32.0); + this.sigconfig = Array(this.sheets * 2); + } else if (format == '8_zine') { + this.sheets = 1; + this.sigconfig = [1]; + } + } + + page_8_zine_builder() { + return { + sheetMaker: this.build_8_zine_sheetList.bind(this), + lineMaker: this.build_8_zine_lineFunction.bind(this), + isLandscape: false, + fileNameMod: `8_zine${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + page_1_3rd_builder() { + return { + sheetMaker: this.build_1_3rd_sheetList.bind(this), + lineMaker: this.build_1_3rd_lineFunction.bind(this), + isLandscape: false, + fileNameMod: `one_third${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + a9_3_3_4_builder() { + return { + sheetMaker: this.build_3_3_4_sheetList.bind(this), + lineMaker: this.build_3_3_4_lineFunction.bind(this), + isLandscape: false, + fileNameMod: `little${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + a10_6_10s_builder() { + return { + sheetMaker: this.build_6_10s_sheetList.bind(this), + lineMaker: this.build_6_10s_lineFunction.bind(this), + isLandscape: true, + fileNameMod: `mini${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + a_3_6s_builder() { + return { + sheetMaker: this.build_3_6s_sheetList.bind(this), + lineMaker: this.build_strip_lineFunction.bind(this, 3, 6), + isLandscape: true, + fileNameMod: `3_by_6${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + a_4_8s_builder() { + return { + sheetMaker: this.build_4_8s_sheetList.bind(this), + lineMaker: this.build_strip_lineFunction.bind(this, 4, 8), + isLandscape: true, + fileNameMod: `4_by_8${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + a7_2_16s_builder() { + return { + sheetMaker: this.build_2_16s_sheetList.bind(this), + lineMaker: this.build_2_16s_lineFunction.bind(this), + isLandscape: false, + fileNameMod: `4_by_4_two_signatures${this.isPacked ? '_packed' : '_spread'}`, + }; + } + + // ---------------- the real guts of the layout + + /** + * It's an 8 page zine. Same page count every time.... + * + * @param pageCount - total pages in document (to add blanks if < 8) + * @return an array of a single sheets. It's just one printed page (face) + * The sheet is an array of rows, containing a list of page objects + */ + build_8_zine_sheetList(pageCount) { + const p = this.page; + const f = this.flipPage; + return [ + [ + this.auditForBlanks([p(7), p(0), p(1), p(2)], pageCount), + this.auditForBlanks([f(6), f(5), f(4), f(3)], pageCount), + ], + ]; + } + + /** + * Produces a 3 folio signature per sheet + * @param pageCount - total pages in document + * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. + * Each sheet is an array of rows, containing a list of page objects + */ + build_1_3rd_sheetList(pageCount) { + const p = this.page; + const f = this.flipPage; + const sheets = []; + const sheetCount = Math.ceil(pageCount / 12.0); + console.log( + `Building the 1/3rd page layout. Given ${pageCount} page count, there will be ${sheetCount} sheets...` + ); + for (let sheet = 0; sheet < sheetCount; ++sheet) { + const i = sheet * 12 - 1; + const front = [ + this.auditForBlanks([p(i + 8), p(i + 5)], pageCount), + this.auditForBlanks([f(i + 9), f(i + 4)], pageCount), + this.auditForBlanks([p(i + 12), p(i + 1)], pageCount), + ]; + const back = [ + this.auditForBlanks([p(i + 6), p(i + 7)], pageCount), + this.auditForBlanks([f(i + 3), f(i + 10)], pageCount), + this.auditForBlanks([p(i + 2), p(i + 11)], pageCount), + ]; + sheets.push(front); + sheets.push(back); + } + return sheets; + } + + /** + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_1_3rd_lineFunction() { + return (info) => { + const vGap = (row) => { + return info.isPacked ? info.gap[1] : info.gap[1] * row; + }; + const hGap = (col) => { + return info.isPacked ? info.gap[0] : info.gap[0] * col; + }; + let foldMarks = []; + [0, 1, 2, 3].forEach((row) => { + [0, 1, 2].forEach((page) => { + foldMarks = foldMarks.concat( + this.crosshairMark( + hGap(page) + info.renderPageSize[0] * page, + vGap(row) + info.renderPageSize[1] * row, + 5 + ) + ); + }); + }); + return [ + this.foldHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1]), + this.foldHorizontal(info.paperSize[0], vGap(2) + info.renderPageSize[1] * 2), + this.cutHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), + ].concat(foldMarks); + }; + } + + /** + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_8_zine_lineFunction() { + // eslint-disable-next-line no-unused-vars + return (info) => { + return []; + }; + } + + /** + * Produces two 3 folio foldup signatures and a 4 folio foldup signature. + * 5 rows, 4 pages across. 1-12 / 13-24 / 25 - 40 + * @param pageCount - total pages in document + * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. + * Each sheet is an array of rows, containing a list of page objects + */ + build_3_3_4_sheetList(pageCount) { + const p = this.page; + const f = this.flipPage; + const sheets = []; + const sheetCount = Math.ceil(pageCount / 40.0); + console.log( + `Building the 3/3/4 pages. Given ${pageCount} page count, there will be ${sheetCount} sheets...` + ); + for (let sheet = 0; sheet < sheetCount; ++sheet) { + const i = sheet * 40 - 1; + const frontThrees = [ + this.auditForBlanks([f(i + 5), f(i + 8), f(i + 17), f(i + 20)], pageCount), + this.auditForBlanks([p(i + 4), p(i + 9), p(i + 16), p(i + 21)], pageCount), + this.auditForBlanks([f(i + 1), f(i + 12), f(i + 13), f(i + 24)], pageCount), + ]; + const frontFour = [ + this.auditForBlanks([p(i + 36), p(i + 29), p(i + 28), p(i + 37)], pageCount), + this.auditForBlanks([f(i + 33), f(i + 32), f(i + 25), f(i + 40)], pageCount), + ]; + const backThrees = [ + this.auditForBlanks([f(i + 19), f(i + 18), f(i + 7), f(i + 6)], pageCount), + this.auditForBlanks([p(i + 22), p(i + 15), p(i + 10), p(i + 3)], pageCount), + this.auditForBlanks([f(i + 23), f(i + 14), f(i + 11), f(i + 2)], pageCount), + ]; + const backFour = [ + this.auditForBlanks([p(i + 38), p(i + 27), p(i + 30), p(i + 35)], pageCount), + this.auditForBlanks([f(i + 39), f(i + 26), f(i + 31), f(i + 34)], pageCount), + ]; + console.log( + `Sheet ${sheet} has front 3s ${frontThrees} and front 4 ${frontFour}, w/ back 3s ${backThrees} and back 4',${backFour}` + ); + sheets.push(frontFour.concat(frontThrees)); + sheets.push(backFour.concat(backThrees)); + } + return sheets; + } + + /** + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_3_3_4_lineFunction() { + return (info) => { + const vGap = (row) => { + return info.isPacked ? info.gap[1] : info.gap[1] * row; + }; + const hGap = (col) => { + return info.isPacked ? info.gap[0] : info.gap[0] * col; + }; + const cutBetweenTheThrees = { + start: { + x: hGap(2) + 2 * info.renderPageSize[0], + y: info.renderPageSize[1] * 2 + info.gap[1], + }, + end: { x: hGap(2) + 2 * info.renderPageSize[0], y: info.paperSize[1] }, + thickness: 0.25, + opacity: 0.4, + }; + const foldBetweenTheFours = { + start: { + x: hGap(2) + 2 * info.renderPageSize[0], + y: info.renderPageSize[1] * 2 + info.gap[1], }, - { - start: { x: x, y: y - size/2 }, - end: { x: x, y: y + size/2}, - thickness: 0.5, - opacity: 0.4 + end: { x: hGap(2) + 2 * info.renderPageSize[0], y: 0 }, + thickness: 0.5, + opacity: 0.4, + dashArray: [2, 5], + }; + let foldMarks = []; + + [0, 1, 2, 3, 4, 5].forEach((row) => { + [0, 1, 2, 3, 4].forEach((page) => { + foldMarks = foldMarks.concat( + this.crosshairMark( + hGap(page) + info.renderPageSize[0] * page, + vGap(row) + info.renderPageSize[1] * row, + 5 + ) + ); + }); + }); + return [ + this.foldHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1]), + this.cutHorizontal(info.paperSize[0], vGap(2) + info.renderPageSize[1] * 2), + this.foldHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), + this.foldHorizontal(info.paperSize[0], vGap(4) + info.renderPageSize[1] * 4), + this.cutVertical(info.paperSize[1], hGap(0)), + this.cutVertical(info.paperSize[1], hGap(4) + info.renderPageSize[0] * 4), + cutBetweenTheThrees, + foldBetweenTheFours, + ].concat(foldMarks); + }; + } + + /** + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_6_10s_lineFunction() { + return (info) => { + const cutOffset = !this.duplex || info.isFront ? 4 : 6; + const vGap = (row) => { + return info.isPacked ? info.gap[1] : info.gap[1] * (2 * row); + }; + const hGap = (col) => { + return info.isPacked ? info.gap[0] : col * info.gap[0]; + }; + const baseCuts = [ + //this.cutHorizontal(info.paperSize[0], vGap(0)), + this.cutHorizontal(info.paperSize[0], vGap(0) + info.renderPageSize[1] * 0), + this.cutHorizontal(info.paperSize[0], vGap(1) + info.renderPageSize[1] * 1), + this.cutHorizontal(info.paperSize[0], vGap(2) + info.renderPageSize[1] * 2), + this.cutHorizontal(info.paperSize[0], vGap(3) + info.renderPageSize[1] * 3), + this.cutHorizontal(info.paperSize[0], vGap(4) + info.renderPageSize[1] * 4), + this.cutHorizontal(info.paperSize[0], vGap(5) + info.renderPageSize[1] * 5), + this.cutHorizontal(info.paperSize[0], vGap(6) + info.renderPageSize[1] * 6), + + // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 2), + this.cutVertical(info.paperSize[1], hGap(cutOffset) + info.renderPageSize[0] * cutOffset), + // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 6), + // this.foldVertical(info.paperSize[1], info.gap[0] + info.renderPageSize[0] * 8), + this.cutVertical(info.paperSize[1], hGap(0)), + this.cutVertical(info.paperSize[1], hGap(10) + info.renderPageSize[0] * 10), + ]; + let foldMarks = []; + [0, 1, 2, 3, 4, 5, 6].forEach((row) => { + [...Array(10).keys()].forEach((page) => { + foldMarks = foldMarks.concat( + this.crosshairMark( + hGap(page) + info.renderPageSize[0] * page, + vGap(row) + info.renderPageSize[1] * row, + 5 + ) + ); + }); + }); + console.log( + `Providing lines: \nbase cuts: ${baseCuts}\nfold marks: ${foldMarks}\ntotal: ${baseCuts.concat(foldMarks)}` + ); + return baseCuts.concat(foldMarks); + }; + } + + build_3_6s_sheetList(pageCount) { + const page = this.page; + const frontFunc = (i) => { + return [page(i + 2), page(i + 11), page(i + 10), page(i + 3), page(i + 6), page(i + 7)]; + }; + const backFunc = (i) => { + return [page(i + 8), page(i + 5), page(i + 4), page(i + 9), page(i + 12), page(i + 1)]; + }; + return this.build_strips_sheetList(3, 3, pageCount, frontFunc, backFunc); + } + build_4_8s_sheetList(pageCount) { + const page = this.page; + const frontFunc = (i) => { + return [ + page(i + 2), + page(i + 15), + page(i + 14), + page(i + 3), + page(i + 6), + page(i + 11), + page(i + 10), + page(i + 7), + ]; + }; + const backFunc = (i) => { + return [ + page(i + 8), + page(i + 9), + page(i + 12), + page(i + 5), + page(i + 4), + page(i + 13), + page(i + 16), + page(i + 1), + ]; + }; + return this.build_strips_sheetList(4, 4, pageCount, frontFunc, backFunc); + } + build_6_10s_sheetList(pageCount) { + const page = this.page; + const frontFunc = (i) => { + return [ + page(i + 6), + page(i + 3), + page(i + 2), + page(i + 7), + page(i + 10), + page(i + 19), + page(i + 18), + page(i + 11), + page(i + 14), + page(i + 15), + ]; + }; + const backFunc = (i) => { + return [ + page(i + 16), + page(i + 13), + page(i + 12), + page(i + 17), + page(i + 20), + page(i + 9), + page(i + 8), + page(i + 1), + page(i + 4), + page(i + 5), + ]; + }; + return this.build_strips_sheetList(6, 5, pageCount, frontFunc, backFunc); + } + + /** + * @param rows - number of rows for page + * @param folioPerRow - number of folios per row (not pages!) + * @param pageCount - total pages in document + * @param frontPageFunc - function (takes page number i to start (0)) that lays out the front pages as an array of pages + * @param backPageFunc - function (takes page number i to start (0)) that lays out the back pages as an array of pages + * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. + * Each sheet is an array of rows, containing a list of page objects + */ + build_strips_sheetList(rows, folioPerRow, pageCount, frontPageFunc, backPageFunc) { + const fronts = []; + const backs = []; + // const page = this.page; + const blank = this.blankPage; + const totalPagesPerRow = folioPerRow * 4; + const rowCount = Math.ceil(pageCount / totalPagesPerRow); + console.log( + `Building the ${rows} rows of ${folioPerRow * 2} pages. Given ${pageCount} page count, there will be ${rowCount} rows...` + ); + for (let row = 0; row < rowCount; ++row) { + const i = row * totalPagesPerRow - 1; + const front = frontPageFunc(i); + const back = backPageFunc(i); + if (!this.duplex) { + console.log('in duplex mode, reversing'); + back.reverse(); + } + fronts.push(this.auditForBlanks(front, pageCount)); + backs.push(this.auditForBlanks(back, pageCount)); + console.log( + ` -> adding front ${fronts[fronts.length - 1]} and back ${backs[fronts.length - 1]}` + ); + } + const sheets = []; + for (let row = 0; row < rowCount; ++row) { + const sheet = Math.floor(row / rows); + if (row % rows == 0) { + sheets[sheet * 2] = []; + sheets[sheet * 2 + 1] = []; + console.log(sheets); + } + sheets[sheet * 2].unshift(fronts[row]); + sheets[sheet * 2 + 1].unshift(backs[row]); + console.log( + ` -> row ${row} => sheet ${sheet} grabs front ${fronts[row]} and back ${backs[row]}` + ); + } + if (sheets[sheets.length - 1].length < rows) { + const rowOfBlanks = new Array(folioPerRow * 2); + for (let j = 0; j < rowOfBlanks.length; ++j) { + rowOfBlanks[j] = blank(); + } + console.log(`I present you my blanks! [for ${folioPerRow}] : ${rowOfBlanks}`); + for (let filler = 0; filler < rows - (rowCount % rows); ++filler) { + const sheet = Math.floor(rowCount / rows); + if (filler % 2 == 0) { + sheets[sheet * 2].unshift(rowOfBlanks); + sheets[sheet * 2 + 1].unshift(rowOfBlanks); + } else { + sheets[sheet * 2].push(rowOfBlanks); + sheets[sheet * 2 + 1].push(rowOfBlanks); } - ]; + } } - - - - - - // ---------------- page layout helpers - /** - * @param page - page object, will have it's `isBlank` modified if it exceeds bounds - * @param pageCount - total pages in document - * - * @return pageList - the possibly modified list provided - */ - auditForBlanks(pageList, pageCount) { - pageList.forEach( page => { - if (page.num >= pageCount) { - page.isBlank = true; - page.num = 0; - } + for (let i = 0; i < sheets.length; ++i) { + if (i % 2 == 1 && !this.duplex) { + // stupid "flip on short edge" rotation plans... + sheets[i].reverse(); + sheets[i].forEach((row) => { + row.forEach((page) => { + this.rotate180(page); + }); }); - return pageList; - } - - page(num) { - return { num: num, isBlank: false, vFlip: false} - } - flipPage(num) { - return { num: num, isBlank: false, vFlip: true} - } - blankPage() { - return { num: 0, isBlank: true, vFlip: false} - } - /** - * @param page - the page object, which will be mutated, vertically flipping it - */ - rotate180(page) { - page.vFlip = !page.vFlip; + } } + return sheets; + } + + /** + * @param rowCount - how many rows expected to be cut out + * @param colCount - how many column fold lines + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_strip_lineFunction(rowCount, colCount) { + return (info) => { + const vGap = (row) => { + return info.isPacked ? info.gap[1] : info.gap[1] * (2 * row); + }; + const hGap = (col) => { + return info.isPacked ? info.gap[0] : col * info.gap[0]; + }; + const baseCuts = [ + this.cutVertical(info.paperSize[1], hGap(0)), + this.cutVertical(info.paperSize[1], hGap(colCount) + info.renderPageSize[0] * colCount), + ]; + for (let i = 0; i <= rowCount; ++i) { + baseCuts.push(this.cutHorizontal(info.paperSize[0], vGap(i) + info.renderPageSize[1] * i)); + } + let foldMarks = []; + [...Array(rowCount).keys()].forEach((row) => { + [...Array(colCount).keys()].forEach((page) => { + foldMarks = foldMarks.concat( + this.crosshairMark( + hGap(page) + info.renderPageSize[0] * page, + vGap(row) + info.renderPageSize[1] * row, + 5 + ) + ); + }); + }); + console.log( + `Providing lines: \nbase cuts: ${baseCuts}\nfold marks:${foldMarks}\ntotal: ${baseCuts.concat(foldMarks)}` + ); + return baseCuts.concat(foldMarks); + }; + } + + /** + * @param pageCount - total pages in document + * @return an array of sheets. Assumes 1st is "front", 2nd is "back", 3rd is "front", etc. + * Each sheet is an array of rows, containing a list of page objects + */ + build_2_16s_sheetList(pageCount) { + const p = this.page; + const f = this.flipPage; + const sheets = []; + const sheetCount = Math.ceil(pageCount / 32.0); + console.log( + `Building the 32 pages. Given ${pageCount} page count, there will be ${sheetCount} sheets...` + ); + for (let sheet = 0; sheet < sheetCount; ++sheet) { + const i = sheet * 32 - 1; + const front = [ + this.auditForBlanks([p(i + 8), p(i + 9), p(i + 24), p(i + 25)], pageCount), + this.auditForBlanks([f(i + 1), f(i + 16), f(i + 17), f(i + 32)], pageCount), + this.auditForBlanks([p(i + 4), p(i + 13), p(i + 20), p(i + 29)], pageCount), + this.auditForBlanks([f(i + 5), f(i + 12), f(i + 21), f(i + 28)], pageCount), + ]; + const back = [ + this.auditForBlanks([p(i + 26), p(i + 23), p(i + 10), p(i + 7)], pageCount), + this.auditForBlanks([f(i + 31), f(i + 18), f(i + 15), f(i + 2)], pageCount), + this.auditForBlanks([p(i + 30), p(i + 19), p(i + 14), p(i + 3)], pageCount), + this.auditForBlanks([f(i + 27), f(i + 22), f(i + 11), f(i + 6)], pageCount), + ]; + sheets.push(front); + sheets.push(back); + } + return sheets; + } + + /** + * @return a FUNCTION. The function takes as it's parameter: + * Object definition: { + * gap: [leftGap, topGap], + * renderPageSize: [width, height], + * paperSize: [width, height], + * isFront: boolean, + * isPacked: boolean + * } + * and returns: a list of lines, as described by PDF-lib.js's `PDFPageDrawLineOptions` object + */ + build_2_16s_lineFunction() { + return (info) => { + const vGap = (row) => { + return info.isPacked ? info.gap[1] : info.gap[1] * (2 * row); + }; + const hGap = (col) => { + return info.isPacked ? info.gap[0] : col * info.gap[0]; + }; + let foldMarks = []; + [0, 1, 2, 3, 4].forEach((row) => { + [0, 1, 2, 3, 4].forEach((page) => { + foldMarks = foldMarks.concat( + this.crosshairMark( + hGap(page) + info.renderPageSize[0] * page, + vGap(row) + info.renderPageSize[1] * row, + 5 + ) + ); + }); + }); + return [ + { ...this.foldHorizontal(info.paperSize[0], info.paperSize[1] / 2, 0), opacity: 0.4 }, + this.cutVertical(info.paperSize[1], info.paperSize[0] / 2), + this.cutVertical(info.paperSize[1], info.paperSize[0] - info.gap[0]), + this.cutVertical(info.paperSize[1], info.gap[0]), + ].concat(foldMarks); + }; + } + + // ---------------- drawing lines helpers + + foldHorizontal(paperWidth, y) { + return { + start: { x: 0, y: y }, + end: { x: paperWidth, y: y }, + thickness: 0.25, + opacity: 0.4, + dashArray: [2, 5], + }; + } + foldVertical(paperHeight, x) { + return { + start: { x: x, y: 0 }, + end: { x: x, y: paperHeight }, + thickness: 0.25, + opacity: 0.4, + dashArray: [2, 5], + }; + } + cutHorizontal(paperWidth, y) { + return { + start: { x: 0, y: y }, + end: { x: paperWidth, y: y }, + thickness: 0.5, + opacity: 0.4, + }; + } + cutVertical(paperHeight, x) { + return { + start: { x: x, y: 0 }, + end: { x: x, y: paperHeight }, + thickness: 0.5, + opacity: 0.4, + }; + } + crosshairMark(x, y, size) { + return [ + { + start: { x: x - size / 2, y: y }, + end: { x: x + size / 2, y: y }, + thickness: 0.5, + opacity: 0.4, + }, + { + start: { x: x, y: y - size / 2 }, + end: { x: x, y: y + size / 2 }, + thickness: 0.5, + opacity: 0.4, + }, + ]; + } + + // ---------------- page layout helpers + /** + * @param page - page object, will have it's `isBlank` modified if it exceeds bounds + * @param pageCount - total pages in document + * + * @return pageList - the possibly modified list provided + */ + auditForBlanks(pageList, pageCount) { + pageList.forEach((page) => { + if (page.num >= pageCount) { + page.isBlank = true; + page.num = 0; + } + }); + return pageList; + } + + page(num) { + return { num: num, isBlank: false, vFlip: false }; + } + flipPage(num) { + return { num: num, isBlank: false, vFlip: true }; + } + blankPage() { + return { num: 0, isBlank: true, vFlip: false }; + } + /** + * @param page - the page object, which will be mutated, vertically flipping it + */ + rotate180(page) { + page.vFlip = !page.vFlip; + } } diff --git a/src/wacky_imposition.test.js b/src/wacky_imposition.test.js index b53b217..7415c7e 100644 --- a/src/wacky_imposition.test.js +++ b/src/wacky_imposition.test.js @@ -1,25 +1,25 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// file, You can obtain one at https://mozilla.org/MPL/2.0/. import { expect, describe, it } from 'vitest'; import { WackyImposition } from './wacky_imposition'; describe('WackyImposition model', () => { - it('returns a new instance of a WackyImposition', () => { - const testPages = []; - const testDuplex = true; - const testFormat = 'standardsig'; - const expected = { - duplex: true, - sigconfig: [], - pagelistdetails: [[]], - sheets: 0, - }; - const actual = new WackyImposition(testPages, testDuplex, testFormat); - console.log(actual); - expect(actual).toEqual(expected); - }); - // TODO add tests with actual pages + it('returns a new instance of a WackyImposition', () => { + const testPages = []; + const testDuplex = true; + const testFormat = 'standardsig'; + const expected = { + duplex: true, + sigconfig: [], + pagelistdetails: [[]], + sheets: 0, + }; + const actual = new WackyImposition(testPages, testDuplex, testFormat); + console.log(actual); + expect(actual).toEqual(expected); + }); + // TODO add tests with actual pages }); diff --git a/vite.config.js b/vite.config.js index 5fe821a..f0f7009 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,5 +6,5 @@ export default defineConfig({ test: { environment: 'jsdom', }, - plugins: [version()] -}); \ No newline at end of file + plugins: [version()], +});