Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b1615d359 | |||
| c1208b5216 | |||
| d0c5821f80 | |||
| bd6705ca4a | |||
| 5bc84ffaa0 | |||
| 6435d0f042 | |||
| 9fbd735088 | |||
| fe05713d57 | |||
| be574df599 | |||
| 6a4aeed3e1 | |||
| a4c3415838 | |||
| f535eacd97 | |||
| 9908897aa2 | |||
| 29d3cbb0b6 | |||
| babc20649a | |||
| 1188643c4b | |||
| 6b74301588 | |||
| 168527573c | |||
| 3d7bb37849 | |||
| 12a581ced9 | |||
| 857e1717a5 | |||
| 186bfb9d12 | |||
| c5bc354f65 | |||
| c48bb0428f | |||
| 46fbb615a0 | |||
| 3df4e103f9 | |||
| addff418c6 | |||
| 14d653e701 | |||
| 040bac5256 | |||
| bf44901a0a | |||
| b4d0f4e949 | |||
| b2b47b1f6a | |||
| 1b1398653b | |||
| 1f61dcb115 | |||
| 1476fc9174 | |||
| d157a3acd9 | |||
| fe6be928a9 | |||
| 8e537be454 | |||
| 6947529e02 | |||
| b6d78929b9 | |||
| e997189a64 | |||
| 31940deb7a | |||
| 922c6da234 | |||
| aa8fb165eb | |||
| 4cd4431565 | |||
| 826183be8c | |||
| 6d9b7ee0e4 | |||
| 3c66acb653 | |||
| aa0ad1dce5 | |||
| 65756457aa | |||
| 67f21d2500 | |||
| 1cf095a5cc | |||
| ffeaec7fe9 | |||
| a31e196e5b | |||
| 57851d90a5 | |||
| 1e4b16b734 | |||
| 876466b18d | |||
| 82b1c187ee | |||
| 4f93d258b8 | |||
| 60993fc005 | |||
| db4965c8f5 | |||
| 36a964d931 | |||
| 6d90f4c9b4 | |||
| 839eafd73f | |||
| f4141bf201 | |||
| 5a46028053 | |||
| 18d26647e1 | |||
| 0b5ec86780 | |||
| 5e15729045 | |||
| c98a5f1ac3 | |||
| 7f02146a0e | |||
| 5dab8c7351 | |||
| 505e0e9a30 | |||
| 9e3510955a | |||
| c006d4fbc7 | |||
| 5e02c44647 | |||
| 47f7cb18b5 | |||
| 3faf065c66 | |||
| dfffd03790 | |||
| 796aa905d2 | |||
| 253fb95143 | |||
| f3ea075b72 | |||
| af725a7f78 | |||
| 016e0db797 | |||
| 4cf8b2e1f8 | |||
| 67b0aa9d47 | |||
| 567c6eafea | |||
| ff890fb2af | |||
| a512fd64b5 | |||
| 377318a62a | |||
| 671c871304 | |||
| e0cc6b5655 | |||
| e74b44b49c | |||
| d6f0d88d4a | |||
| 9674e5b8dc | |||
| ea4cf777a9 | |||
| 8a308fa9e3 | |||
| 56fa53b701 | |||
| 2a4ddd4e41 | |||
| 3d8a63fddd | |||
| 9d9b1d0399 | |||
| 739d60d410 | |||
| 9003034d0d | |||
| 5002513d21 | |||
| 12ede2be02 | |||
| 163ec2bd50 | |||
| 7744839613 | |||
| df803d90cb | |||
| 5fc22585bf | |||
| b8387458db | |||
| 04aed2556b | |||
| 4cde3a7fe0 | |||
| 466d6d47ba | |||
| fa99b7f068 | |||
| 3e865b6c92 | |||
| 94dd576d3b | |||
| 9c19b4a3e4 | |||
| 1155656c2b | |||
| 5cf23c1134 | |||
| 67e9fdd10a | |||
| b0e388c50d | |||
| f37c9d8375 | |||
| 18c8ef9606 | |||
| 6766a3d0dc | |||
| d16f447048 | |||
| 37c752c2ae | |||
| b088d26e6f | |||
| 425f11a334 | |||
|
|
5ddbea1a5a | ||
|
|
42c5121784 | ||
|
|
d960d85539 | ||
|
|
ca60217a18 | ||
| 16f2829785 | |||
| 0381022b68 | |||
| aae8ec0ee1 | |||
| ec4768e1cc | |||
| 3308895b3b | |||
| 37e47fc33f | |||
| b4a3a36b07 | |||
| baf26dc492 | |||
| 30e4b4665c | |||
| 74a0a27fc1 | |||
| 59eed53644 | |||
| 473aaa004a | |||
| ecfd4115a1 | |||
| 0fbbfaac7c | |||
| 86d2fc2c5b | |||
| f97866fe82 | |||
| 5c0b8c4df0 | |||
| 8da88be5e8 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Default (not tags)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags-ignore:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
|
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm and npmci
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
|
||||||
|
- name: Run npm prepare
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Test stable
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm test
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm build
|
||||||
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
name: Default (tags)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
|
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Test stable
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm test
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm build
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm publish
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Code quality
|
||||||
|
run: |
|
||||||
|
npmci command npm install -g typescript
|
||||||
|
npmci npm install
|
||||||
|
|
||||||
|
- name: Trigger
|
||||||
|
run: npmci trigger
|
||||||
|
|
||||||
|
- name: Build docs and upload artifacts
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
pnpm install -g @git.zone/tsdoc
|
||||||
|
npmci command tsdoc
|
||||||
|
continue-on-error: true
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -15,9 +15,6 @@ node_modules/
|
|||||||
|
|
||||||
# builds
|
# builds
|
||||||
dist/
|
dist/
|
||||||
dist_web/
|
dist_*/
|
||||||
dist_serve/
|
|
||||||
dist_ts_web/
|
|
||||||
|
|
||||||
# custom
|
# custom
|
||||||
assets/pdfdir
|
|
||||||
125
.gitlab-ci.yml
125
.gitlab-ci.yml
@@ -1,125 +0,0 @@
|
|||||||
# gitzone standard
|
|
||||||
image: hosttoday/ht-docker-node:npmci
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .npmci_cache/
|
|
||||||
key: "$CI_BUILD_STAGE"
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- security
|
|
||||||
- test
|
|
||||||
- release
|
|
||||||
- metadata
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# security stage
|
|
||||||
# ====================
|
|
||||||
mirror:
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci git mirror
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
snyk:
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci command npm install -g snyk
|
|
||||||
- npmci command npm install --ignore-scripts
|
|
||||||
- npmci command snyk test
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# test stage
|
|
||||||
# ====================
|
|
||||||
|
|
||||||
testLTS:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install lts
|
|
||||||
- npmci npm install
|
|
||||||
- npmci npm test
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
testSTABLE:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci npm test
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm publish
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# metadata stage
|
|
||||||
# ====================
|
|
||||||
codequality:
|
|
||||||
stage: metadata
|
|
||||||
image: docker:stable
|
|
||||||
allow_failure: true
|
|
||||||
services:
|
|
||||||
- docker:stable-dind
|
|
||||||
script:
|
|
||||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
|
||||||
- docker run
|
|
||||||
--env SOURCE_CODE="$PWD"
|
|
||||||
--volume "$PWD":/code
|
|
||||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
|
|
||||||
artifacts:
|
|
||||||
paths: [codeclimate.json]
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- priv
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci trigger
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
pages:
|
|
||||||
image: hosttoday/ht-docker-node:npmci
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci command npm install -g @gitzone/tsdoc
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command tsdoc
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1 week
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
allow_failure: true
|
|
||||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "npm test",
|
||||||
|
"name": "Run npm test",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["/npmextra.json"],
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"npmci": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "settings for npmci"
|
||||||
|
},
|
||||||
|
"gitzone": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "settings for gitzone",
|
||||||
|
"properties": {
|
||||||
|
"projectType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
192
changelog.md
Normal file
192
changelog.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-09 - 4.2.0 - feat(smartpdf)
|
||||||
|
replace internal Express server with @push.rocks/smartserve, add PDF→WebP rendering, improve start/stop handling and bump dependencies
|
||||||
|
|
||||||
|
- Replace internal Express HTTP implementation with @push.rocks/smartserve and update README wording to reflect HTTP server usage
|
||||||
|
- Add PDF→WebP rendering: use pdf.js in-page rendering, capture canvases via Puppeteer to produce WebP buffers; added robust wait/timeout and error handling
|
||||||
|
- Add start/stop guards: _isRunning flag, reset readiness Deferred on start, and throw if start called while running
|
||||||
|
- Remove direct http/express exports from plugins and stop exporting express; export smartserve from plugins
|
||||||
|
- Improve JPEG conversion to produce progressive JPEGs via SmartJimp (sharp mode)
|
||||||
|
- Bump dependencies/devDependencies: @push.rocks/smartfs to ^1.5.0, add @push.rocks/smartserve ^2.0.1; devDeps @git.zone/tsbuild ^4.3.0, @git.zone/tstest ^3.3.0, @types/node ^25.3.5
|
||||||
|
|
||||||
|
## 2026-03-01 - 4.1.3 - fix(tests)
|
||||||
|
use example.com in image conversion test and relax JPEG size assertion
|
||||||
|
|
||||||
|
- Replaced https://www.wikipedia.org with https://example.com in test/test.ts for the third PDF generation test
|
||||||
|
- Removed the strict expectation that JPEG size must be smaller than PNG; now only asserts that WebP is smaller than PNG
|
||||||
|
- Updated test comment to note that JPEG may not be smaller for simple graphics pages
|
||||||
|
|
||||||
|
## 2026-03-01 - 4.1.2 - fix(smartfs)
|
||||||
|
replace smartfile with smartfs, update file reading to use SmartFs, remove GraphicsMagick/Ghostscript dependency checks, bump dev and runtime dependencies, update tests and docs, and adjust npmextra configuration
|
||||||
|
|
||||||
|
- Replace usage/export of @push.rocks/smartfile with @push.rocks/smartfs and update readFileToPdfObject to use SmartFs + SmartFsProviderNode
|
||||||
|
- Remove execFile import and the GraphicsMagick/Ghostscript dependency-checking helpers from smartpdf (no more gm/gs checks)
|
||||||
|
- Bump devDependencies: @git.zone/tsbuild ^4.1.2, @git.zone/tsdoc ^1.12.0, @git.zone/tsrun ^2.0.1, @git.zone/tstest ^3.1.8, @types/node ^25.3.2
|
||||||
|
- Bump runtime dependencies: @push.rocks/smartfs ^1.3.1, @push.rocks/smartnetwork ^4.4.0, @tsclass/tsclass ^9.3.0, @types/express ^5.0.6, express ^5.2.1, pdf2json ^4.0.2
|
||||||
|
- Tests updated: switched example URLs to example.com, added a third PDF generation test that writes .nogit/3.pdf, and exported tap.start() as default
|
||||||
|
- npmextra.json reorganized to namespaced keys, added release.registries and accessLevel, and adjusted tsdoc/legal entries
|
||||||
|
- Documentation/readme refreshed: added issue reporting/security section, feature table, and various wording/formatting updates
|
||||||
|
|
||||||
|
## 2025-08-02 - 4.1.0 - feat(smartpdf)
|
||||||
|
Add progressive JPEG generation support
|
||||||
|
|
||||||
|
- Added new convertPDFToJpegBytes method for progressive JPEG generation
|
||||||
|
- Integrated @push.rocks/smartjimp for true progressive JPEG encoding
|
||||||
|
- Progressive JPEGs load in multiple passes, showing low-quality preview first
|
||||||
|
- Supports quality and scale options like other image generation methods
|
||||||
|
- Updated readme with comprehensive documentation and modern styling
|
||||||
|
- Updated legal section to reflect Task Venture Capital GmbH ownership
|
||||||
|
|
||||||
|
## 2025-08-02 - 4.0.0 - BREAKING CHANGE(smartpdf)
|
||||||
|
Improve image generation quality and API consistency
|
||||||
|
|
||||||
|
- BREAKING: Renamed `convertPDFToWebpPreviews` to `convertPDFToWebpBytes` for API consistency
|
||||||
|
- Added configurable scale options to `convertPDFToPngBytes` method
|
||||||
|
- Changed default scale from 1.0 to 3.0 for PNG generation (216 DPI)
|
||||||
|
- Changed default scale from 0.5 to 3.0 for WebP generation (216 DPI)
|
||||||
|
- Added DPI helper methods: `getScaleForDPI()` and scale constants (SCALE_SCREEN, SCALE_HIGH, SCALE_PRINT)
|
||||||
|
- Added maxWidth/maxHeight constraints for both PNG and WebP generation
|
||||||
|
- Improved test file organization with clear naming conventions
|
||||||
|
- Updated documentation with DPI/scale guidance and examples
|
||||||
|
|
||||||
|
## 2025-08-01 - 3.3.0 - feat(smartpdf)
|
||||||
|
Add automatic port allocation and multi-instance support
|
||||||
|
|
||||||
|
- Added ISmartPdfOptions interface with port configuration options
|
||||||
|
- Implemented automatic port allocation between 20000-30000 by default
|
||||||
|
- Added support for custom port ranges via portRangeStart/portRangeEnd options
|
||||||
|
- Added support for specific port assignment via port option
|
||||||
|
- Fixed resource cleanup when port allocation fails
|
||||||
|
- Multiple SmartPdf instances can now run simultaneously without port conflicts
|
||||||
|
- Updated readme with comprehensive documentation for all features
|
||||||
|
|
||||||
|
## 2025-02-25 - 3.2.2 - fix(SmartPdf)
|
||||||
|
Fix buffer handling for PDF conversion and text extraction
|
||||||
|
|
||||||
|
- Ensure Uint8Array is converted to Node Buffer for PDF conversion.
|
||||||
|
- Correct the PDF page viewport handling by using document dimensions.
|
||||||
|
- Fix extractTextFromPdfBuffer argument type from Uint8Array to Buffer.
|
||||||
|
|
||||||
|
## 2025-02-25 - 3.2.1 - fix(SmartPdf)
|
||||||
|
Fix type for extractTextFromPdfBuffer function
|
||||||
|
|
||||||
|
- Corrected the parameter type from Buffer to Uint8Array for extractTextFromPdfBuffer function.
|
||||||
|
|
||||||
|
## 2025-02-25 - 3.2.0 - feat(smartpdf)
|
||||||
|
Improve dependency versions and optimize PDF to PNG conversion.
|
||||||
|
|
||||||
|
- Update several dependencies to newer versions for better stability and performance.
|
||||||
|
- Refactor tests to enhance readability and add directory creation validations.
|
||||||
|
- Optimize PDF to PNG conversion by switching to a more efficient Puppeteer and PDF.js-based method.
|
||||||
|
- Add checks for presence of required dependencies (GraphicsMagick and Ghostscript).
|
||||||
|
- Fix media emulation issue by properly awaiting the emulateMediaType function.
|
||||||
|
|
||||||
|
## 2024-11-30 - 3.1.8 - fix(core)
|
||||||
|
Fix candidate handling in PDF generation
|
||||||
|
|
||||||
|
- Added error handling for missing PDF candidates in server requests.
|
||||||
|
- Updated devDependencies and dependencies to latest versions for better stability and new features.
|
||||||
|
- Patched header retrieval logic during PDF generation for security check.
|
||||||
|
|
||||||
|
## 2024-09-27 - 3.1.7 - fix(dependencies)
|
||||||
|
Update dependencies to latest versions
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild to version ^2.1.84
|
||||||
|
- Updated @git.zone/tsdoc to version ^1.3.12
|
||||||
|
- Updated @git.zone/tsrun to version ^1.2.49
|
||||||
|
- Updated @push.rocks/tapbundle to version ^5.3.0
|
||||||
|
- Updated @types/node to version ^22.7.4
|
||||||
|
- Updated @push.rocks/smartfile to version ^11.0.21
|
||||||
|
- Updated @push.rocks/smartpromise to version ^4.0.4
|
||||||
|
- Updated @tsclass/tsclass to version ^4.1.2
|
||||||
|
- Updated express to version ^4.21.0
|
||||||
|
- Updated pdf2pic to version ^3.1.3
|
||||||
|
|
||||||
|
## 2024-05-29 - 3.1.6 - Core
|
||||||
|
Updated description
|
||||||
|
|
||||||
|
- Minor changes to documentation and internal text.
|
||||||
|
|
||||||
|
## 2024-04-25 to 2024-04-30 - 3.1.0 to 3.1.5 - Core
|
||||||
|
Fix updates in core functionality
|
||||||
|
|
||||||
|
- Fixes and updates in core function in versions 3.1.0 to 3.1.5.
|
||||||
|
|
||||||
|
## 2024-04-25 - 3.0.17 - Feature
|
||||||
|
Now supports PDF to JPG conversion
|
||||||
|
|
||||||
|
- Added support for converting PDF files to JPG format.
|
||||||
|
|
||||||
|
## 2024-03-19 to 2024-04-14 - 3.0.17 - Maintenance
|
||||||
|
Various updates to project configuration files
|
||||||
|
|
||||||
|
- Updated `tsconfig`.
|
||||||
|
- Updated `npmextra.json`.
|
||||||
|
|
||||||
|
## 2023-07-11 to 2024-03-19 - 3.0.15 to 3.0.16 - Organization
|
||||||
|
Switch to new organization scheme and core updates
|
||||||
|
|
||||||
|
- Switched to new organization scheme.
|
||||||
|
- Applied core updates and bug fixes.
|
||||||
|
|
||||||
|
## 2022-11-07 to 2023-07-10 - 3.0.13 to 3.0.14 - Core
|
||||||
|
Fixes and updates to core functionality
|
||||||
|
|
||||||
|
- Various minor bug fixes and updates to core components.
|
||||||
|
|
||||||
|
## 2022-09-13 to 2022-11-07 - 3.0.10 to 3.0.12 - Core
|
||||||
|
Ongoing core updates and maintenance
|
||||||
|
|
||||||
|
- Regular fixes and operational improvements in core functionalities.
|
||||||
|
|
||||||
|
## 2022-06-12 to 2022-09-13 - 3.0.7 to 3.0.9 - Core
|
||||||
|
Continued focus on high-priority bug fixes and core functionalities
|
||||||
|
|
||||||
|
- Regular fixes for critical bugs and enhancements.
|
||||||
|
|
||||||
|
## 2022-03-24 to 2022-06-29 - 3.0.3 to 3.0.6 - Core
|
||||||
|
Further optimization and maintenance releases
|
||||||
|
|
||||||
|
- Further improvements and refinements of issues in core functionalities.
|
||||||
|
|
||||||
|
## 2022-01-05 to 2022-03-25 - 3.0.0 to 3.0.2 - Major Version Release
|
||||||
|
Major release for version 3.0.x, including core fixes
|
||||||
|
|
||||||
|
- Increased version from 2.x to 3.0. New significant changes and fixes.
|
||||||
|
|
||||||
|
## 2022-01-05 to 2022-03-24 - 2.0.13 to 2.0.19 - Core
|
||||||
|
Routine core updates and bug fixes
|
||||||
|
|
||||||
|
- Regular bug fixes in core components.
|
||||||
|
|
||||||
|
## 2019-11-19 to 2022-01-06 - 2.0.0 to 2.0.11 - Core
|
||||||
|
Multiple core updates and a few performance improvements
|
||||||
|
|
||||||
|
- Some performance enhancements and multiple bug fixes.
|
||||||
|
|
||||||
|
## 2019-11-16 to 2019-11-19 - 1.0.27 to 1.0.29 - API
|
||||||
|
Breaking change in API
|
||||||
|
|
||||||
|
- Naming PDF results to better represent their content.
|
||||||
|
|
||||||
|
## 2019-05-29 to 2019-11-15 - 1.0.13 to 1.0.26 - Core
|
||||||
|
Core functional updates and some major restructuring
|
||||||
|
|
||||||
|
- Introduced multiple updates to the core, addressing bugs and improving stability.
|
||||||
|
|
||||||
|
## 2019-04-10 to 2019-05-28 - 1.0.4 to 1.0.12 - Core
|
||||||
|
Fixes and updates in the core
|
||||||
|
|
||||||
|
- Implementation of multiple essential fixes for core components.
|
||||||
|
|
||||||
|
## 2018-10-06 - 1.0.1 to 1.0.3 - Core and Typings
|
||||||
|
Initial implementation and core fixes
|
||||||
|
|
||||||
|
- Initial implementation of the project.
|
||||||
|
- Fixed compilation problems in typings.
|
||||||
|
|
||||||
|
## 2016-01-29 - unknown - Initial
|
||||||
|
Initial commit
|
||||||
|
|
||||||
|
- Initial commit for the project setup.
|
||||||
@@ -1,16 +1,40 @@
|
|||||||
{
|
{
|
||||||
"npmci": {
|
"@git.zone/cli": {
|
||||||
"npmGlobalTools": [],
|
"projectType": "npm",
|
||||||
"npmAccessLevel": "public"
|
|
||||||
},
|
|
||||||
"gitzone": {
|
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.om",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "pushrocks",
|
"gitscope": "push.rocks",
|
||||||
"gitrepo": "smartpdf",
|
"gitrepo": "smartpdf",
|
||||||
"shortDescription": "Create PDFs fast and smoothly",
|
"description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.",
|
||||||
"npmPackagename": "@pushrocks/smartpdf",
|
"npmPackagename": "@push.rocks/smartpdf",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"PDF generation",
|
||||||
|
"HTML to PDF",
|
||||||
|
"website to PDF",
|
||||||
|
"PDF manipulation",
|
||||||
|
"puppeteer",
|
||||||
|
"express",
|
||||||
|
"node.js",
|
||||||
|
"typescript",
|
||||||
|
"automation",
|
||||||
|
"PDF merging",
|
||||||
|
"text extraction",
|
||||||
|
"PDF management"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsdoc": {
|
||||||
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {
|
||||||
|
"npmGlobalTools": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2443
package-lock.json
generated
2443
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -1,42 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "@pushrocks/smartpdf",
|
"name": "@push.rocks/smartpdf",
|
||||||
"version": "1.0.12",
|
"version": "4.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "create pdfs on the fly",
|
"description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.",
|
||||||
"main": "dist/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose --timeout 120)",
|
||||||
"format": "(gitzone format)",
|
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||||
"build": "(tsbuild)"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gitzone/tsbuild": "^2.1.11",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@gitzone/tsrun": "^1.2.6",
|
"@git.zone/tsdoc": "^1.12.0",
|
||||||
"@gitzone/tstest": "^1.0.24",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@pushrocks/tapbundle": "^3.0.9",
|
"@git.zone/tstest": "^3.3.0",
|
||||||
"@types/node": "^12.0.3"
|
"@types/node": "^25.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pushrocks/smartfile": "^7.0.2",
|
"@push.rocks/smartbuffer": "^3.0.5",
|
||||||
"@pushrocks/smartnetwork": "^1.1.6",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@pushrocks/smartpromise": "^3.0.2",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@pushrocks/smartunique": "^3.0.1",
|
"@push.rocks/smartjimp": "^1.2.0",
|
||||||
"@types/express": "^4.16.1",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@types/puppeteer": "^1.12.4",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"express": "^4.17.1",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"puppeteer": "^1.17.0"
|
"@push.rocks/smartpuppeteer": "^2.0.5",
|
||||||
|
"@push.rocks/smartserve": "^2.0.1",
|
||||||
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdf2json": "^4.0.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/*",
|
"ts/**/*",
|
||||||
"ts_web/*",
|
"ts_web/**/*",
|
||||||
"dist/*",
|
"dist/**/*",
|
||||||
"dist_web/*",
|
"dist_*/**/*",
|
||||||
"assets/*",
|
"dist_ts/**/*",
|
||||||
|
"dist_ts_web/**/*",
|
||||||
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
]
|
],
|
||||||
|
"browserslist": [
|
||||||
|
"last 1 chrome versions"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"PDF generation",
|
||||||
|
"HTML to PDF",
|
||||||
|
"website to PDF",
|
||||||
|
"PDF manipulation",
|
||||||
|
"puppeteer",
|
||||||
|
"express",
|
||||||
|
"node.js",
|
||||||
|
"typescript",
|
||||||
|
"automation",
|
||||||
|
"PDF merging",
|
||||||
|
"text extraction",
|
||||||
|
"PDF management"
|
||||||
|
],
|
||||||
|
"homepage": "https://code.foss.global/push.rocks/smartpdf",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://code.foss.global/push.rocks/smartpdf.git"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||||
}
|
}
|
||||||
|
|||||||
10780
pnpm-lock.yaml
generated
Normal file
10780
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- sharp
|
||||||
1
readme.hints.md
Normal file
1
readme.hints.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
437
readme.md
437
readme.md
@@ -1,26 +1,423 @@
|
|||||||
# @pushrocks/smartpdf
|
# @push.rocks/smartpdf 📄✨
|
||||||
Create PDFs fast and smoothly
|
|
||||||
|
|
||||||
## Availabililty and Links
|
> **Transform HTML, websites, and PDFs into beautiful documents and images with just a few lines of code.**
|
||||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartpdf)
|
|
||||||
* [gitlab.com (source)](https://gitlab.om/pushrocks/smartpdf)
|
|
||||||
* [github.com (source mirror)](https://github.com/pushrocks/smartpdf)
|
|
||||||
* [docs (typedoc)](https://pushrocks.gitlab.io/smartpdf/)
|
|
||||||
|
|
||||||
## Status for master
|
[](https://www.npmjs.com/package/@push.rocks/smartpdf)
|
||||||
[](https://gitlab.om/pushrocks/smartpdf/commits/master)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://gitlab.om/pushrocks/smartpdf/commits/master)
|
[](./license)
|
||||||
[](https://www.npmjs.com/package/@pushrocks/smartpdf)
|
|
||||||
[](https://snyk.io/test/npm/@pushrocks/smartpdf)
|
|
||||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
|
||||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
|
||||||
[](https://prettier.io/)
|
|
||||||
|
|
||||||
## Usage
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For further information read the linked docs at the top of this readme.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
## 🚀 Why SmartPDF?
|
||||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy.html)
|
|
||||||
|
|
||||||
[](https://maintainedby.lossless.com)
|
SmartPDF is your Swiss Army knife for PDF operations in Node.js. Whether you're generating invoices from HTML, snapshotting web pages, merging documents, or converting PDF pages to images — SmartPDF handles it all through a clean, async-first TypeScript API backed by headless Chromium.
|
||||||
|
|
||||||
|
### ✨ Features at a Glance
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| 📝 **HTML → PDF** | Render any HTML string (with full CSS) into an A4-sized PDF |
|
||||||
|
| 🌐 **Website → PDF** | Capture a live URL as a PDF — either A4 or full-page scroll |
|
||||||
|
| 🔀 **PDF Merging** | Combine multiple PDF buffers into a single document |
|
||||||
|
| 🖼️ **PDF → Images** | Convert PDF pages to **PNG**, **WebP**, or progressive **JPEG** |
|
||||||
|
| 📑 **Text Extraction** | Pull raw text content from any PDF buffer |
|
||||||
|
| 🔌 **Smart Port Management** | Automatic port allocation so multiple instances never collide |
|
||||||
|
| 🎛️ **DPI Control** | Built-in scale constants for screen, high-quality, and print resolutions |
|
||||||
|
| 🌐 **BYO Browser** | Optionally pass your own Puppeteer `Browser` instance |
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @push.rocks/smartpdf
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Prerequisites:** SmartPDF uses headless Chromium via Puppeteer under the hood. On most systems this is handled automatically. If you run into browser-launch issues (CI, Docker, etc.), make sure the required system libraries are installed — see the [Puppeteer troubleshooting guide](https://pptr.dev/troubleshooting).
|
||||||
|
|
||||||
|
## 🎯 Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SmartPdf } from '@push.rocks/smartpdf';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// 1. Create and start
|
||||||
|
const smartPdf = await SmartPdf.create();
|
||||||
|
await smartPdf.start();
|
||||||
|
|
||||||
|
// 2. Generate a PDF from HTML
|
||||||
|
const pdf = await smartPdf.getA4PdfResultForHtmlString(`
|
||||||
|
<h1>Hello, PDF World! 🌍</h1>
|
||||||
|
<p>Generated with SmartPDF.</p>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3. Write to disk
|
||||||
|
fs.writeFileSync('my-first.pdf', pdf.buffer);
|
||||||
|
|
||||||
|
// 4. Clean up
|
||||||
|
await smartPdf.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
Every method returns an `IPdf` object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IPdf {
|
||||||
|
id: string | null; // Unique identifier
|
||||||
|
name: string; // Filename
|
||||||
|
buffer: Buffer; // Raw PDF bytes
|
||||||
|
metadata?: {
|
||||||
|
textExtraction?: string; // Extracted text (when available)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 How It Works
|
||||||
|
|
||||||
|
SmartPDF spins up a lightweight HTTP server (via `@push.rocks/smartserve`) bound to `localhost` and a headless Chromium browser. When you call a generation method:
|
||||||
|
|
||||||
|
1. Your HTML is registered internally and served at `http://localhost:{port}/{id}`
|
||||||
|
2. Puppeteer navigates to that URL, waits for the page to fully render, and captures a PDF
|
||||||
|
3. A header-based security check ensures only the correct content is captured
|
||||||
|
4. The server and browser are torn down when you call `stop()`
|
||||||
|
|
||||||
|
This architecture means you get **pixel-perfect CSS rendering**, **web font support**, and **full JavaScript execution** — the same rendering engine that powers Chrome.
|
||||||
|
|
||||||
|
## 🏗️ Instance Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const smartPdf = await SmartPdf.create();
|
||||||
|
await smartPdf.start();
|
||||||
|
|
||||||
|
// ... your operations ...
|
||||||
|
|
||||||
|
await smartPdf.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
For production use, wrap in try/finally:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const smartPdf = await SmartPdf.create();
|
||||||
|
try {
|
||||||
|
await smartPdf.start();
|
||||||
|
// ... generate PDFs ...
|
||||||
|
} finally {
|
||||||
|
await smartPdf.stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔌 Smart Port Allocation
|
||||||
|
|
||||||
|
Run multiple instances without conflicts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Each instance auto-selects a free port (default range: 20000–30000)
|
||||||
|
const instance1 = new SmartPdf();
|
||||||
|
const instance2 = new SmartPdf();
|
||||||
|
await instance1.start(); // e.g. port 20000
|
||||||
|
await instance2.start(); // e.g. port 20001
|
||||||
|
|
||||||
|
console.log(instance1.serverPort); // 20000
|
||||||
|
console.log(instance2.serverPort); // 20001
|
||||||
|
|
||||||
|
// Custom range
|
||||||
|
const custom = new SmartPdf({ portRangeStart: 4000, portRangeEnd: 5000 });
|
||||||
|
|
||||||
|
// Or pin a specific port
|
||||||
|
const pinned = new SmartPdf({ port: 3000 });
|
||||||
|
```
|
||||||
|
|
||||||
|
If a specific port is already in use, `start()` throws an error immediately instead of silently failing.
|
||||||
|
|
||||||
|
### 🌐 Bring Your Own Browser
|
||||||
|
|
||||||
|
Pass an existing Puppeteer `Browser` instance — SmartPDF won't close it when you call `stop()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
args: ['--no-sandbox'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const smartPdf = await SmartPdf.create();
|
||||||
|
await smartPdf.start(browser); // uses your browser
|
||||||
|
|
||||||
|
await smartPdf.stop(); // server stops, browser stays open
|
||||||
|
await browser.close(); // you manage browser lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 PDF Generation
|
||||||
|
|
||||||
|
### 📝 HTML → A4 PDF
|
||||||
|
|
||||||
|
Renders at a 794×1122 viewport (A4 at 96 DPI) with full CSS support:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pdf = await smartPdf.getA4PdfResultForHtmlString(`
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Helvetica', sans-serif; margin: 40px; }
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white; padding: 30px; border-radius: 10px; text-align: center;
|
||||||
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { background: #f5f5f5; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1>Invoice #2024-001</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
|
||||||
|
<tr><td>Widget Pro</td><td>5</td><td>$49.99</td></tr>
|
||||||
|
<tr><td>Gizmo Ultra</td><td>2</td><td>$129.99</td></tr>
|
||||||
|
</table>
|
||||||
|
`);
|
||||||
|
|
||||||
|
fs.writeFileSync('invoice.pdf', pdf.buffer);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 Website → PDF
|
||||||
|
|
||||||
|
Two methods depending on your needs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Standard capture — uses the document's own dimensions
|
||||||
|
const pdf = await smartPdf.getPdfResultForWebsite('https://example.com');
|
||||||
|
|
||||||
|
// Full-page capture — scrolls to bottom, captures everything as a single page
|
||||||
|
const fullPdf = await smartPdf.getFullWebsiteAsSinglePdf('https://example.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
`getPdfResultForWebsite` uses a 1980×1200 viewport and respects the page's own width/height. `getFullWebsiteAsSinglePdf` uses a 1920px-wide viewport and measures the full scroll height, producing a single tall page.
|
||||||
|
|
||||||
|
### 🔀 Merge Multiple PDFs
|
||||||
|
|
||||||
|
Combine any number of PDF buffers into one document using `pdf-lib`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const invoice = await smartPdf.readFileToPdfObject('./invoice.pdf');
|
||||||
|
const terms = await smartPdf.readFileToPdfObject('./terms.pdf');
|
||||||
|
const appendix = await smartPdf.getA4PdfResultForHtmlString('<h1>Appendix</h1>...');
|
||||||
|
|
||||||
|
const merged = await smartPdf.mergePdfs([
|
||||||
|
invoice.buffer,
|
||||||
|
terms.buffer,
|
||||||
|
appendix.buffer,
|
||||||
|
]);
|
||||||
|
|
||||||
|
fs.writeFileSync('complete-package.pdf', merged);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📑 Read a PDF from Disk
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pdfObject = await smartPdf.readFileToPdfObject('./document.pdf');
|
||||||
|
console.log(pdfObject.name); // "document.pdf"
|
||||||
|
console.log(pdfObject.buffer); // <Buffer ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📖 Extract Text
|
||||||
|
|
||||||
|
Pull raw text from any PDF buffer:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const text = await smartPdf.extractTextFromPdfBuffer(pdf.buffer);
|
||||||
|
console.log(text);
|
||||||
|
```
|
||||||
|
|
||||||
|
> Uses [pdf2json](https://github.com/modesty/pdf2json) under the hood. Works best with text-based PDFs; scanned documents may return limited results.
|
||||||
|
|
||||||
|
## 🖼️ PDF → Image Conversion
|
||||||
|
|
||||||
|
Convert PDF pages to raster images using Puppeteer + PDF.js. Each page becomes a separate image buffer.
|
||||||
|
|
||||||
|
### PNG — Lossless Quality
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pngPages = await smartPdf.convertPDFToPngBytes(pdf.buffer, {
|
||||||
|
scale: SmartPdf.SCALE_HIGH, // 3.0 = ~216 DPI (default)
|
||||||
|
});
|
||||||
|
|
||||||
|
pngPages.forEach((png, i) => {
|
||||||
|
fs.writeFileSync(`page-${i + 1}.png`, Buffer.from(png));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebP — Modern & Efficient
|
||||||
|
|
||||||
|
25–60% smaller than PNG at similar visual quality:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const webpPages = await smartPdf.convertPDFToWebpBytes(pdf.buffer, {
|
||||||
|
scale: 2.0, // ~144 DPI
|
||||||
|
quality: 90, // 0–100 (default: 85)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### JPEG — Progressive Loading
|
||||||
|
|
||||||
|
Generates true progressive JPEGs (multi-pass rendering) via sharp:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const jpegPages = await smartPdf.convertPDFToJpegBytes(pdf.buffer, {
|
||||||
|
scale: SmartPdf.SCALE_HIGH,
|
||||||
|
quality: 85, // 0–100 (default: 85)
|
||||||
|
maxWidth: 1920, // optional dimension constraints
|
||||||
|
maxHeight: 1080,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📏 DPI & Scale Reference
|
||||||
|
|
||||||
|
All image methods accept a `scale` parameter. PDF.js renders at 72 DPI by default, so `scale` is a multiplier:
|
||||||
|
|
||||||
|
| Constant | Value | DPI | Use Case |
|
||||||
|
|----------|-------|-----|----------|
|
||||||
|
| `SmartPdf.SCALE_SCREEN` | 2.0 | ~144 | Web display, thumbnails |
|
||||||
|
| `SmartPdf.SCALE_HIGH` | 3.0 | ~216 | General purpose (default) |
|
||||||
|
| `SmartPdf.SCALE_PRINT` | 6.0 | ~432 | Print-quality output |
|
||||||
|
|
||||||
|
Or calculate a custom scale:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const scale = SmartPdf.getScaleForDPI(300); // → 4.167
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🖼️ Dimension Constraints
|
||||||
|
|
||||||
|
All image methods support `maxWidth` and `maxHeight` to cap output size while preserving aspect ratio:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// High-res render, but capped at 800×1000 px
|
||||||
|
const constrained = await smartPdf.convertPDFToWebpBytes(pdf.buffer, {
|
||||||
|
scale: SmartPdf.SCALE_HIGH,
|
||||||
|
quality: 90,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 1000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Format Comparison
|
||||||
|
|
||||||
|
| Format | Typical Size vs PNG | Lossy? | Transparency | Progressive | Best For |
|
||||||
|
|--------|-------------------|--------|--------------|-------------|----------|
|
||||||
|
| **PNG** | baseline | No | ✅ | — | Screenshots, diagrams, text-heavy docs |
|
||||||
|
| **WebP** | 40–75% | Yes | ✅ | — | Modern web apps, thumbnails |
|
||||||
|
| **JPEG** | 50–70% | Yes | ❌ | ✅ | Photos, complex graphics, email |
|
||||||
|
|
||||||
|
## ⚡ Parallel Processing
|
||||||
|
|
||||||
|
Process multiple URLs concurrently with separate instances:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const urls = [
|
||||||
|
'https://example.com/page1',
|
||||||
|
'https://example.com/page2',
|
||||||
|
'https://example.com/page3',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Spin up parallel instances
|
||||||
|
const instances = await Promise.all(
|
||||||
|
urls.map(() => SmartPdf.create())
|
||||||
|
);
|
||||||
|
await Promise.all(instances.map(i => i.start()));
|
||||||
|
|
||||||
|
// Generate in parallel
|
||||||
|
const pdfs = await Promise.all(
|
||||||
|
urls.map((url, i) => instances[i].getFullWebsiteAsSinglePdf(url))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge all results
|
||||||
|
const merged = await instances[0].mergePdfs(pdfs.map(p => p.buffer));
|
||||||
|
fs.writeFileSync('all-pages.pdf', merged);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await Promise.all(instances.map(i => i.stop()));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Full API Reference
|
||||||
|
|
||||||
|
### `SmartPdf` Class
|
||||||
|
|
||||||
|
#### Static Properties
|
||||||
|
|
||||||
|
| Property | Type | Value | Description |
|
||||||
|
|----------|------|-------|-------------|
|
||||||
|
| `SCALE_SCREEN` | `number` | `2.0` | ~144 DPI scale factor |
|
||||||
|
| `SCALE_HIGH` | `number` | `3.0` | ~216 DPI scale factor (default) |
|
||||||
|
| `SCALE_PRINT` | `number` | `6.0` | ~432 DPI scale factor |
|
||||||
|
|
||||||
|
#### Static Methods
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `create(options?)` | `Promise<SmartPdf>` | Factory method to create an instance |
|
||||||
|
| `getScaleForDPI(dpi)` | `number` | Converts a DPI value to a scale factor (`dpi / 72`) |
|
||||||
|
|
||||||
|
#### Instance Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `serverPort` | `number` | The port the internal HTTP server is listening on |
|
||||||
|
|
||||||
|
#### Instance Methods
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `start(browser?)` | `Promise<void>` | Starts internal server + browser. Optionally accepts an existing Puppeteer `Browser`. |
|
||||||
|
| `stop()` | `Promise<void>` | Shuts down server and browser (unless external browser was provided). |
|
||||||
|
| `getA4PdfResultForHtmlString(html)` | `Promise<IPdf>` | Renders HTML at 794×1122 viewport → A4 PDF |
|
||||||
|
| `getPdfResultForWebsite(url)` | `Promise<IPdf>` | Captures website at 1980×1200 viewport → PDF |
|
||||||
|
| `getFullWebsiteAsSinglePdf(url)` | `Promise<IPdf>` | Captures full scrollable page at 1920px wide → single-page PDF |
|
||||||
|
| `mergePdfs(buffers)` | `Promise<Uint8Array>` | Merges an array of PDF `Uint8Array` buffers |
|
||||||
|
| `readFileToPdfObject(path)` | `Promise<IPdf>` | Reads a PDF file from disk into an `IPdf` object |
|
||||||
|
| `extractTextFromPdfBuffer(buffer)` | `Promise<string>` | Extracts raw text from a PDF buffer |
|
||||||
|
| `convertPDFToPngBytes(buffer, opts?)` | `Promise<Uint8Array[]>` | Converts each PDF page to a PNG buffer |
|
||||||
|
| `convertPDFToWebpBytes(buffer, opts?)` | `Promise<Uint8Array[]>` | Converts each PDF page to a WebP buffer |
|
||||||
|
| `convertPDFToJpegBytes(buffer, opts?)` | `Promise<Uint8Array[]>` | Converts each PDF page to a progressive JPEG buffer |
|
||||||
|
|
||||||
|
#### Image Conversion Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
scale?: number; // DPI multiplier (default: 3.0)
|
||||||
|
quality?: number; // 0–100, WebP/JPEG only (default: 85)
|
||||||
|
maxWidth?: number; // Max output width in pixels
|
||||||
|
maxHeight?: number; // Max output height in pixels
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ISmartPdfOptions` Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
port?: number; // Use a specific port
|
||||||
|
portRangeStart?: number; // Auto-allocation range start (default: 20000)
|
||||||
|
portRangeEnd?: number; // Auto-allocation range end (default: 30000)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
97
test/test.port.ts
Normal file
97
test/test.port.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as smartpdf from '../ts/index.js';
|
||||||
|
|
||||||
|
tap.test('should create multiple SmartPdf instances with automatic port allocation', async () => {
|
||||||
|
const instance1 = new smartpdf.SmartPdf();
|
||||||
|
const instance2 = new smartpdf.SmartPdf();
|
||||||
|
const instance3 = new smartpdf.SmartPdf();
|
||||||
|
|
||||||
|
// Start all instances
|
||||||
|
await instance1.start();
|
||||||
|
await instance2.start();
|
||||||
|
await instance3.start();
|
||||||
|
|
||||||
|
// Verify all instances have different ports
|
||||||
|
expect(instance1.serverPort).toBeGreaterThanOrEqual(20000);
|
||||||
|
expect(instance1.serverPort).toBeLessThanOrEqual(30000);
|
||||||
|
expect(instance2.serverPort).toBeGreaterThanOrEqual(20000);
|
||||||
|
expect(instance2.serverPort).toBeLessThanOrEqual(30000);
|
||||||
|
expect(instance3.serverPort).toBeGreaterThanOrEqual(20000);
|
||||||
|
expect(instance3.serverPort).toBeLessThanOrEqual(30000);
|
||||||
|
|
||||||
|
// Ensure all ports are different
|
||||||
|
expect(instance1.serverPort).not.toEqual(instance2.serverPort);
|
||||||
|
expect(instance1.serverPort).not.toEqual(instance3.serverPort);
|
||||||
|
expect(instance2.serverPort).not.toEqual(instance3.serverPort);
|
||||||
|
|
||||||
|
console.log(`Instance 1 port: ${instance1.serverPort}`);
|
||||||
|
console.log(`Instance 2 port: ${instance2.serverPort}`);
|
||||||
|
console.log(`Instance 3 port: ${instance3.serverPort}`);
|
||||||
|
|
||||||
|
// Test that all instances work correctly
|
||||||
|
const pdf1 = await instance1.getA4PdfResultForHtmlString('<h1>Instance 1</h1>');
|
||||||
|
const pdf2 = await instance2.getA4PdfResultForHtmlString('<h1>Instance 2</h1>');
|
||||||
|
const pdf3 = await instance3.getA4PdfResultForHtmlString('<h1>Instance 3</h1>');
|
||||||
|
|
||||||
|
expect(pdf1.buffer).toBeInstanceOf(Buffer);
|
||||||
|
expect(pdf2.buffer).toBeInstanceOf(Buffer);
|
||||||
|
expect(pdf3.buffer).toBeInstanceOf(Buffer);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await instance1.stop();
|
||||||
|
await instance2.stop();
|
||||||
|
await instance3.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create SmartPdf instance with custom port range', async () => {
|
||||||
|
const customInstance = new smartpdf.SmartPdf({
|
||||||
|
portRangeStart: 25000,
|
||||||
|
portRangeEnd: 26000
|
||||||
|
});
|
||||||
|
|
||||||
|
await customInstance.start();
|
||||||
|
|
||||||
|
expect(customInstance.serverPort).toBeGreaterThanOrEqual(25000);
|
||||||
|
expect(customInstance.serverPort).toBeLessThanOrEqual(26000);
|
||||||
|
|
||||||
|
console.log(`Custom range instance port: ${customInstance.serverPort}`);
|
||||||
|
|
||||||
|
await customInstance.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create SmartPdf instance with specific port', async () => {
|
||||||
|
const specificPortInstance = new smartpdf.SmartPdf({
|
||||||
|
port: 28888
|
||||||
|
});
|
||||||
|
|
||||||
|
await specificPortInstance.start();
|
||||||
|
|
||||||
|
expect(specificPortInstance.serverPort).toEqual(28888);
|
||||||
|
|
||||||
|
console.log(`Specific port instance: ${specificPortInstance.serverPort}`);
|
||||||
|
|
||||||
|
await specificPortInstance.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should throw error when specific port is already in use', async () => {
|
||||||
|
const instance1 = new smartpdf.SmartPdf({ port: 29999 });
|
||||||
|
await instance1.start();
|
||||||
|
|
||||||
|
const instance2 = new smartpdf.SmartPdf({ port: 29999 });
|
||||||
|
|
||||||
|
let errorThrown = false;
|
||||||
|
try {
|
||||||
|
await instance2.start();
|
||||||
|
} catch (error) {
|
||||||
|
errorThrown = true;
|
||||||
|
expect(error.message).toInclude('already in use');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(errorThrown).toBeTrue();
|
||||||
|
|
||||||
|
await instance1.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
290
test/test.ts
290
test/test.ts
@@ -1,36 +1,294 @@
|
|||||||
import { expect, tap } from '@pushrocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as smartpdf from '../ts/index';
|
import * as smartpdf from '../ts/index.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
let testSmartPdf: smartpdf.SmartPdf;
|
let testSmartPdf: smartpdf.SmartPdf;
|
||||||
|
|
||||||
tap.test('should create a valid instance of smartpdf', async () => {
|
/**
|
||||||
|
* Ensures that a directory exists.
|
||||||
|
* @param dirPath - The directory path to ensure.
|
||||||
|
*/
|
||||||
|
function ensureDir(dirPath: string): void {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean test results directory at start
|
||||||
|
const testResultsDir = path.join('.nogit', 'testresults');
|
||||||
|
if (fs.existsSync(testResultsDir)) {
|
||||||
|
fs.rmSync(testResultsDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
ensureDir(testResultsDir);
|
||||||
|
|
||||||
|
tap.test('should create a valid instance of SmartPdf', async () => {
|
||||||
testSmartPdf = new smartpdf.SmartPdf();
|
testSmartPdf = new smartpdf.SmartPdf();
|
||||||
expect(testSmartPdf).to.be.instanceof(smartpdf.SmartPdf);
|
expect(testSmartPdf).toBeInstanceOf(smartpdf.SmartPdf);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should start the instance', async () => {
|
tap.test('should start the SmartPdf instance', async () => {
|
||||||
await testSmartPdf.start();
|
await testSmartPdf.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a pdf from html string', async () => {
|
tap.test('should create PDFs from HTML string', async () => {
|
||||||
await testSmartPdf.getPdfForHtmlString('hi');
|
const pdf1 = await testSmartPdf.getA4PdfResultForHtmlString('hi');
|
||||||
|
const pdf2 = await testSmartPdf.getA4PdfResultForHtmlString('hello');
|
||||||
|
expect(pdf1.buffer).toBeInstanceOf(Buffer);
|
||||||
|
expect(pdf2.buffer).toBeInstanceOf(Buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a pdf from website as A4', async () => {
|
tap.test('should create PDFs from websites', async () => {
|
||||||
await testSmartPdf.getPdfForWebsite('https://maintainedby.lossless.com');
|
const pdfA4 = await testSmartPdf.getPdfResultForWebsite('https://example.com');
|
||||||
|
const pdfSingle = await testSmartPdf.getFullWebsiteAsSinglePdf('https://example.com');
|
||||||
|
expect(pdfA4.buffer).toBeInstanceOf(Buffer);
|
||||||
|
expect(pdfSingle.buffer).toBeInstanceOf(Buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a pdf from website as single page PDF', async () => {
|
tap.test('should create valid PDF results and write them to disk', async () => {
|
||||||
await testSmartPdf.getFullWebsiteAsSinglePdf('https://maintainedby.lossless.com');
|
const writePdfToDisk = async (urlArg: string, fileName: string) => {
|
||||||
|
const pdfResult = await testSmartPdf.getFullWebsiteAsSinglePdf(urlArg);
|
||||||
|
expect(pdfResult.buffer).toBeInstanceOf(Buffer);
|
||||||
|
ensureDir('.nogit');
|
||||||
|
fs.writeFileSync(path.join('.nogit', fileName), pdfResult.buffer as Buffer);
|
||||||
|
};
|
||||||
|
await writePdfToDisk('https://lossless.com/', '1.pdf');
|
||||||
|
await writePdfToDisk('https://layer.io', '2.pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a valid PDFResult', async () => {
|
tap.test('should merge PDFs into a combined PDF', async () => {
|
||||||
const pdfResult = await testSmartPdf.getFullWebsiteAsSinglePdf('https://maintainedby.lossless.com');
|
const pdf1 = await testSmartPdf.readFileToPdfObject('.nogit/1.pdf');
|
||||||
expect(pdfResult.buffer).to.be.instanceOf(Buffer);
|
const pdf2 = await testSmartPdf.readFileToPdfObject('.nogit/2.pdf');
|
||||||
|
const mergedBuffer = await testSmartPdf.mergePdfs([pdf1.buffer, pdf2.buffer]);
|
||||||
|
ensureDir('.nogit');
|
||||||
|
fs.writeFileSync(path.join('.nogit', 'combined.pdf'), mergedBuffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should be able to close properly', async () => {
|
tap.test('should create PNG images from combined PDF using Puppeteer conversion', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/combined.pdf');
|
||||||
|
const images = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
|
||||||
|
expect(images.length).toBeGreaterThan(0);
|
||||||
|
console.log('Puppeteer-based conversion image sizes:', images.map(img => img.length));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should store PNG results from both conversion functions in .nogit/testresults', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/combined.pdf');
|
||||||
|
|
||||||
|
// Convert using Puppeteer-based function and store images
|
||||||
|
const imagesPuppeteer = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
|
||||||
|
imagesPuppeteer.forEach((img, index) => {
|
||||||
|
const filePath = path.join(testResultsDir, `png_combined_page${index + 1}.png`);
|
||||||
|
fs.writeFileSync(filePath, Buffer.from(img));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create a third PDF for image conversion tests', async () => {
|
||||||
|
const pdfResult = await testSmartPdf.getFullWebsiteAsSinglePdf('https://example.com');
|
||||||
|
expect(pdfResult.buffer).toBeInstanceOf(Buffer);
|
||||||
|
ensureDir('.nogit');
|
||||||
|
fs.writeFileSync(path.join('.nogit', '3.pdf'), pdfResult.buffer as Buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create WebP preview images from PDF', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
const webpPreviews = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer);
|
||||||
|
expect(webpPreviews.length).toBeGreaterThan(0);
|
||||||
|
console.log('WebP preview sizes:', webpPreviews.map(img => img.length));
|
||||||
|
|
||||||
|
// Also create PNG previews for comparison
|
||||||
|
const pngPreviews = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
|
||||||
|
console.log('PNG preview sizes:', pngPreviews.map(img => img.length));
|
||||||
|
|
||||||
|
// Save the first page as both WebP and PNG preview
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, 'webp_default_page1.webp'), Buffer.from(webpPreviews[0]));
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, 'png_default_page1.png'), Buffer.from(pngPreviews[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create WebP previews with custom scale and quality', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Create smaller previews with lower quality for thumbnails
|
||||||
|
const thumbnails = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||||
|
scale: 0.5, // Create readable thumbnails at ~36 DPI
|
||||||
|
quality: 70
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(thumbnails.length).toBeGreaterThan(0);
|
||||||
|
console.log('Thumbnail sizes:', thumbnails.map(img => img.length));
|
||||||
|
|
||||||
|
// Save thumbnails
|
||||||
|
thumbnails.forEach((thumb, index) => {
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, `webp_thumbnail_page${index + 1}.webp`), Buffer.from(thumb));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create WebP previews with max dimensions', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Create previews with maximum dimensions (will use high scale but constrain to max size)
|
||||||
|
const constrainedPreviews = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||||
|
scale: smartpdf.SmartPdf.SCALE_HIGH, // Start with high quality
|
||||||
|
quality: 90,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(constrainedPreviews.length).toBeGreaterThan(0);
|
||||||
|
console.log('Constrained preview sizes:', constrainedPreviews.map(img => img.length));
|
||||||
|
|
||||||
|
// Save constrained preview
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, 'webp_constrained_page1.webp'), Buffer.from(constrainedPreviews[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should verify WebP files are smaller than PNG', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Generate both PNG and WebP versions at the same scale for fair comparison
|
||||||
|
const comparisonScale = smartpdf.SmartPdf.SCALE_HIGH; // Both use 3.0 scale
|
||||||
|
|
||||||
|
const pngImages = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer, {
|
||||||
|
scale: comparisonScale
|
||||||
|
});
|
||||||
|
const webpImages = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||||
|
scale: comparisonScale,
|
||||||
|
quality: 85
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pngImages.length).toEqual(webpImages.length);
|
||||||
|
|
||||||
|
// Compare sizes
|
||||||
|
let totalPngSize = 0;
|
||||||
|
let totalWebpSize = 0;
|
||||||
|
|
||||||
|
pngImages.forEach((png, index) => {
|
||||||
|
const pngSize = png.length;
|
||||||
|
const webpSize = webpImages[index].length;
|
||||||
|
totalPngSize += pngSize;
|
||||||
|
totalWebpSize += webpSize;
|
||||||
|
|
||||||
|
const reduction = ((pngSize - webpSize) / pngSize * 100).toFixed(1);
|
||||||
|
console.log(`Page ${index + 1}: PNG=${pngSize} bytes, WebP=${webpSize} bytes, Reduction=${reduction}%`);
|
||||||
|
|
||||||
|
// Save comparison files
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, `comparison_png_page${index + 1}.png`), Buffer.from(png));
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, `comparison_webp_page${index + 1}.webp`), Buffer.from(webpImages[index]));
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalReduction = ((totalPngSize - totalWebpSize) / totalPngSize * 100).toFixed(1);
|
||||||
|
console.log(`Total size reduction: ${totalReduction}% (PNG: ${totalPngSize} bytes, WebP: ${totalWebpSize} bytes)`);
|
||||||
|
|
||||||
|
// WebP should be smaller
|
||||||
|
expect(totalWebpSize).toBeLessThan(totalPngSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create JPEG images from PDF', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
const jpegImages = await testSmartPdf.convertPDFToJpegBytes(pdfObject.buffer);
|
||||||
|
expect(jpegImages.length).toBeGreaterThan(0);
|
||||||
|
console.log('JPEG image sizes:', jpegImages.map(img => img.length));
|
||||||
|
|
||||||
|
// Save the first page as JPEG
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, 'jpeg_default_page1.jpg'), Buffer.from(jpegImages[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create JPEG images with different quality levels', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Test different quality levels
|
||||||
|
const qualityLevels = [50, 70, 85, 95];
|
||||||
|
|
||||||
|
for (const quality of qualityLevels) {
|
||||||
|
const jpegImages = await testSmartPdf.convertPDFToJpegBytes(pdfObject.buffer, {
|
||||||
|
scale: smartpdf.SmartPdf.SCALE_HIGH,
|
||||||
|
quality: quality
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`JPEG quality ${quality}: ${jpegImages[0].length} bytes`);
|
||||||
|
|
||||||
|
// Save first page at each quality level
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(testResultsDir, `jpeg_quality_${quality}_page1.jpg`),
|
||||||
|
Buffer.from(jpegImages[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should create JPEG images with max dimensions', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Create constrained JPEG images
|
||||||
|
const constrainedJpegs = await testSmartPdf.convertPDFToJpegBytes(pdfObject.buffer, {
|
||||||
|
scale: smartpdf.SmartPdf.SCALE_HIGH,
|
||||||
|
quality: 85,
|
||||||
|
maxWidth: 1200,
|
||||||
|
maxHeight: 1200
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(constrainedJpegs.length).toBeGreaterThan(0);
|
||||||
|
console.log('Constrained JPEG sizes:', constrainedJpegs.map(img => img.length));
|
||||||
|
|
||||||
|
// Save constrained JPEG
|
||||||
|
fs.writeFileSync(path.join(testResultsDir, 'jpeg_constrained_page1.jpg'), Buffer.from(constrainedJpegs[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should compare file sizes between PNG, WebP, and JPEG', async () => {
|
||||||
|
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||||
|
|
||||||
|
// Generate all three formats at the same scale
|
||||||
|
const comparisonScale = smartpdf.SmartPdf.SCALE_HIGH; // 3.0 scale
|
||||||
|
|
||||||
|
const pngImages = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer, {
|
||||||
|
scale: comparisonScale
|
||||||
|
});
|
||||||
|
const webpImages = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||||
|
scale: comparisonScale,
|
||||||
|
quality: 85
|
||||||
|
});
|
||||||
|
const jpegImages = await testSmartPdf.convertPDFToJpegBytes(pdfObject.buffer, {
|
||||||
|
scale: comparisonScale,
|
||||||
|
quality: 85
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pngImages.length).toEqual(webpImages.length);
|
||||||
|
expect(pngImages.length).toEqual(jpegImages.length);
|
||||||
|
|
||||||
|
// Compare sizes
|
||||||
|
let totalPngSize = 0;
|
||||||
|
let totalWebpSize = 0;
|
||||||
|
let totalJpegSize = 0;
|
||||||
|
|
||||||
|
pngImages.forEach((png, index) => {
|
||||||
|
const pngSize = png.length;
|
||||||
|
const webpSize = webpImages[index].length;
|
||||||
|
const jpegSize = jpegImages[index].length;
|
||||||
|
|
||||||
|
totalPngSize += pngSize;
|
||||||
|
totalWebpSize += webpSize;
|
||||||
|
totalJpegSize += jpegSize;
|
||||||
|
|
||||||
|
const webpReduction = ((pngSize - webpSize) / pngSize * 100).toFixed(1);
|
||||||
|
const jpegReduction = ((pngSize - jpegSize) / pngSize * 100).toFixed(1);
|
||||||
|
|
||||||
|
console.log(`Page ${index + 1}:`);
|
||||||
|
console.log(` PNG: ${pngSize} bytes`);
|
||||||
|
console.log(` WebP: ${webpSize} bytes (${webpReduction}% smaller than PNG)`);
|
||||||
|
console.log(` JPEG: ${jpegSize} bytes (${jpegReduction}% smaller than PNG)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalWebpReduction = ((totalPngSize - totalWebpSize) / totalPngSize * 100).toFixed(1);
|
||||||
|
const totalJpegReduction = ((totalPngSize - totalJpegSize) / totalPngSize * 100).toFixed(1);
|
||||||
|
|
||||||
|
console.log('\nTotal size comparison:');
|
||||||
|
console.log(`PNG: ${totalPngSize} bytes`);
|
||||||
|
console.log(`WebP: ${totalWebpSize} bytes (${totalWebpReduction}% reduction)`);
|
||||||
|
console.log(`JPEG: ${totalJpegSize} bytes (${totalJpegReduction}% reduction)`);
|
||||||
|
|
||||||
|
// WebP should be smaller than PNG; JPEG may not be for simple graphics pages
|
||||||
|
expect(totalWebpSize).toBeLessThan(totalPngSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should close the SmartPdf instance properly', async () => {
|
||||||
await testSmartPdf.stop();
|
await testSmartPdf.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@push.rocks/smartpdf',
|
||||||
|
version: '4.2.0',
|
||||||
|
description: 'A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.'
|
||||||
|
}
|
||||||
@@ -6,4 +6,10 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// normal
|
// normal
|
||||||
export * from './smartpdf.classes.smartpdf';
|
export * from './smartpdf.classes.smartpdf.js';
|
||||||
|
|
||||||
|
// additional types
|
||||||
|
import type * as tsclassTypes from '@tsclass/tsclass';
|
||||||
|
type IPdf = tsclassTypes.business.IPdf;
|
||||||
|
|
||||||
|
export type { IPdf };
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './interface.pdfresult';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface IPdfResult {
|
|
||||||
name: string,
|
|
||||||
id: string,
|
|
||||||
buffer: Buffer;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import * as plugins from './smartpdf.plugins';
|
/*
|
||||||
|
* pdf candidate is a construct used internally for mapping html
|
||||||
|
* to pdf buffers delivered by puppeteer
|
||||||
|
*/
|
||||||
|
import * as plugins from './smartpdf.plugins.js';
|
||||||
|
|
||||||
export class PdfCandidate {
|
export class PdfCandidate {
|
||||||
pdfId = plugins.smartunique.shortId();
|
public htmlString: string;
|
||||||
doneDeferred = plugins.smartpromise.defer();
|
public pdfId = plugins.smartunique.shortId();
|
||||||
|
public doneDeferred = plugins.smartpromise.defer();
|
||||||
|
|
||||||
constructor(public htmlString) {}
|
constructor(htmlStringArg: string) {
|
||||||
|
this.htmlString = htmlStringArg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,616 @@
|
|||||||
import * as plugins from './smartpdf.plugins';
|
import * as plugins from './smartpdf.plugins.js';
|
||||||
import * as paths from './smartpdf.paths';
|
import * as paths from './smartpdf.paths.js';
|
||||||
import { Server } from 'http';
|
import { PdfCandidate } from './smartpdf.classes.pdfcandidate.js';
|
||||||
import { PdfCandidate } from './smartpdf.classes.pdfcandidate';
|
import { type IPdf } from '@tsclass/tsclass/dist_ts/business/pdf.js';
|
||||||
|
declare const document: any;
|
||||||
|
|
||||||
declare const document;
|
export interface ISmartPdfOptions {
|
||||||
|
port?: number;
|
||||||
import { IPdfResult } from './interfaces';
|
portRangeStart?: number;
|
||||||
|
portRangeEnd?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class SmartPdf {
|
export class SmartPdf {
|
||||||
htmlServerInstance: Server;
|
// STATIC SCALE CONSTANTS
|
||||||
|
public static readonly SCALE_SCREEN = 2.0; // ~144 DPI - Good for screen display
|
||||||
|
public static readonly SCALE_HIGH = 3.0; // ~216 DPI - High quality (default)
|
||||||
|
public static readonly SCALE_PRINT = 6.0; // ~432 DPI - Print quality
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate scale factor for desired DPI
|
||||||
|
* PDF.js default is 72 DPI, so scale = desiredDPI / 72
|
||||||
|
*/
|
||||||
|
public static getScaleForDPI(dpi: number): number {
|
||||||
|
return dpi / 72;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATIC
|
||||||
|
public static async create(optionsArg?: ISmartPdfOptions) {
|
||||||
|
const smartpdfInstance = new SmartPdf(optionsArg);
|
||||||
|
return smartpdfInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
private smartserveInstance: plugins.smartserve.SmartServe;
|
||||||
serverPort: number;
|
serverPort: number;
|
||||||
headlessBrowser: plugins.puppeteer.Browser;
|
headlessBrowser: plugins.smartpuppeteer.puppeteer.Browser;
|
||||||
externalBrowser: boolean = false;
|
externalBrowserBool: boolean = false;
|
||||||
private _readyDeferred: plugins.smartpromise.Deferred<void>;
|
private _readyDeferred: plugins.smartpromise.Deferred<void>;
|
||||||
private _candidates: { [key: string]: PdfCandidate } = {};
|
private _candidates: { [key: string]: PdfCandidate } = {};
|
||||||
|
private _options: ISmartPdfOptions;
|
||||||
|
private _isRunning: boolean = false;
|
||||||
|
|
||||||
constructor(headlessBrowserArg?) {
|
constructor(optionsArg?: ISmartPdfOptions) {
|
||||||
this.headlessBrowser = headlessBrowserArg
|
|
||||||
this._readyDeferred = new plugins.smartpromise.Deferred();
|
this._readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
|
this._options = {
|
||||||
|
portRangeStart: 20000,
|
||||||
|
portRangeEnd: 30000,
|
||||||
|
...optionsArg
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start(headlessBrowserArg?: plugins.smartpuppeteer.puppeteer.Browser) {
|
||||||
// setup puppeteer
|
if (this._isRunning) {
|
||||||
if (!this.headlessBrowser) {
|
throw new Error('SmartPdf is already running. Call stop() before starting again.');
|
||||||
this.headlessBrowser = await plugins.puppeteer.launch();
|
|
||||||
} else {
|
|
||||||
this.externalBrowser = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup server
|
// Reset readiness deferred for this start cycle
|
||||||
const app = plugins.express();
|
this._readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
app.get('/:pdfId', (req, res) => {
|
|
||||||
res.setHeader('PDF-ID', this._candidates[req.params.pdfId].pdfId);
|
// lets set the external browser in case one is provided
|
||||||
res.send(this._candidates[req.params.pdfId].htmlString);
|
this.headlessBrowser = headlessBrowserArg;
|
||||||
});
|
// setup puppeteer
|
||||||
this.htmlServerInstance = plugins.http.createServer(app);
|
if (this.headlessBrowser) {
|
||||||
|
this.externalBrowserBool = true;
|
||||||
|
} else {
|
||||||
|
this.headlessBrowser = await plugins.smartpuppeteer.getEnvAwareBrowserInstance({
|
||||||
|
forceNoSandbox: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an available port BEFORE creating server
|
||||||
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||||
const portAvailable = smartnetworkInstance.isLocalPortAvailable(3210);
|
|
||||||
this.htmlServerInstance.listen(3210, 'localhost');
|
if (this._options.port) {
|
||||||
this.htmlServerInstance.on('listening', () => {
|
// If a specific port is requested, check if it's available
|
||||||
this._readyDeferred.resolve();
|
const isPortAvailable = await smartnetworkInstance.isLocalPortUnused(this._options.port);
|
||||||
|
if (isPortAvailable) {
|
||||||
|
this.serverPort = this._options.port;
|
||||||
|
} else {
|
||||||
|
// Clean up browser if we created one
|
||||||
|
if (!this.externalBrowserBool && this.headlessBrowser) {
|
||||||
|
await this.headlessBrowser.close();
|
||||||
|
this.headlessBrowser = null;
|
||||||
|
}
|
||||||
|
throw new Error(`Requested port ${this._options.port} is already in use`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find a free port in the specified range
|
||||||
|
this.serverPort = await smartnetworkInstance.findFreePort(
|
||||||
|
this._options.portRangeStart,
|
||||||
|
this._options.portRangeEnd
|
||||||
|
);
|
||||||
|
if (!this.serverPort) {
|
||||||
|
// Clean up browser if we created one
|
||||||
|
if (!this.externalBrowserBool && this.headlessBrowser) {
|
||||||
|
await this.headlessBrowser.close();
|
||||||
|
this.headlessBrowser = null;
|
||||||
|
}
|
||||||
|
throw new Error(`No free ports available in range ${this._options.portRangeStart}-${this._options.portRangeEnd}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now setup server using smartserve
|
||||||
|
this.smartserveInstance = new plugins.smartserve.SmartServe({
|
||||||
|
port: this.serverPort,
|
||||||
|
hostname: 'localhost',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.smartserveInstance.setHandler(async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const pdfId = url.pathname.slice(1); // Remove leading /
|
||||||
|
const candidate = this._candidates[pdfId];
|
||||||
|
if (!candidate) {
|
||||||
|
console.log(`${url.pathname} not attached to a candidate`);
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
return new Response(candidate.htmlString, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'pdf-id': candidate.pdfId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.smartserveInstance.start();
|
||||||
|
console.log(`SmartPdf server listening on port ${this.serverPort}`);
|
||||||
|
this._isRunning = true;
|
||||||
|
this._readyDeferred.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop
|
||||||
async stop() {
|
async stop() {
|
||||||
const done = plugins.smartpromise.defer<void>();
|
if (!this._isRunning) {
|
||||||
this.htmlServerInstance.close(() => {
|
return;
|
||||||
done.resolve();
|
}
|
||||||
});
|
|
||||||
await this.headlessBrowser.close();
|
this._isRunning = false;
|
||||||
await done.promise;
|
|
||||||
|
// Close browser first to cleanly terminate keepalive connections
|
||||||
|
// before the server shuts down (prevents ECONNRESET errors)
|
||||||
|
if (!this.externalBrowserBool && this.headlessBrowser) {
|
||||||
|
await this.headlessBrowser.close();
|
||||||
|
}
|
||||||
|
this.headlessBrowser = null;
|
||||||
|
|
||||||
|
if (this.smartserveInstance) {
|
||||||
|
await this.smartserveInstance.stop();
|
||||||
|
this.smartserveInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any remaining candidates
|
||||||
|
this._candidates = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a pdf for a given html string;
|
* Returns a PDF for a given HTML string.
|
||||||
*/
|
*/
|
||||||
async getPdfForHtmlString(htmlStringArg: string): Promise<IPdfResult> {
|
async getA4PdfResultForHtmlString(htmlStringArg: string): Promise<plugins.tsclass.business.IPdf> {
|
||||||
await this._readyDeferred.promise;
|
await this._readyDeferred.promise;
|
||||||
const pdfCandidate = new PdfCandidate(htmlStringArg);
|
const pdfCandidate = new PdfCandidate(htmlStringArg);
|
||||||
this._candidates[pdfCandidate.pdfId] = pdfCandidate;
|
this._candidates[pdfCandidate.pdfId] = pdfCandidate;
|
||||||
|
let page: plugins.smartpuppeteer.puppeteer.Page;
|
||||||
|
try {
|
||||||
|
page = await this.headlessBrowser.newPage();
|
||||||
|
await page.setViewport({
|
||||||
|
width: 794,
|
||||||
|
height: 1122,
|
||||||
|
});
|
||||||
|
const response = await page.goto(`http://localhost:${this.serverPort}/${pdfCandidate.pdfId}`, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
});
|
||||||
|
const headers = response.headers();
|
||||||
|
if (headers['pdf-id'] !== pdfCandidate.pdfId) {
|
||||||
|
console.log('Error! Headers do not match. For security reasons no pdf is being emitted!');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`id security check passed for ${pdfCandidate.pdfId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBuffer = await page.pdf({
|
||||||
|
width: 794,
|
||||||
|
height: 1122,
|
||||||
|
printBackground: true,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
});
|
||||||
|
// Convert Uint8Array to Node Buffer
|
||||||
|
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||||
|
await page.close();
|
||||||
|
delete this._candidates[pdfCandidate.pdfId];
|
||||||
|
pdfCandidate.doneDeferred.resolve();
|
||||||
|
await pdfCandidate.doneDeferred.promise;
|
||||||
|
return {
|
||||||
|
id: pdfCandidate.pdfId,
|
||||||
|
name: `${pdfCandidate.pdfId}.js`,
|
||||||
|
metadata: {
|
||||||
|
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||||
|
},
|
||||||
|
buffer: nodePdfBuffer,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// Clean up candidate on error
|
||||||
|
delete this._candidates[pdfCandidate.pdfId];
|
||||||
|
if (page) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPdfResultForWebsite(websiteUrl: string): Promise<plugins.tsclass.business.IPdf> {
|
||||||
const page = await this.headlessBrowser.newPage();
|
const page = await this.headlessBrowser.newPage();
|
||||||
const response = await page.goto(`http://localhost:3210/${pdfCandidate.pdfId}`, {
|
try {
|
||||||
waitUntil: 'networkidle2'
|
await page.setViewport({
|
||||||
});
|
width: 1980,
|
||||||
const headers = response.headers();
|
height: 1200,
|
||||||
if (headers['pdf-id'] !== pdfCandidate.pdfId) {
|
});
|
||||||
console.log('Error! Headers do not match. For security reasons no pdf is being emitted!');
|
await page.emulateMediaType('screen');
|
||||||
return;
|
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
|
||||||
} else {
|
const pdfId = plugins.smartunique.shortId();
|
||||||
console.log(`id security check passed for ${pdfCandidate.pdfId}`);
|
const { documentHeight, documentWidth } = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
documentHeight: document.height,
|
||||||
|
documentWidth: document.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const pdfBuffer = await page.pdf({
|
||||||
|
height: documentHeight,
|
||||||
|
width: documentWidth,
|
||||||
|
printBackground: true,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
});
|
||||||
|
// Convert Uint8Array to Node Buffer
|
||||||
|
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||||
|
await page.close();
|
||||||
|
return {
|
||||||
|
id: pdfId,
|
||||||
|
name: `${pdfId}.js`,
|
||||||
|
metadata: {
|
||||||
|
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||||
|
},
|
||||||
|
buffer: nodePdfBuffer,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFullWebsiteAsSinglePdf(websiteUrl: string): Promise<plugins.tsclass.business.IPdf> {
|
||||||
|
const page = await this.headlessBrowser.newPage();
|
||||||
|
try {
|
||||||
|
await page.setViewport({
|
||||||
|
width: 1920,
|
||||||
|
height: 1200,
|
||||||
|
});
|
||||||
|
await page.emulateMediaType('screen');
|
||||||
|
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
|
||||||
|
const pdfId = plugins.smartunique.shortId();
|
||||||
|
// Use both document.body and document.documentElement to ensure we have a valid height and width.
|
||||||
|
const { documentHeight, documentWidth } = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
documentHeight: Math.max(
|
||||||
|
document.body.scrollHeight,
|
||||||
|
document.documentElement.scrollHeight
|
||||||
|
) || 1200,
|
||||||
|
documentWidth: Math.max(
|
||||||
|
document.body.clientWidth,
|
||||||
|
document.documentElement.clientWidth
|
||||||
|
) || 1920,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Update viewport height to the full document height.
|
||||||
|
await page.setViewport({
|
||||||
|
width: 1920,
|
||||||
|
height: documentHeight,
|
||||||
|
});
|
||||||
|
const pdfBuffer = await page.pdf({
|
||||||
|
height: documentHeight,
|
||||||
|
width: 1920,
|
||||||
|
printBackground: true,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
scale: 1,
|
||||||
|
pageRanges: '1',
|
||||||
|
});
|
||||||
|
// Convert Uint8Array to Node Buffer
|
||||||
|
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||||
|
await page.close();
|
||||||
|
return {
|
||||||
|
id: pdfId,
|
||||||
|
name: `${pdfId}.js`,
|
||||||
|
metadata: {
|
||||||
|
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||||
|
},
|
||||||
|
buffer: nodePdfBuffer,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mergePdfs(inputPdfBuffers: Uint8Array[]): Promise<Uint8Array> {
|
||||||
|
const mergedPdf = await plugins.pdfLib.PDFDocument.create();
|
||||||
|
for (const pdfBytes of inputPdfBuffers) {
|
||||||
|
const pdfDoc = await plugins.pdfLib.PDFDocument.load(pdfBytes);
|
||||||
|
const pages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
|
||||||
|
pages.forEach((page) => mergedPdf.addPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBuffer = await page.pdf({
|
const mergedPdfBytes = await mergedPdf.save();
|
||||||
format: 'A4'
|
return mergedPdfBytes;
|
||||||
});
|
}
|
||||||
await page.close();
|
|
||||||
delete this._candidates[pdfCandidate.pdfId];
|
public async readFileToPdfObject(pathArg: string): Promise<plugins.tsclass.business.IPdf> {
|
||||||
pdfCandidate.doneDeferred.resolve();
|
const absolutePath = plugins.smartpath.transform.makeAbsolute(pathArg);
|
||||||
await pdfCandidate.doneDeferred.promise;
|
const parsedPath = plugins.path.parse(absolutePath);
|
||||||
|
const smartfsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||||
|
const fileContent = await smartfsInstance.file(absolutePath).read();
|
||||||
|
const buffer = Buffer.from(fileContent);
|
||||||
return {
|
return {
|
||||||
id: pdfCandidate.pdfId,
|
name: parsedPath.base,
|
||||||
name: `${pdfCandidate.pdfId}.js`,
|
buffer,
|
||||||
buffer: pdfBuffer
|
id: null,
|
||||||
|
metadata: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPdfForWebsite(websiteUrl: string): Promise<IPdfResult> {
|
public async extractTextFromPdfBuffer(pdfBufferArg: Buffer): Promise<string> {
|
||||||
const page = await this.headlessBrowser.newPage();
|
const deferred = plugins.smartpromise.defer<string>();
|
||||||
page.emulateMedia('screen');
|
const pdfParser: any = new plugins.pdf2json();
|
||||||
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
|
pdfParser.on('pdfParser_dataReady', (pdfData: any) => {
|
||||||
const pdfId = plugins.smartunique.shortId();
|
let finalText = '';
|
||||||
const pdfBuffer = await page.pdf({
|
for (const page of pdfData.Pages) {
|
||||||
format: 'A4',
|
for (const text of page.Texts) {
|
||||||
printBackground: true,
|
for (const letter of text.R) {
|
||||||
displayHeaderFooter: false,
|
finalText = finalText + letter.T;
|
||||||
preferCSSPageSize: true
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deferred.resolve(finalText);
|
||||||
});
|
});
|
||||||
await page.close();
|
pdfParser.parseBuffer(pdfBufferArg);
|
||||||
return {
|
return deferred.promise;
|
||||||
id: pdfId,
|
|
||||||
name: `${pdfId}.js`,
|
|
||||||
buffer: pdfBuffer
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFullWebsiteAsSinglePdf(websiteUrl: string) {
|
/**
|
||||||
|
* Converts a PDF to PNG bytes for each page using Puppeteer and PDF.js.
|
||||||
|
* This method creates a temporary HTML page that loads PDF.js from a CDN,
|
||||||
|
* renders each PDF page to a canvas, and then screenshots each canvas element.
|
||||||
|
*/
|
||||||
|
public async convertPDFToPngBytes(
|
||||||
|
pdfBytes: Uint8Array,
|
||||||
|
options: {
|
||||||
|
scale?: number; // Scale factor for output size (default: 3.0 for 216 DPI)
|
||||||
|
maxWidth?: number; // Maximum width in pixels (optional)
|
||||||
|
maxHeight?: number; // Maximum height in pixels (optional)
|
||||||
|
} = {}
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
// Set default scale for higher quality output (3.0 = ~216 DPI)
|
||||||
|
const scale = options.scale || 3.0;
|
||||||
|
|
||||||
|
// Create a new page using the headless browser.
|
||||||
const page = await this.headlessBrowser.newPage();
|
const page = await this.headlessBrowser.newPage();
|
||||||
page.emulateMedia('screen');
|
|
||||||
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
|
try {
|
||||||
const pdfId = plugins.smartunique.shortId();
|
// Prepare PDF data as a base64 string.
|
||||||
const { documentHeight, documentWidth } = await page.evaluate(() => {
|
const base64Pdf: string = Buffer.from(pdfBytes).toString('base64');
|
||||||
return {
|
|
||||||
documentHeight: document.height,
|
// HTML template that loads PDF.js and renders the PDF.
|
||||||
documentWidth: document.width
|
const htmlTemplate: string = `
|
||||||
};
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>PDF to PNG Converter</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
canvas { display: block; margin: 10px auto; }
|
||||||
|
</style>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
(async function() {
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
|
||||||
|
const pdfData = "__PDF_DATA__";
|
||||||
|
const raw = atob(pdfData);
|
||||||
|
const pdfArray = new Uint8Array([...raw].map(c => c.charCodeAt(0)));
|
||||||
|
const loadingTask = pdfjsLib.getDocument({data: pdfArray});
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
const numPages = pdf.numPages;
|
||||||
|
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||||
|
const page = await pdf.getPage(pageNum);
|
||||||
|
// Apply scale factor to viewport
|
||||||
|
const viewport = page.getViewport({ scale: ${scale} });
|
||||||
|
|
||||||
|
// Apply max width/height constraints if specified
|
||||||
|
let finalScale = ${scale};
|
||||||
|
${options.maxWidth ? `
|
||||||
|
if (viewport.width > ${options.maxWidth}) {
|
||||||
|
finalScale = ${options.maxWidth} / (viewport.width / ${scale});
|
||||||
|
}` : ''}
|
||||||
|
${options.maxHeight ? `
|
||||||
|
if (viewport.height > ${options.maxHeight}) {
|
||||||
|
const heightScale = ${options.maxHeight} / (viewport.height / ${scale});
|
||||||
|
finalScale = Math.min(finalScale, heightScale);
|
||||||
|
}` : ''}
|
||||||
|
|
||||||
|
// Get final viewport with adjusted scale
|
||||||
|
const finalViewport = page.getViewport({ scale: finalScale });
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.width = finalViewport.width;
|
||||||
|
canvas.height = finalViewport.height;
|
||||||
|
canvas.setAttribute('data-page', pageNum);
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport: finalViewport }).promise;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
}
|
||||||
|
window.renderComplete = true;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Replace the placeholder with the actual base64 PDF data.
|
||||||
|
const htmlContent: string = htmlTemplate.replace("__PDF_DATA__", base64Pdf);
|
||||||
|
|
||||||
|
// Set the page content.
|
||||||
|
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
// Wait until the PDF.js rendering is complete.
|
||||||
|
await page.waitForFunction(() => (window as any).renderComplete === true, { timeout: 30000 });
|
||||||
|
|
||||||
|
// Query all canvas elements (each representing a rendered PDF page).
|
||||||
|
const canvasElements = await page.$$('canvas');
|
||||||
|
const pngBuffers: Uint8Array[] = [];
|
||||||
|
|
||||||
|
for (const canvasElement of canvasElements) {
|
||||||
|
// Screenshot the canvas element. The screenshot will be a PNG buffer.
|
||||||
|
const screenshotBuffer = (await canvasElement.screenshot({ encoding: 'binary' })) as Buffer;
|
||||||
|
pngBuffers.push(new Uint8Array(screenshotBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.close();
|
||||||
|
return pngBuffers;
|
||||||
|
} catch (err) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PDF to WebP bytes for each page.
|
||||||
|
* This method creates web-optimized images using WebP format.
|
||||||
|
* WebP provides 25-35% better compression than JPEG/PNG while maintaining quality.
|
||||||
|
*/
|
||||||
|
public async convertPDFToWebpBytes(
|
||||||
|
pdfBytes: Uint8Array,
|
||||||
|
options: {
|
||||||
|
scale?: number; // Scale factor for preview size (default: 3.0 for 216 DPI)
|
||||||
|
quality?: number; // WebP quality 0-100 (default: 85)
|
||||||
|
maxWidth?: number; // Maximum width in pixels (optional)
|
||||||
|
maxHeight?: number; // Maximum height in pixels (optional)
|
||||||
|
} = {}
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
// Set default options for higher quality output (3.0 = ~216 DPI)
|
||||||
|
const scale = options.scale || 3.0;
|
||||||
|
const quality = options.quality || 85;
|
||||||
|
|
||||||
|
// Create a new page using the headless browser
|
||||||
|
const page = await this.headlessBrowser.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare PDF data as a base64 string
|
||||||
|
const base64Pdf: string = Buffer.from(pdfBytes).toString('base64');
|
||||||
|
|
||||||
|
// HTML template that loads PDF.js and renders the PDF with scaling
|
||||||
|
const htmlTemplate: string = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>PDF to WebP Preview Converter</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
canvas { display: block; margin: 10px auto; }
|
||||||
|
</style>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
(async function() {
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
|
||||||
|
const pdfData = "__PDF_DATA__";
|
||||||
|
const raw = atob(pdfData);
|
||||||
|
const pdfArray = new Uint8Array([...raw].map(c => c.charCodeAt(0)));
|
||||||
|
const loadingTask = pdfjsLib.getDocument({data: pdfArray});
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
const numPages = pdf.numPages;
|
||||||
|
|
||||||
|
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||||
|
const page = await pdf.getPage(pageNum);
|
||||||
|
// Apply scale factor to viewport
|
||||||
|
const viewport = page.getViewport({ scale: ${scale} });
|
||||||
|
|
||||||
|
// Apply max width/height constraints if specified
|
||||||
|
let finalScale = ${scale};
|
||||||
|
${options.maxWidth ? `
|
||||||
|
if (viewport.width > ${options.maxWidth}) {
|
||||||
|
finalScale = ${options.maxWidth} / (viewport.width / ${scale});
|
||||||
|
}` : ''}
|
||||||
|
${options.maxHeight ? `
|
||||||
|
if (viewport.height > ${options.maxHeight}) {
|
||||||
|
const heightScale = ${options.maxHeight} / (viewport.height / ${scale});
|
||||||
|
finalScale = Math.min(finalScale, heightScale);
|
||||||
|
}` : ''}
|
||||||
|
|
||||||
|
// Get final viewport with adjusted scale
|
||||||
|
const finalViewport = page.getViewport({ scale: finalScale });
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.width = finalViewport.width;
|
||||||
|
canvas.height = finalViewport.height;
|
||||||
|
canvas.setAttribute('data-page', pageNum);
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport: finalViewport }).promise;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
}
|
||||||
|
window.renderComplete = true;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Replace the placeholder with the actual base64 PDF data
|
||||||
|
const htmlContent: string = htmlTemplate.replace("__PDF_DATA__", base64Pdf);
|
||||||
|
|
||||||
|
// Set the page content
|
||||||
|
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
// Wait until the PDF.js rendering is complete
|
||||||
|
await page.waitForFunction(() => (window as any).renderComplete === true, { timeout: 30000 });
|
||||||
|
|
||||||
|
// Query all canvas elements (each representing a rendered PDF page)
|
||||||
|
const canvasElements = await page.$$('canvas');
|
||||||
|
const webpBuffers: Uint8Array[] = [];
|
||||||
|
|
||||||
|
for (const canvasElement of canvasElements) {
|
||||||
|
// Screenshot the canvas element as WebP
|
||||||
|
const screenshotBuffer = (await canvasElement.screenshot({
|
||||||
|
type: 'webp',
|
||||||
|
quality: quality,
|
||||||
|
encoding: 'binary'
|
||||||
|
})) as Buffer;
|
||||||
|
webpBuffers.push(new Uint8Array(screenshotBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.close();
|
||||||
|
return webpBuffers;
|
||||||
|
} catch (err) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PDF to progressive JPEG bytes for each page.
|
||||||
|
* This method creates progressive JPEG images that load in multiple passes,
|
||||||
|
* showing a low-quality preview first, then progressively improving.
|
||||||
|
* Uses SmartJimp for true progressive JPEG encoding.
|
||||||
|
*/
|
||||||
|
public async convertPDFToJpegBytes(
|
||||||
|
pdfBytes: Uint8Array,
|
||||||
|
options: {
|
||||||
|
scale?: number; // Scale factor for output size (default: 3.0 for 216 DPI)
|
||||||
|
quality?: number; // JPEG quality 0-100 (default: 85)
|
||||||
|
maxWidth?: number; // Maximum width in pixels (optional)
|
||||||
|
maxHeight?: number; // Maximum height in pixels (optional)
|
||||||
|
} = {}
|
||||||
|
): Promise<Uint8Array[]> {
|
||||||
|
// First, convert PDF to PNG using our existing method
|
||||||
|
const pngBuffers = await this.convertPDFToPngBytes(pdfBytes, {
|
||||||
|
scale: options.scale,
|
||||||
|
maxWidth: options.maxWidth,
|
||||||
|
maxHeight: options.maxHeight
|
||||||
});
|
});
|
||||||
const pdfBuffer = await page.pdf({
|
|
||||||
height: documentWidth,
|
// Initialize SmartJimp in sharp mode for progressive JPEG support
|
||||||
width: documentWidth,
|
const smartJimpInstance = new plugins.smartjimp.SmartJimp({ mode: 'sharp' });
|
||||||
printBackground: true,
|
|
||||||
displayHeaderFooter: false,
|
// Convert each PNG to progressive JPEG
|
||||||
preferCSSPageSize: true
|
const jpegBuffers: Uint8Array[] = [];
|
||||||
});
|
const quality = options.quality || 85;
|
||||||
await page.close();
|
|
||||||
return {
|
for (const pngBuffer of pngBuffers) {
|
||||||
id: pdfId,
|
// Convert PNG buffer to progressive JPEG
|
||||||
name: `${pdfId}.js`,
|
const jpegBuffer = await smartJimpInstance.computeAssetVariation(
|
||||||
buffer: pdfBuffer
|
Buffer.from(pngBuffer),
|
||||||
};
|
{
|
||||||
|
format: 'jpeg',
|
||||||
|
progressive: true,
|
||||||
|
quality
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jpegBuffers.push(new Uint8Array(jpegBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
return jpegBuffers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as plugins from './smartpdf.plugins';
|
import * as plugins from './smartpdf.plugins.js';
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(__dirname, '../');
|
export const packageDir = plugins.path.join(
|
||||||
export const pdfDir = plugins.path.join(packageDir, 'assets/pdfdir');
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
|
'../'
|
||||||
plugins.smartfile.fs.ensureDirSync(pdfDir);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,40 @@
|
|||||||
// native
|
// native
|
||||||
import * as http from 'http';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export { http, path };
|
export { path };
|
||||||
|
|
||||||
// @pushrocks
|
// @pushrocks
|
||||||
import * as smartfile from '@pushrocks/smartfile';
|
import * as smartbuffer from '@push.rocks/smartbuffer';
|
||||||
import * as smartpromise from '@pushrocks/smartpromise';
|
import * as smartfs from '@push.rocks/smartfs';
|
||||||
import * as smartnetwork from '@pushrocks/smartnetwork';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartunique from '@pushrocks/smartunique';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartpuppeteer from '@push.rocks/smartpuppeteer';
|
||||||
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
|
import * as smartserve from '@push.rocks/smartserve';
|
||||||
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
import * as smartjimp from '@push.rocks/smartjimp';
|
||||||
|
|
||||||
export { smartfile, smartpromise, smartunique, smartnetwork };
|
export {
|
||||||
|
smartbuffer,
|
||||||
|
smartfs,
|
||||||
|
smartdelay,
|
||||||
|
smartpromise,
|
||||||
|
smartpath,
|
||||||
|
smartpuppeteer,
|
||||||
|
smartunique,
|
||||||
|
smartnetwork,
|
||||||
|
smartserve,
|
||||||
|
smartjimp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// tsclass scope
|
||||||
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
|
||||||
|
export { tsclass };
|
||||||
|
|
||||||
// thirdparty
|
// thirdparty
|
||||||
import express from 'express';
|
import pdf2json from 'pdf2json';
|
||||||
import puppeteer from 'puppeteer';
|
import pdfLib from 'pdf-lib';
|
||||||
|
|
||||||
export { express, puppeteer };
|
export { pdf2json, pdfLib };
|
||||||
|
|||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
tslint.json
17
tslint.json
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
|
||||||
"rules": {
|
|
||||||
"semicolon": [true, "always"],
|
|
||||||
"no-console": false,
|
|
||||||
"ordered-imports": false,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"member-ordering": {
|
|
||||||
"options":{
|
|
||||||
"order": [
|
|
||||||
"static-method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultSeverity": "warning"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user