Compare commits

..
7 Commits
Author SHA1 Message Date
LudyandGitHub 9a6394e997 Merge branch 'main' into test_file 2025-11-25 10:46:08 +01:00
LudyandGitHub 45443a67ff Merge branch 'main' into test_file 2025-11-25 10:44:49 +01:00
Ludy87 0cac7e02e8 Update build.gradle 2025-11-25 10:44:19 +01:00
Ludy87 2245333622 Update build.gradle 2025-11-25 10:40:49 +01:00
Ludy87 e27276392c Update build.yml 2025-11-25 10:37:00 +01:00
Ludy87 17699fadb1 Update build.yml 2025-11-25 10:34:07 +01:00
Ludy87 ed67440a10 test 2025-11-25 10:32:05 +01:00
35 changed files with 501 additions and 1638 deletions
+69 -1
View File
@@ -224,10 +224,78 @@ 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-portable]
needs: [read_versions, sign_verify, sign_verify-portable]
runs-on: ubuntu-latest
permissions:
contents: write
-4
View File
@@ -33,7 +33,6 @@ 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
@@ -48,7 +47,6 @@ 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
@@ -70,7 +68,6 @@ 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
@@ -90,7 +87,6 @@ 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,6 +478,11 @@ 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,8 +5,11 @@ 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;
@@ -145,6 +148,64 @@ 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,6 +94,32 @@ 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,7 +258,6 @@ 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");
@@ -450,7 +449,6 @@ public class EndpointConfiguration {
// Calibre dependent endpoints
addEndpointToGroup("Calibre", "ebook-to-pdf");
addEndpointToGroup("Calibre", "pdf-to-epub");
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
@@ -1,204 +0,0 @@
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,17 +54,6 @@ 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) {
@@ -1,58 +0,0 @@
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,10 +706,6 @@ 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
@@ -1596,20 +1592,6 @@ 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=Il contenuto dell'URL fa riferimento a risorse non consentite e non può essere convertito
error.disallowedUrlContent=URL content references disallowed resources and cannot be converted
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 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.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.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 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.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.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=Dettagli tecnici
getPdfInfo.summary.technical.section=Technical Details
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=Ritaglio automatico (rileva e rimuove gli spazi bianchi)
crop.autoCrop=Auto-crop (detect and remove white space)
#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=Duplicata
multiTool.duplicate=Duplicate
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 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à.
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.
#pdfToCBR
pdfToCBR.title=PDF in CBR
@@ -1635,15 +1635,15 @@ unlockPDFForms.header=Sbloccare i moduli PDF
unlockPDFForms.submit=Rimuovi
#pdfToPDFA
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.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.submit=Converti
pdfToPDFA.tip=Converti PDF in PDF/A (archiviazione a lungo termine) o PDF/X (produzione di stampa)
pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production)
pdfToPDFA.outputFormat=Formato di output
pdfToPDFA.pdfWithDigitalSignature=Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo.
pdfToPDFA.pdfaFormats=Formati PDF/A (archiviazione a lungo termine)
pdfToPDFA.pdfxFormats=Formati PDF/X (produzione di stampa)
pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving)
pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production)
#PDFToWord
@@ -235,92 +235,6 @@ 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=Пароль
@@ -702,9 +616,9 @@ home.cbrToPdf.title=CBR в PDF
home.cbrToPdf.desc=Конвертируйте архивы комиксов CBR в формат PDF.
cbrToPdf.tags=конвертация,комикс,книга,архив,cbr,rar
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.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.pdfToCbz.title=PDF в CBZ
home.pdfToCbz.desc=Конвертируйте PDF-файлы в архивы комиксов CBZ.
@@ -784,9 +698,9 @@ home.extractImages.title=Извлечь изображения
home.extractImages.desc=Извлекает все изображения из PDF и сохраняет их в zip-архив
extractImages.tags=картинка,фото,сохранение,архив,zip,захват,извлечение
home.pdfToPDFA.title=PDF в PDF/A или PDF/X
home.pdfToPDFA.desc=Преобразуйте PDF в формат PDF/A для длительного хранения или PDF/X для печати
pdfToPDFA.tags=архив,длительное хранение,преобразование,хранение,печать,pdf-a,pdf-x
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.PDFToWord.title=PDF в Word
home.PDFToWord.desc=Преобразование PDF в форматы Word (DOC, DOCX и ODT)
@@ -1229,7 +1143,7 @@ adjustContrast.download=Скачать
crop.title=Обрезка
crop.header=Обрезка PDF
crop.submit=Отправить
crop.autoCrop=Автоматическая обрезка (обнаружение и удаление пробелов)
crop.autoCrop=Auto-crop (detect and remove white space)
#autoSplitPDF
autoSplitPDF.title=Автоматическое разделение PDF
@@ -1415,7 +1329,7 @@ compress.title=Сжать
compress.header=Сжать PDF
compress.credit=Этот сервис использует qpdf для сжатия/оптимизации PDF.
compress.grayscale.label=Примените оттенки серого для сжатия
compress.linearize.label=Линеаризовать PDF-файл для более быстрого просмотра в Интернете
compress.linearize.label=Linearize PDF for faster web viewing
compress.selectText.1=Параметры сжатия
compress.selectText.1.1=1-3 сжатие PDF,</br> 4-6 лёгкое сжатие изображений,</br> 7-9 интенсивное сжатие изображений (значительно снижает качество изображений)
compress.selectText.2=Уровень оптимизации:
@@ -1582,15 +1496,15 @@ cbrToPDF.selectText=Выбрать файл CBR
cbrToPDF.optimizeForEbook=Оптимизировать PDF для чтения электронных книг (используется Ghostscript)
#ebookToPDF
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.
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.
#pdfToCBR
pdfToCBR.title=PDF в CBR
@@ -1619,16 +1533,16 @@ pdfToImage.includeAnnotations=Добавьте аннотации (коммен
#pdfToVideo
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=Преобразовать
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
#addPassword
@@ -1721,15 +1635,15 @@ unlockPDFForms.header=Разблокировать PDF-формы
unlockPDFForms.submit=Удалить
#pdfToPDFA
pdfToPDFA.title=PDF в PDF/A или PDF/X
pdfToPDFA.header=PDF в PDF/A или PDF/X
pdfToPDFA.credit=Этот сервис использует Ghostscript (предпочтительнее) или LibreOffice для преобразования PDF/A, и Ghostscript для преобразования PDF/X
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.submit=Преобразовать
pdfToPDFA.tip=Преобразовать PDF в PDF/A (для длительного хранения) или PDF/X (для печати)
pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production)
pdfToPDFA.outputFormat=Формат вывода
pdfToPDFA.pdfWithDigitalSignature=PDF содержит цифровую подпись. Она будет удалена на следующем шаге.
pdfToPDFA.pdfaFormats=PDF/A форматы (для длительного хранения)
pdfToPDFA.pdfxFormats=PDF/X форматы (для печати)
pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving)
pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production)
#PDFToWord
@@ -2086,7 +2000,7 @@ scannerEffect.resolution=Разрешение (DPI)
# Table of Contents Feature
home.editTableOfContents.title=Редактирование оглавления
home.editTableOfContents.title=Редактир оглавления
home.editTableOfContents.desc=Добавление или редактирование закладок и оглавления в PDF-документах
editTableOfContents.tags=закладки, указатель, индекс, оглавление, главы, разделы, схема, навигация, структура
@@ -2123,209 +2037,3 @@ 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,9 +1,3 @@
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;
@@ -244,7 +238,7 @@ export class DecryptFile {
return {isEncrypted: false, requiresPassword: false};
}
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const arrayBuffer = await file.arrayBuffer();
const arrayBufferForPdfLib = arrayBuffer.slice(0);
@@ -252,14 +246,12 @@ 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,39 +1,18 @@
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 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 { text1, text2, color1, color2 } = e.data;
console.log('Received text for comparison:', { text1, text2 });
const startTime = performance.now();
// Safe Trim
if (!text1 || !text2 || text1.trim() === "" || text2.trim() === "") {
if (text1.trim() === "" || text2.trim() === "") {
self.postMessage({ status: 'error', message: 'One or both of the texts are empty.' });
return;
}
// 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 words1 = text1.split(' ');
const words2 = text2.split(' ');
const MAX_WORD_COUNT = 150000;
const COMPLEX_WORD_COUNT = 50000;
const BATCH_SIZE = 5000; // Define a suitable batch size for processing
@@ -42,28 +21,44 @@ 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: 'error', message: largeFilesMessage });
self.postMessage({
status: 'warning',
message: tooLargeMessage,
});
return;
}
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 });
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 });
}
};
// 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;
@@ -72,9 +67,10 @@ 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
// Track processed indices to dedupe overlaps
const processed1 = new Set();
const processed2 = new Set();
// Function to determine if differences are large, differences that are too large indicate potential error in batching
const isLargeDifference = (differences) => {
return differences.length > 50;
};
while (previousEnd1 < totalWords1 || previousEnd2 < totalWords2) {
// Define the next chunk boundaries
@@ -84,130 +80,66 @@ async function staggeredBatchDiff(words1, words2, color1, color2, batchSize, ove
const start2 = previousEnd2;
const end2 = Math.min(start2 + batchSize, totalWords2);
// 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);
//If difference is too high decrease batch size for more granular check
const dynamicBatchSize = isLargeDifference(differences) ? batchSize / 2 : batchSize;
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);
// 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);
// Include overlap from the previous chunk
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) : [];
const overlapWords1 = previousEnd1 > 0 ? words1.slice(Math.max(0, previousEnd1 - overlapSize), previousEnd1) : [];
const overlapWords2 = previousEnd2 > 0 ? words2.slice(Math.max(0, previousEnd2 - overlapSize), previousEnd2) : [];
// Combine overlaps and current batches for comparison
const combinedWords1 = [...overlapWords1, ...batchWords1];
const combinedWords2 = [...overlapWords2, ...batchWords2];
const combinedWords1 = overlapWords1.concat(batchWords1);
const combinedWords2 = overlapWords2.concat(batchWords2);
// Perform the diff on the combined words
const batchDifferences = diff(combinedWords1, combinedWords2, color1, color2);
differences.push(...batchDifferences);
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);
// Update the previous end indices based on the results of this batch
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(`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 });
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));
// Map changes to [color, word] format (change.value and added/removed)
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;
const differences = [];
changes.forEach(change => {
const value = change.value;
const op = change.added ? 1 : change.removed ? -1 : 0;
// 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]);
}
});
});
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--;
}
}
return differences;
}
@@ -2,12 +2,6 @@
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');
@@ -240,13 +234,8 @@
async function getPDFPageCount(file) {
try {
const arrayBuffer = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
})
.promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise;
return pdf.numPages;
} catch (error) {
console.error('Error getting PDF page count:', error);
@@ -256,7 +245,7 @@
async function checkAndDecryptFiles(url, files) {
const decryptedFiles = [];
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
// Extract the base URL
const baseUrl = new URL(url);
@@ -282,10 +271,7 @@
}
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: arrayBuffer,
});
const loadingTask = pdfjsLib.getDocument({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';
+1 -13
View File
@@ -1,9 +1,3 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
let currentSort = {
field: null,
descending: false,
@@ -79,13 +73,7 @@ async function displayFiles(files) {
async function getPDFPageCount(file) {
const blobUrl = URL.createObjectURL(file);
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
url: blobUrl,
})
.promise;
const pdf = await pdfjsLib.getDocument(blobUrl).promise;
URL.revokeObjectURL(blobUrl);
return pdf.numPages;
}
@@ -8,12 +8,6 @@ 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();
@@ -485,11 +479,8 @@ class PdfContainer {
}
async toRenderer(objectUrl) {
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument({
url: objectUrl,
...PDFJS_DEFAULT_OPTIONS,
}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(objectUrl).promise;
return {
document: pdf,
pageCount: pdf.numPages,
@@ -1,9 +1,3 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
window.goToFirstOrLastPage = goToFirstOrLastPage;
document.getElementById('download-pdf').addEventListener('click', async () => {
@@ -37,11 +31,8 @@ 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 = pdfjsPath + 'pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: pdfData,
}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
@@ -1,9 +1,3 @@
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;
@@ -15,11 +9,8 @@ async function renderPDFAndSaveOriginalImageData(file) {
var fileReader = new FileReader();
fileReader.onload = async function () {
var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: data,
}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({data: data}).promise;
// Get the number of pages in the PDF
var numPages = pdf.numPages;
@@ -1,9 +1,3 @@
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');
@@ -49,13 +43,8 @@ fileInput.addEventListener('change', async function () {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
var url = URL.createObjectURL(file);
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdf = await pdfjsLib
.getDocument({
...PDFJS_DEFAULT_OPTIONS,
url: url,
})
.promise;
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info;
console.log(pdfMetadata);
@@ -1,9 +1,3 @@
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');
@@ -48,17 +42,12 @@ function renderPageFromFile(file) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
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);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -1,9 +1,3 @@
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');
@@ -43,17 +37,12 @@ btn1Object.addEventListener('click', function (e) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
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);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -69,17 +58,12 @@ btn2Object.addEventListener('click', function (e) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
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);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
@@ -91,17 +75,12 @@ function renderPageFromFile(file) {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
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);
});
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageNumbers.value = currentPage;
};
reader.readAsArrayBuffer(file);
@@ -1,9 +1,3 @@
const PDFJS_DEFAULT_OPTIONS = {
cMapUrl: pdfjsPath + 'cmaps/',
cMapPacked: true,
standardFontDataUrl: pdfjsPath + 'standard_fonts/',
};
window.toggleSignatureView = toggleSignatureView;
window.previewSignature = previewSignature;
window.addSignatureFromPreview = addSignatureFromPreview;
@@ -76,11 +70,9 @@ document
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({
...PDFJS_DEFAULT_OPTIONS,
data: pdfData,
}).promise;
pdfjsLib.GlobalWorkerOptions.workerSrc =
"./pdfjs-legacy/pdf.worker.mjs";
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach((el) => {
@@ -38,31 +38,56 @@
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-3">
<input id="embedAllFonts" name="embedAllFonts" type="checkbox" value="true">
<label for="embedAllFonts" th:text="#{ebookToPDF.embedAllFonts}"></label>
<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>
<div class="form-check mb-3">
<input id="includeTableOfContents"
<div class="form-check mb-2">
<input class="form-check-input"
id="includeTableOfContents"
name="includeTableOfContents"
type="checkbox"
value="true">
<label for="includeTableOfContents" th:text="#{ebookToPDF.includeTableOfContents}"></label>
<label
for="includeTableOfContents"
th:text="#{ebookToPDF.includeTableOfContents}">
Add table of contents
</label>
</div>
<div class="form-check mb-3">
<input id="includePageNumbers" name="includePageNumbers" type="checkbox" value="true">
<label for="includePageNumbers" th:text="#{ebookToPDF.includePageNumbers}"></label>
<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>
<div class="form-check mb-3"
th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
<input id="optimizeForEbook"
<input class="form-check-input"
id="optimizeForEbook"
name="optimizeForEbook"
type="checkbox"
value="true">
<label for="optimizeForEbook" th:text="#{ebookToPDF.optimizeForEbook}"></label>
<label
for="optimizeForEbook"
th:text="#{ebookToPDF.optimizeForEbook}">
Optimize PDF for ebook readers (uses Ghostscript)
</label>
</div>
<button class="btn btn-primary"
@@ -1,87 +0,0 @@
<!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 = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const fileInput = document.getElementById('fileInput-input');
const resultDiv = document.getElementById('result');
@@ -89,9 +89,6 @@
<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>
@@ -166,9 +163,6 @@
<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,9 +49,6 @@
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 = pdfjsPath + 'pdf.worker.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
</script>
<script th:src="@{'/js/merge.js'}"></script>
</div>
@@ -79,7 +79,7 @@
</div>
</div>
<button class="btn btn-primary" id="compareBtn" onclick="comparePDFs(event)" th:text="#{compare.submit}"></button>
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
<div class="row">
<div class="col-md-6">
@@ -105,8 +105,7 @@
result2.addEventListener('scroll', function () {
result1.scrollTop = result2.scrollTop;
});
async function comparePDFs(event) {
async function comparePDFs() {
const file1 = document.getElementById("fileInput-input").files[0];
const file2 = document.getElementById("fileInput2-input").files[0];
var color1 = document.getElementById('color-box1').value;
@@ -114,216 +113,137 @@
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 invalidPdfMessage = /*[[#{compare.invalid.pdf.message}]]*/ 'One or both files are not valid PDFs. Please check and re-upload.';
const submitText = /*[[#{compare.submit}]]*/ 'Compare';
const noTextMessage = /*[[#{compare.no.text.message}]]*/ 'One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison."';
if (!file1 || !file2) {
alert('Please select two PDF files to compare');
console.error("Please select two PDF files to compare");
return;
}
// 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;
}
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
// 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
};
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
const [pdf1, pdf2] = await Promise.all([
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
]);
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);
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(" ");
};
// Worker diff
await processWithWorker(text1, text2, color1, color2, complexMessage, largeFilesMessage);
const [text1, text2] = await Promise.all([
extractText(pdf1),
extractText(pdf2)
]);
} catch (error) {
console.error('Comparison failed:', error);
alert(error.message || invalidPdfMessage);
result1.innerHTML = '';
result2.innerHTML = '';
} finally {
button.disabled = false;
button.textContent = submitText;
if (text1.trim() === "" || text2.trim() === "") {
alert(noTextMessage);
return;
}
}
// 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 = "";
const loading = /*[[#{loading}]]*/ 'Loading...';
differences.forEach(([color, word]) => {
const span1 = document.createElement("span");
const span2 = document.createElement("span");
resultDiv1.innerHTML = loading;
resultDiv2.innerHTML = loading;
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';
}
// Create a new Worker
const worker = new Worker('./js/compare/pdfWorker.js');
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);
// 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);
};
worker.onmessage = function (e) {
const { status, differences, message } = e.data;
if (status === 'error') {
resultDiv1.innerHTML = '';
resultDiv2.innerHTML = '';
alert(message);
return;
}
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 });
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);
});
};
}
</script>
</div>
</div>
@@ -59,24 +59,12 @@
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<script type="module">
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath + 'pdf.worker.mjs';
window.pdfjsLib = pdfjsLib;
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
const angleInput = document.getElementById("angleInput");
const fileInput = document.getElementById("fileInput-input");
const previewContainer = document.getElementById("previewContainer");
// 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
};
// const preview = document.getElementById("pdf-preview");
fileInput.addEventListener("change", async function () {
console.log("loading pdf");
@@ -86,9 +74,9 @@
if (existingPreview) {
existingPreview.remove();
}
const url = URL.createObjectURL(fileInput.files[0]);
const pdf = await window.pdfjsLib.getDocument({ ...PDFJS_DEFAULT_OPTIONS, url }).promise;
var url = URL.createObjectURL(fileInput.files[0])
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
const pdf = await pdfjsLib.getDocument(url).promise;
const page = await pdf.getPage(1);
const canvas = document.createElement("canvas");
@@ -103,7 +91,7 @@
}
// render the page onto the canvas
const renderContext = {
var renderContext = {
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 })
};
@@ -1,328 +0,0 @@
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,42 +110,6 @@ 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 {
+2 -3
View File
@@ -30,7 +30,6 @@ ext {
openSamlVersion = "4.3.2"
commonmarkVersion = "0.27.0"
googleJavaFormatVersion = "1.28.0"
logback = "1.5.21"
tempJrePath = null
}
@@ -127,8 +126,8 @@ subprojects {
implementation 'io.github.pixee:java-security-toolkit:1.2.2'
//tmp for security bumps
implementation "ch.qos.logback:logback-core:$logback"
implementation "ch.qos.logback:logback-classic:$logback"
implementation 'ch.qos.logback:logback-core:1.5.20'
implementation 'ch.qos.logback:logback-classic:1.5.20'
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"