Compare commits

...
Author SHA1 Message Date
Anthony StirlingandGitHub db907f76d8 Remove sign_verify job from multiOSReleases workflow
Removed the sign_verify job from the multiOSReleases workflow to streamline the release process.
2025-11-25 11:11:10 +00:00
albanobattistellaandGitHub c53000786a Update messages_it_IT.properties (#4944)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [x] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [x] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 10:49:02 +00:00
LudyandGitHub af2e418129 ci: simplify docker-compose test workflow conditions (#4989)
# Description of Changes

This pull request simplifies the workflow logic for the
`docker-compose-tests` job in `.github/workflows/build.yml`.

### What was changed
- Removed the extended conditional logic that depended on the results of
the `test-build-docker-images` job.
- Updated the `needs` configuration so that `docker-compose-tests`
depends solely on `files-changed`.

### Why the change was made
The previous logic attempted to optimize execution flow but introduced
unnecessary complexity and conditional branches. By simplifying the
workflow:
- Job relationships become easier to understand and maintain.
- The workflow executes more predictably.
- The dependency graph becomes clearer, reducing potential misfires
caused by mixed job states.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 10:20:07 +00:00
Balázs SzücsandGitHub 97f3b88222 feat(pdf-EPUB): add PDF to EPUB/AZW3 conversion functionality via Calibre (#4947)
# Description of Changes

This PR introduces a new conversion tool allowing users to convert PDF
documents into EPUB format. This is particularly useful for reading
documents on e-readers (like Kindles or Kobos) where standard PDFs often
suffer from fixed formatting and unreadable text sizes.

The implementation leverages the existing **Calibre** integration
(`ebook-convert`) to produce reflowable e-books with specific
optimizations for layout and chapter structure.

**Backend Implementation**
* Added `ConvertPDFToEpubController` to handle the conversion workflow.
* Created `ConvertPdfToEpubRequest` to support new conversion parameters
(Device profile and Chapter detection).
* Integrated standard Stirling-PDF temporary file management and process
execution patterns.

**Frontend & UI**
* Added a new view `pdf-to-epub.html` containing the upload form and
configuration options.
* Updated `navElements.html` and `messages.properties` to expose the
tool in the navigation menu under the "Convert" group.
* Minor cleanup of HTML formatting in the existing `ebook-to-pdf`
template for consistency.

**Configuration & Testing**
* Registered the `pdf-to-epub` endpoint in `EndpointConfiguration`,
placing it under the **Calibre** dependency group.
* Added comprehensive unit tests covering command generation, parameter
handling, and temporary file cleanup.


The conversion process utilizes specific calibre, `ebook-convert` flags
to ensure high-quality output:

* **Heuristic Processing** (`--enable-heuristics`): Automatically
detects and fixes common PDF scanning issues, such as broken lines,
hyphens at line ends, and inconsistent paragraph spacing.
* **CSS Filtering** (`--filter-css`): Strips hardcoded styling (font
families, fixed margins, colors) from the PDF. This ensures the
resulting EPUB respects the user's e-reader settings (font size, dark
mode, etc.).
* **Smart Chapter Detection** (`--chapter`): Optionally uses an XPath
expression (`//h:*[re:test(., '\\s*Chapter\\s+', 'i')]`) to detect
headers and insert proper page breaks in the EPUB structure.
* **Device Optimization Profiles**:
* **Tablet/Phone:** Uses the default profile to maintain image
resolution and color.
* **Kindle/E-Ink:** Uses a specific profile to resize images and
optimize contrast for grayscale screens.



<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [X] I have performed a self-review of my own code
- [X] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [X] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2025-11-25 10:02:50 +00:00
AngelandGitHub e68871bad3 🌐 Update messages_ru_RU.properties (#4938)
Updated Russian translation
2025-11-25 10:01:14 +00:00
LudyandGitHub dd96584bf8 docs(README): add new tool descriptions and features overview (#4870)
# Description of Changes

This pull request updates the `README.md` documentation to include
several newly supported PDF operations, making the feature list more
comprehensive and informative for users. The most important changes are
grouped by the type of PDF operation added.

New multi-function and conversion features:

* Added **PDF Multi Tool** to the Organise section, allowing users to
access merge, rotate, rearrange, split, and delete actions from a single
dashboard.
* Added **URL/Website to PDF** to the Convert to PDF section, enabling
users to capture live webpages as PDFs.
* Added **PDF to Video Slideshow** to the Export section, allowing users
to export PDF pages as an automated video presentation.

New extraction and analysis features:

* Added **Extract Attachments** to the Edit & Extract section, making it
possible to retrieve embedded attachments from PDFs.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 10:00:46 +00:00
LudyandGitHub a266187d68 deps(build): centralize Logback version management and update to 1.5.21 (#4868)
# Description of Changes

- Introduced a new Gradle property `logback` in the `ext` block for
centralized version management.
- Updated Logback dependencies (`logback-core`, `logback-classic`) from
version `1.5.20` to `1.5.21`.
- Replaced hardcoded versions in subprojects with dynamic references to
the new `logback` variable.

This improves maintainability and ensures consistent versioning across
all modules.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 09:59:15 +00:00
LudyandGitHub bb4d313b55 refactor(common): remove unused temp directory & HTML unzip helpers, prune imports (#4857)
# Description of Changes

This pull request primarily removes several unused or redundant utility
methods related to temporary directory and file management across the
codebase. The changes help simplify the code and reduce maintenance
overhead by eliminating code that is no longer needed.

**Cleanup of temporary file and directory utilities:**

* Removed the `createTempDirectory` method from
`CustomPDFDocumentFactory`, which created uniquely named temporary
directories.
* Removed the `getTempDirectory` method from `GeneralUtils`, which
handled custom and default temporary directory configuration.

**Codebase simplification in file utilities:**

* Deleted the `deleteDirectory` and `unzipAndGetMainHtml` methods from
`FileToPdf`, which were used for recursively deleting directories and
extracting the main HTML file from a ZIP archive, respectively.
* Cleaned up unused imports in `FileToPdf` that were only needed for the
removed methods.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 09:54:31 +00:00
LudyandGitHub c760d1a93a fix(frontend/pdfjs): ensure CID character rendering via CMaps & stabilize PDF compare/preview (#4762)
# Description of Changes

## What was changed
- Introduced a shared `PDFJS_DEFAULT_OPTIONS` object and applied it
across frontend modules using PDF.js:
- Sets `cMapUrl`, `cMapPacked`, and `standardFontDataUrl` so PDF.js can
correctly load CMaps and standard fonts.
- Switches all `GlobalWorkerOptions.workerSrc` usages to the dynamic
`pdfjsPath + 'pdf.worker.mjs'`.
- Exposed `pdfjsPath` globally in `navbar.html` to support deployments
under subpaths/reverse proxies.
- Updated multiple pages and utilities to use the new defaults:
- `DecryptFiles.js`, `downloader.js`, `merge.js`, Multi-Tool
(`PdfContainer.js`), and feature pages (`add-image.js`,
`adjust-contrast.js`, `change-metadata.js`, `crop.js`, `pdf-to-csv.js`,
`sign.js`, `rotate-pdf.html`, `convert/pdf-to-pdfa.html`,
`merge-pdfs.html`).
- Comparison tool hardening:
- Added robust worker protocol (`type: 'COMPARE' | 'SET_*'`) and safer
logs.
- Improved text tokenization, adaptive batch diffing with overlap
de-duplication, and color fallbacks.
- Early validation for empty/oversized/invalid PDFs with clearer user
messages.
- Disabled PDF.js worker in specific templates where legacy CMap
handling caused issues (`disableWorker: true`) to prevent rendering
failures.
- UI/UX tweaks: processing state on the compare button, progress hints
during text extraction, and more resilient error handling.
- Fixed relative path to popularity data (`./files/popularity.txt`) to
respect base paths.

## Why the change was made
- PDFs using CID fonts (e.g., CJK and other complex scripts) were
rendering with missing glyphs or falling back incorrectly because CMaps
and standard font data were not being provided to PDF.js. Providing
proper CMap and font resources resolves CID character visibility issues
and related console warnings.
- Some environments (subpath deployments, reverse proxies) broke PDF.js
worker/static asset resolution; centralizing `pdfjsPath` and using it
consistently fixes this.
- The comparison feature struggled with large/complex documents and
lacked robust validation; improvements reduce timeouts, improve
accuracy, and provide clearer feedback.

Closes #4391

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 09:52:42 +00:00
36 changed files with 1645 additions and 514 deletions
+2 -8
View File
@@ -204,14 +204,8 @@ jobs:
retention-days: 3
docker-compose-tests:
if: |
needs.files-changed.outputs.project == 'true' &&
(
needs.files-changed.outputs.docker != 'true' ||
needs.test-build-docker-images.result == 'success' ||
needs.test-build-docker-images.result == 'skipped'
)
needs: [files-changed, test-build-docker-images]
if: needs.files-changed.outputs.project == 'true'
needs: files-changed
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
# (github.event_name == 'pull_request' &&
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
+1 -69
View File
@@ -224,78 +224,10 @@ jobs:
path: |
./binaries/*
sign_verify:
needs: [read_versions, build-installers]
strategy:
matrix:
include:
- os: windows-latest
platform: win-
- os: macos-latest
platform: mac-
# - os: ubuntu-latest
# platform: linux-
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: ${{ matrix.platform }}binaries
- name: Display structure of downloaded files
run: ls -R
- name: Install Cosign
if: matrix.os == 'windows-latest'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Generate key pair
if: matrix.os == 'windows-latest'
run: cosign generate-key-pair
- name: Sign and generate attestations
if: matrix.os == 'windows-latest'
run: |
cosign sign-blob \
--key ./cosign.key \
--yes \
--output-signature ./Stirling-PDF-win-installer.exe.sig \
./Stirling-PDF-win-installer.exe
cosign attest-blob \
--predicate - \
--key ./cosign.key \
--yes \
--output-attestation ./Stirling-PDF-win-installer.exe.intoto.jsonl \
./Stirling-PDF-win-installer.exe
cosign verify-blob \
--key ./cosign.pub \
--signature ./Stirling-PDF-win-installer.exe.sig \
./Stirling-PDF-win-installer.exe
- name: Display structure of downloaded files
run: ls -R
- name: Upload signed artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
retention-days: 1
if-no-files-found: error
name: ${{ matrix.platform }}signed
path: |
./Stirling-PDF-${{ matrix.platform }}installer.*
./Stirling-PDF-${{ matrix.platform }}x86_64-installer.*
!cosign.*
create-release:
if: github.event_name != 'workflow_dispatch' || github.event.inputs.test_mode != 'true'
needs: [read_versions, sign_verify, sign_verify-portable]
needs: [read_versions, sign_verify-portable]
runs-on: ubuntu-latest
permissions:
contents: write
+4
View File
@@ -33,6 +33,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
### 50+ PDF Operations
#### Organise
- **PDF Multi Tool**: Access merge, rotate, rearrange, split and delete actions from a single dashboard
- **Merge**: Combine multiple PDFs into one
- **Split**: Divide PDFs into multiple files
- **Extract page(s)**: Extract specific pages from PDF
@@ -47,6 +48,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
#### Convert to PDF
- **Image to PDF**: Convert images to PDF format
- **Convert file to PDF**: Convert various common file types to PDF
- **URL/Website to PDF**: Capture live webpages as PDFs
- **HTML to PDF**: Transform HTML documents to PDF
- **Markdown to PDF**: Convert Markdown files to PDF
- **CBZ to PDF**: Convert comic book archives
@@ -68,6 +70,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- **PDF to CBZ**: Convert to comic book archive
- **PDF to CBR**: Convert to comic book rar archive
- **PDF to Vector Image**: Convert PDF to vector image (EPS, PS, PCL, XPS) format
- **PDF to Video Slideshow**: Export pages as an automated video presentation
#### Sign & Security
- **Sign**: Add digital signatures
@@ -87,6 +90,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- **OCR / Cleanup scans**: Optical Character Recognition
- **Add Image**: Insert images into PDF
- **Extract Images**: Extract embedded images
- **Extract Attachments**: Retrieve embedded attachments
- **Change Metadata**: Edit PDF metadata
- **Get ALL Info on PDF**: Comprehensive PDF analysis
- **Advanced Colour options**: Colour manipulation (various options for colour inversion, CMYK conversion)
@@ -478,11 +478,6 @@ public class CustomPDFDocumentFactory {
return file;
}
/** Create a uniquely named temporary directory */
private Path createTempDirectory(String prefix) throws IOException {
return Files.createTempDirectory(prefix + tempCounter.incrementAndGet() + "-");
}
/** Create new document bytes based on an existing document */
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
try (PDDocument document = load(oldDocument)) {
@@ -5,11 +5,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -148,64 +145,6 @@ public class FileToPdf {
}
}
private static void deleteDirectory(Path dir) throws IOException {
Files.walkFileTree(
dir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
Path tempDirectory = Files.createTempDirectory("unzipped_");
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure
} else {
Files.createDirectories(
filePath.getParent()); // Create parent directories if they don't exist
Files.copy(zipIn, filePath);
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
}
// Search for the main HTML file.
try (Stream<Path> walk = Files.walk(tempDirectory)) {
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
if (htmlFiles.isEmpty()) {
throw new IOException("No HTML files found in the unzipped directory.");
}
// Prioritize 'index.html' if it exists, otherwise use the first .html file
for (Path htmlFile : htmlFiles) {
if ("index.html".equals(htmlFile.getFileName().toString())) {
return htmlFile;
}
}
return htmlFiles.get(0);
}
}
static String sanitizeZipFilename(String entryName) {
if (entryName == null || entryName.trim().isEmpty()) {
return "";
@@ -94,32 +94,6 @@ public class GeneralUtils {
return tempFile;
}
/*
* Gets the configured temporary directory, creating it if necessary.
*
* @return Path to the temporary directory
* @throws IOException if directory creation fails
*/
private Path getTempDirectory() throws IOException {
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
if (customTempDir == null || customTempDir.isEmpty()) {
customTempDir = System.getProperty("stirling.tempfiles.directory");
}
Path tempDir;
if (customTempDir != null && !customTempDir.isEmpty()) {
tempDir = Path.of(customTempDir);
} else {
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
}
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
return tempDir;
}
/*
* Remove file extension
*
@@ -258,6 +258,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Convert", "ebook-to-pdf");
addEndpointToGroup("Convert", "pdf-to-epub");
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
@@ -449,6 +450,7 @@ public class EndpointConfiguration {
// Calibre dependent endpoints
addEndpointToGroup("Calibre", "ebook-to-pdf");
addEndpointToGroup("Calibre", "pdf-to-epub");
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
@@ -0,0 +1,204 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.OutputFormat;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
@RequiredArgsConstructor
@Slf4j
public class ConvertPDFToEpubController {
private static final String CALIBRE_GROUP = "Calibre";
private static final String DEFAULT_EXTENSION = "pdf";
private static final String FILTERED_CSS =
"font-family,color,background-color,margin-left,margin-right";
private static final String SMART_CHAPTER_EXPRESSION =
"//h:*[re:test(., '\\s*Chapter\\s+', 'i')]";
private final TempFileManager tempFileManager;
private final EndpointConfiguration endpointConfiguration;
private static List<String> buildCalibreCommand(
Path inputPath, Path outputPath, boolean detectChapters, TargetDevice targetDevice) {
List<String> command = new ArrayList<>();
command.add("ebook-convert");
command.add(inputPath.toString());
command.add(outputPath.toString());
// Golden defaults
command.add("--enable-heuristics");
command.add("--insert-blank-line");
command.add("--filter-css");
command.add(FILTERED_CSS);
if (detectChapters) {
command.add("--chapter");
command.add(SMART_CHAPTER_EXPRESSION);
}
if (targetDevice != null) {
command.add("--output-profile");
command.add(targetDevice.getCalibreProfile());
}
return command;
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
@Operation(
summary = "Convert PDF to EPUB/AZW3",
description =
"Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre. Input:PDF"
+ " Output:EPUB/AZW3 Type:SISO")
public ResponseEntity<byte[]> convertPdfToEpub(@ModelAttribute ConvertPdfToEpubRequest request)
throws Exception {
if (!endpointConfiguration.isGroupEnabled(CALIBRE_GROUP)) {
throw new IllegalStateException(
"Calibre support is disabled. Enable the Calibre group or install Calibre to use"
+ " this feature.");
}
MultipartFile inputFile = request.getFileInput();
if (inputFile == null || inputFile.isEmpty()) {
throw new IllegalArgumentException("No input file provided");
}
boolean detectChapters = !Boolean.FALSE.equals(request.getDetectChapters());
TargetDevice targetDevice =
request.getTargetDevice() == null
? TargetDevice.TABLET_PHONE_IMAGES
: request.getTargetDevice();
OutputFormat outputFormat =
request.getOutputFormat() == null ? OutputFormat.EPUB : request.getOutputFormat();
String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
if (originalFilename == null || originalFilename.isBlank()) {
originalFilename = "document." + DEFAULT_EXTENSION;
}
String extension = FilenameUtils.getExtension(originalFilename);
if (extension.isBlank()) {
throw new IllegalArgumentException("Unable to determine file type");
}
if (!DEFAULT_EXTENSION.equalsIgnoreCase(extension)) {
throw new IllegalArgumentException("Input file must be a PDF");
}
String baseName = FilenameUtils.getBaseName(originalFilename);
if (baseName == null || baseName.isBlank()) {
baseName = "document";
}
Path workingDirectory = null;
Path inputPath = null;
Path outputPath = null;
try {
workingDirectory = tempFileManager.createTempDirectory();
inputPath = workingDirectory.resolve(baseName + "." + DEFAULT_EXTENSION);
outputPath = workingDirectory.resolve(baseName + "." + outputFormat.getExtension());
try (InputStream inputStream = inputFile.getInputStream()) {
Files.copy(inputStream, inputPath, StandardCopyOption.REPLACE_EXISTING);
}
List<String> command =
buildCalibreCommand(inputPath, outputPath, detectChapters, targetDevice);
ProcessExecutorResult result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command, workingDirectory.toFile());
if (result == null) {
throw new IllegalStateException("Calibre conversion returned no result");
}
if (result.getRc() != 0) {
String errorMessage = result.getMessages();
if (errorMessage == null || errorMessage.isBlank()) {
errorMessage = "Calibre conversion failed";
}
throw new IllegalStateException(errorMessage);
}
if (!Files.exists(outputPath) || Files.size(outputPath) == 0L) {
throw new IllegalStateException(
"Calibre did not produce a " + outputFormat.name() + " output");
}
String outputFilename =
GeneralUtils.generateFilename(
originalFilename,
"_convertedTo"
+ outputFormat.name()
+ "."
+ outputFormat.getExtension());
byte[] outputBytes = Files.readAllBytes(outputPath);
MediaType mediaType = MediaType.valueOf(outputFormat.getMediaType());
return WebResponseUtils.bytesToWebResponse(outputBytes, outputFilename, mediaType);
} finally {
cleanupTempFiles(workingDirectory, inputPath, outputPath);
}
}
private void cleanupTempFiles(Path workingDirectory, Path inputPath, Path outputPath) {
if (workingDirectory == null) {
return;
}
List<Path> pathsToDelete = new ArrayList<>();
if (inputPath != null) {
pathsToDelete.add(inputPath);
}
if (outputPath != null) {
pathsToDelete.add(outputPath);
}
for (Path path : pathsToDelete) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.warn("Failed to delete temporary file: {}", path, e);
}
}
try {
tempFileManager.deleteTempDirectory(workingDirectory);
} catch (Exception e) {
log.warn("Failed to delete temporary directory: {}", workingDirectory, e);
}
}
}
@@ -54,6 +54,17 @@ public class ConverterWebController {
return "convert/ebook-to-pdf";
}
@GetMapping("/pdf-to-epub")
@Hidden
public String convertPdfToEpubForm(Model model) {
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
.isEndpointEnabled("pdf-to-epub")) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
model.addAttribute("currentPage", "pdf-to-epub");
return "convert/pdf-to-epub";
}
@GetMapping("/pdf-to-cbr")
@Hidden
public String convertPdfToCbrForm(Model model) {
@@ -0,0 +1,58 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import stirling.software.common.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class ConvertPdfToEpubRequest extends PDFFile {
@Schema(
description = "Detect headings that look like chapters and insert EPUB page breaks.",
allowableValues = {"true", "false"},
defaultValue = "true")
private Boolean detectChapters = Boolean.TRUE;
@Schema(
description = "Choose an output profile optimized for the reader device.",
allowableValues = {"TABLET_PHONE_IMAGES", "KINDLE_EINK_TEXT"},
defaultValue = "TABLET_PHONE_IMAGES")
private TargetDevice targetDevice = TargetDevice.TABLET_PHONE_IMAGES;
@Schema(
description = "Choose the output format for the ebook.",
allowableValues = {"EPUB", "AZW3"},
defaultValue = "EPUB")
private OutputFormat outputFormat = OutputFormat.EPUB;
@Getter
public enum TargetDevice {
TABLET_PHONE_IMAGES("tablet"),
KINDLE_EINK_TEXT("kindle");
private final String calibreProfile;
TargetDevice(String calibreProfile) {
this.calibreProfile = calibreProfile;
}
}
@Getter
public enum OutputFormat {
EPUB("epub", "application/epub+zip"),
AZW3("azw3", "application/vnd.amazon.ebook");
private final String extension;
private final String mediaType;
OutputFormat(String extension, String mediaType) {
this.extension = extension;
this.mediaType = mediaType;
}
}
}
@@ -706,6 +706,10 @@ home.ebookToPdf.title=eBook to PDF
home.ebookToPdf.desc=Convert eBook files (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre.
ebookToPdf.tags=conversion,ebook,calibre,epub,mobi,azw3
home.pdfToEpub.title=PDF to EPUB/AZW3
home.pdfToEpub.desc=Convert PDF files into EPUB or AZW3 ebooks optimised for e-readers using Calibre.
pdfToEpub.tags=conversion,ebook,epub,azw3,calibre
home.pdfToCbz.title=PDF to CBZ
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
@@ -1592,6 +1596,20 @@ ebookToPDF.includePageNumbers=Add page numbers to the generated PDF
ebookToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
ebookToPDF.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature.
#pdfToEpub
pdfToEpub.title=PDF to EPUB/AZW3
pdfToEpub.header=PDF to EPUB/AZW3
pdfToEpub.submit=Convert
pdfToEpub.selectText=Select PDF file
pdfToEpub.outputFormat=Output format
pdfToEpub.outputFormat.epub=EPUB
pdfToEpub.outputFormat.azw3=AZW3
pdfToEpub.detectChapters=Detect chapters and insert automatic breaks
pdfToEpub.targetDevice=Target device
pdfToEpub.targetDevice.tablet=Tablet / Phone (keeps images high quality)
pdfToEpub.targetDevice.kindle=Kindle / E-Ink (text-focused, smaller images)
pdfToEpub.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature.
#pdfToCBR
pdfToCBR.title=PDF to CBR
pdfToCBR.header=PDF to CBR
@@ -194,7 +194,7 @@ error.fileFormatRequired=Il file deve essere nel formato {0}
error.invalidFormat=Formato {0} non valido:{1}
error.endpointDisabled=Questo endpoint è stato disabilitato dall'amministratore
error.urlNotReachable=L'URL non è raggiungibile, inserisci un URL valido
error.disallowedUrlContent=URL content references disallowed resources and cannot be converted
error.disallowedUrlContent=Il contenuto dell'URL fa riferimento a risorse non consentite e non può essere convertito
error.invalidUrlFormat=Formato URL non valido. Il formato fornito non è valido.
# DPI and image rendering messages - used by frontend for dynamic translation
@@ -616,9 +616,9 @@ home.cbrToPdf.title=CBR in PDF
home.cbrToPdf.desc=Converti gli archivi dei fumetti CBR in formato PDF.
cbrToPdf.tags=conversione,fumetto,libro,archivio,cbr,rar
home.ebookToPdf.title=eBook to PDF
home.ebookToPdf.desc=Convert eBook files (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre.
ebookToPdf.tags=conversion,ebook,calibre,epub,mobi,azw3
home.ebookToPdf.title=eBook in PDF
home.ebookToPdf.desc=Converti i file eBook (EPUB, MOBI, AZW3, FB2, TXT, DOCX) in PDF utilizzando Calibre.
ebookToPdf.tags=conversione,ebook,calibre,epub,mobi,azw3
home.pdfToCbz.title=PDF in CBZ
home.pdfToCbz.desc=Converti i file PDF negli archivi di fumetti CBZ.
@@ -698,9 +698,9 @@ home.extractImages.title=Estrai immagini
home.extractImages.desc=Estrai tutte le immagini da un PDF e salvale come zip.
extractImages.tags=immagine,foto,salva,archivio,zip,catturare,prendere
home.pdfToPDFA.title=PDF to PDF/A & PDF/X
home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x
home.pdfToPDFA.title=PDF in PDF/A e PDF/X
home.pdfToPDFA.desc=Converti PDF in PDF/A per l'archiviazione a lungo termine o PDF/X per la produzione di stampa
pdfToPDFA.tags=archivio,lungo termine,standard,conversione,archiviazione,conservazione,stampa,pdf-x
home.PDFToWord.title=Da PDF a Word
home.PDFToWord.desc=Converti un PDF nei formati Word (DOC, DOCX e ODT)
@@ -1025,7 +1025,7 @@ getPdfInfo.summary.all.permissions.alert=Tutti i permessi consentiti
getPdfInfo.summary.compliance.alert={0} Conforme
getPdfInfo.summary.no.compliance.alert=Nessuno standard di conformità
getPdfInfo.summary.security.section=Stato di sicurezza
getPdfInfo.summary.technical.section=Technical Details
getPdfInfo.summary.technical.section=Dettagli tecnici
getPdfInfo.section.BasicInfo=Informazioni di base sul documento PDF, tra cui dimensione del file, numero di pagine e lingua
getPdfInfo.section.Metadata=Metadati del documento, inclusi titolo, autore, data di creazione e altre proprietà del documento
getPdfInfo.section.DocumentInfo=Dettagli tecnici sulla struttura e la versione del documento PDF
@@ -1143,7 +1143,7 @@ adjustContrast.download=Download
crop.title=Ritaglia
crop.header=Ritaglia PDF
crop.submit=Invia
crop.autoCrop=Auto-crop (detect and remove white space)
crop.autoCrop=Ritaglio automatico (rileva e rimuove gli spazi bianchi)
#autoSplitPDF
autoSplitPDF.title=PDF diviso automaticamente
@@ -1411,7 +1411,7 @@ multiTool.delete=Elimina
multiTool.dragDropMessage=Pagina(e) selezionata(e)
multiTool.undo=Annulla
multiTool.redo=Rifai
multiTool.duplicate=Duplicate
multiTool.duplicate=Duplicata
multiTool.svgNotSupported=I file SVG non sono supportati in Strumenti multipli e sono stati ignorati.
#decrypt
@@ -1496,15 +1496,15 @@ cbrToPDF.selectText=Seleziona file CBR
cbrToPDF.optimizeForEbook=Ottimizza PDF per lettori di ebook (utilizza Ghostscript)
#ebookToPDF
ebookToPDF.title=eBook to PDF
ebookToPDF.header=eBook to PDF
ebookToPDF.submit=Convert to PDF
ebookToPDF.selectText=Select eBook file
ebookToPDF.embedAllFonts=Embed all fonts in the output PDF (may increase file size)
ebookToPDF.includeTableOfContents=Add a generated table of contents to the PDF
ebookToPDF.includePageNumbers=Add page numbers to the generated PDF
ebookToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
ebookToPDF.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature.
ebookToPDF.title=eBook in PDF
ebookToPDF.header=eBook in PDF
ebookToPDF.submit=Converti in PDF
ebookToPDF.selectText=Seleziona file eBook
ebookToPDF.embedAllFonts=Incorpora tutti i font nel PDF di output (potrebbe aumentare le dimensioni del file)
ebookToPDF.includeTableOfContents=Aggiungere un indice generato al PDF
ebookToPDF.includePageNumbers=Aggiungere i numeri di pagina al PDF generato
ebookToPDF.optimizeForEbook=Ottimizzazione PDF per lettori di ebook (utilizza Ghostscript)
ebookToPDF.calibreDisabled=Il supporto di Calibre è disabilitato. Abilita il gruppo di strumenti Calibre o installa Calibre per utilizzare questa funzionalità.
#pdfToCBR
pdfToCBR.title=PDF in CBR
@@ -1635,15 +1635,15 @@ unlockPDFForms.header=Sbloccare i moduli PDF
unlockPDFForms.submit=Rimuovi
#pdfToPDFA
pdfToPDFA.title=PDF to PDF/A or PDF/X
pdfToPDFA.header=PDF to PDF/A or PDF/X
pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion
pdfToPDFA.title=PDF in PDF/A o PDF/X
pdfToPDFA.header=PDF in PDF/A o PDF/X
pdfToPDFA.credit=Questo servizio utilizza Ghostscript (preferito) o LibreOffice per la conversione PDF/A e Ghostscript per la conversione PDF/X
pdfToPDFA.submit=Converti
pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production)
pdfToPDFA.tip=Converti PDF in PDF/A (archiviazione a lungo termine) o PDF/X (produzione di stampa)
pdfToPDFA.outputFormat=Formato di output
pdfToPDFA.pdfWithDigitalSignature=Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo.
pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving)
pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production)
pdfToPDFA.pdfaFormats=Formati PDF/A (archiviazione a lungo termine)
pdfToPDFA.pdfxFormats=Formati PDF/X (produzione di stampa)
#PDFToWord
@@ -235,6 +235,92 @@ error.pdfBookmarksNotFound=В PDF-документе не найдены зак
error.fontLoadingFailed=Ошибка при обработке файла шрифта
error.fontDirectoryReadFailed=Не удалось прочитать каталог шрифтов
error.noAttachmentsFound=В предоставленном PDF-файле не было обнаружено встроенных вложений.
error.nullArgument={0} не должно быть пустым
error.invalidPageSize=Недопустимый формат размера страницы: {0}
error.invalidComparator=Недопустимый формат сравнения: поддерживаются только значения 'greater', 'equal', и 'less' (больше, равно и меньше)
# Error titles for GlobalExceptionHandler
error.pdfPassword.title=Требуется пароль для PDF-файла
error.outOfMemoryDpi.title=Недостаточно памяти - Слишком высокое разрешение DPI
error.pdfCorrupted.title=PDF-файл повреждён
error.multiplePdfCorrupted=Один или несколько PDF-файлов, по-видимому, повреждены. Пожалуйста, попробуйте сначала использовать функцию "Восстановить PDF" для каждого файла, прежде чем пытаться объединить их.
error.pdfEncryption.title=Ошибка шифрования PDF
error.application.title=Ошибка приложения
error.cbrFormat.title=Недопустимый формат файла CBR
error.cbzFormat.title=Недопустимый формат файла CBZ
error.emlFormat.title=Недопустимый формат файла EML
error.formatError.title=Недопустимый формат файла
error.validation.title=Не удалось выполнить проверку запроса
error.validation.detail=Не удалось выполнить проверку
error.missingParameter.title=Отсутствует параметр запроса
error.missingParameter.detail=Отсутствует обязательный параметр ''{0}'' типа ''{1}''
error.missingFile.title=Отсутствует загрузка файла
error.missingFile.detail=Отсутствует требуемая часть файла ''{0}''
error.fileTooLarge.title=Слишком большой файл
error.fileTooLarge.detail=Размер файла превышает максимально допустимый предел {0} MB
error.fileTooLarge.detailUnknown=Размер файла превышает максимально допустимый предел
error.methodNotAllowed.title=HTTP-метод не разрешен
error.methodNotAllowed.detail=HTTP метод ''{0}'' не поддерживается для этой конечной точки. Поддерживаемые методы: {1}
error.unsupportedMediaType.title=Неподдерживаемый тип медиа
error.unsupportedMediaType.detail=Тип медиа ''{0}'' не поддерживается. Поддерживаемые типы медиа: {1}
error.malformedRequest.title=Неправильно сформированный текст запроса
error.malformedRequest.detail=Неправильно сформированный запрос JSON или неверный формат тела запроса
error.malformedRequest.detailWithCause=Недопустимый запрос: {0}
error.notFound.title=Конечная точка не найдена
error.notFound.detail=Конечная точка для {0} {1} не найдена
error.invalidArgument.title=Недопустимый аргумент
error.ioError.title=Ошибка при обработке файла
error.ioError.detail=При обработке файла произошла ошибка
error.unexpected.title=Внутренняя ошибка сервера
error.unexpected=Произошла непредвиденная ошибка. Пожалуйста, повторите попытку позже.
# PDF-related error messages from ErrorCode enum
error.pdfNoPages=PDF-файл не содержит страниц
error.notPdfFile=Файл должен быть в формате PDF
# CBR/CBZ error messages from ErrorCode enum
error.cbrInvalidFormat=Недопустимый или поврежденный CBR/RAR архив. Возможно, файл поврежден, используется неподдерживаемый формат RAR (RAR5+), зашифрован или может быть недействительным RAR архивом.
error.cbrNoImages=В файле CBR не найдено допустимых изображений. Возможно, архив пуст, или все изображения повреждены, или они имеют неподдерживаемые форматы.
error.notCbrFile=Файл должен быть CBR или архивом RAR
error.cbzInvalidFormat=Недействительный или поврежденный CBZ/ZIP архив. Файл может быть пустым, поврежденным или не являться действительным ZIP-архивом.
error.cbzNoImages=В файле CBZ не найдено допустимых изображений. Возможно, архив пуст, или все изображения повреждены, или они имеют неподдерживаемые форматы.
error.notCbzFile=Файл должен быть в формате CBZ или ZIP-архив
# EML error messages from ErrorCode enum
error.emlEmpty=EML-файл пуст или null
error.emlInvalidFormat=Недопустимый формат файла EML
# File processing error messages from ErrorCode enum
error.fileNullOrEmpty=Файл не может быть null или пустым
error.fileNoName=Файл должен иметь имя
error.imageReadError=Не удается прочитать изображение из файла: {0}
# OCR error messages from ErrorCode enum
error.ocrLanguageRequired=Не указаны языковые параметры распознавания
error.ocrInvalidLanguages=Недопустимый формат распознавания языков: ни один из выбранных языков не является допустимым
error.ocrToolsUnavailable=Инструменты распознавания текста не установлены
error.ocrInvalidRenderType=Недопустимый тип распознавания текста. Должно быть 'hocr' или 'sandwich'
error.ocrProcessingFailed=Ошибка OCRmyPDF с кодом возврата: {0}
# Compression error messages from ErrorCode enum
error.compressionOptions=Параметры сжатия не указаны (ожидаемый выходной размер и уровень оптимизации).
error.ghostscriptCompression=Не удалось выполнить команду сжатия Ghostscript
error.ghostscriptCompression.title=Ошибка обработки Ghostscript
error.ghostscriptPageDrawing=Ghostscript не удалось отобразить {0}. {1}
error.ghostscriptDefaultDiagnostic=Исходный файл содержит содержимое, которое Ghostscript не может отобразить.
error.qpdfCompression=Не удалось выполнить команду QPDF
error.processingInterrupted={0} обработка была прервана
# Conversion error messages from ErrorCode enum
error.pdfaConversionFailed=Не удалось выполнить преобразование PDF/A
error.htmlFileRequired=Файл должен быть в формате HTML или ZIP
error.pythonRequiredWebp=Для преобразования WebP требуется Python
error.ffmpegRequired=Для преобразования PDF-файлов в видео должен быть установлен FFmpeg. Установите FFmpeg и убедитесь, что он доступен в переменной среды PATH.
error.ffmpegRequired.title=Требуется FFmpeg
# System error messages from ErrorCode enum
error.md5Algorithm=Алгоритм MD5 недоступен
error.outOfMemoryDpi=Ошибка нехватки памяти или слишком большой размера изображений при отрисовки PDF-страницы {0} с разрешением {1} точек на дюйм. Это может произойти, когда результирующее изображение превышает установленные в Java ограничения по размеру массива/памяти (например, NegativeArraySizeException). Пожалуйста, используйте меньшее значение разрешения (рекомендуется 150 или меньше) или обрабатывайте документ небольшими фрагментами.
delete=Удалить
username=Имя пользователя
password=Пароль
@@ -616,9 +702,9 @@ home.cbrToPdf.title=CBR в PDF
home.cbrToPdf.desc=Конвертируйте архивы комиксов CBR в формат PDF.
cbrToPdf.tags=конвертация,комикс,книга,архив,cbr,rar
home.ebookToPdf.title=eBook to PDF
home.ebookToPdf.desc=Convert eBook files (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre.
ebookToPdf.tags=conversion,ebook,calibre,epub,mobi,azw3
home.ebookToPdf.title=eBook в PDF
home.ebookToPdf.desc=Преобразование eBook файлов (EPUB, MOBI, AZW3, FB2, TXT, DOCX) в PDF используя Calibre.
ebookToPdf.tags=конвертация,ebook,книга,calibre,epub,mobi,azw3,fb2
home.pdfToCbz.title=PDF в CBZ
home.pdfToCbz.desc=Конвертируйте PDF-файлы в архивы комиксов CBZ.
@@ -698,9 +784,9 @@ home.extractImages.title=Извлечь изображения
home.extractImages.desc=Извлекает все изображения из PDF и сохраняет их в zip-архив
extractImages.tags=картинка,фото,сохранение,архив,zip,захват,извлечение
home.pdfToPDFA.title=PDF to PDF/A & PDF/X
home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x
home.pdfToPDFA.title=PDF в PDF/A или PDF/X
home.pdfToPDFA.desc=Преобразуйте PDF в формат PDF/A для длительного хранения или PDF/X для печати
pdfToPDFA.tags=архив,длительное хранение,преобразование,хранение,печать,pdf-a,pdf-x
home.PDFToWord.title=PDF в Word
home.PDFToWord.desc=Преобразование PDF в форматы Word (DOC, DOCX и ODT)
@@ -1143,7 +1229,7 @@ adjustContrast.download=Скачать
crop.title=Обрезка
crop.header=Обрезка PDF
crop.submit=Отправить
crop.autoCrop=Auto-crop (detect and remove white space)
crop.autoCrop=Автоматическая обрезка (обнаружение и удаление пробелов)
#autoSplitPDF
autoSplitPDF.title=Автоматическое разделение PDF
@@ -1329,7 +1415,7 @@ compress.title=Сжать
compress.header=Сжать PDF
compress.credit=Этот сервис использует qpdf для сжатия/оптимизации PDF.
compress.grayscale.label=Примените оттенки серого для сжатия
compress.linearize.label=Linearize PDF for faster web viewing
compress.linearize.label=Линеаризовать PDF-файл для более быстрого просмотра в Интернете
compress.selectText.1=Параметры сжатия
compress.selectText.1.1=1-3 сжатие PDF,</br> 4-6 лёгкое сжатие изображений,</br> 7-9 интенсивное сжатие изображений (значительно снижает качество изображений)
compress.selectText.2=Уровень оптимизации:
@@ -1496,15 +1582,15 @@ cbrToPDF.selectText=Выбрать файл CBR
cbrToPDF.optimizeForEbook=Оптимизировать PDF для чтения электронных книг (используется Ghostscript)
#ebookToPDF
ebookToPDF.title=eBook to PDF
ebookToPDF.header=eBook to PDF
ebookToPDF.submit=Convert to PDF
ebookToPDF.selectText=Select eBook file
ebookToPDF.embedAllFonts=Embed all fonts in the output PDF (may increase file size)
ebookToPDF.includeTableOfContents=Add a generated table of contents to the PDF
ebookToPDF.includePageNumbers=Add page numbers to the generated PDF
ebookToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
ebookToPDF.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature.
ebookToPDF.title=eBook в PDF
ebookToPDF.header=eBook в PDF
ebookToPDF.submit=Конвертация в PDF
ebookToPDF.selectText=Выберите файл eBook
ebookToPDF.embedAllFonts=Внедрить все шрифты в выходной PDF-файл (возможно увеличение размера файла)
ebookToPDF.includeTableOfContents=Добавить сгенерированное оглавление в PDF-файл
ebookToPDF.includePageNumbers=Добавить номера страниц в генерируемый PDF-файл
ebookToPDF.optimizeForEbook=Оптимизировать PDF для чтения электронных книг (используется Ghostscript)
ebookToPDF.calibreDisabled=Поддержка Calibre отключена. Чтобы использовать эту функцию, включите группу инструментов Calibre или установите Calibre.
#pdfToCBR
pdfToCBR.title=PDF в CBR
@@ -1533,16 +1619,16 @@ pdfToImage.includeAnnotations=Добавьте аннотации (коммен
#pdfToVideo
pdfToVideo.title=PDF to Video Slideshow
pdfToVideo.header=PDF to Video Slideshow
pdfToVideo.videoFormat=Video format
pdfToVideo.secondsPerPage=Seconds per page
pdfToVideo.resolution=Resolution
pdfToVideo.watermarkText=Watermark Text (empty for no watermark)
pdfToVideo.opacity=Opacity of the watermark (only applied if a watermark text is specified)
pdfToVideo.resolutionOriginal=Original (keep PDF page size)
pdfToVideo.dpiLabel=DPI (The server limit is {0} dpi)
pdfToVideo.submit=Convert
pdfToVideo.title=PDF в Видео Слайд-шоу
pdfToVideo.header=PDF в Видео Слайд-шоу
pdfToVideo.videoFormat=Формат видео
pdfToVideo.secondsPerPage=Секунд на страницу
pdfToVideo.resolution=Разрешение
pdfToVideo.watermarkText=Текст водяного знака (оставьте пустым, если водяной знак отсутствует)
pdfToVideo.opacity=Прозрачность водяного знака (применяется в том случае, если указан текст водяного знака)
pdfToVideo.resolutionOriginal=Оригинал (сохранить размер страниц как в исходном формате PDF)
pdfToVideo.dpiLabel=DPI (ограничение сервера {0} точек на дюйм)
pdfToVideo.submit=Преобразовать
#addPassword
@@ -1635,15 +1721,15 @@ unlockPDFForms.header=Разблокировать PDF-формы
unlockPDFForms.submit=Удалить
#pdfToPDFA
pdfToPDFA.title=PDF to PDF/A or PDF/X
pdfToPDFA.header=PDF to PDF/A or PDF/X
pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion
pdfToPDFA.title=PDF в PDF/A или PDF/X
pdfToPDFA.header=PDF в PDF/A или PDF/X
pdfToPDFA.credit=Этот сервис использует Ghostscript (предпочтительнее) или LibreOffice для преобразования PDF/A, и Ghostscript для преобразования PDF/X
pdfToPDFA.submit=Преобразовать
pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production)
pdfToPDFA.tip=Преобразовать PDF в PDF/A (для длительного хранения) или PDF/X (для печати)
pdfToPDFA.outputFormat=Формат вывода
pdfToPDFA.pdfWithDigitalSignature=PDF содержит цифровую подпись. Она будет удалена на следующем шаге.
pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving)
pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production)
pdfToPDFA.pdfaFormats=PDF/A форматы (для длительного хранения)
pdfToPDFA.pdfxFormats=PDF/X форматы (для печати)
#PDFToWord
@@ -2000,7 +2086,7 @@ scannerEffect.resolution=Разрешение (DPI)
# Table of Contents Feature
home.editTableOfContents.title=Редактир оглавления
home.editTableOfContents.title=Редактирование оглавления
home.editTableOfContents.desc=Добавление или редактирование закладок и оглавления в PDF-документах
editTableOfContents.tags=закладки, указатель, индекс, оглавление, главы, разделы, схема, навигация, структура
@@ -2037,3 +2123,209 @@ pdfToVector.header=PDF в векторное изображение
pdfToVector.description=Конвертируйте PDF-файл в векторные форматы, созданные с помощью Ghostscript (EPS, PS, PCL, or XPS).
pdfToVector.outputFormat=Выходной формат
pdfToVector.submit=Конвертировать
#####################
# Exception Hints #
#####################
# PDF-related errors
error.E001.hint.1=Попробуйте функцию "Восстановить PDF", затем повторите эту операцию.
error.E001.hint.2=По возможности повторно экспортируйте PDF-файл из исходного приложения.
error.E001.hint.3=Избегайте передачи файла с помощью инструментов, которые изменяют PDF-файлы (например, конвертеры факсов/электронной почты).
error.E001.action=Восстановите PDF-файл и повторите операцию.
error.E002.hint.1=Определите, какие файлы повреждены, обработав их один за другим.
error.E002.hint.2=Запустите "Восстановить PDF" для каждого проблемного файла перед объединением.
error.E002.hint.3=Если восстановление завершится неудачно, повторно экспортируйте каждый исходный документ в формат PDF.
error.E002.action=Восстановите или повторно экспортируйте поврежденные PDF-файлы по отдельности, а затем повторите операцию.
error.E003.hint.1=Используйте функцию "Восстановить PDF" для нормализации метаданных шифрования.
error.E003.hint.2=Если файл использует необычное шифрование, пересохраните его с помощью современного инструмента для работы с PDF-файлами.
error.E003.hint.3=Если PDF-файл защищён паролем, сначала введите правильный пароль.
error.E003.action=Восстановите PDF-файл или повторно экспортируйте его с совместимым шифрованием.
error.E004.hint.1=PDF-файлы могут иметь два пароля: пароль пользователя (открывает документ) и пароль владельца (управляет правами доступа). Для этой операции требуется пароль владельца.
error.E004.hint.2=Если PDF-файл открывается без пароля, возможно, установлен только пароль владельца. Попробуйте ввести пароль для управления правами доступа.
error.E004.hint.3=PDF-файлы с цифровой подписью не могут быть сняты с защиты, пока не будет удалена подпись.
error.E004.hint.4=Пароли чувствительны к регистру. Проверьте регистр, пробелы и специальные символы.
error.E063.hint.1=Установите FFmpeg на хост-систему и убедитесь, что он доступен в переменной среды PATH.
error.E063.hint.2=Перезапустите Stirling-PDF после установки FFmpeg, чтобы процесс мог его обнаружить.
error.E063.action=Установите FFmpeg и перезапустите Stirling-PDF перед повторной попыткой конвертации.
error.E004.hint.5=Некоторые разработчики используют разные стандарты шифрования (40-битное, 128-битное, 256-битное AES). Убедитесь, что ваш пароль соответствует используемому шифрованию.
error.E004.hint.6=Если у вас есть только пароль пользователя, вы не сможете снять ограничения безопасности. Обратитесь к владельцу документа за паролем с правами доступа.
error.E004.action=Укажите пароль владельца/прав доступа, а не только пароль для открытия документа.
error.E005.hint.1=Убедитесь, что PDF-файл не пустой и не содержит только неподдерживаемые объекты.
error.E005.hint.2=Откройте файл в программе просмотра PDF, чтобы убедиться, что в нем есть страницы.
error.E005.hint.3=Пересоздайте PDF-файл, убедившись, что в него включены страницы (а не только вложения).
error.E005.action=Предоставьте PDF-файл, содержащий хотя бы одну страницу.
error.E006.hint.1=Убедитесь, что загруженный файл имеет формат PDF, а не какой-либо другой формат.
error.E006.hint.2=Если файл имеет расширение .pdf, убедитесь, что он не относится к другому формату (например, Word, изображение).
error.E006.hint.3=Попробуйте открыть файл в программе для чтения PDF-файлов, чтобы убедиться в его корректности.
error.E006.action=Загрузите корректный PDF-файл.
# CBR/CBZ errors
error.E010.hint.1=Архивы формата RAR5 не поддерживаются. Перепакуйте архив в формат RAR4 или преобразуйте в CBZ (ZIP).
error.E010.hint.2=Убедитесь, что архив не зашифрован и содержит корректные файлы изображений.
error.E010.hint.3=Попробуйте извлечь архив с помощью десктопной утилиты для проверки целостности.
error.E010.action=Преобразуйте архив в формат CBZ/ZIP или RAR4, затем повторите попытку.
error.E012.hint.1=Убедитесь, что в архиве содержатся файлы изображений (например, .jpg, .png).
error.E012.hint.2=Удалите неподдерживаемые или повреждённые файлы из архива.
error.E012.hint.3=Переупакуйте архив, убедившись, что изображения находятся в корневой папке или в соответствующих папках.
error.E012.action=Добавьте в архив хотя бы одно корректное изображение и повторите попытку.
error.E014.hint.1=Для этой операции необходимо загрузить файл формата CBR (RAR).
error.E014.hint.2=Если у вас есть файл формата ZIP/CBZ, используйте преобразование в формат CBZ.
error.E014.hint.3=Проверьте расширение файла и актуальный формат с помощью архиватора.
error.E014.action=Укажите корректный файл CBR/RAR.
error.E015.hint.1=Убедитесь, что ZIP/CBZ-архив не зашифрован и является допустимым архивом.
error.E015.hint.2=Убедитесь, что архив не пустой и содержит файлы изображений.
error.E015.hint.3=Попробуйте повторно заархивировать изображения с помощью стандартного архиватора ZIP (без аномалий сжатия).
error.E015.action=Пересоздайте файл CBZ/ZIP без шифрования и с допустимыми изображениями, затем повторите попытку.
error.E016.hint.1=Добавьте изображения (.jpg, .png и т.д.) в ZIP-архив.
error.E016.hint.2=Удалите файлы, не являющиеся изображениями, или вложенные архивы, которые не поддерживаются.
error.E016.hint.3=Убедитесь, что изображения не повреждены и их можно открыть локально.
error.E016.action=Добавьте хотя бы одно корректное изображение в CBZ-файл.
error.E018.hint.1=Для этой операции необходимо загрузить CBZ (ZIP) файл.
error.E018.hint.2=Если у вас есть формат RAR/CBR, используйте преобразование в формат CBR.
error.E018.hint.3=Проверьте расширение файла и актуальный формат с помощью архиватора.
error.E018.action=Укажите корректный файл CBZ/ZIP.
# EML errors
error.E020.hint.1=Убедитесь, что размер загруженного файла не равен нулю.
error.E020.hint.2=Повторно экспортируйте EML-файл из вашего почтового клиента.
error.E020.hint.3=Убедитесь, что содержимое файла не было удалено с помощью электронной почты или средств обеспечения безопасности.
error.E020.action=Загрузите непустой EML-файл.
error.E021.hint.1=Убедитесь, что файл представляет собой исходное EML-сообщение, а не MSG или другой формат электронной почты.
error.E021.hint.2=Повторно экспортируйте электронное письмо из вашего клиента в формате EML.
error.E021.hint.3=Откройте файл в текстовом редакторе, чтобы проверить наличие стандартных заголовков EML.
error.E021.action=Предоставьте корректный экспорт файла EML.
# File processing errors
error.E030.hint.1=Подтвердите правильность идентификатора файла или пути к нему.
error.E030.hint.2=Убедитесь, что файл не был удален или перемещен.
error.E030.hint.3=Если используется временная загрузка, повторно загрузите файл и повторите попытку.
error.E030.action=Укажите ссылку на существующий файл и повторите попытку.
error.E031.hint.1=Убедитесь, что файл не поврежден и поддерживается этой операцией.
error.E031.hint.2=Повторите операцию; могли возникнуть временные проблемы ввода-вывода.
error.E031.hint.3=Если проблема не устранена, упростите документ (уменьшите количество страниц, размер изображений).
error.E031.action=Проверьте файл и параметры операции, затем повторите попытку.
error.E032.hint.1=Прикрепите файл к запросу.
error.E032.hint.2=Убедитесь, что размер файла не равен нулю.
error.E032.hint.3=При загрузке нескольких файлов убедитесь, что указан хотя бы один из них.
error.E032.action=Загрузите непустой файл и повторите попытку.
error.E033.hint.1=Укажите имя файла с расширением.
error.E033.hint.2=Убедитесь, что ваш клиент указал исходное имя файла при загрузке.
error.E033.action=Укажите имя загруженного файла.
error.E034.hint.1=Убедитесь, что файл изображения не поврежден и его можно открыть локально.
error.E034.hint.2=Убедитесь, что формат файла соответствует поддерживаемому типу изображений.
error.E034.hint.3=Повторно экспортируйте или преобразуйте изображение в стандартный формат (JPEG/PNG).
error.E034.action=Укажите доступный для чтения, поддерживаемый файл изображения.
# OCR errors
error.E040.hint.1=Выберите хотя бы один язык распознавания текста из предложенных.
error.E040.hint.2=Если вы не уверены, выберите основной язык текста документа.
error.E040.hint.3=При наличии смешанного текста можно выбрать несколько языков.
error.E040.action=Укажите один или несколько языков распознавания текста.
error.E041.hint.1=Используйте допустимые языковые коды (например, eng, fra, deu).
error.E041.hint.2=Удалите неподдерживаемые или неправильно написанные языковые коды.
error.E041.hint.3=Проверьте установленные языковые пакеты распознавания текста и установите отсутствующие.
error.E041.action=Укажите допустимые языковые коды распознавания текста или установите отсутствующие языковые пакеты.
error.E042.hint.1=Установите инструменты распознавания текста (например, OCRmyPDF/Tesseract) в соответствии с документацией.
error.E042.hint.2=Убедитесь, что инструменты находятся в системной переменной PATH и доступны приложению.
error.E042.hint.3=При запуске в Docker используйте вариант образа, который включает инструменты распознавания текста.
error.E042.action=Установите и настройте инструменты распознавания текста.
error.E043.hint.1=Используйте 'hocr' для распознавания текста в формате HTML или 'sandwich' для встраивания текста в PDF.
error.E043.hint.2=Проверьте документацию API на наличие допустимых типов рендеринга.
error.E043.hint.3=Избегайте опечаток; значения чувствительны к регистру.
error.E043.action=Выберите 'hocr' или 'sandwich' в качестве типа рендеринга.
error.E044.hint.1=Проверьте журналы сервера на наличие подробной информации об ошибке OCRmyPDF.
error.E044.hint.2=Убедитесь, что установлены необходимые зависимости для распознавания текста и языковые пакеты.
error.E044.hint.3=Попробуйте запустить распознавание текста локально для файла, чтобы воспроизвести проблему.
error.E044.action=Изучите журналы распознавания текста и исправьте отсутствующие зависимости или входные данные, затем повторите попытку.
# Compression/processing errors
error.E050.hint.1=Укажите целевой размер выходного файла и уровень оптимизации.
error.E050.hint.2=Ознакомьтесь с документацией API для получения информации о необходимых параметрах сжатия.
error.E050.hint.3=Если не уверены, начните с оптимизации по умолчанию и подстройте.
error.E050.action=Укажите ожидаемый размер выходного файла и уровень оптимизации для сжатия.
error.E051.hint.1=Проверьте, что Ghostscript установлен и доступен.
error.E051.hint.2=Упростите PDF-файл (например, уменьшите размер изображений) и повторите попытку.
error.E051.hint.3=Просмотрите аргументы командной строки, созданные для Ghostscript, в журналах.
error.E051.action=Убедитесь, что Ghostscript установлен и команда выполняется успешно.
error.E054.hint.1=Преобразуйте файлы EPS или PS в одностраничный PDF-файл, прежде чем повторить эту операцию.
error.E054.hint.2=Экспортируйте каждую страницу по отдельности из приложения-автора, чтобы Ghostscript обрабатывал по одной странице за раз.
error.E054.hint.3=Используйте инструмент «Преобразовать в PDF» для сглаживания изображений или применения дополнительных эффектов, затем повторите команду.
error.E054.action=Преобразуйте исходный файл в одностраничный документ (или PDF) и повторите операцию.
error.E052.hint.1=Убедитесь, что qpdf установлен и находится в системной переменной PATH.
error.E052.hint.2=Убедитесь, что PDF-файл не поврежден перед сжатием.
error.E052.hint.3=Измените параметры сжатия, если команда не выполняется.
error.E052.action=Установите qpdf и повторите попытку с корректными входными данными.
error.E053.hint.1=Операция была отменена или прервана системой.
error.E053.hint.2=Не прерывайте процесс и не закрывайте браузер во время выполнения.
error.E053.hint.3=Повторите операцию; если проблема не устранена, проверьте ограничения ресурсов сервера.
error.E053.action=Повторите операцию, избегая прерывания.
# Conversion/System errors
error.E060.hint.1=Убедитесь, что PDF-файл корректен и поддерживается конвертером.
error.E060.hint.2=Попробуйте преобразовать его в другой формат PDF/A или сначала повторно экспортируйте исходный текст в PDF.
error.E060.hint.3=Удалите проблемные элементы (например, сложную прозрачность) и повторите попытку.
error.E060.action=Измените параметры преобразования или нормализуйте формат PDF, затем повторите попытку.
error.E061.hint.1=Укажите либо отдельный HTML-файл, либо ZIP-архив, содержащий HTML-код и ресурсы.
error.E061.hint.2=Убедитесь, что относительные ссылки в HTML указывают на включенные ресурсы в ZIP-архиве.
error.E061.action=Загрузите HTML-файл или ZIP-архив с содержимым веб-сайта.
error.E062.hint.1=Установите Python и необходимые библиотеки WebP, чтобы включить преобразование.
error.E062.hint.2=При использовании Docker используйте вариант образа с поддержкой Python/WebP.
error.E062.hint.3=Проверьте системную переменную PATH и окружение, чтобы убедиться в доступности Python.
error.E062.action=Установите Python и WebP зависимости.
# Validation errors
error.E070.hint.1=Проверьте допустимые значения параметра в документации API.
error.E070.hint.2=Убедитесь, что формат значения соответствует ожиданиям (регистр, диапазон, шаблон).
error.E070.hint.3=Исправьте аргумент и отправьте запрос повторно.
error.E070.action=Укажите допустимое значение параметра и повторите попытку.
error.E071.hint.1=Включите отсутствующий параметр в запрос.
error.E071.hint.2=Убедитесь, что ваш клиент отправил все необходимые поля.
error.E071.action=Добавьте обязательный параметр и повторите попытку.
error.E072.hint.1=Используйте такие форматы, как 'A4', 'Letter' или 'WIDTHxHEIGHT' (например, 800x600).
error.E072.hint.2=Убедитесь, что единицы измерения и разделители указаны правильно.
error.E072.hint.3=Сведения о поддерживаемых размерах указаны в документации.
error.E072.action=Укажите поддерживаемое значение размера страницы.
error.E073.hint.1=Допустимые значения: 'greater', 'equal', 'less' (больше, равно, меньше).
error.E073.hint.2=Проверьте, нет ли опечаток, и используйте строчные буквы.
error.E073.hint.3=Обратитесь к документации API для получения примеров использования компаратора.
error.E073.action=Используйте в качестве компаратора одно из значений: 'greater', 'equal', or 'less' (больше, равно и меньше).
# System errors
error.E080.hint.1=Возможно, в вашей среде выполнения Java отсутствует MD5. Используйте альтернативный алгоритм.
error.E080.hint.2=Если хеширование необязательно, переключитесь на SHA-256 или другой поддерживаемый алгоритм.
error.E080.hint.3=Установите соответствующие поставщики безопасности, если требуется MD5.
error.E080.action=Используйте поддерживаемый алгоритм хэширования (например, SHA-256) или установите поставщик MD5.
error.E081.hint.1=Уменьшите разрешение DPI (попробуйте 150 или меньше).
error.E081.hint.2=Обрабатывайте документ небольшими фрагментами или меньшим количеством страниц за раз.
error.E081.hint.3=Уменьшите размеры страницы или сложность изображения перед рендерингом.
error.E081.hint.4=Увеличьте доступную динамическую память для приложения, если это возможно.
error.E081.action=Уменьшите разрешение DPI и повторите попытку; для больших страниц рекомендуется использовать разрешение 150 точек на дюйм.
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
function formatProblemDetailsJson(input) {
try {
const obj = typeof input === 'string' ? JSON.parse(input) : input;
@@ -238,7 +244,7 @@ export class DecryptFile {
return {isEncrypted: false, requiresPassword: false};
}
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const arrayBuffer = await file.arrayBuffer();
const arrayBufferForPdfLib = arrayBuffer.slice(0);
@@ -246,12 +252,14 @@ export class DecryptFile {
if(this.decryptWorker == null){
loadingTask = pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
});
this.decryptWorker = loadingTask._worker
}else {
loadingTask = pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
worker: this.decryptWorker
});
@@ -1,18 +1,39 @@
importScripts('./diff.js');
let complexMessage = 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
let largeFilesMessage = 'One or Both of the provided documents are too large to process';
// Early: Listener for SET messages (before onmessage)
self.addEventListener('message', (event) => {
if (event.data.type === 'SET_COMPLEX_MESSAGE') {
complexMessage = event.data.message;
} else if (event.data.type === 'SET_TOO_LARGE_MESSAGE') {
largeFilesMessage = event.data.message;
}
});
self.onmessage = async function (e) {
const { text1, text2, color1, color2 } = e.data;
console.log('Received text for comparison:', { text1, text2 });
const data = e.data;
if (data.type !== 'COMPARE') {
console.log('Worker ignored non-COMPARE message');
return;
}
const { text1, text2, color1, color2 } = data;
console.log('Received text for comparison:', { lengths: { text1: text1.length, text2: text2.length } }); // Safe Log
const startTime = performance.now();
if (text1.trim() === "" || text2.trim() === "") {
// Safe Trim
if (!text1 || !text2 || text1.trim() === "" || text2.trim() === "") {
self.postMessage({ status: 'error', message: 'One or both of the texts are empty.' });
return;
}
const words1 = text1.split(' ');
const words2 = text2.split(' ');
// Robust Word-Split (handles spaces/punctuation better)
const words1 = text1.trim().split(/\s+/).filter(w => w.length > 0);
const words2 = text2.trim().split(/\s+/).filter(w => w.length > 0);
const MAX_WORD_COUNT = 150000;
const COMPLEX_WORD_COUNT = 50000;
const BATCH_SIZE = 5000; // Define a suitable batch size for processing
@@ -21,44 +42,28 @@ self.onmessage = async function (e) {
const isComplex = words1.length > COMPLEX_WORD_COUNT || words2.length > COMPLEX_WORD_COUNT;
const isTooLarge = words1.length > MAX_WORD_COUNT || words2.length > MAX_WORD_COUNT;
let complexMessage = 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
let tooLargeMessage = 'One or Both of the provided documents are too large to process';
// Listen for messages from the main thread
self.addEventListener('message', (event) => {
if (event.data.type === 'SET_TOO_LARGE_MESSAGE') {
tooLargeMessage = event.data.message;
}
if (event.data.type === 'SET_COMPLEX_MESSAGE') {
complexMessage = event.data.message;
}
});
if (isTooLarge) {
self.postMessage({
status: 'warning',
message: tooLargeMessage,
});
self.postMessage({ status: 'error', message: largeFilesMessage });
return;
} else {
if (isComplex) {
self.postMessage({
status: 'warning',
message: complexMessage,
});
}
// Perform diff operation depending on document size
const differences = isComplex
? await staggeredBatchDiff(words1, words2, color1, color2, BATCH_SIZE, OVERLAP_SIZE)
: diff(words1, words2, color1, color2);
console.log(`Diff operation took ${performance.now() - startTime} milliseconds`);
self.postMessage({ status: 'success', differences });
}
if (isComplex) {
self.postMessage({ status: 'warning', message: complexMessage });
}
// Diff based on size
let differences;
if (isComplex) {
differences = await staggeredBatchDiff(words1, words2, color1 || '#ff0000', color2 || '#008000', BATCH_SIZE, OVERLAP_SIZE);
} else {
differences = diff(words1, words2, color1 || '#ff0000', color2 || '#008000');
}
console.log(`Diff took ${performance.now() - startTime} ms for ${words1.length + words2.length} words`);
self.postMessage({ status: 'success', differences });
};
//Splits text into smaller batches to run through diff checking algorithms. overlaps the batches to help ensure
// Splits text into smaller batches to run through diff checking algorithms. overlaps the batches to help ensure
async function staggeredBatchDiff(words1, words2, color1, color2, batchSize, overlapSize) {
const differences = [];
const totalWords1 = words1.length;
@@ -67,10 +72,9 @@ async function staggeredBatchDiff(words1, words2, color1, color2, batchSize, ove
let previousEnd1 = 0; // Track where the last batch ended in words1
let previousEnd2 = 0; // Track where the last batch ended in words2
// Function to determine if differences are large, differences that are too large indicate potential error in batching
const isLargeDifference = (differences) => {
return differences.length > 50;
};
// Track processed indices to dedupe overlaps
const processed1 = new Set();
const processed2 = new Set();
while (previousEnd1 < totalWords1 || previousEnd2 < totalWords2) {
// Define the next chunk boundaries
@@ -80,66 +84,130 @@ async function staggeredBatchDiff(words1, words2, color1, color2, batchSize, ove
const start2 = previousEnd2;
const end2 = Math.min(start2 + batchSize, totalWords2);
//If difference is too high decrease batch size for more granular check
const dynamicBatchSize = isLargeDifference(differences) ? batchSize / 2 : batchSize;
// Adaptive: If many diffs, smaller batch (max 3x downscale)
const recentDiffs = differences.slice(-100).filter(([c]) => c !== 'black').length;
// If difference is too high decrease batch size for more granular check
const dynamicBatchSize = Math.max(batchSize / Math.min(8, 1 + recentDiffs / 50), batchSize / 8);
// Adjust the size of the current chunk using dynamic batch size
const batchWords1 = words1.slice(start1, end1 + dynamicBatchSize);
const batchWords2 = words2.slice(start2, end2 + dynamicBatchSize);
const extendedEnd1 = Math.min(end1 + dynamicBatchSize, totalWords1);
const extendedEnd2 = Math.min(end2 + dynamicBatchSize, totalWords2);
const batchWords1 = words1.slice(start1, extendedEnd1);
const batchWords2 = words2.slice(start2, extendedEnd2);
// Include overlap from the previous chunk
const overlapWords1 = previousEnd1 > 0 ? words1.slice(Math.max(0, previousEnd1 - overlapSize), previousEnd1) : [];
const overlapWords2 = previousEnd2 > 0 ? words2.slice(Math.max(0, previousEnd2 - overlapSize), previousEnd2) : [];
const overlapStart1 = Math.max(0, previousEnd1 - overlapSize);
const overlapStart2 = Math.max(0, previousEnd2 - overlapSize);
const overlapWords1 = previousEnd1 > 0 ? words1.slice(overlapStart1, previousEnd1) : [];
const overlapWords2 = previousEnd2 > 0 ? words2.slice(overlapStart2, previousEnd2) : [];
// Combine overlaps and current batches for comparison
const combinedWords1 = overlapWords1.concat(batchWords1);
const combinedWords2 = overlapWords2.concat(batchWords2);
const combinedWords1 = [...overlapWords1, ...batchWords1];
const combinedWords2 = [...overlapWords2, ...batchWords2];
// Perform the diff on the combined words
const batchDifferences = diff(combinedWords1, combinedWords2, color1, color2);
differences.push(...batchDifferences);
// Update the previous end indices based on the results of this batch
const combinedIndices1 = [];
for (let i = overlapStart1; i < previousEnd1; i++) {
combinedIndices1.push(i);
}
for (let i = start1; i < extendedEnd1; i++) {
combinedIndices1.push(i);
}
const combinedIndices2 = [];
for (let i = overlapStart2; i < previousEnd2; i++) {
combinedIndices2.push(i);
}
for (let i = start2; i < extendedEnd2; i++) {
combinedIndices2.push(i);
}
let pointer1 = 0;
let pointer2 = 0;
const filteredBatch = [];
batchDifferences.forEach(([color, word]) => {
if (color === color1) {
const globalIndex1 = combinedIndices1[pointer1];
if (globalIndex1 === undefined || !processed1.has(globalIndex1)) {
filteredBatch.push([color, word]);
}
if (globalIndex1 !== undefined) {
processed1.add(globalIndex1);
}
pointer1++;
} else if (color === color2) {
const globalIndex2 = combinedIndices2[pointer2];
if (globalIndex2 === undefined || !processed2.has(globalIndex2)) {
filteredBatch.push([color, word]);
}
if (globalIndex2 !== undefined) {
processed2.add(globalIndex2);
}
pointer2++;
} else {
const globalIndex1 = combinedIndices1[pointer1];
const globalIndex2 = combinedIndices2[pointer2];
const alreadyProcessed = (globalIndex1 !== undefined && processed1.has(globalIndex1)) && (globalIndex2 !== undefined && processed2.has(globalIndex2));
if (!alreadyProcessed) {
filteredBatch.push([color, word]);
}
if (globalIndex1 !== undefined) {
processed1.add(globalIndex1);
}
if (globalIndex2 !== undefined) {
processed2.add(globalIndex2);
}
pointer1++;
pointer2++;
}
});
differences.push(...filteredBatch);
// Mark as processed
for (let k = start1; k < end1; k++) processed1.add(k);
for (let k = start2; k < end2; k++) processed2.add(k);
previousEnd1 = end1;
previousEnd2 = end2;
// Yield for async (avoids blocking)
await new Promise(resolve => setTimeout(resolve, 0));
}
return differences;
}
// Standard diff function for small text comparisons
function diff(words1, words2, color1, color2) {
console.log(`Starting diff between ${words1.length} words and ${words2.length} words`);
const matrix = Array.from({ length: words1.length + 1 }, () => Array(words2.length + 1).fill(0));
console.log(`Diff: ${words1.length} vs ${words2.length} words`);
const oldStr = words1.join(' '); // As string for diff.js
const newStr = words2.join(' ');
// Static method: No 'new' needed, avoids constructor error
const changes = Diff.diffWords(oldStr, newStr, { ignoreWhitespace: true });
for (let i = 1; i <= words1.length; i++) {
for (let j = 1; j <= words2.length; j++) {
matrix[i][j] = words1[i - 1] === words2[j - 1]
? matrix[i - 1][j - 1] + 1
: Math.max(matrix[i][j - 1], matrix[i - 1][j]);
}
}
return backtrack(matrix, words1, words2, color1, color2);
}
// Backtrack function to find differences
function backtrack(matrix, words1, words2, color1, color2) {
let i = words1.length, j = words2.length;
// Map changes to [color, word] format (change.value and added/removed)
const differences = [];
changes.forEach(change => {
const value = change.value;
const op = change.added ? 1 : change.removed ? -1 : 0;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
differences.unshift(['black', words1[i - 1]]);
i--; j--;
} else if (j > 0 && (i === 0 || matrix[i][j] === matrix[i][j - 1])) {
differences.unshift([color2, words2[j - 1]]);
j--;
} else {
differences.unshift([color1, words1[i - 1]]);
i--;
}
}
// Split value into words and process
const words = value.split(/\s+/).filter(w => w.length > 0);
words.forEach(word => {
if (op === 0) { // Equal
differences.push(['black', word]);
} else if (op === 1) { // Insert
differences.push([color2, word]);
} else if (op === -1) { // Delete
differences.push([color1, word]);
}
});
});
return differences;
}
@@ -2,6 +2,12 @@
if (window.isDownloadScriptInitialized) return; // Prevent re-execution
window.isDownloadScriptInitialized = true;
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
// Global PDF processing count tracking for survey system
window.incrementPdfProcessingCount = function() {
let pdfProcessingCount = parseInt(localStorage.getItem('pdfProcessingCount') || '0');
@@ -234,8 +240,13 @@
async function getPDFPageCount(file) {
try {
const arrayBuffer = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
})
.promise;
return pdf.numPages;
} catch (error) {
console.error('Error getting PDF page count:', error);
@@ -245,7 +256,7 @@
async function checkAndDecryptFiles(url, files) {
const decryptedFiles = [];
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
// Extract the base URL
const baseUrl = new URL(url);
@@ -271,7 +282,10 @@
}
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
const loadingTask = pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
});
console.log(`Attempting to load PDF: ${file.name}`);
const pdf = await loadingTask.promise;
@@ -220,7 +220,7 @@ document.addEventListener('DOMContentLoaded', async function () {
});
}
try {
const response = await fetch('/files/popularity.txt');
const response = await fetch('./files/popularity.txt');
if (!response.ok) {
const errorText = await response.text().catch(() => '');
const errorMsg = errorText || response.statusText || 'Request failed';
+13 -1
View File
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
let currentSort = {
field: null,
descending: false,
@@ -73,7 +79,13 @@ async function displayFiles(files) {
async function getPDFPageCount(file) {
const blobUrl = URL.createObjectURL(file);
const pdf = await pdfjsLib.getDocument(blobUrl).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
url: blobUrl,
})
.promise;
URL.revokeObjectURL(blobUrl);
return pdf.numPages;
}
@@ -8,6 +8,12 @@ import { AddFilesCommand } from './commands/add-page.js';
import { DecryptFile } from '../DecryptFiles.js';
import { CommandSequence } from './commands/commands-sequence.js';
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
const isSvgFile = (file) => {
if (!file) return false;
const type = (file.type || '').toLowerCase();
@@ -479,8 +485,11 @@ class PdfContainer {
}
async toRenderer(objectUrl) {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(objectUrl).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument({
url: objectUrl,
...PDFJS_DEFAULT_OPTIONS,
}).promise;
return {
document: pdf,
pageCount: pdf.numPages,
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
window.goToFirstOrLastPage = goToFirstOrLastPage;
document.getElementById('download-pdf').addEventListener('click', async () => {
@@ -31,8 +37,11 @@ document.querySelector('input[name=pdf-upload]').addEventListener('change', asyn
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, '');
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: pdfData,
}).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
var canvas = document.getElementById('contrast-pdf-canvas');
var context = canvas.getContext('2d');
var originalImageData = null;
@@ -9,8 +15,11 @@ async function renderPDFAndSaveOriginalImageData(file) {
var fileReader = new FileReader();
fileReader.onload = async function () {
var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({data: data}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: data,
}).promise;
// Get the number of pages in the PDF
var numPages = pdf.numPages;
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
const deleteAllCheckbox = document.querySelector('#deleteAll');
let inputs = document.querySelectorAll('input');
const customMetadataDiv = document.getElementById('customMetadata');
@@ -43,8 +49,13 @@ fileInput.addEventListener('change', async function () {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
var url = URL.createObjectURL(file);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(url).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
url: url,
})
.promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info;
console.log(pdfMetadata);
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
@@ -42,12 +48,17 @@ function renderPageFromFile(file) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: typedArray,
})
.promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
@@ -37,12 +43,17 @@ btn1Object.addEventListener('click', function (e) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: typedArray,
})
.promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -58,12 +69,17 @@ btn2Object.addEventListener('click', function (e) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: typedArray,
})
.promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -75,12 +91,17 @@ function renderPageFromFile(file) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: typedArray,
})
.promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageNumbers.value = currentPage;
};
reader.readAsArrayBuffer(file);
@@ -1,3 +1,9 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
window.toggleSignatureView = toggleSignatureView;
window.previewSignature = previewSignature;
window.addSignatureFromPreview = addSignatureFromPreview;
@@ -70,9 +76,11 @@ document
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc =
"./pdfjs-legacy/pdf.worker.mjs";
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: pdfData,
}).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
@@ -38,56 +38,31 @@
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.epub,.mobi,.azw3,.fb2,.txt,.docx', inputText=#{ebookToPDF.selectText})}">
</div>
<div class="form-check mb-2">
<input class="form-check-input"
id="embedAllFonts"
name="embedAllFonts"
type="checkbox"
value="true">
<label for="embedAllFonts"
th:text="#{ebookToPDF.embedAllFonts}">
Embed all fonts in PDF
</label>
<div class="form-check mb-3">
<input id="embedAllFonts" name="embedAllFonts" type="checkbox" value="true">
<label for="embedAllFonts" th:text="#{ebookToPDF.embedAllFonts}"></label>
</div>
<div class="form-check mb-2">
<input class="form-check-input"
id="includeTableOfContents"
<div class="form-check mb-3">
<input id="includeTableOfContents"
name="includeTableOfContents"
type="checkbox"
value="true">
<label
for="includeTableOfContents"
th:text="#{ebookToPDF.includeTableOfContents}">
Add table of contents
</label>
<label for="includeTableOfContents" th:text="#{ebookToPDF.includeTableOfContents}"></label>
</div>
<div class="form-check mb-2">
<input class="form-check-input"
id="includePageNumbers"
name="includePageNumbers"
type="checkbox"
value="true">
<label
for="includePageNumbers"
th:text="#{ebookToPDF.includePageNumbers}">
Add page numbers
</label>
<div class="form-check mb-3">
<input id="includePageNumbers" name="includePageNumbers" type="checkbox" value="true">
<label for="includePageNumbers" th:text="#{ebookToPDF.includePageNumbers}"></label>
</div>
<div class="form-check mb-3"
th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
<input class="form-check-input"
id="optimizeForEbook"
<input id="optimizeForEbook"
name="optimizeForEbook"
type="checkbox"
value="true">
<label
for="optimizeForEbook"
th:text="#{ebookToPDF.optimizeForEbook}">
Optimize PDF for ebook readers (uses Ghostscript)
</label>
<label for="optimizeForEbook" th:text="#{ebookToPDF.optimizeForEbook}"></label>
</div>
<button class="btn btn-primary"
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html th:data-language="${#locale.toString()}"
th:dir="#{language.direction}"
th:lang="${#locale.language}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToEpub.title}, header=#{pdfToEpub.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon convert">menu_book</span>
<span class="tool-header-text"
th:text="#{pdfToEpub.header}"></span>
</div>
<p th:text="#{processTimeWarning}"></p>
<div class="alert alert-warning"
th:if="${!@endpointConfiguration.isGroupEnabled('Calibre')}">
<span th:text="#{pdfToEpub.calibreDisabled}">Calibre support is disabled.</span>
</div>
<form enctype="multipart/form-data"
id="pdfToEpubForm"
method="post"
th:action="@{'/api/v1/convert/pdf/epub'}"
th:if="${@endpointConfiguration.isGroupEnabled('Calibre')}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf', inputText=#{pdfToEpub.selectText})}">
</div>
<div class="mb-3">
<label class="form-label" for="outputFormat"
th:text="#{pdfToEpub.outputFormat}"></label>
<select class="form-select"
id="outputFormat"
name="outputFormat">
<option selected
th:text="#{pdfToEpub.outputFormat.epub}"
value="EPUB"></option>
<option th:text="#{pdfToEpub.outputFormat.azw3}"
value="AZW3"></option>
</select>
</div>
<div class="form-check mb-3">
<input checked id="detectChapters" name="detectChapters" type="checkbox">
<label for="detectChapters" th:text="#{pdfToEpub.detectChapters}"></label>
</div>
<div class="mb-3">
<label class="form-label" for="targetDevice"
th:text="#{pdfToEpub.targetDevice}"></label>
<select class="form-select"
id="targetDevice"
name="targetDevice">
<option selected
th:text="#{pdfToEpub.targetDevice.tablet}"
value="TABLET_PHONE_IMAGES"></option>
<option th:text="#{pdfToEpub.targetDevice.kindle}"
value="KINDLE_EINK_TEXT"></option>
</select>
</div>
<button class="btn btn-primary"
id="submitBtn"
th:text="#{pdfToEpub.submit}"
type="submit">Convert</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -41,7 +41,7 @@
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:inline="javascript">
document.getElementById('fileInput-input').addEventListener('change', async () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const fileInput = document.getElementById('fileInput-input');
const resultDiv = document.getElementById('result');
@@ -89,6 +89,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-cbr', 'auto_stories', 'home.pdfToCbr.title', 'home.pdfToCbr.desc', 'pdfToCbr.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-epub', 'menu_book', 'home.pdfToEpub.title', 'home.pdfToEpub.desc', 'pdfToEpub.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
</div>
@@ -163,6 +166,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-epub', 'menu_book', 'home.pdfToEpub.title', 'home.pdfToEpub.desc', 'pdfToEpub.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'convert')}">
</div>
@@ -49,6 +49,9 @@
const updateBreakingChanges = /*[[#{update.breakingChanges}]]*/ 'Breaking Changes:';
const updateBreakingChangesDefault = /*[[#{update.breakingChangesDefault}]]*/ 'This version contains breaking changes';
const updateMigrationGuide = /*[[#{update.migrationGuide}]]*/ 'Migration Guide';
// PDF.js path
const pdfjsPath = /*[[@{'/pdfjs-legacy/'}]]*/ './pdfjs-legacy/';
</script>
<script th:src="@{'/js/homecard.js'}"></script>
<script th:src="@{'/js/githubVersion.js'}"></script>
@@ -58,7 +58,7 @@
</script>
<script type="module">
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
</script>
<script th:src="@{'/js/merge.js'}"></script>
</div>
@@ -79,7 +79,7 @@
</div>
</div>
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
<button class="btn btn-primary" id="compareBtn" onclick="comparePDFs(event)" th:text="#{compare.submit}"></button>
<div class="row">
<div class="col-md-6">
@@ -105,7 +105,8 @@
result2.addEventListener('scroll', function () {
result1.scrollTop = result2.scrollTop;
});
async function comparePDFs() {
async function comparePDFs(event) {
const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0];
var color1 = document.getElementById('color-box1').value;
@@ -113,137 +114,216 @@
const complexMessage = /*[[#{compare.complex.message}]]*/ 'One or both of the provided documents are large files, accuracy of comparison may be reduced';
const largeFilesMessage = /*[[#{compare.large.file.message}]]*/ 'One or Both of the provided documents are too large to process';
const noTextMessage = /*[[#{compare.no.text.message}]]*/ 'One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison."';
const noTextMessage = /*[[#{compare.no.text.message}]]*/ 'One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.';
const invalidPdfMessage = /*[[#{compare.invalid.pdf.message}]]*/ 'One or both files are not valid PDFs. Please check and re-upload.';
const submitText = /*[[#{compare.submit}]]*/ 'Compare';
if (!file1 || !file2) {
console.error("Please select two PDF files to compare");
alert('Please select two PDF files to compare');
return;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]);
const extractText = async (pdf) => {
const pages = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const strings = content.items.map(item => item.str);
pages.push(strings.join(" "));
}
return pages.join(" ");
};
const [text1, text2] = await Promise.all([
extractText(pdf1),
extractText(pdf2)
]);
if (text1.trim() === "" || text2.trim() === "") {
alert(noTextMessage);
// Basic checks
if (file1.size === 0 || file2.size === 0) {
alert('One or both files are empty.');
return;
}
if (file1.size > 100 * 1024 * 1024 || file2.size > 100 * 1024 * 1024) {
alert(largeFilesMessage);
return;
}
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
const loading = /*[[#{loading}]]*/ 'Loading...';
resultDiv1.innerHTML = loading;
resultDiv2.innerHTML = loading;
// Create a new Worker
const worker = new Worker('./js/compare/pdfWorker.js');
// Post messages to the worker
worker.postMessage({
type: 'SET_COMPLEX_MESSAGE',
message: complexMessage
});
worker.postMessage({
type: 'SET_TOO_LARGE_MESSAGE',
message: largeFilesMessage
});
// Error handling for the worker
worker.onerror = function (error) {
console.error('Worker error:', error);
// PDF.js setup (Legacy-safe: Worker disabled)
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
disableWorker: true // Avoids Legacy CMap errors without changing PDF.js
};
worker.onmessage = function (e) {
const { status, differences, message } = e.data;
if (status === 'error') {
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
resultDiv1.innerHTML = '';
resultDiv2.innerHTML = '';
alert(message);
return;
const button = event.target;
button.disabled = true;
button.textContent = 'Processing...';
try {
// Load ArrayBuffer
const [data1, data2] = await Promise.all([
readFileAsArrayBuffer(file1),
readFileAsArrayBuffer(file2)
]);
// Header validation (prevents InvalidPDFException)
await validatePdfHeader(data1, 'File 1');
await validatePdfHeader(data2, 'File 2');
// Load PDFs
const [pdf1, pdf2] = await Promise.all([
loadPdfWithErrorHandling({ ...PDFJS_DEFAULT_OPTIONS, data: data1 }, 'File 1'),
loadPdfWithErrorHandling({ ...PDFJS_DEFAULT_OPTIONS, data: data2 }, 'File 2')
]);
// Extract text
result1.innerHTML = 'Extracting text from File 1...';
result2.innerHTML = 'Extracting text from File 2...';
const [text1, text2] = await Promise.all([
extractText(pdf1, 'File 1', result1),
extractText(pdf2, 'File 2', result2)
]);
if (text1.trim() === "" || text2.trim() === "") {
throw new Error(noTextMessage);
}
if (status === 'success' && differences) {
console.log('Differences:', differences);
displayDifferences(differences);
}
if (event.data.status === 'warning') {
console.warn(event.data.message);
alert(event.data.message);
}
};
worker.postMessage({ text1, text2, color1, color2 });
// Worker diff
await processWithWorker(text1, text2, color1, color2, complexMessage, largeFilesMessage);
const displayDifferences = (differences) => {
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = "";
resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
if (color === color2) {
span1.style.color = "transparent";
span1.style.userSelect = "none";
span2.style.color = color;
}
// If it's a deletion, show it in in the first document and transparent in the second
else if (color === color1) {
span1.style.color = color;
span2.style.color = "transparent";
span2.style.userSelect = "none";
}
// If it's unchanged, show it in black in both
else {
span1.style.color = color;
span2.style.color = color;
}
span1.textContent = word;
span2.textContent = word;
resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2);
// Add space after each word, or a new line if the word ends with a full stop
const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>";
} else {
spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " ";
}
resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2);
});
};
} catch (error) {
console.error('Comparison failed:', error);
alert(error.message || invalidPdfMessage);
result1.innerHTML = '';
result2.innerHTML = '';
} finally {
button.disabled = false;
button.textContent = submitText;
}
}
// FileReader helper
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
// Header validation (PDF.js-specific, but client-side)
async function validatePdfHeader(data, fileName) {
const header = new Uint8Array(data.slice(0, 8));
const headerStr = String.fromCharCode(...header);
console.log(`${fileName} header:`, headerStr);
if (!headerStr.startsWith('%PDF-')) {
throw new Error(`${fileName} is not a valid PDF (header: ${headerStr}).`);
}
if (data.byteLength < 100) {
throw new Error(`${fileName} is too short.`);
}
}
// PDF loading with catch
function loadPdfWithErrorHandling(options, fileName) {
return pdfjsLib.getDocument(options).promise
.then(pdf => {
console.log(`${fileName} loaded: ${pdf.numPages} pages`);
return pdf;
})
.catch(err => {
console.error(`${fileName} load failed:`, err);
if (err.name === 'InvalidPDFException') {
throw new Error(`${fileName}: Invalid PDF structure. Re-upload.`);
}
throw err;
});
}
// Text extraction
async function extractText(pdf, fileName, statusElement) {
const pages = [];
const totalPages = pdf.numPages;
for (let i = 1; i <= totalPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const strings = content.items.map(item => item.str).join(' ');
pages.push(strings);
statusElement.innerHTML = `${fileName}: ${Math.round((i / totalPages) * 100)}%`;
}
return pages.join(' ');
}
// Worker processing
async function processWithWorker(text1, text2, color1, color2, complexMessage, largeFilesMessage) {
return new Promise((resolve, reject) => {
const worker = new Worker('./js/compare/pdfWorker.js');
const timeout = setTimeout(() => {
worker.terminate();
reject(new Error('Timeout: Files too complex.'));
}, 30000);
worker.postMessage({ type: 'SET_COMPLEX_MESSAGE', message: complexMessage });
worker.postMessage({ type: 'SET_TOO_LARGE_MESSAGE', message: largeFilesMessage });
worker.onerror = (error) => {
clearTimeout(timeout);
worker.terminate();
reject(new Error('Worker error: ' + error.message));
};
worker.onmessage = (e) => {
clearTimeout(timeout);
const { status, differences, message } = e.data;
if (status === 'error') {
worker.terminate();
reject(new Error(message));
return;
}
if (status === 'warning') {
alert(message);
}
if (status === 'success' && differences) {
displayDifferences(differences, color1, color2);
worker.terminate();
resolve();
}
};
worker.postMessage({ type: 'COMPARE', text1, text2, color1, color2 });
});
}
// Display differences
function displayDifferences(differences, color1, color2) {
const resultDiv1 = document.getElementById("result1");
const resultDiv2 = document.getElementById("result2");
resultDiv1.innerHTML = "";
resultDiv2.innerHTML = "";
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
if (color === color2) {
span1.style.color = "transparent";
span1.style.userSelect = "none";
span2.style.color = color;
} else if (color === color1) {
span1.style.color = color;
span2.style.color = "transparent";
span2.style.userSelect = "none";
} else {
span1.style.color = color || 'black';
span2.style.color = color || 'black';
}
span1.textContent = word;
span2.textContent = word;
resultDiv1.appendChild(span1);
resultDiv2.appendChild(span2);
const spaceOrNewline1 = document.createElement("span");
const spaceOrNewline2 = document.createElement("span");
if (word.endsWith(".")) {
spaceOrNewline1.innerHTML = "<br>";
spaceOrNewline2.innerHTML = "<br>";
} else {
spaceOrNewline1.textContent = " ";
spaceOrNewline2.textContent = " ";
}
resultDiv1.appendChild(spaceOrNewline1);
resultDiv2.appendChild(spaceOrNewline2);
});
}
</script>
</div>
</div>
@@ -59,12 +59,24 @@
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script type="module">
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
window.pdfjsLib = pdfjsLib;
</script>
<script>
const angleInput = document.getElementById("angleInput");
const fileInput = document.getElementById("fileInput-input");
const previewContainer = document.getElementById("previewContainer");
// const preview = document.getElementById("pdf-preview");
// PDF.js setup (with CMap options to fix font loading warning)
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
disableWorker: true // Avoids Legacy CMap errors
};
fileInput.addEventListener("change", async function () {
console.log("loading pdf");
@@ -74,9 +86,9 @@
if (existingPreview) {
existingPreview.remove();
}
var url = URL.createObjectURL(fileInput.files[0])
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
const pdf = await pdfjsLib.getDocument(url).promise;
const url = URL.createObjectURL(fileInput.files[0]);
const pdf = await window.pdfjsLib.getDocument({ ...PDFJS_DEFAULT_OPTIONS, url }).promise;
const page = await pdf.getPage(1);
const canvas = document.createElement("canvas");
@@ -91,7 +103,7 @@
}
// render the page onto the canvas
var renderContext = {
const renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 })
};
@@ -0,0 +1,328 @@
package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.OutputFormat;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ConvertPDFToEpubControllerTest {
private static final MediaType EPUB_MEDIA_TYPE = MediaType.valueOf("application/epub+zip");
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@InjectMocks private ConvertPDFToEpubController controller;
@Test
void convertPdfToEpub_buildsGoldenCommandAndCleansUp() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "novel.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
Path workingDir = Files.createTempDirectory("pdf-epub-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
AtomicReference<Path> deletedDir = new AtomicReference<>();
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
deletedDir.set(dir);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedInput = workingDir.resolve("novel.pdf");
Path expectedOutput = workingDir.resolve("novel.epub");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "epub");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("novel.pdf", "_convertedToEPUB.epub"))
.thenReturn("novel_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals(11, command.size());
assertEquals("ebook-convert", command.get(0));
assertEquals(expectedInput.toString(), command.get(1));
assertEquals(expectedOutput.toString(), command.get(2));
assertTrue(command.contains("--enable-heuristics"));
assertTrue(command.contains("--insert-blank-line"));
assertTrue(command.contains("--filter-css"));
assertTrue(
command.contains(
"font-family,color,background-color,margin-left,margin-right"));
assertTrue(command.contains("--chapter"));
assertTrue(command.stream().anyMatch(arg -> arg.contains("Chapter\\s+")));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.TABLET_PHONE_IMAGES.getCalibreProfile()));
assertEquals(EPUB_MEDIA_TYPE, response.getHeaders().getContentType());
assertEquals(
"novel_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
assertEquals(workingDir, deletedDir.get());
} finally {
deleteIfExists(workingDir);
}
}
@Test
void convertPdfToEpub_respectsOptions() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "story.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
request.setDetectChapters(false);
request.setTargetDevice(TargetDevice.KINDLE_EINK_TEXT);
Path workingDir = Files.createTempDirectory("pdf-epub-options-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedOutput = workingDir.resolve("story.epub");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "epub");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("story.pdf", "_convertedToEPUB.epub"))
.thenReturn("story_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg)));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.KINDLE_EINK_TEXT.getCalibreProfile()));
assertTrue(command.contains("--filter-css"));
assertTrue(
command.contains(
"font-family,color,background-color,margin-left,margin-right"));
assertTrue(command.size() >= 9);
assertEquals(EPUB_MEDIA_TYPE, response.getHeaders().getContentType());
assertEquals(
"story_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
} finally {
deleteIfExists(workingDir);
}
}
@Test
void convertPdfToAzw3_buildsCorrectCommandAndOutput() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "book.pdf", "application/pdf", "content".getBytes());
ConvertPdfToEpubRequest request = new ConvertPdfToEpubRequest();
request.setFileInput(pdfFile);
request.setOutputFormat(OutputFormat.AZW3);
request.setDetectChapters(false);
request.setTargetDevice(TargetDevice.KINDLE_EINK_TEXT);
Path workingDir = Files.createTempDirectory("pdf-azw3-test-");
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
doAnswer(
invocation -> {
Path dir = invocation.getArgument(0);
if (Files.exists(dir)) {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
return null;
})
.when(tempFileManager)
.deleteTempDirectory(any(Path.class));
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
ProcessExecutor executor = mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
ProcessExecutorResult execResult = mock(ProcessExecutorResult.class);
when(execResult.getRc()).thenReturn(0);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
Path expectedInput = workingDir.resolve("book.pdf");
Path expectedOutput = workingDir.resolve("book.azw3");
when(executor.runCommandWithOutputHandling(
commandCaptor.capture(), eq(workingDir.toFile())))
.thenAnswer(
invocation -> {
Files.writeString(expectedOutput, "azw3");
return execResult;
});
gu.when(() -> GeneralUtils.generateFilename("book.pdf", "_convertedToAZW3.azw3"))
.thenReturn("book_convertedToAZW3.azw3");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals("ebook-convert", command.get(0));
assertEquals(expectedInput.toString(), command.get(1));
assertEquals(expectedOutput.toString(), command.get(2));
assertTrue(command.contains("--enable-heuristics"));
assertTrue(command.contains("--insert-blank-line"));
assertTrue(command.contains("--filter-css"));
assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg)));
assertTrue(command.contains("--output-profile"));
assertTrue(command.contains(TargetDevice.KINDLE_EINK_TEXT.getCalibreProfile()));
assertEquals(
MediaType.valueOf("application/vnd.amazon.ebook"),
response.getHeaders().getContentType());
assertEquals(
"book_convertedToAZW3.azw3",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("azw3", new String(response.getBody(), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
} finally {
deleteIfExists(workingDir);
}
}
private void deleteIfExists(Path directory) throws IOException {
if (directory == null || !Files.exists(directory)) {
return;
}
try (Stream<Path> paths = Files.walk(directory)) {
paths.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
});
}
}
}
@@ -110,6 +110,42 @@ class ConverterWebControllerTest {
}
}
@Nested
@DisplayName("PDF to EPUB endpoint tests")
class PdfToEpubTests {
@Test
@DisplayName("Should return 404 when endpoint disabled")
void shouldReturn404WhenDisabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-epub"))).thenReturn(false);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-epub")).andExpect(status().isNotFound());
}
}
@Test
@DisplayName("Should return OK when endpoint enabled")
void shouldReturnOkWhenEnabled() throws Exception {
try (MockedStatic<ApplicationContextProvider> acp =
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
when(endpointConfig.isEndpointEnabled(eq("pdf-to-epub"))).thenReturn(true);
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
.thenReturn(endpointConfig);
mockMvc.perform(get("/pdf-to-epub"))
.andExpect(status().isOk())
.andExpect(view().name("convert/pdf-to-epub"))
.andExpect(model().attribute("currentPage", "pdf-to-epub"));
}
}
}
@Test
@DisplayName("Should handle pdf-to-img with default maxDPI=500")
void shouldHandlePdfToImgWithDefaultMaxDpi() throws Exception {
+3 -2
View File
@@ -30,6 +30,7 @@ ext {
openSamlVersion = "4.3.2"
commonmarkVersion = "0.27.0"
googleJavaFormatVersion = "1.28.0"
logback = "1.5.21"
tempJrePath = null
}
@@ -126,8 +127,8 @@ subprojects {
implementation 'io.github.pixee:java-security-toolkit:1.2.2'
//tmp for security bumps
implementation 'ch.qos.logback:logback-core:1.5.20'
implementation 'ch.qos.logback:logback-classic:1.5.20'
implementation "ch.qos.logback:logback-core:$logback"
implementation "ch.qos.logback:logback-classic:$logback"
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"