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