From 0652299bec3ac3a9268fcaffe3e2b511e3d199fb Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Mon, 9 Dec 2024 20:40:59 +0000 Subject: [PATCH 01/49] fixes --- .../controller/api/SplitPDFController.java | 157 +++++++----- .../api/SplitPdfByChaptersController.java | 136 ++++++----- .../api/SplitPdfBySectionsController.java | 10 +- .../converters/ConvertImgPDFController.java | 223 ++++++++++-------- .../controller/api/misc/OCRController.java | 8 +- 5 files changed, 304 insertions(+), 230 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index e27df1033..a8e74e2a9 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -52,84 +52,115 @@ public class SplitPDFController { "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") public ResponseEntity splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException { - MultipartFile file = request.getFileInput(); - String pages = request.getPageNumbers(); - // open the pdf document - PDDocument document = Loader.loadPDF(file.getBytes()); - // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); - int totalPages = document.getNumberOfPages(); - List pageNumbers = request.getPageNumbersList(document, false); - if (!pageNumbers.contains(totalPages - 1)) { - // Create a mutable ArrayList so we can add to it - pageNumbers = new ArrayList<>(pageNumbers); - pageNumbers.add(totalPages - 1); - } - - logger.info( - "Splitting PDF into pages: {}", - pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); - - // split the document + PDDocument document = null; + Path zipFile = null; List splitDocumentsBoas = new ArrayList<>(); - int previousPageNumber = 0; - for (int splitPoint : pageNumbers) { - try (PDDocument splitDocument = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) { - for (int i = previousPageNumber; i <= splitPoint; i++) { - PDPage page = document.getPage(i); - splitDocument.addPage(page); - logger.info("Adding page {} to split document", i); + + try { + + MultipartFile file = request.getFileInput(); + String pages = request.getPageNumbers(); + // open the pdf document + + document = Loader.loadPDF(file.getBytes()); + // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); + int totalPages = document.getNumberOfPages(); + List pageNumbers = request.getPageNumbersList(document, false); + if (!pageNumbers.contains(totalPages - 1)) { + // Create a mutable ArrayList so we can add to it + pageNumbers = new ArrayList<>(pageNumbers); + pageNumbers.add(totalPages - 1); + } + + logger.info( + "Splitting PDF into pages: {}", + pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); + + // split the document + splitDocumentsBoas = new ArrayList<>(); + int previousPageNumber = 0; + for (int splitPoint : pageNumbers) { + try (PDDocument splitDocument = + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) { + for (int i = previousPageNumber; i <= splitPoint; i++) { + PDPage page = document.getPage(i); + splitDocument.addPage(page); + logger.info("Adding page {} to split document", i); + } + previousPageNumber = splitPoint + 1; + + // Transfer metadata to split pdf + // PdfMetadataService.setMetadataToPdf(splitDocument, metadata); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + splitDocument.save(baos); + + splitDocumentsBoas.add(baos); + } catch (Exception e) { + logger.error("Failed splitting documents and saving them", e); + throw e; } - previousPageNumber = splitPoint + 1; + } - // Transfer metadata to split pdf - // PdfMetadataService.setMetadataToPdf(splitDocument, metadata); + // closing the original document + document.close(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - splitDocument.save(baos); + zipFile = Files.createTempFile("split_documents", ".zip"); - splitDocumentsBoas.add(baos); + String filename = + Filenames.toSimpleFileName(file.getOriginalFilename()) + .replaceFirst("[.][^.]+$", ""); + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { + // loop through the split documents and write them to the zip file + for (int i = 0; i < splitDocumentsBoas.size(); i++) { + String fileName = filename + "_" + (i + 1) + ".pdf"; + ByteArrayOutputStream baos = splitDocumentsBoas.get(i); + byte[] pdf = baos.toByteArray(); + + // Add PDF file to the zip + ZipEntry pdfEntry = new ZipEntry(fileName); + zipOut.putNextEntry(pdfEntry); + zipOut.write(pdf); + zipOut.closeEntry(); + + logger.info("Wrote split document {} to zip file", fileName); + } } catch (Exception e) { - logger.error("Failed splitting documents and saving them", e); + logger.error("Failed writing to zip", e); throw e; } - } - // closing the original document - document.close(); + logger.info( + "Successfully created zip file with split documents: {}", zipFile.toString()); + byte[] data = Files.readAllBytes(zipFile); + Files.deleteIfExists(zipFile); - Path zipFile = Files.createTempFile("split_documents", ".zip"); + // return the Resource in the response + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { - // loop through the split documents and write them to the zip file - for (int i = 0; i < splitDocumentsBoas.size(); i++) { - String fileName = filename + "_" + (i + 1) + ".pdf"; - ByteArrayOutputStream baos = splitDocumentsBoas.get(i); - byte[] pdf = baos.toByteArray(); + } finally { + try { + // Close the main document + if (document != null) { + document.close(); + } - // Add PDF file to the zip - ZipEntry pdfEntry = new ZipEntry(fileName); - zipOut.putNextEntry(pdfEntry); - zipOut.write(pdf); - zipOut.closeEntry(); + // Close all ByteArrayOutputStreams + for (ByteArrayOutputStream baos : splitDocumentsBoas) { + if (baos != null) { + baos.close(); + } + } - logger.info("Wrote split document {} to zip file", fileName); + // Delete temporary zip file + if (zipFile != null) { + Files.deleteIfExists(zipFile); + } + } catch (Exception e) { + logger.error("Error while cleaning up resources", e); } - } catch (Exception e) { - logger.error("Failed writing to zip", e); - throw e; } - - logger.info("Successfully created zip file with split documents: {}", zipFile.toString()); - byte[] data = Files.readAllBytes(zipFile); - Files.deleteIfExists(zipFile); - - // return the Resource in the response - return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index f344c2763..c74ed2940 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -59,70 +59,86 @@ public class SplitPdfByChaptersController { public ResponseEntity splitPdf(@ModelAttribute SplitPdfByChaptersRequest request) throws Exception { MultipartFile file = request.getFileInput(); - boolean includeMetadata = request.getIncludeMetadata(); - Integer bookmarkLevel = - request.getBookmarkLevel(); // levels start from 0 (top most bookmarks) - if (bookmarkLevel < 0) { - return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes()); - } - PDDocument sourceDocument = Loader.loadPDF(file.getBytes()); + PDDocument sourceDocument = null; + Path zipFile = null; - PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline(); - - if (outline == null) { - logger.warn("No outline found for {}", file.getOriginalFilename()); - return ResponseEntity.badRequest().body("No outline found".getBytes()); - } - List bookmarks = new ArrayList<>(); try { - bookmarks = - extractOutlineItems( - sourceDocument, - outline.getFirstChild(), - bookmarks, - outline.getFirstChild().getNextSibling(), - 0, - bookmarkLevel); - // to handle last page edge case - bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages()); - Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1); + boolean includeMetadata = request.getIncludeMetadata(); + Integer bookmarkLevel = + request.getBookmarkLevel(); // levels start from 0 (top most bookmarks) + if (bookmarkLevel < 0) { + return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes()); + } + sourceDocument = Loader.loadPDF(file.getBytes()); - } catch (Exception e) { - logger.error("Unable to extract outline items", e); - return ResponseEntity.internalServerError() - .body("Unable to extract outline items".getBytes()); + PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline(); + + if (outline == null) { + logger.warn("No outline found for {}", file.getOriginalFilename()); + return ResponseEntity.badRequest().body("No outline found".getBytes()); + } + List bookmarks = new ArrayList<>(); + try { + bookmarks = + extractOutlineItems( + sourceDocument, + outline.getFirstChild(), + bookmarks, + outline.getFirstChild().getNextSibling(), + 0, + bookmarkLevel); + // to handle last page edge case + bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages()); + Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1); + + } catch (Exception e) { + logger.error("Unable to extract outline items", e); + return ResponseEntity.internalServerError() + .body("Unable to extract outline items".getBytes()); + } + + boolean allowDuplicates = request.getAllowDuplicates(); + if (!allowDuplicates) { + /* + duplicates are generated when multiple bookmarks correspond to the same page, + if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all + the bookmarks that correspond to the same page, and treat them as a single bookmark + */ + bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks); + } + for (Bookmark bookmark : bookmarks) { + logger.info( + "{}::::{} to {}", + bookmark.getTitle(), + bookmark.getStartPage(), + bookmark.getEndPage()); + } + List splitDocumentsBoas = + getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata); + + zipFile = createZipFile(bookmarks, splitDocumentsBoas); + + byte[] data = Files.readAllBytes(zipFile); + Files.deleteIfExists(zipFile); + + String filename = + Filenames.toSimpleFileName(file.getOriginalFilename()) + .replaceFirst("[.][^.]+$", ""); + sourceDocument.close(); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + } finally { + try { + if (sourceDocument != null) { + sourceDocument.close(); + } + if (zipFile != null) { + Files.deleteIfExists(zipFile); + } + } catch (Exception e) { + logger.error("Error while cleaning up resources", e); + } } - - boolean allowDuplicates = request.getAllowDuplicates(); - if (!allowDuplicates) { - /* - duplicates are generated when multiple bookmarks correspond to the same page, - if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all - the bookmarks that correspond to the same page, and treat them as a single bookmark - */ - bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks); - } - for (Bookmark bookmark : bookmarks) { - logger.info( - "{}::::{} to {}", - bookmark.getTitle(), - bookmark.getStartPage(), - bookmark.getEndPage()); - } - List splitDocumentsBoas = - getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata); - - Path zipFile = createZipFile(bookmarks, splitDocumentsBoas); - - byte[] data = Files.readAllBytes(zipFile); - Files.deleteIfExists(zipFile); - - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); - sourceDocument.close(); - return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } private List mergeBookmarksThatCorrespondToSamePage(List bookmarks) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index 2b4f13133..eaa9c86d9 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -105,15 +105,13 @@ public class SplitPdfBySectionsController { if (sectionNum == horiz * verti) pageNum++; } - } catch (Exception e) { - logger.error("exception", e); - } finally { data = Files.readAllBytes(zipFile); + return WebResponseUtils.bytesToWebResponse( + data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); + + } finally { Files.deleteIfExists(zipFile); } - - return WebResponseUtils.bytesToWebResponse( - data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); } public List splitPdfPages( diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index be955dbd3..b5eec3923 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -65,112 +65,137 @@ public class ConvertImgPDFController { String colorType = request.getColorType(); String dpi = request.getDpi(); - byte[] pdfBytes = file.getBytes(); - ImageType colorTypeResult = ImageType.RGB; - if ("greyscale".equals(colorType)) { - colorTypeResult = ImageType.GRAY; - } else if ("blackwhite".equals(colorType)) { - colorTypeResult = ImageType.BINARY; - } - // returns bytes for image - boolean singleImage = "single".equals(singleOrMultiple); + Path tempFile = null; + Path tempOutputDir = null; + Path tempPdfPath = null; byte[] result = null; - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); - result = - PdfUtils.convertFromPdf( - pdfBytes, - "webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(), - colorTypeResult, - singleImage, - Integer.valueOf(dpi), - filename); - if (result == null || result.length == 0) { - logger.error("resultant bytes for {} is null, error converting ", filename); - } - if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) { - throw new IOException("Python is not installed. Required for WebP conversion."); - } else if ("webp".equalsIgnoreCase(imageFormat) - && CheckProgramInstall.isPythonAvailable()) { - // Write the output stream to a temp file - Path tempFile = Files.createTempFile("temp_png", ".png"); - try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) { - fos.write(result); - fos.flush(); + try { + byte[] pdfBytes = file.getBytes(); + ImageType colorTypeResult = ImageType.RGB; + if ("greyscale".equals(colorType)) { + colorTypeResult = ImageType.GRAY; + } else if ("blackwhite".equals(colorType)) { + colorTypeResult = ImageType.BINARY; } + // returns bytes for image + boolean singleImage = "single".equals(singleOrMultiple); + String filename = + Filenames.toSimpleFileName(file.getOriginalFilename()) + .replaceFirst("[.][^.]+$", ""); - String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); - - List command = new ArrayList<>(); - command.add(pythonVersion); - command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion - - // Create a temporary directory for the output WebP files - Path tempOutputDir = Files.createTempDirectory("webp_output"); - if (singleImage) { - // Run the Python script to convert PNG to WebP - command.add(tempFile.toString()); - command.add(tempOutputDir.toString()); - command.add("--single"); - } else { - // Save the uploaded PDF to a temporary file - Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf"); - file.transferTo(tempPdfPath.toFile()); - // Run the Python script to convert PDF to WebP - command.add(tempPdfPath.toString()); - command.add(tempOutputDir.toString()); + result = + PdfUtils.convertFromPdf( + pdfBytes, + "webp".equalsIgnoreCase(imageFormat) + ? "png" + : imageFormat.toUpperCase(), + colorTypeResult, + singleImage, + Integer.valueOf(dpi), + filename); + if (result == null || result.length == 0) { + logger.error("resultant bytes for {} is null, error converting ", filename); } - command.add("--dpi"); - command.add(dpi); - ProcessExecutorResult resultProcess = - ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) - .runCommandWithOutputHandling(command); - - // Find all WebP files in the output directory - List webpFiles = - Files.walk(tempOutputDir) - .filter(path -> path.toString().endsWith(".webp")) - .collect(Collectors.toList()); - - if (webpFiles.isEmpty()) { - logger.error("No WebP files were created in: {}", tempOutputDir.toString()); - throw new IOException("No WebP files were created. " + resultProcess.getMessages()); - } - - byte[] bodyBytes = new byte[0]; - - if (webpFiles.size() == 1) { - // Return the single WebP file directly - Path webpFilePath = webpFiles.get(0); - bodyBytes = Files.readAllBytes(webpFilePath); - } else { - // Create a ZIP file containing all WebP images - ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) { - for (Path webpFile : webpFiles) { - zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString())); - Files.copy(webpFile, zos); - zos.closeEntry(); - } + if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) { + throw new IOException("Python is not installed. Required for WebP conversion."); + } else if ("webp".equalsIgnoreCase(imageFormat) + && CheckProgramInstall.isPythonAvailable()) { + // Write the output stream to a temp file + tempFile = Files.createTempFile("temp_png", ".png"); + try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) { + fos.write(result); + fos.flush(); } - bodyBytes = zipOutputStream.toByteArray(); - } - // Clean up the temporary files - Files.deleteIfExists(tempFile); - if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); - result = bodyBytes; - } - if (singleImage) { - String docName = filename + "." + imageFormat; - MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); - return WebResponseUtils.bytesToWebResponse(result, docName, mediaType); - } else { - String zipFilename = filename + "_convertedToImages.zip"; - return WebResponseUtils.bytesToWebResponse( - result, zipFilename, MediaType.APPLICATION_OCTET_STREAM); + String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); + + List command = new ArrayList<>(); + command.add(pythonVersion); + command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion + + // Create a temporary directory for the output WebP files + tempOutputDir = Files.createTempDirectory("webp_output"); + if (singleImage) { + // Run the Python script to convert PNG to WebP + command.add(tempFile.toString()); + command.add(tempOutputDir.toString()); + command.add("--single"); + } else { + // Save the uploaded PDF to a temporary file + tempPdfPath = Files.createTempFile("temp_pdf", ".pdf"); + file.transferTo(tempPdfPath.toFile()); + // Run the Python script to convert PDF to WebP + command.add(tempPdfPath.toString()); + command.add(tempOutputDir.toString()); + } + command.add("--dpi"); + command.add(dpi); + ProcessExecutorResult resultProcess = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) + .runCommandWithOutputHandling(command); + + // Find all WebP files in the output directory + List webpFiles = + Files.walk(tempOutputDir) + .filter(path -> path.toString().endsWith(".webp")) + .collect(Collectors.toList()); + + if (webpFiles.isEmpty()) { + logger.error("No WebP files were created in: {}", tempOutputDir.toString()); + throw new IOException( + "No WebP files were created. " + resultProcess.getMessages()); + } + + byte[] bodyBytes = new byte[0]; + + if (webpFiles.size() == 1) { + // Return the single WebP file directly + Path webpFilePath = webpFiles.get(0); + bodyBytes = Files.readAllBytes(webpFilePath); + } else { + // Create a ZIP file containing all WebP images + ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) { + for (Path webpFile : webpFiles) { + zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString())); + Files.copy(webpFile, zos); + zos.closeEntry(); + } + } + bodyBytes = zipOutputStream.toByteArray(); + } + // Clean up the temporary files + Files.deleteIfExists(tempFile); + if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); + result = bodyBytes; + } + + if (singleImage) { + String docName = filename + "." + imageFormat; + MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); + return WebResponseUtils.bytesToWebResponse(result, docName, mediaType); + } else { + String zipFilename = filename + "_convertedToImages.zip"; + return WebResponseUtils.bytesToWebResponse( + result, zipFilename, MediaType.APPLICATION_OCTET_STREAM); + } + + } finally { + try { + // Clean up temporary files + if (tempFile != null) { + Files.deleteIfExists(tempFile); + } + if (tempPdfPath != null) { + Files.deleteIfExists(tempPdfPath); + } + if (tempOutputDir != null) { + FileUtils.deleteDirectory(tempOutputDir.toFile()); + } + } catch (Exception e) { + logger.error("Error cleaning up temporary files", e); + } } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 6c5f3993a..f503c107a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -87,7 +87,7 @@ public class OCRController { Files.createDirectories(tempOutputDir); Files.createDirectories(tempImagesDir); - + Process process = null; try { // Save input file inputFile.transferTo(tempInputFile.toFile()); @@ -139,7 +139,7 @@ public class OCRController { command.add("pdf"); // Always output PDF ProcessBuilder pb = new ProcessBuilder(command); - Process process = pb.start(); + process = pb.start(); // Capture any error output try (BufferedReader reader = @@ -188,6 +188,10 @@ public class OCRController { .body(pdfContent); } finally { + if (process != null) { + process.destroy(); + } + // Clean up temporary files deleteDirectory(tempDir); } From 1639e0fc4c18f7bf9802c56d1245047a6e32a202 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Mon, 9 Dec 2024 20:41:13 +0000 Subject: [PATCH 02/49] format --- .../software/SPDF/EE/LicenseKeyChecker.java | 2 +- .../controller/api/security/GetInfoOnPDF.java | 4 +- .../security/ValidateSignatureController.java | 40 ++++++++++++------- .../security/SignatureValidationResult.java | 23 +++++------ .../service/CertificateValidationService.java | 3 +- 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java index 93648dfa2..108ee3020 100644 --- a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java +++ b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java @@ -28,7 +28,7 @@ public class LicenseKeyChecker { this.checkLicense(); } - @Scheduled(initialDelay = 604800000,fixedRate = 604800000) // 7 days in milliseconds + @Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds public void checkLicensePeriodically() { checkLicense(); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 96745c4a5..d6950e955 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -595,7 +595,9 @@ public class GetInfoOnPDF { permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument())); permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent())); - permissionsNode.put("Extracting for accessibility", getPermissionState(ap.canExtractForAccessibility())); + permissionsNode.put( + "Extracting for accessibility", + getPermissionState(ap.canExtractForAccessibility())); permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm())); permissionsNode.put("Modifying", getPermissionState(ap.canModify())); permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations())); diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java b/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java index 94e99dd93..317c6424b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java @@ -92,20 +92,29 @@ public class ValidateSignatureController { SignerInformationStore signerStore = signedData.getSignerInfos(); for (SignerInformation signer : signerStore.getSigners()) { - X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next(); - X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder); + X509CertificateHolder certHolder = + (X509CertificateHolder) + certStore.getMatches(signer.getSID()).iterator().next(); + X509Certificate cert = + new JcaX509CertificateConverter().getCertificate(certHolder); - boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert)); + boolean isValid = + signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert)); result.setValid(isValid); // Additional validations - result.setChainValid(customCert != null - ? certValidationService.validateCertificateChainWithCustomCert(cert, customCert) - : certValidationService.validateCertificateChain(cert)); + result.setChainValid( + customCert != null + ? certValidationService + .validateCertificateChainWithCustomCert( + cert, customCert) + : certValidationService.validateCertificateChain(cert)); - result.setTrustValid(customCert != null - ? certValidationService.validateTrustWithCustomCert(cert, customCert) - : certValidationService.validateTrustStore(cert)); + result.setTrustValid( + customCert != null + ? certValidationService.validateTrustWithCustomCert( + cert, customCert) + : certValidationService.validateTrustStore(cert)); result.setNotRevoked(!certValidationService.isRevoked(cert)); result.setNotExpired(!cert.getNotAfter().before(new Date())); @@ -123,17 +132,18 @@ public class ValidateSignatureController { result.setValidFrom(cert.getNotBefore().toString()); result.setValidUntil(cert.getNotAfter().toString()); result.setSignatureAlgorithm(cert.getSigAlgName()); - + // Get key size (if possible) try { - result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength()); + result.setKeySize( + ((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength()); } catch (Exception e) { // If not RSA or error, set to 0 result.setKeySize(0); } result.setVersion(String.valueOf(cert.getVersion())); - + // Set key usage List keyUsages = new ArrayList<>(); boolean[] keyUsageFlags = cert.getKeyUsage(); @@ -150,9 +160,11 @@ public class ValidateSignatureController { } } result.setKeyUsages(keyUsages); - + // Check if self-signed - result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())); + result.setSelfSigned( + cert.getSubjectX500Principal() + .equals(cert.getIssuerX500Principal())); } } catch (Exception e) { result.setValid(false); diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationResult.java b/src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationResult.java index 1aafd8eca..b4c51f365 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationResult.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SignatureValidationResult.java @@ -16,16 +16,15 @@ public class SignatureValidationResult { private boolean trustValid; private boolean notExpired; private boolean notRevoked; - - private String issuerDN; // Certificate issuer's Distinguished Name - private String subjectDN; // Certificate subject's Distinguished Name - private String serialNumber; // Certificate serial number - private String validFrom; // Certificate validity start date - private String validUntil; // Certificate validity end date - private String signatureAlgorithm;// Algorithm used for signing - private int keySize; // Key size in bits - private String version; // Certificate version - private List keyUsages; // List of key usage purposes - private boolean isSelfSigned; // Whether the certificate is self-signed - + + private String issuerDN; // Certificate issuer's Distinguished Name + private String subjectDN; // Certificate subject's Distinguished Name + private String serialNumber; // Certificate serial number + private String validFrom; // Certificate validity start date + private String validUntil; // Certificate validity end date + private String signatureAlgorithm; // Algorithm used for signing + private int keySize; // Key size in bits + private String version; // Certificate version + private List keyUsages; // List of key usage purposes + private boolean isSelfSigned; // Whether the certificate is self-signed } diff --git a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java index 41f54f4aa..550db6801 100644 --- a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java +++ b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.service; -import io.github.pixee.security.BoundedLineReader; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -24,6 +23,8 @@ import java.util.Set; import org.springframework.stereotype.Service; +import io.github.pixee.security.BoundedLineReader; + import jakarta.annotation.PostConstruct; @Service From 52693541d9a61a3a2366883c2dd677a773a27a8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 22:44:23 +0000 Subject: [PATCH 03/49] Bump org.thymeleaf.extras:thymeleaf-extras-springsecurity5 Bumps org.thymeleaf.extras:thymeleaf-extras-springsecurity5 from 3.1.2.RELEASE to 3.1.3.RELEASE. --- updated-dependencies: - dependency-name: org.thymeleaf.extras:thymeleaf-extras-springsecurity5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eb9c3c922..d4ff54f1d 100644 --- a/build.gradle +++ b/build.gradle @@ -142,7 +142,7 @@ dependencies { if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" - implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE" + implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" From c1c3eba3984ba70453c6bdf00d01a58814a69828 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 10 Dec 2024 11:17:50 +0000 Subject: [PATCH 04/49] ensure csrf is enabled --- .../software/SPDF/config/InitialSetup.java | 49 +++++++++++++++-- .../SPDF/model/ApplicationProperties.java | 1 + .../software/SPDF/utils/GeneralUtils.java | 53 +++++++++++++++++++ src/main/resources/settings.yml.template | 5 +- 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java index 0e0ad2be0..294e31efe 100644 --- a/src/main/java/stirling/software/SPDF/config/InitialSetup.java +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -1,11 +1,14 @@ package stirling.software.SPDF.config; import java.io.IOException; +import java.util.Properties; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import io.micrometer.common.util.StringUtils; @@ -23,6 +26,18 @@ public class InitialSetup { @Autowired private ApplicationProperties applicationProperties; @PostConstruct + public void init() throws IOException { + initUUIDKey(); + + initSecretKey(); + + initEnableCSRFSecurity(); + + initLegalUrls(); + + initSetAppVersion(); + } + public void initUUIDKey() throws IOException { String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); if (!GeneralUtils.isValidUUID(uuid)) { @@ -32,7 +47,6 @@ public class InitialSetup { } } - @PostConstruct public void initSecretKey() throws IOException { String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); if (!GeneralUtils.isValidUUID(secretKey)) { @@ -42,13 +56,24 @@ public class InitialSetup { } } - @PostConstruct + public void initEnableCSRFSecurity() throws IOException { + if(GeneralUtils.isVersionHigher("0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { + Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); + if (!csrf) { + GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); + GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); + applicationProperties.getSecurity().setCsrfDisabled(false); + + } + } + } + public void initLegalUrls() throws IOException { // Initialize Terms and Conditions String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); if (StringUtils.isEmpty(termsUrl)) { String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions"; - GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl); + GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); } @@ -56,8 +81,24 @@ public class InitialSetup { String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); if (StringUtils.isEmpty(privacyUrl)) { String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy"; - GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl); + GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false); applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); } } + + public void initSetAppVersion() throws IOException { + + String appVersion = "0.0.0"; + Resource resource = new ClassPathResource("version.properties"); + Properties props = new Properties(); + try { + props.load(resource.getInputStream()); + appVersion =props.getProperty("version"); + } catch(Exception e) { + + } + applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); + GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion,false); + } + } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 6d4d0a7f4..48fe89142 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -285,6 +285,7 @@ public class ApplicationProperties { public static class AutomaticallyGenerated { @ToString.Exclude private String key; private String UUID; + private String appVersion; } @Data diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 8e56c8df6..3f48997c3 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -288,6 +288,10 @@ public class GeneralUtils { public static void saveKeyToConfig(String id, String key) throws IOException { saveKeyToConfig(id, key, true); } + public static void saveKeyToConfig(String id, boolean key) throws IOException { + saveKeyToConfig(id, key, true); + } + public static void saveKeyToConfig(String id, String key, boolean autoGenerated) throws IOException { @@ -306,6 +310,25 @@ public class GeneralUtils { } settingsYml.save(); } + + public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) + throws IOException { + Path path = Paths.get("configs", "settings.yml"); + + final YamlFile settingsYml = new YamlFile(path.toFile()); + DumperOptions yamlOptionssettingsYml = + ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions(); + yamlOptionssettingsYml.setSplitLines(false); + + settingsYml.loadWithComments(); + + YamlFileWrapper writer = settingsYml.path(id).set(key); + if (autoGenerated) { + writer.comment("# Automatically Generated Settings (Do Not Edit Directly)"); + } + settingsYml.save(); + } + public static String generateMachineFingerprint() { try { @@ -349,4 +372,34 @@ public class GeneralUtils { return "GenericID"; } } + + public static boolean isVersionHigher(String currentVersion, String compareVersion) { + if (currentVersion == null || compareVersion == null) { + return false; + } + + // Split versions into components + String[] current = currentVersion.split("\\."); + String[] compare = compareVersion.split("\\."); + + // Get the length of the shorter version array + int length = Math.min(current.length, compare.length); + + // Compare each component + for (int i = 0; i < length; i++) { + int currentPart = Integer.parseInt(current[i]); + int comparePart = Integer.parseInt(compare[i]); + + if (currentPart > comparePart) { + return true; + } + if (currentPart < comparePart) { + return false; + } + } + + // If all components so far are equal, the longer version is considered higher + return current.length > compare.length; + } + } diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index eded10e7e..a110744a0 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -13,7 +13,7 @@ security: enableLogin: false # set to 'true' to enable login - csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production) + csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production) loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1 loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2) @@ -102,7 +102,8 @@ metrics: AutomaticallyGenerated: key: example UUID: example - + appVersion: 0.35.0 + processExecutor: sessionLimit: # Process executor instances limits libreOfficeSessionLimit: 1 From 58c7d7b9a8f2d97d7892dd810aab1f2db04b4583 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 10 Dec 2024 20:39:24 +0000 Subject: [PATCH 05/49] X-API-key to X-API-KEY --- README.md | 2 +- cucumber/features/steps/step_definitions.py | 10 ++-- exampleYmlFiles/test_cicd.yml | 34 +++++++++++++ .../software/SPDF/config/InitialSetup.java | 49 +++++++++---------- .../config/security/InitialSecuritySetup.java | 2 + .../security/SecurityConfiguration.java | 26 +++++----- .../security/UserAuthenticationFilter.java | 2 +- .../security/UserBasedRateLimitingFilter.java | 4 +- .../SPDF/config/security/UserService.java | 31 ++++++++++++ .../api/pipeline/PipelineProcessor.java | 2 +- .../SPDF/model/ApplicationProperties.java | 1 + .../software/SPDF/utils/GeneralUtils.java | 42 ++++++++-------- test.sh | 2 +- 13 files changed, 138 insertions(+), 69 deletions(-) create mode 100644 exampleYmlFiles/test_cicd.yml diff --git a/README.md b/README.md index e8ad34cbe..543173bbb 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ To access your account settings, go to Account Settings in the settings cog menu To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future. -For API usage, you must provide a header with `X-API-Key` and the associated API key for that user. +For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user. ## FAQ diff --git a/cucumber/features/steps/step_definitions.py b/cucumber/features/steps/step_definitions.py index 65a49fda0..ae8acd2ad 100644 --- a/cucumber/features/steps/step_definitions.py +++ b/cucumber/features/steps/step_definitions.py @@ -15,6 +15,10 @@ import shutil import re from PIL import Image, ImageDraw +API_HEADERS = { + 'X-API-KEY': '123456789' +} + ######### # GIVEN # ######### @@ -227,7 +231,7 @@ def save_generated_pdf(context, filename): def step_send_get_request(context, endpoint): base_url = "http://localhost:8080" full_url = f"{base_url}{endpoint}" - response = requests.get(full_url) + response = requests.get(full_url, headers=API_HEADERS) context.response = response @when('I send a GET request to "{endpoint}" with parameters') @@ -235,7 +239,7 @@ def step_send_get_request_with_params(context, endpoint): base_url = "http://localhost:8080" params = {row['parameter']: row['value'] for row in context.table} full_url = f"{base_url}{endpoint}" - response = requests.get(full_url, params=params) + response = requests.get(full_url, params=params, headers=API_HEADERS) context.response = response @when('I send the API request to the endpoint "{endpoint}"') @@ -256,7 +260,7 @@ def step_send_api_request(context, endpoint): print(f"form_data {file.name} with {mime_type}") form_data.append((key, (file.name, file, mime_type))) - response = requests.post(url, files=form_data) + response = requests.post(url, files=form_data, headers=API_HEADERS) context.response = response ######## diff --git a/exampleYmlFiles/test_cicd.yml b/exampleYmlFiles/test_cicd.yml new file mode 100644 index 000000000..144348a79 --- /dev/null +++ b/exampleYmlFiles/test_cicd.yml @@ -0,0 +1,34 @@ +services: + stirling-pdf: + container_name: Stirling-PDF-Security-Fat + image: stirlingtools/stirling-pdf:latest-fat + deploy: + resources: + limits: + memory: 4G + healthcheck: + test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + interval: 5s + timeout: 10s + retries: 16 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" + PUID: 1002 + PGID: 1002 + UMASK: "022" + SYSTEM_DEFAULTLOCALE: en-US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security + UI_APPNAMENAVBAR: Stirling-PDF Latest-fat + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + SECURITY_CUSTOMGLOBALAPIKEY: "123456789" + restart: on-failure:5 diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java index 294e31efe..81279ce9f 100644 --- a/src/main/java/stirling/software/SPDF/config/InitialSetup.java +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -28,16 +28,16 @@ public class InitialSetup { @PostConstruct public void init() throws IOException { initUUIDKey(); - + initSecretKey(); - + initEnableCSRFSecurity(); - + initLegalUrls(); - + initSetAppVersion(); } - + public void initUUIDKey() throws IOException { String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); if (!GeneralUtils.isValidUUID(uuid)) { @@ -57,17 +57,17 @@ public class InitialSetup { } public void initEnableCSRFSecurity() throws IOException { - if(GeneralUtils.isVersionHigher("0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { - Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); - if (!csrf) { - GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); - GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); - applicationProperties.getSecurity().setCsrfDisabled(false); - - } - } + if (GeneralUtils.isVersionHigher( + "0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { + Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); + if (!csrf) { + GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); + GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); + applicationProperties.getSecurity().setCsrfDisabled(false); + } + } } - + public void initLegalUrls() throws IOException { // Initialize Terms and Conditions String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); @@ -85,20 +85,19 @@ public class InitialSetup { applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); } } - + public void initSetAppVersion() throws IOException { - - String appVersion = "0.0.0"; - Resource resource = new ClassPathResource("version.properties"); + + String appVersion = "0.0.0"; + Resource resource = new ClassPathResource("version.properties"); Properties props = new Properties(); try { props.load(resource.getInputStream()); - appVersion =props.getProperty("version"); - } catch(Exception e) { - + appVersion = props.getProperty("version"); + } catch (Exception e) { + } applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); - GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion,false); - } - + GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false); + } } diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 7e542a003..2fcebcad8 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -75,5 +75,7 @@ public class InitialSecuritySetup { userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } + userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); + System.out.println(applicationProperties.getSecurity().getCustomGlobalAPIKey()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index fa3f5342e..66e6f0f5f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -99,7 +99,7 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - if (applicationProperties.getSecurity().getCsrfDisabled()) { + if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { http.csrf(csrf -> csrf.disable()); } @@ -116,7 +116,7 @@ public class SecurityConfiguration { csrf -> csrf.ignoringRequestMatchers( request -> { - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); // If there's no API key, don't ignore CSRF // (return false) @@ -289,17 +289,17 @@ public class SecurityConfiguration { } } else { - if (!applicationProperties.getSecurity().getCsrfDisabled()) { - CookieCsrfTokenRepository cookieRepo = - CookieCsrfTokenRepository.withHttpOnlyFalse(); - CsrfTokenRequestAttributeHandler requestHandler = - new CsrfTokenRequestAttributeHandler(); - requestHandler.setCsrfRequestAttributeName(null); - http.csrf( - csrf -> - csrf.csrfTokenRepository(cookieRepo) - .csrfTokenRequestHandler(requestHandler)); - } + // if (!applicationProperties.getSecurity().getCsrfDisabled()) { + // CookieCsrfTokenRepository cookieRepo = + // CookieCsrfTokenRepository.withHttpOnlyFalse(); + // CsrfTokenRequestAttributeHandler requestHandler = + // new CsrfTokenRequestAttributeHandler(); + // requestHandler.setCsrfRequestAttributeName(null); + // http.csrf( + // csrf -> + // csrf.csrfTokenRepository(cookieRepo) + // .csrfTokenRequestHandler(requestHandler)); + // } http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index 4b62f6d29..93dff07bb 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // Check for API key in the request headers if no authentication exists if (authentication == null || !authentication.isAuthenticated()) { - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); if (apiKey != null && !apiKey.trim().isEmpty()) { try { // Use API key to authenticate. This requires you to have an authentication diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index bd7e39725..b17334941 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { String identifier = null; // Check for API key in the request headers - String apiKey = request.getHeader("X-API-Key"); + String apiKey = request.getHeader("X-API-KEY"); if (apiKey != null && !apiKey.trim().isEmpty()) { identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames @@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); - if (request.getHeader("X-API-Key") != null) { + if (request.getHeader("X-API-KEY") != null) { // It's an API call processRequest( userRole.getApiCallsPerDay(), diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index d7f35d387..d5fea5942 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -390,6 +390,37 @@ public class UserService implements UserServiceInterface { } } + @Transactional + public void syncCustomApiUser(String customApiKey) throws IOException { + if (customApiKey == null || customApiKey.trim().length() == 0) { + return; + } + String username = "CUSTOM_API_USER"; + Optional existingUser = findByUsernameIgnoreCase(username); + + if (!existingUser.isPresent()) { + // Create new user with API role + User user = new User(); + user.setUsername(username); + user.setPassword(UUID.randomUUID().toString()); + user.setEnabled(true); + user.setFirstLogin(false); + user.setAuthenticationType(AuthenticationType.WEB); + user.setApiKey(customApiKey); + user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user)); + userRepository.save(user); + databaseBackupHelper.exportDatabase(); + } else { + // Update API key if it has changed + User user = existingUser.get(); + if (!customApiKey.equals(user.getApiKey())) { + user.setApiKey(customApiKey); + userRepository.save(user); + databaseBackupHelper.exportDatabase(); + } + } + } + @Override public long getTotalUsersCount() { return userRepository.count(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 2e62c3505..004960369 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -221,7 +221,7 @@ public class PipelineProcessor { HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); - headers.add("X-API-Key", apiKey); + headers.add("X-API-KEY", apiKey); headers.setContentType(MediaType.MULTIPART_FORM_DATA); // Create HttpEntity with the body and headers diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 48fe89142..703b5ee6f 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -73,6 +73,7 @@ public class ApplicationProperties { private int loginAttemptCount; private long loginResetTimeMinutes; private String loginMethod = "all"; + private String customGlobalAPIKey; public Boolean isAltLogin() { return saml2.getEnabled() || oauth2.getEnabled(); diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 3f48997c3..b5654c7dc 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -288,10 +288,10 @@ public class GeneralUtils { public static void saveKeyToConfig(String id, String key) throws IOException { saveKeyToConfig(id, key, true); } + public static void saveKeyToConfig(String id, boolean key) throws IOException { saveKeyToConfig(id, key, true); } - public static void saveKeyToConfig(String id, String key, boolean autoGenerated) throws IOException { @@ -310,25 +310,24 @@ public class GeneralUtils { } settingsYml.save(); } - - public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) - throws IOException { - Path path = Paths.get("configs", "settings.yml"); - - final YamlFile settingsYml = new YamlFile(path.toFile()); - DumperOptions yamlOptionssettingsYml = - ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions(); - yamlOptionssettingsYml.setSplitLines(false); - - settingsYml.loadWithComments(); - - YamlFileWrapper writer = settingsYml.path(id).set(key); - if (autoGenerated) { - writer.comment("# Automatically Generated Settings (Do Not Edit Directly)"); - } - settingsYml.save(); - } - + + public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) + throws IOException { + Path path = Paths.get("configs", "settings.yml"); + + final YamlFile settingsYml = new YamlFile(path.toFile()); + DumperOptions yamlOptionssettingsYml = + ((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions(); + yamlOptionssettingsYml.setSplitLines(false); + + settingsYml.loadWithComments(); + + YamlFileWrapper writer = settingsYml.path(id).set(key); + if (autoGenerated) { + writer.comment("# Automatically Generated Settings (Do Not Edit Directly)"); + } + settingsYml.save(); + } public static String generateMachineFingerprint() { try { @@ -372,7 +371,7 @@ public class GeneralUtils { return "GenericID"; } } - + public static boolean isVersionHigher(String currentVersion, String compareVersion) { if (currentVersion == null || compareVersion == null) { return false; @@ -401,5 +400,4 @@ public class GeneralUtils { // If all components so far are equal, the longer version is considered higher return current.length > compare.length; } - } diff --git a/test.sh b/test.sh index 7674c6dcd..2ad259054 100644 --- a/test.sh +++ b/test.sh @@ -104,7 +104,7 @@ main() { # run_tests "Stirling-PDF-Security" "./exampleYmlFiles/docker-compose-latest-security.yml" # docker-compose -f "./exampleYmlFiles/docker-compose-latest-security.yml" down - run_tests "Stirling-PDF-Security-Fat" "./exampleYmlFiles/docker-compose-latest-fat-security.yml" + run_tests "Stirling-PDF-Security-Fat" "./exampleYmlFiles/test_cicd.yml" if [ $? -eq 0 ]; then cd cucumber if python -m behave; then From 9167f12296d548a16f7a98a46d367c496550e2f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:28:28 +0000 Subject: [PATCH 06/49] Update translation files Signed-off-by: GitHub Action --- src/main/resources/messages_ar_AR.properties | 10 +++ src/main/resources/messages_az_AZ.properties | 10 +++ src/main/resources/messages_bg_BG.properties | 10 +++ src/main/resources/messages_ca_CA.properties | 10 +++ src/main/resources/messages_cs_CZ.properties | 10 +++ src/main/resources/messages_da_DK.properties | 10 +++ src/main/resources/messages_de_DE.properties | 10 +++ src/main/resources/messages_el_GR.properties | 10 +++ src/main/resources/messages_en_US.properties | 10 +++ src/main/resources/messages_es_ES.properties | 10 +++ src/main/resources/messages_eu_ES.properties | 10 +++ src/main/resources/messages_fa_IR.properties | 82 +++++++++++-------- src/main/resources/messages_fr_FR.properties | 10 +++ src/main/resources/messages_ga_IE.properties | 10 +++ src/main/resources/messages_hi_IN.properties | 10 +++ src/main/resources/messages_hr_HR.properties | 10 +++ src/main/resources/messages_hu_HU.properties | 10 +++ src/main/resources/messages_id_ID.properties | 10 +++ src/main/resources/messages_it_IT.properties | 10 +++ src/main/resources/messages_ja_JP.properties | 10 +++ src/main/resources/messages_ko_KR.properties | 10 +++ src/main/resources/messages_nl_NL.properties | 10 +++ src/main/resources/messages_no_NB.properties | 10 +++ src/main/resources/messages_pl_PL.properties | 10 +++ src/main/resources/messages_pt_BR.properties | 10 +++ src/main/resources/messages_pt_PT.properties | 10 +++ src/main/resources/messages_ro_RO.properties | 10 +++ src/main/resources/messages_ru_RU.properties | 10 +++ src/main/resources/messages_sk_SK.properties | 10 +++ .../resources/messages_sr_LATN_RS.properties | 10 +++ src/main/resources/messages_sv_SE.properties | 10 +++ src/main/resources/messages_th_TH.properties | 10 +++ src/main/resources/messages_tr_TR.properties | 10 +++ src/main/resources/messages_uk_UA.properties | 10 +++ src/main/resources/messages_vi_VN.properties | 10 +++ src/main/resources/messages_zh_CN.properties | 10 +++ src/main/resources/messages_zh_TW.properties | 10 +++ 37 files changed, 406 insertions(+), 36 deletions(-) diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index a519beccf..954ce64b3 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=الصفحات المحددة multiTool.undo=تراجع multiTool.redo=إعادة إجراء +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=هذه الميزة متوفرة في صفحة الأدوات المتعددة لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية! diff --git a/src/main/resources/messages_az_AZ.properties b/src/main/resources/messages_az_AZ.properties index 07d170b05..dcffafa66 100644 --- a/src/main/resources/messages_az_AZ.properties +++ b/src/main/resources/messages_az_AZ.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Seçilmiş Səhifə(lər) multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=Bu xüsusiyyət bizim multi-alət səhifəmizdə də mövcuddur. Əlavə xüsusiyyətlər və səhifə-səhifə interfeys üçün sınaqdan keçirin! diff --git a/src/main/resources/messages_bg_BG.properties b/src/main/resources/messages_bg_BG.properties index 16f9d5bcc..8c6b510f6 100644 --- a/src/main/resources/messages_bg_BG.properties +++ b/src/main/resources/messages_bg_BG.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index b847662db..2cf560b14 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_cs_CZ.properties b/src/main/resources/messages_cs_CZ.properties index 86147c844..fbf4b21f9 100644 --- a/src/main/resources/messages_cs_CZ.properties +++ b/src/main/resources/messages_cs_CZ.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_da_DK.properties b/src/main/resources/messages_da_DK.properties index 127a8cdde..ad383ff81 100644 --- a/src/main/resources/messages_da_DK.properties +++ b/src/main/resources/messages_da_DK.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 9e00754dc..06de0a844 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Ausgewählte Seite(n) multiTool.undo=Rückgängig machen multiTool.redo=Wiederherstellen +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=Diese Funktion ist auch auf unserer PDF-Multitool-Seite verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen! diff --git a/src/main/resources/messages_el_GR.properties b/src/main/resources/messages_el_GR.properties index 6b3f1a8ec..9c2e427df 100644 --- a/src/main/resources/messages_el_GR.properties +++ b/src/main/resources/messages_el_GR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index f5f2b8f83..dbd67e2c2 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index 4f3fcfbe6..d1c50df73 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index 393722c4e..aebd93e3e 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_fa_IR.properties b/src/main/resources/messages_fa_IR.properties index 750776f2a..13c0a09de 100644 --- a/src/main/resources/messages_fa_IR.properties +++ b/src/main/resources/messages_fa_IR.properties @@ -122,6 +122,7 @@ enterpriseEdition.warning=این ویژگی فقط برای کاربران حر enterpriseEdition.yamlAdvert=Stirling PDF Pro از فایل‌های پیکربندی YAML و دیگر ویژگی‌های SSO پشتیبانی می‌کند. enterpriseEdition.ssoAdvert=به دنبال ویژگی‌های بیشتر برای مدیریت کاربران هستید؟ Stirling PDF Pro را بررسی کنید + ################# # Analytics # ################# @@ -515,6 +516,7 @@ home.validateSignature.title=اعتبارسنجی امضای PDF home.validateSignature.desc=تأیید امضاها و گواهی‌های دیجیتال در اسناد PDF validateSignature.tags=امضا، تأیید، اعتبارسنجی، PDF، گواهی‌نامه، امضای دیجیتال +#replace-invert-color replace-color.title=جایگزینی/معکوس کردن رنگ replace-color.header=جایگزینی/معکوس کردن رنگ PDF home.replaceColorPdf.title=جایگزینی و معکوس کردن رنگ @@ -535,7 +537,6 @@ replace-color.submit=جایگزینی - ########################### # # # WEB PAGES # @@ -611,6 +612,7 @@ MarkdownToPDF.help=در حال پیشرفت MarkdownToPDF.credit=از WeasyPrint استفاده می‌کند + #url-to-pdf URLToPDF.title=URL به PDF URLToPDF.header=URL به PDF @@ -903,7 +905,7 @@ compress.selectText.5=اندازه PDF مورد انتظار (مثلاً ۲۵MB compress.submit=فشرده‌سازی -# Add image +#Add image addImage.title=افزودن تصویر addImage.header=افزودن تصویر به PDF addImage.everyPage=هر صفحه؟ @@ -911,7 +913,7 @@ addImage.upload=افزودن تصویر addImage.submit=افزودن تصویر -# Merge +#merge merge.title=ادغام merge.header=ادغام چندین PDF (۲+) merge.sortByName=مرتب‌سازی بر اساس نام @@ -920,7 +922,7 @@ merge.removeCertSign=حذف امضای دیجیتال در فایل ادغام merge.submit=ادغام -# PDF Organizer +#pdfOrganiser pdfOrganiser.title=سازماندهی صفحات pdfOrganiser.header=سازماندهی صفحات PDF pdfOrganiser.submit=بازآرایی صفحات @@ -938,7 +940,7 @@ pdfOrganiser.mode.10=ادغام فرد-زوج pdfOrganiser.placeholder=(مثال: ۱,۳,۲ یا ۴-۸,۲,۱۰-۱۲ یا 2n-1) -# Multi Tool +#multiTool multiTool.title=ابزار چندگانه PDF multiTool.header=ابزار چندگانه PDF multiTool.uploadPrompts=نام فایل @@ -963,14 +965,24 @@ multiTool.dragDropMessage=صفحه(ها) انتخاب شده‌اند multiTool.undo=واگرد multiTool.redo=بازگرداندن -# Multi Tool Advert +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + +#multiTool-advert multiTool-advert.message=این ویژگی همچنین در صفحه ابزار چندگانه ما موجود است. برای رابط کاربری صفحه به صفحه پیشرفته و ویژگی‌های اضافی بررسی کنید! -# View PDF +#view pdf viewPdf.title=مشاهده PDF viewPdf.header=مشاهده PDF -# Page Remover +#pageRemover pageRemover.title=حذف صفحات pageRemover.header=حذف صفحات PDF pageRemover.pagesToDelete=صفحات برای حذف (یک لیست از اعداد صفحه جدا شده با کاما وارد کنید): @@ -978,14 +990,14 @@ pageRemover.submit=حذف صفحات pageRemover.placeholder=(مثال: ۱,۲,۶ یا ۱-۱۰,۱۵-۳۰) -# Rotate +#rotate rotate.title=چرخش PDF rotate.header=چرخش PDF rotate.selectAngle=زاویه چرخش را انتخاب کنید (به مضرب‌های ۹۰ درجه): rotate.submit=چرخش -# Split PDFs +#split-pdfs split.title=تقسیم PDF split.header=تقسیم PDF split.desc.1=اعدادی که انتخاب می‌کنید شماره صفحه‌هایی هستند که می‌خواهید بر روی آنها تقسیم انجام دهید @@ -1014,7 +1026,7 @@ imageToPDF.selectText.4=ادغام در یک PDF واحد imageToPDF.selectText.5=تبدیل به PDF های جداگانه -# PDF to Image +#pdfToImage pdfToImage.title=PDF به تصویر pdfToImage.header=PDF به تصویر pdfToImage.selectText=فرمت تصویر @@ -1029,7 +1041,7 @@ pdfToImage.submit=تبدیل pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است. -# Add Password +#addPassword addPassword.title=افزودن گذرواژه addPassword.header=افزودن گذرواژه (رمزنگاری) addPassword.selectText.1=انتخاب PDF برای رمزنگاری @@ -1051,7 +1063,7 @@ addPassword.selectText.16=محدودیت‌های باز شدن خود سند addPassword.submit=رمزنگاری -# Watermark +#watermark watermark.title=افزودن واترمارک watermark.header=افزودن واترمارک watermark.customColor=رنگ متن سفارشی @@ -1070,7 +1082,7 @@ watermark.type.1=متن watermark.type.2=تصویر -# Change Permissions +#Change permissions permissions.title=تغییر مجوزها permissions.header=تغییر مجوزها permissions.warning=برای اینکه این مجوزها غیرقابل تغییر باشند، توصیه می‌شود آنها را با گذرواژه از طریق صفحه افزودن گذرواژه تنظیم کنید @@ -1087,7 +1099,7 @@ permissions.selectText.10=جلوگیری از چاپ فرمت‌های مختل permissions.submit=تغییر -# Remove Password +#remove password removePassword.title=حذف گذرواژه removePassword.header=حذف گذرواژه (رمزگشایی) removePassword.selectText.1=PDFی را برای رمزگشایی انتخاب کنید @@ -1095,7 +1107,7 @@ removePassword.selectText.2=گذرواژه removePassword.submit=حذف -# Change Metadata +#changeMetadata changeMetadata.title=عنوان: changeMetadata.header=تغییر متاداده‌ها changeMetadata.selectText.1=لطفاً متغیرهایی که مایل به تغییر آنها هستید را ویرایش کنید @@ -1114,7 +1126,7 @@ changeMetadata.selectText.5=افزودن ورودی متاداده سفارشی changeMetadata.submit=تغییر -# PDF to PDF/A +#pdfToPDFA pdfToPDFA.title=PDF به PDF/A pdfToPDFA.header=PDF به PDF/A pdfToPDFA.credit=این سرویس از qpdf برای تبدیل PDF/A استفاده می‌کند @@ -1124,7 +1136,7 @@ pdfToPDFA.outputFormat=فرمت خروجی pdfToPDFA.pdfWithDigitalSignature=PDF حاوی یک امضای دیجیتال است. این در مرحله بعد حذف خواهد شد. -# PDF to Word +#PDFToWord PDFToWord.title=PDF به ورد PDFToWord.header=PDF به ورد PDFToWord.selectText.1=فرمت فایل خروجی @@ -1132,7 +1144,7 @@ PDFToWord.credit=این سرویس از LibreOffice برای تبدیل فایل PDFToWord.submit=تبدیل -# PDF to Presentation +#PDFToPresentation PDFToPresentation.title=PDF به ارائه PDFToPresentation.header=PDF به ارائه PDFToPresentation.selectText.1=فرمت فایل خروجی @@ -1140,7 +1152,7 @@ PDFToPresentation.credit=این سرویس از LibreOffice برای تبدیل PDFToPresentation.submit=تبدیل -# PDF to Text +#PDFToText PDFToText.title=PDF به RTF (متن) PDFToText.header=PDF به RTF (متن) PDFToText.selectText.1=فرمت فایل خروجی @@ -1148,27 +1160,26 @@ PDFToText.credit=این سرویس از LibreOffice برای تبدیل فایل PDFToText.submit=تبدیل -# PDF to HTML +#PDFToHTML PDFToHTML.title=PDF به HTML PDFToHTML.header=PDF به HTML PDFToHTML.credit=این سرویس از pdftohtml برای تبدیل فایل استفاده می‌کند. PDFToHTML.submit=تبدیل -# PDF to XML +#PDFToXML PDFToXML.title=PDF به XML PDFToXML.header=PDF به XML PDFToXML.credit=این سرویس از LibreOffice برای تبدیل فایل استفاده می‌کند. PDFToXML.submit=تبدیل -# PDF to CSV +#PDFToCSV PDFToCSV.title=PDF به CSV PDFToCSV.header=PDF به CSV PDFToCSV.prompt=صفحه‌ای که می‌خواهید جدول استخراج شود را انتخاب کنید PDFToCSV.submit=استخراج - -# Split by Size or Count +#split-by-size-or-count split-by-size-or-count.title=تقسیم PDF بر اساس اندازه یا تعداد split-by-size-or-count.header=تقسیم PDF بر اساس اندازه یا تعداد split-by-size-or-count.type.label=انتخاب نوع تقسیم @@ -1180,7 +1191,7 @@ split-by-size-or-count.value.placeholder=اندازه را وارد کنید (م split-by-size-or-count.submit=ارسال -# Overlay PDFs +#overlay-pdfs overlay-pdfs.header=ترکیب فایل‌های PDF overlay-pdfs.baseFile.label=انتخاب فایل پایه PDF overlay-pdfs.overlayFiles.label=انتخاب فایل‌های ترکیبی PDF @@ -1196,7 +1207,7 @@ overlay-pdfs.position.background=پس‌زمینه overlay-pdfs.submit=ارسال -# Split by Sections +#split-by-sections split-by-sections.title=تقسیم PDF به بخش‌ها split-by-sections.header=تقسیم PDF به بخش‌ها split-by-sections.horizontal.label=تقسیمات افقی @@ -1207,7 +1218,7 @@ split-by-sections.submit=تقسیم PDF split-by-sections.merge=ادغام به یک PDF -# Print File +#printFile printFile.title=چاپ فایل printFile.header=چاپ فایل به چاپگر printFile.selectText.1=انتخاب فایل برای چاپ @@ -1215,7 +1226,7 @@ printFile.selectText.2=نام چاپگر را وارد کنید printFile.submit=چاپ -# Licenses +#licenses licenses.nav=مجوزها licenses.title=مجوزهای شخص ثالث licenses.header=مجوزهای شخص ثالث @@ -1223,7 +1234,7 @@ licenses.module=ماژول licenses.version=نسخه licenses.license=مجوز -# Survey +#survey survey.nav=نظرسنجی survey.title=نظرسنجی Stirling-PDF survey.description=Stirling-PDF هیچ ردیابی ندارد، بنابراین ما می‌خواهیم از کاربران خود بشنویم تا Stirling-PDF را بهبود دهیم! @@ -1235,7 +1246,7 @@ survey.button=شرکت در نظرسنجی survey.dontShowAgain=دیگر نشان نده -# Error +#error error.sorry=متأسفیم برای مشکل موجود! error.needHelp=نیاز به کمک / یافتن مشکلی؟ error.contactTip=اگر هنوز مشکلی دارید، دریغ نکنید که با ما تماس بگیرید. می‌توانید یک تیکت در صفحه GitHub ما ارسال کنید یا از طریق Discord با ما تماس بگیرید: @@ -1249,14 +1260,13 @@ error.githubSubmit=GitHub - ارسال تیکت error.discordSubmit=Discord - ارسال پست پشتیبانی -# Remove Image +#remove-image removeImage.title=حذف تصویر removeImage.header=حذف تصویر removeImage.removeImage=حذف تصویر removeImage.submit=حذف تصویر -# Split by Chapters splitByChapters.title=تقسیم PDF بر اساس فصل‌ها splitByChapters.header=تقسیم PDF بر اساس فصل‌ها splitByChapters.bookmarkLevel=سطح نشانک @@ -1268,20 +1278,20 @@ splitByChapters.desc.3=شامل متادیتا: اگر انتخاب شده، م splitByChapters.desc.4=اجازه‌ی تکرار: اگر انتخاب شده باشد، اجازه می‌دهد نشانک‌های متعدد در یک صفحه، فایل‌های PDF جداگانه ایجاد کنند. splitByChapters.submit=تقسیم PDF -# File Chooser +#File Chooser fileChooser.click=کلیک کنید fileChooser.or=یا fileChooser.dragAndDrop=بکشید و رها کنید fileChooser.hoveredDragAndDrop=فایل(های) خود را اینجا بکشید و رها کنید -# Release Notes +#release notes releases.footer=نسخه‌ها releases.title=یادداشت‌های نسخه releases.header=یادداشت‌های نسخه releases.current.version=نسخه فعلی releases.note=یادداشت‌های نسخه فقط به زبان انگلیسی موجود است -# Validate Signature +#Validate Signature validateSignature.title=اعتبارسنجی امضاهای PDF validateSignature.header=اعتبارسنجی امضای دیجیتال validateSignature.selectPDF=فایل PDF امضاشده را انتخاب کنید diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index 2e26c3ede..90f670c3f 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) sélectionnées multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=Cette fonctionnalité est aussi disponible dans la page de l'outil multifonction. Allez-y pour une interface page par page améliorée et des fonctionnalités additionnelles ! diff --git a/src/main/resources/messages_ga_IE.properties b/src/main/resources/messages_ga_IE.properties index 6dfacc52c..087924ef3 100644 --- a/src/main/resources/messages_ga_IE.properties +++ b/src/main/resources/messages_ga_IE.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_hi_IN.properties b/src/main/resources/messages_hi_IN.properties index 4a991aee6..957238e06 100644 --- a/src/main/resources/messages_hi_IN.properties +++ b/src/main/resources/messages_hi_IN.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_hr_HR.properties b/src/main/resources/messages_hr_HR.properties index a1b50f5c2..f22c1272f 100644 --- a/src/main/resources/messages_hr_HR.properties +++ b/src/main/resources/messages_hr_HR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_hu_HU.properties b/src/main/resources/messages_hu_HU.properties index 17ad8054d..0ac19e2f8 100644 --- a/src/main/resources/messages_hu_HU.properties +++ b/src/main/resources/messages_hu_HU.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_id_ID.properties b/src/main/resources/messages_id_ID.properties index 486760f2e..4acf4dd44 100644 --- a/src/main/resources/messages_id_ID.properties +++ b/src/main/resources/messages_id_ID.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 3586a3f64..ef184cb48 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Pagina(e) selezionata(e) multiTool.undo=Annulla multiTool.redo=Rifai +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=Questa funzione è disponibile anche nella nostra pagina multi-strumento. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive! diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index 7d19771e7..709b40b88 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index bd0c4f405..d418fb1e0 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_nl_NL.properties b/src/main/resources/messages_nl_NL.properties index 7bbf83236..63425f1ab 100644 --- a/src/main/resources/messages_nl_NL.properties +++ b/src/main/resources/messages_nl_NL.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_no_NB.properties b/src/main/resources/messages_no_NB.properties index 930f9754b..25bd2d4ac 100644 --- a/src/main/resources/messages_no_NB.properties +++ b/src/main/resources/messages_no_NB.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 1d44e0276..2e142b822 100644 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_pt_BR.properties b/src/main/resources/messages_pt_BR.properties index 5b9515cf4..e5431c417 100644 --- a/src/main/resources/messages_pt_BR.properties +++ b/src/main/resources/messages_pt_BR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Página(s) Selecionadas multiTool.undo=Desfazer multiTool.redo=Refazer +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=Esta função também está disponível em Multiferramentas de PDF. Com uma interface mais completa e funções adicionais. diff --git a/src/main/resources/messages_pt_PT.properties b/src/main/resources/messages_pt_PT.properties index 9038ffa2d..f8e3a1269 100644 --- a/src/main/resources/messages_pt_PT.properties +++ b/src/main/resources/messages_pt_PT.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_ro_RO.properties b/src/main/resources/messages_ro_RO.properties index bd0d8fb1f..e8547092c 100644 --- a/src/main/resources/messages_ro_RO.properties +++ b/src/main/resources/messages_ro_RO.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index 4aaa428aa..55f20961b 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_sk_SK.properties b/src/main/resources/messages_sk_SK.properties index 925f113d4..4a3e09c64 100644 --- a/src/main/resources/messages_sk_SK.properties +++ b/src/main/resources/messages_sk_SK.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_sr_LATN_RS.properties b/src/main/resources/messages_sr_LATN_RS.properties index 3c87ec58d..eca995603 100644 --- a/src/main/resources/messages_sr_LATN_RS.properties +++ b/src/main/resources/messages_sr_LATN_RS.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index de9f606e1..006ef41da 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_th_TH.properties b/src/main/resources/messages_th_TH.properties index 27f0ef9d5..1d60581ed 100644 --- a/src/main/resources/messages_th_TH.properties +++ b/src/main/resources/messages_th_TH.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_tr_TR.properties b/src/main/resources/messages_tr_TR.properties index ebf41c730..d6c750764 100644 --- a/src/main/resources/messages_tr_TR.properties +++ b/src/main/resources/messages_tr_TR.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_uk_UA.properties b/src/main/resources/messages_uk_UA.properties index e1ae0fc43..7a96d49ed 100644 --- a/src/main/resources/messages_uk_UA.properties +++ b/src/main/resources/messages_uk_UA.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_vi_VN.properties b/src/main/resources/messages_vi_VN.properties index 55ac301cb..1b97853f9 100644 --- a/src/main/resources/messages_vi_VN.properties +++ b/src/main/resources/messages_vi_VN.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index 18558dc78..606f5c0d0 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! diff --git a/src/main/resources/messages_zh_TW.properties b/src/main/resources/messages_zh_TW.properties index 478e6966c..6938f6837 100644 --- a/src/main/resources/messages_zh_TW.properties +++ b/src/main/resources/messages_zh_TW.properties @@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected multiTool.undo=Undo multiTool.redo=Redo +#decrypt +decrypt.passwordPrompt=This file is password-protected. Please enter the password: +decrypt.cancelled=Operation cancelled for PDF: {0} +decrypt.noPassword=No password provided for encrypted PDF: {0} +decrypt.invalidPassword=Please try again with the correct password. +decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} +decrypt.unexpectedError=There was an error processing the file. Please try again. +decrypt.serverError=Server error while decrypting: {0} +decrypt.success=File decrypted successfully. + #multiTool-advert multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! From 67f983f00d591d7c09a06963439574e762723593 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 11 Dec 2024 21:06:07 +0100 Subject: [PATCH 07/49] Security fix: Server-Side Request Forgery https://github.com/Stirling-Tools/Stirling-PDF/security/advisories/GHSA-4v4c-9hpr-93vx --- .../software/SPDF/utils/GeneralUtils.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 8e56c8df6..4824db367 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -88,15 +88,45 @@ public class GeneralUtils { public static boolean isURLReachable(String urlStr) { try { + // Parse the URL URL url = URI.create(urlStr).toURL(); + + // Allow only http and https protocols + String protocol = url.getProtocol(); + if (!protocol.equals("http") && !protocol.equals("https")) { + return false; // Disallow other protocols + } + + // Check if the host is a local address + String host = url.getHost(); + if (isLocalAddress(host)) { + return false; // Exclude local addresses + } + + // Check if the URL is reachable HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(5000); // Set connection timeout + connection.setReadTimeout(5000); // Set read timeout int responseCode = connection.getResponseCode(); return (200 <= responseCode && responseCode <= 399); - } catch (MalformedURLException e) { - return false; - } catch (IOException e) { - return false; + } catch (Exception e) { + return false; // Return false in case of any exception + } + } + + private static boolean isLocalAddress(String host) { + try { + // Resolve DNS to IP address + InetAddress address = InetAddress.getByName(host); + + // Check for local addresses + return address.isAnyLocalAddress() || // Matches 0.0.0.0 or similar + address.isLoopbackAddress() || // Matches 127.0.0.1 or ::1 + address.isSiteLocalAddress() || // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x + address.getHostAddress().startsWith("fe80:"); // Matches link-local IPv6 addresses + } catch (Exception e) { + return false; // Return false for invalid or unresolved addresses } } From c3f88f716c214a57492c0f3745f97814c4c38475 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 11 Dec 2024 21:10:18 +0100 Subject: [PATCH 08/49] Update GeneralUtils.java --- src/main/java/stirling/software/SPDF/utils/GeneralUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 4824db367..ce09430c0 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -106,8 +106,8 @@ public class GeneralUtils { // Check if the URL is reachable HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); - connection.setConnectTimeout(5000); // Set connection timeout - connection.setReadTimeout(5000); // Set read timeout + // connection.setConnectTimeout(5000); // Set connection timeout + // connection.setReadTimeout(5000); // Set read timeout int responseCode = connection.getResponseCode(); return (200 <= responseCode && responseCode <= 399); } catch (Exception e) { From 97d28ac6d24a52e4e36bae5b1cabc4bc120c05ff Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 21:54:05 +0000 Subject: [PATCH 09/49] Windows UI .exe --- .github/workflows/releaseArtifacts.yml | 58 ++- .gitignore | 1 + build.gradle | 28 +- .../software/SPDF/SPdfApplication.java | 65 ++-- .../stirling/software/SPDF/UI/WebBrowser.java | 7 + .../software/SPDF/UI/impl/DesktopBrowser.java | 358 ++++++++++++++++++ .../software/SPDF/UI/impl/LoadingWindow.java | 115 ++++++ .../SPDF/config/EndpointConfiguration.java | 3 + .../config/security/InitialSecuritySetup.java | 1 - 9 files changed, 597 insertions(+), 39 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/UI/WebBrowser.java create mode 100644 src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java create mode 100644 src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 8adfa0fee..9d34d3faf 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -35,6 +35,7 @@ jobs: run: ./gradlew clean createExe env: DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} + STIRLING_PDF_DESKTOP_UI: false - name: Get version number id: versionNumber @@ -42,13 +43,13 @@ jobs: - name: Rename binarie if: matrix.file_suffix != '' - run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe + run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Upload Assets binarie uses: actions/upload-artifact@v4 with: - path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe - name: Stirling-PDF${{ matrix.file_suffix }}.exe + path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe + name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe overwrite: true retention-days: 1 if-no-files-found: error @@ -58,13 +59,13 @@ jobs: files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe - name: Rename jar binaries - run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar + run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar - name: Upload Assets jar binaries uses: actions/upload-artifact@v4 with: - path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar - name: Stirling-PDF${{ matrix.file_suffix }}.jar + path: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar + name: Stirling-PDF-Server${{ matrix.file_suffix }}.jar overwrite: true retention-days: 1 if-no-files-found: error @@ -72,4 +73,47 @@ jobs: - name: Upload jar binaries to release uses: softprops/action-gh-release@v2 with: - files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar + files: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar + + + push-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.7 + + - name: Generate exe + run: ./gradlew clean createExe + env: + DOCKER_ENABLE_SECURITY: false + STIRLING_PDF_DESKTOP_UI: true + + - name: Get version number + id: versionNumber + run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + + - name: Rename binarie + run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF.exe + + - name: Upload Assets binarie + uses: actions/upload-artifact@v4 + with: + path: ./build/launch4j/Stirling-PDF.exe + name: Stirling-PDF.exe + overwrite: true + retention-days: 1 + if-no-files-found: error + + - name: Upload binaries to release + uses: softprops/action-gh-release@v2 + with: + files: ./build/launch4j/Stirling-PDF.exe diff --git a/.gitignore b/.gitignore index b0bbfb9d3..a79a2712f 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ out/ .pytest_cache .ipynb_checkpoints +**/jcef-bundle/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index eb9c3c922..6034b9f90 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,9 @@ version = "0.36.0" java { // 17 is lowest but we support and recommend 21 sourceCompatibility = JavaVersion.VERSION_17 + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } repositories { @@ -41,6 +44,10 @@ repositories { maven { url 'https://build.shibboleth.net/maven/releases' } + maven { url "https://build.shibboleth.net/maven/releases" } + // Add Maven repository for JCEF + maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" } + } licenseReport { @@ -64,6 +71,12 @@ sourceSets { exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/repository/**" } + + if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") { + exclude "stirling/software/SPDF/UI/impl/**" + } + + } } } @@ -78,12 +91,16 @@ launch4j { icon = "${projectDir}/src/main/resources/static/favicon.ico" outfile="Stirling-PDF.exe" - headerType="console" + if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') { + headerType = "gui" + } else { + headerType = "console" + } jarTask = tasks.bootJar errTitle="Encountered error, Do you have Java 21?" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" - variables=["BROWSER_OPEN=true"] + variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"] jreMinVersion="17" mutexName="Stirling-PDF" @@ -123,6 +140,13 @@ configurations.all { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" } dependencies { + + if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") { + implementation "me.friwi:jcefmaven:127.3.1" + implementation "org.openjfx:javafx-controls:21" + implementation "org.openjfx:javafx-swing:21" + } + //security updates implementation "org.springframework:spring-webmvc:6.2.0" diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index eddf7306c..191086a48 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -1,5 +1,6 @@ package stirling.software.SPDF; +import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.nio.file.Files; @@ -8,6 +9,9 @@ import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Properties; + +import javax.swing.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,14 +22,16 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableScheduling; -import io.github.pixee.security.SystemCommand; - import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.model.ApplicationProperties; @SpringBootApplication @EnableScheduling +@Slf4j public class SPdfApplication { private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class); @@ -67,36 +73,12 @@ public class SPdfApplication { } } - @PostConstruct - public void init() { - baseUrlStatic = this.baseUrl; - // Check if the BROWSER_OPEN environment variable is set to true - String browserOpenEnv = env.getProperty("BROWSER_OPEN"); - boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); - if (browserOpen) { - try { - String url = baseUrl + ":" + getStaticPort(); - - String os = System.getProperty("os.name").toLowerCase(); - Runtime rt = Runtime.getRuntime(); - if (os.contains("win")) { - // For Windows - SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); - } else if (os.contains("mac")) { - SystemCommand.runCommand(rt, "open " + url); - } else if (os.contains("nix") || os.contains("nux")) { - SystemCommand.runCommand(rt, "xdg-open " + url); - } - } catch (Exception e) { - logger.error("Error opening browser: {}", e.getMessage()); - } - } - logger.info("Running configs {}", applicationProperties.toString()); - } - public static void main(String[] args) throws IOException, InterruptedException { + System.setProperty("java.awt.headless", "false"); + SpringApplication app = new SpringApplication(SPdfApplication.class); + app.setHeadless(false); app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); @@ -128,6 +110,11 @@ public class SPdfApplication { propertyFiles.get("spring.config.additional-location"))); } + Properties props = new Properties(); + props.put("java.awt.headless", "false"); + props.put("spring.main.web-application-type", "servlet"); + app.setDefaultProperties(props); + app.run(args); // Ensure directories are created @@ -147,6 +134,26 @@ public class SPdfApplication { logger.info("Navigate to {}", url); } + @Autowired(required = false) + private WebBrowser webBrowser; + + @PostConstruct + public void init() { + baseUrlStatic = this.baseUrl; + String url = baseUrl + ":" + getStaticPort(); + if (webBrowser != null && "true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { + + webBrowser.initWebUI(url); + } + } + + @PreDestroy + public void cleanup() { + if (webBrowser != null) { + webBrowser.cleanup(); + } + } + public static String getStaticBaseUrl() { return baseUrlStatic; } diff --git a/src/main/java/stirling/software/SPDF/UI/WebBrowser.java b/src/main/java/stirling/software/SPDF/UI/WebBrowser.java new file mode 100644 index 000000000..b884888fe --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/WebBrowser.java @@ -0,0 +1,7 @@ +package stirling.software.SPDF.UI; + +public interface WebBrowser { + void initWebUI(String url); + + void cleanup(); +} diff --git a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java new file mode 100644 index 000000000..835c36a99 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java @@ -0,0 +1,358 @@ +package stirling.software.SPDF.UI.impl; + +import java.awt.AWTException; +import java.awt.BorderLayout; +import java.awt.Frame; +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.TrayIcon; +import java.awt.event.WindowEvent; +import java.awt.event.WindowStateListener; +import java.io.File; +import java.io.InputStream; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import javax.imageio.ImageIO; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +import org.cef.CefApp; +import org.cef.CefClient; +import org.cef.CefSettings; +import org.cef.browser.CefBrowser; +import org.cef.callback.CefBeforeDownloadCallback; +import org.cef.callback.CefDownloadItem; +import org.cef.callback.CefDownloadItemCallback; +import org.cef.handler.CefDownloadHandlerAdapter; +import org.cef.handler.CefLoadHandlerAdapter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import me.friwi.jcefmaven.CefAppBuilder; +import me.friwi.jcefmaven.EnumProgress; +import me.friwi.jcefmaven.MavenCefAppHandlerAdapter; +import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; +import stirling.software.SPDF.UI.WebBrowser; + +@Component +@Slf4j +@ConditionalOnProperty( + name = "STIRLING_PDF_DESKTOP_UI", + havingValue = "true", + matchIfMissing = false) +public class DesktopBrowser implements WebBrowser { + private static CefApp cefApp; + private static CefClient client; + private static CefBrowser browser; + private static JFrame frame; + private static LoadingWindow loadingWindow; + private static volatile boolean browserInitialized = false; + private static TrayIcon trayIcon; + private static SystemTray systemTray; + + public DesktopBrowser() { + SwingUtilities.invokeLater( + () -> { + loadingWindow = new LoadingWindow(null, "Initializing..."); + loadingWindow.setVisible(true); + }); + } + + public void initWebUI(String url) { + CompletableFuture.runAsync( + () -> { + try { + CefAppBuilder builder = new CefAppBuilder(); + configureCefSettings(builder); + + builder.setProgressHandler(createProgressHandler()); + + // Build and initialize CEF + cefApp = builder.build(); + client = cefApp.createClient(); + + // Set up download handler + setupDownloadHandler(); + + // Create browser and frame on EDT + SwingUtilities.invokeAndWait( + () -> { + browser = client.createBrowser(url, false, false); + setupMainFrame(); + setupLoadHandler(); + + // Show the frame immediately but transparent + frame.setVisible(true); + }); + + } catch (Exception e) { + log.error("Error initializing JCEF browser: ", e); + cleanup(); + } + }); + } + + private void configureCefSettings(CefAppBuilder builder) { + CefSettings settings = builder.getCefSettings(); + settings.cache_path = new File("jcef-bundle").getAbsolutePath(); + settings.root_cache_path = new File("jcef-bundle").getAbsolutePath(); + settings.persist_session_cookies = true; + settings.windowless_rendering_enabled = false; + settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO; + + builder.setAppHandler( + new MavenCefAppHandlerAdapter() { + @Override + public void stateHasChanged(org.cef.CefApp.CefAppState state) { + log.info("CEF state changed: " + state); + if (state == CefApp.CefAppState.TERMINATED) { + System.exit(0); + } + } + }); + } + + private void setupDownloadHandler() { + client.addDownloadHandler( + new CefDownloadHandlerAdapter() { + @Override + public boolean onBeforeDownload( + CefBrowser browser, + CefDownloadItem downloadItem, + String suggestedName, + CefBeforeDownloadCallback callback) { + callback.Continue("", true); + return true; + } + + @Override + public void onDownloadUpdated( + CefBrowser browser, + CefDownloadItem downloadItem, + CefDownloadItemCallback callback) { + if (downloadItem.isComplete()) { + log.info("Download completed: " + downloadItem.getFullPath()); + } else if (downloadItem.isCanceled()) { + log.info("Download canceled: " + downloadItem.getFullPath()); + } + } + }); + } + + private ConsoleProgressHandler createProgressHandler() { + return new ConsoleProgressHandler() { + @Override + public void handleProgress(EnumProgress state, float percent) { + Objects.requireNonNull(state, "state cannot be null"); + SwingUtilities.invokeLater( + () -> { + if (loadingWindow != null) { + switch (state) { + case LOCATING: + loadingWindow.setStatus("Locating Chromium..."); + loadingWindow.setProgress(0); + break; + case DOWNLOADING: + if (percent >= 0) { + loadingWindow.setStatus( + String.format( + "Downloading Chromium: %.0f%%", + percent)); + loadingWindow.setProgress((int) percent); + } + break; + case EXTRACTING: + loadingWindow.setStatus("Extracting Chromium..."); + loadingWindow.setProgress(60); + break; + case INITIALIZING: + loadingWindow.setStatus("Initializing browser..."); + loadingWindow.setProgress(80); + break; + case INITIALIZED: + loadingWindow.setStatus("Finalising startup..."); + loadingWindow.setProgress(90); + break; + } + } + }); + } + }; + } + + private void setupMainFrame() { + frame = new JFrame("Stirling-PDF"); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setUndecorated(true); + frame.setOpacity(0.0f); + + JPanel contentPane = new JPanel(new BorderLayout()); + contentPane.setDoubleBuffered(true); + contentPane.add(browser.getUIComponent(), BorderLayout.CENTER); + frame.setContentPane(contentPane); + + frame.addWindowListener( + new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(java.awt.event.WindowEvent windowEvent) { + cleanup(); + System.exit(0); + } + }); + + frame.setSize(1280, 768); + frame.setLocationRelativeTo(null); + + loadIcon(); + } + + private void setupLoadHandler() { + client.addLoadHandler( + new CefLoadHandlerAdapter() { + @Override + public void onLoadingStateChange( + CefBrowser browser, + boolean isLoading, + boolean canGoBack, + boolean canGoForward) { + log.info("Loading state changed: " + isLoading); + if (!isLoading && !browserInitialized) { + browserInitialized = true; + SwingUtilities.invokeLater( + () -> { + if (loadingWindow != null) { + Timer timer = + new Timer( + 500, + e -> { + loadingWindow.dispose(); + loadingWindow = null; + + frame.dispose(); + frame.setOpacity(1.0f); + frame.setUndecorated(false); + frame.pack(); + frame.setSize(1280, 800); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + frame.requestFocus(); + frame.toFront(); + browser.getUIComponent() + .requestFocus(); + }); + timer.setRepeats(false); + timer.start(); + } + }); + } + } + }); + } + + private void setupTrayIcon(Image icon) { + if (!SystemTray.isSupported()) { + log.warn("System tray is not supported"); + return; + } + + try { + systemTray = SystemTray.getSystemTray(); + + // Create popup menu + PopupMenu popup = new PopupMenu(); + + // Create menu items + MenuItem showItem = new MenuItem("Show"); + showItem.addActionListener( + e -> { + frame.setVisible(true); + frame.setState(Frame.NORMAL); + }); + + MenuItem exitItem = new MenuItem("Exit"); + exitItem.addActionListener( + e -> { + cleanup(); + System.exit(0); + }); + + // Add menu items to popup menu + popup.add(showItem); + popup.addSeparator(); + popup.add(exitItem); + + // Create tray icon + trayIcon = new TrayIcon(icon, "Stirling-PDF", popup); + trayIcon.setImageAutoSize(true); + + // Add double-click behavior + trayIcon.addActionListener( + e -> { + frame.setVisible(true); + frame.setState(Frame.NORMAL); + }); + + // Add tray icon to system tray + systemTray.add(trayIcon); + + // Modify frame behavior to minimize to tray + frame.addWindowStateListener( + new WindowStateListener() { + public void windowStateChanged(WindowEvent e) { + if (e.getNewState() == Frame.ICONIFIED) { + frame.setVisible(false); + } + } + }); + + } catch (AWTException e) { + log.error("Error setting up system tray icon", e); + } + } + + private void loadIcon() { + try { + Image icon = null; + String[] iconPaths = {"/static/favicon.ico"}; + + for (String path : iconPaths) { + if (icon != null) break; + try { + try (InputStream is = getClass().getResourceAsStream(path)) { + if (is != null) { + icon = ImageIO.read(is); + break; + } + } + } catch (Exception e) { + log.debug("Could not load icon from " + path, e); + } + } + + if (icon != null) { + frame.setIconImage(icon); + setupTrayIcon(icon); + log.info("Successfully set frame icon"); + } else { + log.warn("Could not load icon from any source"); + } + } catch (Exception e) { + log.error("Error loading icon", e); + } + } + + @PreDestroy + public void cleanup() { + if (browser != null) browser.close(true); + if (client != null) client.dispose(); + if (cefApp != null) cefApp.dispose(); + if (loadingWindow != null) loadingWindow.dispose(); + } +} diff --git a/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java new file mode 100644 index 000000000..87b253f82 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java @@ -0,0 +1,115 @@ +package stirling.software.SPDF.UI.impl; + +import java.awt.*; +import java.io.InputStream; + +import javax.imageio.ImageIO; +import javax.swing.*; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class LoadingWindow extends JDialog { + private final JProgressBar progressBar; + private final JLabel statusLabel; + private final JPanel mainPanel; + private final JLabel brandLabel; + + public LoadingWindow(Frame parent, String initialUrl) { + super(parent, "Initializing Stirling-PDF", true); + + // Initialize components + mainPanel = new JPanel(); + mainPanel.setBackground(Color.WHITE); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30)); + mainPanel.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + // Configure GridBagConstraints + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + gbc.weightx = 1.0; // Add horizontal weight + gbc.weighty = 0.0; // Add vertical weight + + // Add icon + try { + try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) { + if (is != null) { + Image img = ImageIO.read(is); + if (img != null) { + Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH); + JLabel iconLabel = new JLabel(new ImageIcon(scaledImg)); + iconLabel.setHorizontalAlignment(SwingConstants.CENTER); + gbc.gridy = 0; + mainPanel.add(iconLabel, gbc); + } + } + } + } catch (Exception e) { + log.error("Failed to load icon", e); + } + // URL Label with explicit size + brandLabel = new JLabel(initialUrl); + brandLabel.setHorizontalAlignment(SwingConstants.CENTER); + brandLabel.setPreferredSize(new Dimension(300, 25)); + brandLabel.setText("Stirling-PDF"); + gbc.gridy = 1; + mainPanel.add(brandLabel, gbc); + + // Status label with explicit size + statusLabel = new JLabel("Initializing..."); + statusLabel.setHorizontalAlignment(SwingConstants.CENTER); + statusLabel.setPreferredSize(new Dimension(300, 25)); + gbc.gridy = 2; + mainPanel.add(statusLabel, gbc); + // Progress bar with explicit size + progressBar = new JProgressBar(0, 100); + progressBar.setStringPainted(true); + progressBar.setPreferredSize(new Dimension(300, 25)); + gbc.gridy = 3; + mainPanel.add(progressBar, gbc); + + // Set dialog properties + setContentPane(mainPanel); + setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + setResizable(false); + setUndecorated(false); + + // Set size and position + setSize(400, 200); + setLocationRelativeTo(parent); + setAlwaysOnTop(true); + setProgress(0); + setStatus("Starting..."); + } + + public void setProgress(final int progress) { + SwingUtilities.invokeLater( + () -> { + try { + progressBar.setValue(Math.min(Math.max(progress, 0), 100)); + progressBar.setString(progress + "%"); + log.info(progress + "%"); + mainPanel.revalidate(); + mainPanel.repaint(); + } catch (Exception e) { + log.error("Error updating progress", e); + } + }); + } + + public void setStatus(final String status) { + log.info(status); + SwingUtilities.invokeLater( + () -> { + try { + statusLabel.setText(status != null ? status : ""); + mainPanel.revalidate(); + mainPanel.repaint(); + } catch (Exception e) { + log.error("Error updating status", e); + } + }); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 3ce6f2bb8..c144007b8 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -260,6 +260,9 @@ public class EndpointConfiguration { // Pdftohtml dependent endpoints addEndpointToGroup("Pdftohtml", "pdf-to-html"); + + // disabled for now while we resolve issues + disableEndpoint("pdf-to-pdfa"); } private void processEnvironmentConfigs() { diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 2fcebcad8..3291021cb 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -76,6 +76,5 @@ public class InitialSecuritySetup { log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); } userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); - System.out.println(applicationProperties.getSecurity().getCustomGlobalAPIKey()); } } From eb20f5195837de6e2b9ac89802433f4390b717e8 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 21:56:50 +0000 Subject: [PATCH 10/49] headless --- src/main/java/stirling/software/SPDF/SPdfApplication.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 191086a48..5b2c1bed8 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -75,10 +75,14 @@ public class SPdfApplication { public static void main(String[] args) throws IOException, InterruptedException { - System.setProperty("java.awt.headless", "false"); + SpringApplication app = new SpringApplication(SPdfApplication.class); - app.setHeadless(false); + + if("true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { + System.setProperty("java.awt.headless", "false"); + app.setHeadless(false); + } app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); From c20d37518d6a36db57ec174491fe7b6a512b7930 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 22:09:35 +0000 Subject: [PATCH 11/49] prop fixes --- .../software/SPDF/SPdfApplication.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 5b2c1bed8..2258bb055 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -79,10 +79,15 @@ public class SPdfApplication { SpringApplication app = new SpringApplication(SPdfApplication.class); + Properties props = new Properties(); + if("true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { System.setProperty("java.awt.headless", "false"); app.setHeadless(false); + props.put("java.awt.headless", "false"); + props.put("spring.main.web-application-type", "servlet"); } + app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); @@ -106,18 +111,12 @@ public class SPdfApplication { } else { logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); } - - if (!propertyFiles.isEmpty()) { - app.setDefaultProperties( - Collections.singletonMap( - "spring.config.additional-location", - propertyFiles.get("spring.config.additional-location"))); - } - - Properties props = new Properties(); - props.put("java.awt.headless", "false"); - props.put("spring.main.web-application-type", "servlet"); - app.setDefaultProperties(props); + Properties finalProps = new Properties(); + finalProps.putAll(Collections.singletonMap( + "spring.config.additional-location", + propertyFiles.get("spring.config.additional-location"))); + finalProps.putAll(props); + app.setDefaultProperties(finalProps); app.run(args); From dd5af46906ec46f7ab872042b50bc972621021c9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 22:17:27 +0000 Subject: [PATCH 12/49] gha fix --- .github/workflows/releaseArtifacts.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 9d34d3faf..db1d824ba 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -42,7 +42,6 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Rename binarie - if: matrix.file_suffix != '' run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Upload Assets binarie @@ -101,9 +100,6 @@ jobs: id: versionNumber run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - - name: Rename binarie - run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF.exe - - name: Upload Assets binarie uses: actions/upload-artifact@v4 with: From 11273b1589c8ec403469d88a933f7615aa6410ae Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 22:29:58 +0000 Subject: [PATCH 13/49] fix gha release --- .github/workflows/releaseArtifacts.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index db1d824ba..851fd6cab 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -52,10 +52,11 @@ jobs: overwrite: true retention-days: 1 if-no-files-found: error + - name: Upload binaries to release uses: softprops/action-gh-release@v2 with: - files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe + files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Rename jar binaries run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar From 1c5dfc46a05651025b8aa1e13b6c5353ff30c0fe Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 23:13:23 +0000 Subject: [PATCH 14/49] fixes --- build.gradle | 1 + .../software/SPDF/SPdfApplication.java | 33 +++++++++++-------- .../software/SPDF/UI/impl/LoadingWindow.java | 1 - 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 6034b9f90..b6e2166d2 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,7 @@ launch4j { icon = "${projectDir}/src/main/resources/static/favicon.ico" outfile="Stirling-PDF.exe" + if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') { headerType = "gui" } else { diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 2258bb055..f39c0e188 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -75,19 +75,17 @@ public class SPdfApplication { public static void main(String[] args) throws IOException, InterruptedException { - - SpringApplication app = new SpringApplication(SPdfApplication.class); - + Properties props = new Properties(); - - if("true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { - System.setProperty("java.awt.headless", "false"); - app.setHeadless(false); - props.put("java.awt.headless", "false"); - props.put("spring.main.web-application-type", "servlet"); + + if ("true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { + System.setProperty("java.awt.headless", "false"); + app.setHeadless(false); + // props.put("java.awt.headless", "false"); + // props.put("spring.main.web-application-type", "servlet"); } - + app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); @@ -112,10 +110,17 @@ public class SPdfApplication { logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); } Properties finalProps = new Properties(); - finalProps.putAll(Collections.singletonMap( - "spring.config.additional-location", - propertyFiles.get("spring.config.additional-location"))); - finalProps.putAll(props); + + if (!propertyFiles.isEmpty()) { + finalProps.putAll( + Collections.singletonMap( + "spring.config.additional-location", + propertyFiles.get("spring.config.additional-location"))); + } + + if (props.isEmpty()) { + finalProps.putAll(props); + } app.setDefaultProperties(finalProps); app.run(args); diff --git a/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java index 87b253f82..ad827dc5d 100644 --- a/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java +++ b/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java @@ -90,7 +90,6 @@ public class LoadingWindow extends JDialog { try { progressBar.setValue(Math.min(Math.max(progress, 0), 100)); progressBar.setString(progress + "%"); - log.info(progress + "%"); mainPanel.revalidate(); mainPanel.repaint(); } catch (Exception e) { From e014bb023b7b6f2d74b9369ee8335ae7e61c49e6 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 11 Dec 2024 23:21:56 +0000 Subject: [PATCH 15/49] fixes --- build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b6e2166d2..5363a4cc4 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,15 @@ launch4j { errTitle="Encountered error, Do you have Java 21?" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" - variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"] + + if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') { + variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"] + } else { + variables=["BROWSER_OPEN=true"] + } + + + jreMinVersion="17" mutexName="Stirling-PDF" From a7e7d57b2370b8f4d0a132c7bac9db29b19debe4 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 12 Dec 2024 00:48:00 +0000 Subject: [PATCH 16/49] fix: Dockerfile-ultra-lite to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201 - https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201 --- Dockerfile-ultra-lite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-ultra-lite b/Dockerfile-ultra-lite index c1bdd80de..09e4a5a38 100644 --- a/Dockerfile-ultra-lite +++ b/Dockerfile-ultra-lite @@ -1,5 +1,5 @@ # use alpine -FROM alpine:3.20.3 +FROM alpine:3.21.0 ARG VERSION_TAG From 1c0d1efda63db3edb73150c2f9875ef0865800df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Dec 2024 11:01:38 +0000 Subject: [PATCH 17/49] :memo: Sync README > Made via sync_files.yml --- README.md | 46 ++++++++++++++++----------------- scripts/ignore_translation.toml | 5 ++++ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 35164983d..a390e377b 100644 --- a/README.md +++ b/README.md @@ -191,44 +191,44 @@ Stirling-PDF currently supports 38 languages! | Language | Progress | | -------------------------------------------- | -------------------------------------- | -| Arabic (العربية) (ar_AR) | ![95%](https://geps.dev/progress/95) | +| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) | -| Basque (Euskara) (eu_ES) | ![54%](https://geps.dev/progress/54) | +| Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) | | Bulgarian (Български) (bg_BG) | ![90%](https://geps.dev/progress/90) | -| Catalan (Català) (ca_CA) | ![85%](https://geps.dev/progress/85) | -| Croatian (Hrvatski) (hr_HR) | ![92%](https://geps.dev/progress/92) | +| Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) | +| Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) | | Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) | | Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) | -| Dutch (Nederlands) (nl_NL) | ![90%](https://geps.dev/progress/90) | +| Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | -| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) | -| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) | -| Greek (Ελληνικά) (el_GR) | ![91%](https://geps.dev/progress/91) | -| Hindi (हिंदी) (hi_IN) | ![89%](https://geps.dev/progress/89) | -| Hungarian (Magyar) (hu_HU) | ![92%](https://geps.dev/progress/92) | +| French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) | +| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | +| Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) | +| Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) | +| Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) | | Indonesian (Bahasa Indonesia) (id_ID) | ![91%](https://geps.dev/progress/91) | | Irish (Gaeilge) (ga_IE) | ![83%](https://geps.dev/progress/83) | -| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | +| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | | Japanese (日本語) (ja_JP) | ![81%](https://geps.dev/progress/81) | -| Korean (한국어) (ko_KR) | ![90%](https://geps.dev/progress/90) | -| Norwegian (Norsk) (no_NB) | ![83%](https://geps.dev/progress/83) | -| Persian (فارسی) (fa_IR) | ![100%](https://geps.dev/progress/100) | -| Polish (Polski) (pl_PL) | ![91%](https://geps.dev/progress/91) | +| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) | +| Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) | +| Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) | +| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) | | Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) | -| Portuguese Brazilian (Português) (pt_BR) | ![92%](https://geps.dev/progress/92) | +| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) | | Romanian (Română) (ro_RO) | ![85%](https://geps.dev/progress/85) | -| Russian (Русский) (ru_RU) | ![91%](https://geps.dev/progress/91) | +| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) | | Simplified Chinese (简体中文) (zh_CN) | ![86%](https://geps.dev/progress/86) | | Slovakian (Slovensky) (sk_SK) | ![78%](https://geps.dev/progress/78) | -| Spanish (Español) (es_ES) | ![92%](https://geps.dev/progress/92) | -| Swedish (Svenska) (sv_SE) | ![91%](https://geps.dev/progress/91) | -| Thai (ไทย) (th_TH) | ![91%](https://geps.dev/progress/91) | -| Traditional Chinese (繁體中文) (zh_TW) | ![92%](https://geps.dev/progress/92) | -| Turkish (Türkçe) (tr_TR) | ![87%](https://geps.dev/progress/87) | +| Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) | +| Swedish (Svenska) (sv_SE) | ![90%](https://geps.dev/progress/90) | +| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) | +| Traditional Chinese (繁體中文) (zh_TW) | ![91%](https://geps.dev/progress/91) | +| Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) | | Ukrainian (Українська) (uk_UA) | ![76%](https://geps.dev/progress/76) | -| Vietnamese (Tiếng Việt) (vi_VN) | ![84%](https://geps.dev/progress/84) | +| Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) | ## Contributing (Creating Issues, Translations, Fixing Bugs, etc.) diff --git a/scripts/ignore_translation.toml b/scripts/ignore_translation.toml index 75cd2e0eb..281cf8638 100644 --- a/scripts/ignore_translation.toml +++ b/scripts/ignore_translation.toml @@ -77,6 +77,11 @@ ignore = [ 'language.direction', ] +[fa_IR] +ignore = [ + 'language.direction', +] + [fr_FR] ignore = [ 'AddStampRequest.alphabet', From 44be2b99d2eb5b138e5f7cafa88e1144ee7eca5d Mon Sep 17 00:00:00 2001 From: Harshad Marathe Date: Thu, 12 Dec 2024 16:49:24 +0530 Subject: [PATCH 18/49] Fix collapsed menu after reload on state closed --- src/main/resources/static/css/home.css | 1 + src/main/resources/static/js/homecard.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css index 8faa91638..301abe81b 100644 --- a/src/main/resources/static/css/home.css +++ b/src/main/resources/static/css/home.css @@ -65,6 +65,7 @@ overflow: hidden; margin: -20px; padding: 20px; + box-sizing:content-box; } .feature-group-container.animated-group { diff --git a/src/main/resources/static/js/homecard.js b/src/main/resources/static/js/homecard.js index 6dddfa2bc..9b5297f3c 100644 --- a/src/main/resources/static/js/homecard.js +++ b/src/main/resources/static/js/homecard.js @@ -268,7 +268,7 @@ document.addEventListener("DOMContentLoaded", function () { const parent = header.parentNode; const container = header.parentNode.querySelector(".feature-group-container"); if (parent.id !== "groupFavorites") { - container.style.maxHeight = container.clientHeight + "px"; + container.style.maxHeight = container.scrollHeight + "px"; } header.onclick = () => { expandCollapseToggle(parent); From 83ddfdf152c97df06754b657dbf771dbcc0fc702 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:29:57 +0000 Subject: [PATCH 19/49] Bump alpine from 3.20.3 to 3.21.0 Bumps alpine from 3.20.3 to 3.21.0. --- updated-dependencies: - dependency-name: alpine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile-fat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 08ef76644..82530a885 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Main stage -FROM alpine:3.20.3 +FROM alpine:3.21.0 # Copy necessary files COPY scripts /scripts diff --git a/Dockerfile-fat b/Dockerfile-fat index d34c7daa4..e96f635ab 100644 --- a/Dockerfile-fat +++ b/Dockerfile-fat @@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \ ./gradlew clean build # Main stage -FROM alpine:3.20.3 +FROM alpine:3.21.0 # Copy necessary files COPY scripts /scripts From a7960d992c90cb3bbfa20fb2efa6b7dc686da216 Mon Sep 17 00:00:00 2001 From: lihui Date: Fri, 13 Dec 2024 00:42:18 +0800 Subject: [PATCH 20/49] add and refactor CN translate --- src/main/resources/messages_zh_CN.properties | 618 +++++++++---------- 1 file changed, 309 insertions(+), 309 deletions(-) diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index 606f5c0d0..6f3f67dac 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -3,11 +3,11 @@ ########### # the direction that the language is written (ltr = left to right, rtl = right to left) language.direction=ltr -addPageNumbers.fontSize=Font Size -addPageNumbers.fontName=Font Name -pdfPrompt=选择PDF -multiPdfPrompt=选择多个PDF(2个或更多) -multiPdfDropPrompt=选择(或拖拽)所需的PDF +addPageNumbers.fontSize=字体大小 +addPageNumbers.fontName=字体名称 +pdfPrompt=选择 PDF +multiPdfPrompt=选择多个 PDF(2个或更多) +multiPdfDropPrompt=选择(或拖拽)所需的 PDF imgPrompt=选择图像 genericSubmit=提交 processTimeWarning=警告:此过程可能需要多达一分钟,具体时间取决于文件大小 @@ -25,7 +25,7 @@ noFavourites=没有添加收藏夹 downloadComplete=下载完成 bored=等待时觉得无聊? alphabet=字母表 -downloadPdf=下载PDF +downloadPdf=下载 PDF text=文本 font=字体 selectFillter=-- 选择-- @@ -63,15 +63,15 @@ deleteUsernameExistsMessage=用户名不存在,无法删除。 downgradeCurrentUserMessage=无法降级当前用户的角色 disabledCurrentUserMessage=无法禁用当前用户。 downgradeCurrentUserLongMessage=无法降级当前用户的角色。因此,当前用户将不会显示。 -userAlreadyExistsOAuthMessage=该用户已作为OAuth2用户存在。 -userAlreadyExistsWebMessage=该用户已作为Web用户存在。 +userAlreadyExistsOAuthMessage=该用户已作为 OAuth2 用户存在。 +userAlreadyExistsWebMessage=该用户已作为 Web 用户存在。 error=错误 oops=哎呀! help=帮助 goHomepage=返回主页 -joinDiscord=加入我们的Discord服务器 -seeDockerHub=查看Docker Hub -visitGithub=访问Github仓库 +joinDiscord=加入我们的 Discord 服务器 +seeDockerHub=查看 Docker Hub +visitGithub=访问 Github 仓库 donate=捐款 color=颜色 sponsor=赞助 @@ -79,14 +79,14 @@ info=信息 pro=Pro page=Page pages=Pages -loading=Loading... +loading=加载中... addToDoc=Add to Document -reset=Reset +reset=重置 -legal.privacy=Privacy Policy -legal.terms=Terms and Conditions -legal.accessibility=Accessibility -legal.cookie=Cookie Policy +legal.privacy=隐私政策 +legal.terms=服务条款 +legal.accessibility=无障碍 +legal.cookie=Cookie 政策 legal.impressum=Impressum ############### @@ -117,8 +117,8 @@ pipelineOptions.validateButton=验证 ######################## # ENTERPRISE EDITION # ######################## -enterpriseEdition.button=Upgrade to Pro -enterpriseEdition.warning=This feature is only available to Pro users. +enterpriseEdition.button=升级到 Pro 版本 +enterpriseEdition.warning=此功能仅适用于 Pro 版本 enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro @@ -192,11 +192,11 @@ account.newPassword=新密码 account.changePassword=更改密码 account.confirmNewPassword=确认新密码 account.signOut=退出登录 -account.yourApiKey=您的API密钥 +account.yourApiKey=您的 API 密钥 account.syncTitle=将浏览器设置与账户同步 account.settingsCompare=设置比较: account.property=属性 -account.webBrowserSettings=Web浏览器设置 +account.webBrowserSettings=Web 浏览器设置 account.syncToBrowser=同步账户 -> 浏览器 account.syncToAccount=同步账户 <- 浏览器 @@ -213,11 +213,11 @@ adminUserSettings.usernameInfo=用户名只能包含字母、数字和以下特 adminUserSettings.roles=角色 adminUserSettings.role=角色 adminUserSettings.actions=操作 -adminUserSettings.apiUser=受限制的API用户 -adminUserSettings.extraApiUser=额外受限制的API用户 -adminUserSettings.webOnlyUser=仅限Web用户 +adminUserSettings.apiUser=受限制的 API 用户 +adminUserSettings.extraApiUser=额外受限制的 API 用户 +adminUserSettings.webOnlyUser=仅限 Web 用户 adminUserSettings.demoUser=演示用户(无自定义设置) -adminUserSettings.internalApiUser=内部API用户 +adminUserSettings.internalApiUser=内部 API 用户 adminUserSettings.forceChange=强制用户在登录时更改用户名/密码 adminUserSettings.submit=保存用户 adminUserSettings.changeUserRole=更改用户角色 @@ -247,26 +247,26 @@ database.fileNotFound=未找到文件 database.fileNullOrEmpty=文件不能为空 database.failedImportFile=导入文件失败 -session.expired=Your session has expired. Please refresh the page and try again. -session.refreshPage=Refresh Page +session.expired=您的会话已过期。请刷新页面并重试。 +session.refreshPage=刷新页面 ############# # HOME-PAGE # ############# -home.desc=本地部署的一站式服务,满足您的所有PDF需求。 +home.desc=本地部署的一站式服务,满足您的所有 PDF 需求。 home.searchBar=搜索您需要的功能... -home.viewPdf.title=浏览PDF +home.viewPdf.title=浏览 PDF home.viewPdf.desc=浏览、注释、添加文本或图像 viewPdf.tags=浏览、阅读、注释、文本、图像 -home.multiTool.title=PDF多功能工具 -home.multiTool.desc=合并、旋转、重新排列和删除PDF页面 +home.multiTool.title=PDF 多功能工具 +home.multiTool.desc=合并、旋转、重新排列和删除 PDF 页面 multiTool.tags=多工具,多操作,用户界面,点击拖动,前端,客户端 home.merge.title=合并 -home.merge.desc=轻松合并多个PDF为一个。 +home.merge.desc=轻松将多个 PDF 合并成一个。 merge.tags=合并,页面操作,后端,服务器端 home.split.title=拆分 @@ -274,16 +274,16 @@ home.split.desc=将 PDF 拆分为多个文档。 split.tags=页面操作,划分,多页面,剪切,服务器端 home.rotate.title=旋转 -home.rotate.desc=旋转PDF。 +home.rotate.desc=旋转 PDF。 rotate.tags=服务器端 -home.imageToPdf.title=转换图像到PDF -home.imageToPdf.desc=将图像(PNG、JPEG、GIF)转换为PDF。 +home.imageToPdf.title=转换图像到 PDF +home.imageToPdf.desc=将图像(PNG、JPEG、GIF)转换为 PDF。 imageToPdf.tags=转换、图像、JPG、图片、照片 -home.pdfToImage.title=转换PDF到图像 -home.pdfToImage.desc=将PDF转换为图像(PNG、JPEG、GIF)。 +home.pdfToImage.title=转换 PDF 到图像 +home.pdfToImage.desc=将 PDF 转换为图像(PNG、JPEG、GIF)。 pdfToImage.tags=转换、图像、JPG、图片、照片 home.pdfOrganiser.title=整理 @@ -291,92 +291,92 @@ home.pdfOrganiser.desc=按任意顺序删除/重新排列页面。 pdfOrganiser.tags=双面、偶数、奇数、排序、移动 -home.addImage.title=在PDF中添加图片 -home.addImage.desc=将图像添加到PDF的指定位置。 +home.addImage.title=在 PDF 中添加图片 +home.addImage.desc=将图像添加到 PDF 的指定位置。 addImage.tags=图像、JPG、图片、照片 home.watermark.title=添加水印 -home.watermark.desc=在PDF中添加自定义水印。 +home.watermark.desc=在 PDF 中添加自定义水印。 watermark.tags=文本、重复、标签、自定义、版权、商标、图像、JPG、图片、照片 home.permissions.title=更改权限 -home.permissions.desc=更改PDF文档的权限。 +home.permissions.desc=更改 PDF 文档的权限。 permissions.tags=阅读、写入、编辑、打印 home.removePages.title=删除 -home.removePages.desc=从PDF文档中删除不需要的页面。 +home.removePages.desc=从 PDF 文档中删除不需要的页面。 removePages.tags=删除页面、删除 home.addPassword.title=添加密码 -home.addPassword.desc=使用密码对PDF文档进行加密。 +home.addPassword.desc=使用密码对 PDF 文档进行加密。 addPassword.tags=安全、密码、加密 home.removePassword.title=删除密码 -home.removePassword.desc=从PDF文档中移除密码保护。 +home.removePassword.desc=从 PDF 文档中移除密码保护。 removePassword.tags=安全、解密、密码、安全性、删除密码 home.compressPdfs.title=压缩 -home.compressPdfs.desc=压缩PDF文件以减小文件大小。 +home.compressPdfs.desc=压缩 PDF 文件以减小文件大小。 compressPdfs.tags=压缩、小、微小 home.changeMetadata.title=更改元数据 -home.changeMetadata.desc=更改/删除/添加PDF文档的元数据。 +home.changeMetadata.desc=更改/删除/添加 PDF 文档的元数据。 changeMetadata.tags=标题、作者、日期、创建、时间、发布者、制作人、统计数据 -home.fileToPDF.title=将文件转换为PDF文件 -home.fileToPDF.desc=将几乎所有文件转换为PDF(DOCX、PNG、XLS、PPT、TXT等)。 +home.fileToPDF.title=将文件转换为 PDF 文件 +home.fileToPDF.desc=将几乎所有文件转换为 PDF (DOCX、PNG、XLS、PPT、TXT等)。 fileToPDF.tags=转换、格式、文档、图片、幻灯片、文本、转换、Office、Docs、Word、Excel、PowerPoint -home.ocr.title=运行OCR/清理扫描 -home.ocr.desc=清理和识别PDF中的图像文本,并将其转换为可编辑文本。 +home.ocr.title=运行 OCR /清理扫描 +home.ocr.desc=清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。 ocr.tags=识别、文本、图像、扫描、阅读、识别、检测、可编辑 home.extractImages.title=提取图像 -home.extractImages.desc=从PDF中提取所有图像并保存到压缩包中。 +home.extractImages.desc=从 PDF 中提取所有图像并保存到压缩包中。 extractImages.tags=图片、照片、保存、归档、压缩包、截取、抓取 -home.pdfToPDFA.title=PDF转PDF/A -home.pdfToPDFA.desc=将PDF转换为PDF/A以进行长期保存。 +home.pdfToPDFA.title=PDF 转 PDF/A +home.pdfToPDFA.desc=将 PDF 转换为 PDF/A 以进行长期保存。 pdfToPDFA.tags=归档、长期、标准、转换、存储、保存 -home.PDFToWord.title=PDF转Word +home.PDFToWord.title=PDF 转 Word home.PDFToWord.desc=将PDF转换为Word格式(DOC、DOCX和ODT)。 PDFToWord.tags=doc、docx、odt、word、转换、格式、Office、Microsoft、文档 -home.PDFToPresentation.title=PDF转演示文稿 -home.PDFToPresentation.desc=将PDF转换为演示文稿格式(PPT、PPTX和ODP)。 +home.PDFToPresentation.title=PDF 转演示文稿 +home.PDFToPresentation.desc=将 PDF 转换为演示文稿格式(PPT、PPTX 和 ODP)。 PDFToPresentation.tags=幻灯片、展示、Office、Microsoft -home.PDFToText.title=PDF转RTF(文本) -home.PDFToText.desc=将PDF转换为文本或RTF格式。 +home.PDFToText.title=PDF 转 RTF(文本) +home.PDFToText.desc=将PDF转换为文本或 RTF 格式。 PDFToText.tags=富文本格式、RTF、富文本格式 -home.PDFToHTML.title=PDF转HTML -home.PDFToHTML.desc=将PDF转换为HTML格式。 +home.PDFToHTML.title=PDF 转 HTML +home.PDFToHTML.desc=将 PDF 转换为 HTML 格式。 PDFToHTML.tags=网页内容、浏览器友好 -home.PDFToXML.title=PDF转XML -home.PDFToXML.desc=将PDF转换为XML格式。 +home.PDFToXML.title=PDF 转 XML +home.PDFToXML.desc=将 PDF 转换为 XML 格式。 PDFToXML.tags=数据提取、结构化内容、互操作、转换 home.ScannerImageSplit.title=检测/分割扫描图像 -home.ScannerImageSplit.desc=从一张照片或PDF中分割出多张照片。 +home.ScannerImageSplit.desc=从一张照片或 PDF 中分割出多张照片。 ScannerImageSplit.tags=分离、自动检测、扫描、多张照片、整理 home.sign.title=签名 -home.sign.desc=通过绘图、文字或图像向PDF添加签名 +home.sign.desc=通过绘图、文字或图像向 PDF 添加签名 sign.tags=授权、缩写、手绘签名、文本签名、图像签名 home.flatten.title=展平 -home.flatten.desc=从PDF中删除所有互动元素和表单 +home.flatten.desc=从 PDF 中删除所有互动元素和表单 flatten.tags=静态、停用、非交互、简化 home.repair.title=修复 -home.repair.desc=尝试修复损坏/损坏的PDF +home.repair.desc=尝试修复损坏/损坏的 PDF repair.tags=修复、恢复、纠正、恢复 home.removeBlanks.title=删除空白页 @@ -384,11 +384,11 @@ home.removeBlanks.desc=检测并删除文档中的空白页 removeBlanks.tags=清理、简化、非内容、整理 home.removeAnnotations.title=删除标注 -home.removeAnnotations.desc=删除PDF中的所有标注/评论 +home.removeAnnotations.desc=删除 PDF 中的所有标注/评论 removeAnnotations.tags=评论、高亮、笔记、标注、删除 home.compare.title=比较 -home.compare.desc=比较并显示两个PDF文档之间的差异 +home.compare.desc=比较并显示两个 PDF 文档之间的差异 compare.tags=区分、对比、更改、分析 home.certSign.title=使用证书签名 @@ -396,11 +396,11 @@ home.certSign.desc=使用证书/密钥(PEM/P12)对PDF进行签名 certSign.tags=身份验证、PEM、P12、官方、加密 home.removeCertSign.title=移除证书签名 -home.removeCertSign.desc=移除PDF的证书签名 +home.removeCertSign.desc=移除 PDF 的证书签名 removeCertSign.tags=身份验证、PEM、P12、官方、加密 home.pageLayout.title=多页布局 -home.pageLayout.desc=将PDF文档的多个页面合并成一页 +home.pageLayout.desc=将 PDF 文档的多个页面合并成一页 pageLayout.tags=合并、组合、单视图、整理 home.scalePages.title=调整页面尺寸/缩放 @@ -408,86 +408,86 @@ home.scalePages.desc=调整页面及/或其内容的尺寸/缩放 scalePages.tags=调整大小、修改、尺寸、适应 home.pipeline.title=流水线(高级版) -home.pipeline.desc=通过定义流水线脚本在PDF上运行多个操作 +home.pipeline.desc=通过定义流水线脚本在 PDF 上运行多个操作 pipeline.tags=自动化、顺序、脚本化、批处理 home.add-page-numbers.title=添加页码 home.add-page-numbers.desc=在文档的指定位置添加页码 add-page-numbers.tags=分页、标签、整理、索引 -home.auto-rename.title=自动重命名PDF文件 -home.auto-rename.desc=根据检测到的标题自动对PDF文件进行重命名 +home.auto-rename.title=自动重命名 PDF 文件 +home.auto-rename.desc=根据检测到的标题自动对 PDF 文件进行重命名 auto-rename.tags=自动检测、基于标题、整理、重新标记 home.adjust-contrast.title=调整颜色/对比度 -home.adjust-contrast.desc=调整PDF的对比度、饱和度和亮度 +home.adjust-contrast.desc=调整 PDF 的对比度、饱和度和亮度 adjust-contrast.tags=颜色校正、调节、修改、增强 -home.crop.title=裁剪PDF -home.crop.desc=裁剪PDF以减小其文件大小(保留文本!) +home.crop.title=裁剪 PDF +home.crop.desc=裁剪 PDF 以减小其文件大小(保留文本!) crop.tags=修剪、缩小、编辑、形状 home.autoSplitPDF.title=自动拆分页面 -home.autoSplitPDF.desc=使用物理扫描页面分割器QR代码自动拆分扫描的PDF -autoSplitPDF.tags=基于QR码、分离、扫描分割、整理 +home.autoSplitPDF.desc=使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF +autoSplitPDF.tags=基于 QR 码、分离、扫描分割、整理 home.sanitizePdf.title=清理 -home.sanitizePdf.desc=从PDF文件中删除脚本和其他元素 +home.sanitizePdf.desc=从 PDF 文件中删除脚本和其他元素 sanitizePdf.tags=清理、安全、安全、删除威胁 -home.URLToPDF.title=URL/网站转PDF -home.URLToPDF.desc=将任何http(s)URL转换为PDF +home.URLToPDF.title=URL/网站转 PDF +home.URLToPDF.desc=将任何 http(s)URL 转换为PDF URLToPDF.tags=网页捕获、保存网页、网页转文档、归档 -home.HTMLToPDF.title=HTML转PDF -home.HTMLToPDF.desc=将任何HTML文件或zip文件转换为PDF +home.HTMLToPDF.title=HTML 转 PDF +home.HTMLToPDF.desc=将任何 HTML 文件或 zip 文件转换为 PDF HTMLToPDF.tags=标记、网页内容、转换、转换 -home.MarkdownToPDF.title=Markdown转PDF -home.MarkdownToPDF.desc=将任何Markdown文件转换为PDF +home.MarkdownToPDF.title=Markdown 转 PDF +home.MarkdownToPDF.desc=将任何 Markdown 文件转换为 PDF MarkdownToPDF.tags=标记、网页内容、转换、转换 -home.getPdfInfo.title=获取PDF的所有信息 -home.getPdfInfo.desc=获取PDF的所有可能的信息 +home.getPdfInfo.title=获取 PDF 的所有信息 +home.getPdfInfo.desc=获取 PDF 的所有可能的信息 getPdfInfo.tags=信息、数据、统计、统计数据 home.extractPage.title=提取页面 -home.extractPage.desc=从PDF中提取选定的页面 +home.extractPage.desc=从 PDF 中提取选定的页面 extractPage.tags=提取 -home.PdfToSinglePage.title=PDF转单一大页 -home.PdfToSinglePage.desc=将所有PDF页面合并为一个大的单页 +home.PdfToSinglePage.title=PDF 转单一大页 +home.PdfToSinglePage.desc=将所有 PDF 页面合并为一个大的单页 PdfToSinglePage.tags=单页 -home.showJS.title=显示JavaScript -home.showJS.desc=搜索并显示嵌入到PDF中的任何JavaScript代码 +home.showJS.title=显示 JavaScript +home.showJS.desc=搜索并显示嵌入到 PDF 中的任何 JavaScript 代码 showJS.tags=JavaScript home.autoRedact.title=自动删除 -home.autoRedact.desc=根据输入文本自动删除(覆盖)PDF中的文本 +home.autoRedact.desc=根据输入文本自动删除(覆盖)PDF 中的文本 autoRedact.tags=脱敏、隐藏、涂黑、标记、不可见 -home.tableExtraxt.title=PDF转CSV -home.tableExtraxt.desc=从PDF中提取表格并将其转换为CSV +home.tableExtraxt.title=PDF 转 CSV +home.tableExtraxt.desc=从 PDF 中提取表格并将其转换为 CSV tableExtraxt.tags=CSV、表格提取、提取、转换 -home.autoSizeSplitPDF.title=自动根据大小/数目拆分PDF -home.autoSizeSplitPDF.desc=将单个PDF拆分为多个文档,基于大小、页数或文档数 +home.autoSizeSplitPDF.title=自动根据大小/数目拆分 PDF +home.autoSizeSplitPDF.desc=将单个 PDF 拆分为多个文档,基于大小、页数或文档数 autoSizeSplitPDF.tags=pdf、拆分、文件、组织 -home.overlay-pdfs.title=叠加PDF -home.overlay-pdfs.desc=将PDF叠加在另一个PDF上 +home.overlay-pdfs.title=叠加 PDF +home.overlay-pdfs.desc=将 PDF 叠加在另一个 PDF 上 overlay-pdfs.tags=叠加 -home.split-by-sections.title=拆分PDF成小块 -home.split-by-sections.desc=将PDF的每一页分割成更小的水平和垂直的部分 +home.split-by-sections.title=拆分 PDF 成小块 +home.split-by-sections.desc=将 PDF 的每一页分割成更小的水平和垂直的部分 split-by-sections.tags=章节拆分、分割、自定义 home.AddStampRequest.title=添加图章 @@ -495,34 +495,34 @@ home.AddStampRequest.desc=在指定位置添加文本或图片图章 AddStampRequest.tags=图章、添加图片、图片居中、水印、PDF、嵌入、自定义 -home.PDFToBook.title=PDF转电子书 -home.PDFToBook.desc=使用Calibre将PDF转换成电子书/漫画 +home.PDFToBook.title=PDF 转电子书 +home.PDFToBook.desc=使用 Calibre 将 PDF 转换成电子书/漫画 PDFToBook.tags=电子书、漫画、Calibre、转换、日本漫画、亚马逊、kindle -home.BookToPDF.title=电子书转PDF -home.BookToPDF.desc=使用Calibre将电子书/漫画转换成PDF +home.BookToPDF.title=电子书转 PDF +home.BookToPDF.desc=使用 Calibre 将电子书/漫画转换成 PDF BookToPDF.tags=电子书、漫画、Calibre、转换、日本漫画、亚马逊、kindle home.removeImagePdf.title=删除图像 -home.removeImagePdf.desc=删除图像减少PDF大小 +home.removeImagePdf.desc=删除图像减少 PDF 大小 removeImagePdf.tags=删除图像, 页面操作, 后端, 服务端 -home.splitPdfByChapters.title=Split PDF by Chapters -home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. -splitPdfByChapters.tags=split,chapters,bookmarks,organize +home.splitPdfByChapters.title=按章节拆分 PDF +home.splitPdfByChapters.desc=根据其章节结构将 PDF 拆分为多个文件。 +splitPdfByChapters.tags=分割,章节,书签,组织 -home.validateSignature.title=Validate PDF Signature -home.validateSignature.desc=Verify digital signatures and certificates in PDF documents -validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate +home.validateSignature.title=验证 PDF 签名 +home.validateSignature.desc=验证 PDF 文档中的数字签名和证书 +validateSignature.tags=签名,验证,验证,PDF,证书,数字签名,验证签名,验证证书 #replace-invert-color -replace-color.title=Replace-Invert-Color -replace-color.header=Replace-Invert Color PDF -home.replaceColorPdf.title=Replace and Invert Color -home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size +replace-color.title=替换-反转-颜色 +replace-color.header=替换-反转 PDF 颜色 +home.replaceColorPdf.title=替换和反转颜色 +home.replaceColorPdf.desc=替换 PDF 中文本和背景的颜色,并将PDF全色反转以减小文件大小 replaceColorPdf.tags=Replace Color,Page operations,Back end,server side -replace-color.selectText.1=Replace or Invert color Options +replace-color.selectText.1=替换或反转颜色选项 replace-color.selectText.2=Default(Default high contrast colors) replace-color.selectText.3=Custom(Customized colors) replace-color.selectText.4=Full-Invert(Invert all colors) @@ -551,18 +551,18 @@ login.invalid=用户名或密码无效。 login.locked=您的账户已被锁定。 login.signinTitle=请登录 login.ssoSignIn=通过单点登录登录 -login.oauth2AutoCreateDisabled=OAuth2自动创建用户已禁用 +login.oauth2AutoCreateDisabled=OAuth2 自动创建用户已禁用 login.oauth2AdminBlockedUser=目前已阻止未注册用户的注册或登录。请联系管理员。 login.oauth2RequestNotFound=找不到验证请求 login.oauth2InvalidUserInfoResponse=无效的用户信息响应 login.oauth2invalidRequest=无效请求 login.oauth2AccessDenied=拒绝访问 -login.oauth2InvalidTokenResponse=无效的Token响应 -login.oauth2InvalidIdToken=无效的Token +login.oauth2InvalidTokenResponse=无效的 Token 响应 +login.oauth2InvalidIdToken=无效的 Token login.userIsDisabled=用户被禁用,登录已被阻止。请联系管理员。 -login.alreadyLoggedIn=You are already logged in to -login.alreadyLoggedIn2=devices. Please log out of the devices and try again. -login.toManySessions=You have too many active sessions +login.alreadyLoggedIn=您已经登录到了 +login.alreadyLoggedIn2=设备,请注销设备后重试。 +login.toManySessions=你已经有太多的会话了。请注销一些设备后重试。 #auto-redact autoRedact.title=自动删除 @@ -578,15 +578,15 @@ autoRedact.submitButton=提交 #showJS -showJS.title=显示JavaScript -showJS.header=显示JavaScript -showJS.downloadJS=下载JavaScript +showJS.title=显示 JavaScript +showJS.header=显示 JavaScript +showJS.downloadJS=下载 JavaScript showJS.submit=显示 #pdfToSinglePage -pdfToSinglePage.title=PDF转单页 -pdfToSinglePage.header=将PDF转换为单页 +pdfToSinglePage.title=PDF 转单页 +pdfToSinglePage.header=将 PDF 转换为单页 pdfToSinglePage.submit=转为单页 @@ -598,34 +598,34 @@ pageExtracter.placeholder=(例如:1,2,8 或 4,7,12-16 或 2n-1) #getPdfInfo -getPdfInfo.title=获取PDF信息 -getPdfInfo.header=获取PDF信息 +getPdfInfo.title=获取 PDF 信息 +getPdfInfo.header=获取 PDF 信息 getPdfInfo.submit=获取信息 -getPdfInfo.downloadJson=下载JSON +getPdfInfo.downloadJson=下载 JSON #markdown-to-pdf -MarkdownToPDF.title=Markdown转PDF -MarkdownToPDF.header=将Markdown转换为PDF +MarkdownToPDF.title=Markdown 转 PDF +MarkdownToPDF.header=将 Markdown 转换为 PDF MarkdownToPDF.submit=转换 MarkdownToPDF.help=正在努力中 -MarkdownToPDF.credit=此服务使用WeasyPrint进行文件转换。 +MarkdownToPDF.credit=此服务使用 WeasyPrint 进行文件转换。 #url-to-pdf -URLToPDF.title=URL转PDF -URLToPDF.header=将URL转换为PDF +URLToPDF.title=URL 转 PDF +URLToPDF.header=将 URL 转换为 PDF URLToPDF.submit=转换 -URLToPDF.credit=此服务使用WeasyPrint进行文件转换。 +URLToPDF.credit=此服务使用 WeasyPrint 进行文件转换。 #html-to-pdf -HTMLToPDF.title=HTML转PDF -HTMLToPDF.header=将HTML转换为PDF -HTMLToPDF.help=接受HTML文件和包含所需的html/css/images等的ZIP文件 +HTMLToPDF.title=HTML 转 PDF +HTMLToPDF.header=将 HTML 转换为 PDF +HTMLToPDF.help=接受 HTML 文件和包含所需的 html/css/images 等的 ZIP 文件 HTMLToPDF.submit=转换 -HTMLToPDF.credit=此服务使用WeasyPrint进行文件转换。 +HTMLToPDF.credit=此服务使用 WeasyPrint 进行文件转换。 HTMLToPDF.zoom=网站显示缩放级别 HTMLToPDF.pageWidth=页面宽度-以厘米为单位(填空则使用默认值) HTMLToPDF.pageHeight=页面高度-以厘米为单位(填空则使用默认值) @@ -635,7 +635,7 @@ HTMLToPDF.marginLeft=页面左上边距-以毫米为单位(填空则使用默 HTMLToPDF.marginRight=页面右边距-以毫米为单位(填空则使用默认值) HTMLToPDF.printBackground=页面背景渲染 HTMLToPDF.defaultHeader=启用默认页头(文件名称和页码) -HTMLToPDF.cssMediaType=更换页面的CSS媒体类型。 +HTMLToPDF.cssMediaType=更换页面的 CSS 媒体类型。 HTMLToPDF.none=无 HTMLToPDF.print=打印 HTMLToPDF.screen=屏幕 @@ -660,9 +660,9 @@ AddStampRequest.submit=提交 #sanitizePDF -sanitizePDF.title=清理PDF -sanitizePDF.header=清理PDF文件 -sanitizePDF.selectText.1=移除JavaScript操作 +sanitizePDF.title=清理 PDF +sanitizePDF.header=清理 PDF 文件 +sanitizePDF.selectText.1=移除 JavaScript 操作 sanitizePDF.selectText.2=移除嵌入的文件 sanitizePDF.selectText.3=移除元数据 sanitizePDF.selectText.4=移除链接 @@ -681,13 +681,13 @@ addPageNumbers.selectText.5=添加页码的页数 addPageNumbers.selectText.6=自定义文本 addPageNumbers.customTextDesc=自定义文本 addPageNumbers.numberPagesDesc=要添加页码的页数,默认为“所有”,也可以接受1-5或2,5,9等 -addPageNumbers.customNumberDesc=默认为{n},也可以接受“第{n}页/共{total}页”,“文本-{n}”,“{filename}-{n}” +addPageNumbers.customNumberDesc=默认为 {n},也可以接受“第 {n} 页/共 {total} 页”,“文本-{n}”,“{filename}-{n}” addPageNumbers.submit=添加页码 #auto-rename auto-rename.title=自动重命名 -auto-rename.header=自动重命名PDF +auto-rename.header=自动重命名 PDF auto-rename.submit=自动重命名 @@ -702,19 +702,19 @@ adjustContrast.download=下载 #crop crop.title=裁剪 -crop.header=裁剪PDF +crop.header=裁剪 PDF crop.submit=提交 #autoSplitPDF -autoSplitPDF.title=自动拆分PDF -autoSplitPDF.header=自动拆分PDF +autoSplitPDF.title=自动拆分 PDF +autoSplitPDF.header=自动拆分 PDF autoSplitPDF.description=打印、插入、扫描、上传,让我们自动分离您的文档。无需手动排序。 autoSplitPDF.selectText.1=从下面打印一些分隔页(黑白打印即可)。 autoSplitPDF.selectText.2=在文档之间插入分隔页,一次性扫描所有文档。 -autoSplitPDF.selectText.3=上传单个大型扫描的PDF文件,让Stirling PDF处理剩下的事情。 +autoSplitPDF.selectText.3=上传单个大型扫描的 PDF 文件,让 Stirling PDF 处理剩下的事情。 autoSplitPDF.selectText.4=分隔页会自动检测和删除,确保最终文档整洁。 -autoSplitPDF.formPrompt=提交包含Stirling-PDF分隔页的PDF: +autoSplitPDF.formPrompt=提交包含 Stirling-PDF 分隔页的 PDF: autoSplitPDF.duplexMode=双面模式(正反面扫描) autoSplitPDF.dividerDownload1=下载“自动拆分分隔页(最小化).pdf” autoSplitPDF.dividerDownload2=下载“自动拆分分隔页(带指导说明).pdf” @@ -737,34 +737,34 @@ pageLayout.submit=提交 scalePages.title=调整页面缩放比例 scalePages.header=调整页面缩放比例 scalePages.pageSize=文档页面的尺寸。 -scalePages.keepPageSize=Original Size +scalePages.keepPageSize=保持页面原尺寸 scalePages.scaleFactor=页面的缩放级别(裁剪)。 scalePages.submit=提交 #certSign certSign.title=证书签名 -certSign.header=使用您的证书签名PDF(进行中) -certSign.selectPDF=选择要签名的PDF文件: -certSign.jksNote=注意:如果您的证书类型未在下面列出,请使用keytool命令行工具将其转换为Java Keystore(.jks)文件。 然后,选择下面的.jks文件选项。 +certSign.header=使用您的证书签名 PDF(进行中) +certSign.selectPDF=选择要签名的 PDF 文件: +certSign.jksNote=注意:如果您的证书类型未在下面列出,请使用keytool命令行工具将其转换为 Java Keystore(.jks)文件。 然后,选择下面的.jks文件选项。 certSign.selectKey=选择您的私钥文件(PKCS#8格式,可以是.pem或.der): certSign.selectCert=选择您的证书文件(X.509格式,可以是.pem或.der): -certSign.selectP12=选择您的PKCS#12密钥库文件(.p12或.pfx)(可选,如果提供,它应该包含您的私钥和证书): -certSign.selectJKS=选择你的Java Keystore文件 (.jks或.keystore): +certSign.selectP12=选择您的 PKCS#12 密钥库文件(.p12或.pfx)(可选,如果提供,它应该包含您的私钥和证书): +certSign.selectJKS=选择你的 Java Keystore 文件 (.jks或.keystore): certSign.certType=证书类型 certSign.password=输入您的密钥库或私钥密码(如果有): certSign.showSig=显示签名 certSign.reason=原因 certSign.location=位置 certSign.name=名称 -certSign.showLogo=Show Logo -certSign.submit=给PDF签名 +certSign.showLogo=显示 Logo +certSign.submit=给 PDF 签名 #removeCertSign removeCertSign.title=移除证书签名 -removeCertSign.header=移除PDF的证书签名 -removeCertSign.selectPDF=选择PDF文件: +removeCertSign.header=移除 PDF 的证书签名 +removeCertSign.selectPDF=选择 PDF 文件: removeCertSign.submit=移除签名 @@ -792,21 +792,21 @@ compare.highlightColor.2=高亮颜色 2: compare.document.1=文档 1 compare.document.2=文档 2 compare.submit=比较 -compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced -compare.large.file.message=One or Both of the provided documents are too large to process -compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison. +compare.complex.message=提供的一份或两份文件是大文件,比较的准确性可能会降低。 +compare.large.file.message=提供的文件中有一份或两份过大,无法处理。 +compare.no.text.message=所选的 PDF 文件中有一个或两个没有文本内容。请选择包含文本的 PDF 文件进行对比。 #BookToPDF -BookToPDF.title=电子书和漫画转换成PDF -BookToPDF.header=电子书转PDF -BookToPDF.credit=此服务使用Calibre进行文件转换。 +BookToPDF.title=电子书和漫画转换成 PDF +BookToPDF.header=电子书转 PDF +BookToPDF.credit=此服务使用 Calibre 进行文件转换。 BookToPDF.submit=转换 #PDFToBook -PDFToBook.title=PDF转电子书 -PDFToBook.header=PDF转电子书 +PDFToBook.title=PDF 转电子书 +PDFToBook.header=PDF 转电子书 PDFToBook.selectText.1=格式 -PDFToBook.credit=此服务使用Calibre进行文件转换。 +PDFToBook.credit=此服务使用 Calibre 进行文件转换。 PDFToBook.submit=转换 #sign @@ -817,21 +817,21 @@ sign.draw=绘制签名 sign.text=文本输入 sign.clear=清除 sign.add=添加 -sign.saved=Saved Signatures -sign.save=Save Signature -sign.personalSigs=Personal Signatures +sign.saved=已保存签名 +sign.save=保存签名 +sign.personalSigs=个人签名 sign.sharedSigs=Shared Signatures -sign.noSavedSigs=No saved signatures found -sign.addToAll=Add to all pages -sign.delete=Delete -sign.first=First page -sign.last=Last page -sign.next=Next page -sign.previous=Previous page +sign.noSavedSigs=未找到已保存的签名 +sign.addToAll=添加到所有页面 +sign.delete=删除 +sign.first=首页 +sign.last=末页 +sign.next=下一页 +sign.previous=上一页 #repair repair.title=修复 -repair.header=修复PDF +repair.header=修复 PDF repair.submit=修复 @@ -853,27 +853,27 @@ ScannerImageSplit.selectText.7=最小轮廓面积: ScannerImageSplit.selectText.8=设置照片的最小轮廓面积阈值。 ScannerImageSplit.selectText.9=边框尺寸: ScannerImageSplit.selectText.10=设置添加和删除的边框大小,以防止输出中出现白边(默认值:1)。 -ScannerImageSplit.info=此功能需要安装Python +ScannerImageSplit.info=此功能需要安装 Python #OCR ocr.title=OCR/扫描清理 ocr.header=清理扫描件/OCR(光学字符识别)。 -ocr.selectText.1=选择要在PDF中检测的语言(列出的语言是目前检测到的): -ocr.selectText.2=生成包含OCR文本的文本文件,与OCR编辑的PDF一起。 +ocr.selectText.1=选择要在 PDF 中检测的语言(列出的语言是目前检测到的): +ocr.selectText.2=生成包含 OCR 文本的文本文件,与 OCR 编辑的 PDF 一起。 ocr.selectText.3=通过将页面旋转回原位来纠正偏斜的扫描角度 -ocr.selectText.4=清理页面,降低OCR在噪点中识别到文本的可能。(没有输出变化) -ocr.selectText.5=清洁页面,降低OCR在噪点中识别到文本的可能,保持输出的清洁。 -ocr.selectText.6=忽略有交互式文本的页面,只对有图像的页面进行OCR。 -ocr.selectText.7=强制OCR,将OCR每个页面,删除所有的原始文本元素。 -ocr.selectText.8=正常 (如果PDF包含文本,将出现错误) +ocr.selectText.4=清理页面,降低 OCR 在噪点中识别到文本的可能。(没有输出变化) +ocr.selectText.5=清洁页面,降低 OCR 在噪点中识别到文本的可能,保持输出的清洁。 +ocr.selectText.6=忽略有交互式文本的页面,只对有图像的页面进行 OCR。 +ocr.selectText.7=强制 OCR,将 OCR 每个页面,删除所有的原始文本元素。 +ocr.selectText.8=正常 (如果 PDF 包含文本,将出现错误) ocr.selectText.9=额外设置 -ocr.selectText.10=OCR模式 -ocr.selectText.11=OCR后移除图像(移除所有图像,只有在转换步骤中才有用)。 +ocr.selectText.10=OCR 模式 +ocr.selectText.11=OCR 后移除图像(移除所有图像,只有在转换步骤中才有用)。 ocr.selectText.12=渲染类型(高级) -ocr.help=请阅读此文档,了解如何将其用于其他语言和/或不在docker中使用。 -ocr.credit=此服务使用qpdf和Tesseract进行OCR。 -ocr.submit=用OCR处理PDF +ocr.help=请阅读此文档,了解如何将其用于其他语言和/或不在 docker 中使用。 +ocr.credit=此服务使用 qpdf 和 Tesseract 进行 OCR。 +ocr.submit=用 OCR 处理 PDF #extractImages @@ -885,18 +885,18 @@ extractImages.submit=提取 #File to PDF -fileToPDF.title=文件转换为PDF -fileToPDF.header=将任何文件转换为PDF。 -fileToPDF.credit=此服务使用LibreOffice和Unoconv进行文件转换。 +fileToPDF.title=文件转换为 PDF +fileToPDF.header=将任何文件转换为 PDF。 +fileToPDF.credit=此服务使用 LibreOffice 和 Unoconv 进行文件转换。 fileToPDF.supportedFileTypesInfo=支持的文件类型 -fileToPDF.supportedFileTypes=支持的文件类型应该包括以下几种,但是,对于支持的格式的完整更新列表,请参考LibreOffice文档。 +fileToPDF.supportedFileTypes=支持的文件类型应该包括以下几种,但是,对于支持的格式的完整更新列表,请参考 LibreOffice 文档。 fileToPDF.submit=转换为 PDF #compress compress.title=压缩 -compress.header=压缩PDF -compress.credit=此服务使用qpdf进行PDF压缩/优化。 +compress.header=压缩 PDF +compress.credit=此服务使用qpdf进行 PDF 压缩/优化。 compress.selectText.1=手动模式 - 从 1 到 4 compress.selectText.2=优化级别: compress.selectText.3=4(文本图像很糟糕) @@ -907,7 +907,7 @@ compress.submit=压缩 #Add image addImage.title=添加图像 -addImage.header=添加图片到PDF +addImage.header=添加图片到 PDF addImage.everyPage=每一页? addImage.upload=添加图片 addImage.submit=添加图片 @@ -915,7 +915,7 @@ addImage.submit=添加图片 #merge merge.title=合并 -merge.header=合并多个PDF(2个以上)。 +merge.header=合并多个 PDF(2个以上)。 merge.sortByName=按名称排序 merge.sortByDate=按日期排序 merge.removeCertSign=删除合并文件的数字签名吗? @@ -924,7 +924,7 @@ merge.submit=合并 #pdfOrganiser pdfOrganiser.title=页面排序 -pdfOrganiser.header=PDF页面排序 +pdfOrganiser.header=PDF 页面排序 pdfOrganiser.submit=重新排列页面 pdfOrganiser.mode=模式 pdfOrganiser.mode.1=自定义页面顺序 @@ -941,94 +941,94 @@ pdfOrganiser.placeholder=(例如:1,3,2 或 4-8,2,10-12 或 2n-1) #multiTool -multiTool.title=PDF多功能工具 -multiTool.header=PDF多功能工具 +multiTool.title=PDF 多功能工具 +multiTool.header=PDF 多功能工具 multiTool.uploadPrompts=文件名 -multiTool.selectAll=Select All -multiTool.deselectAll=Deselect All +multiTool.selectAll=选择所有 +multiTool.deselectAll=取消选择所有 multiTool.selectPages=Page Select -multiTool.selectedPages=Selected Pages +multiTool.selectedPages=已选择的页面 multiTool.page=Page -multiTool.deleteSelected=Delete Selected -multiTool.downloadAll=Export -multiTool.downloadSelected=Export Selected +multiTool.deleteSelected=删除已选 +multiTool.downloadAll=导出全部 +multiTool.downloadSelected=导出已选 -multiTool.insertPageBreak=Insert Page Break -multiTool.addFile=Add File -multiTool.rotateLeft=Rotate Left -multiTool.rotateRight=Rotate Right -multiTool.split=Split -multiTool.moveLeft=Move Left -multiTool.moveRight=Move Right -multiTool.delete=Delete -multiTool.dragDropMessage=Page(s) Selected +multiTool.insertPageBreak=插入分页符 +multiTool.addFile=添加文件 +multiTool.rotateLeft=向左旋转 +multiTool.rotateRight=向右旋转 +multiTool.split=分割 +multiTool.moveLeft=向做移动 +multiTool.moveRight=向右移动 +multiTool.delete=删除 +multiTool.dragDropMessage=选择页面 multiTool.undo=Undo multiTool.redo=Redo #decrypt -decrypt.passwordPrompt=This file is password-protected. Please enter the password: -decrypt.cancelled=Operation cancelled for PDF: {0} -decrypt.noPassword=No password provided for encrypted PDF: {0} -decrypt.invalidPassword=Please try again with the correct password. -decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} -decrypt.unexpectedError=There was an error processing the file. Please try again. -decrypt.serverError=Server error while decrypting: {0} -decrypt.success=File decrypted successfully. +decrypt.passwordPrompt=此文件受密码保护。请输入密码: +decrypt.cancelled=PDF 操作已取消: {0} +decrypt.noPassword=未提供加密 PDF 的密码: {0} +decrypt.invalidPassword=请使用正确的密码重试。 +decrypt.invalidPasswordHeader=密码错误或不支持的 PDF 加密: {0} +decrypt.unexpectedError=处理文件时发生错误。请再试一次。 +decrypt.serverError=服务器解密时发生错误: {0} +decrypt.success=文件解密成功。 #multiTool-advert -multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! +multiTool-advert.message=此功能也适用于我们的“多功能工具页面”。查看它以获得增强的逐页 UI 以及其他功能! #view pdf -viewPdf.title=浏览PDF -viewPdf.header=浏览PDF +viewPdf.title=浏览 PDF +viewPdf.header=浏览 PDF #pageRemover pageRemover.title=删除页面 -pageRemover.header=PDF页面移除器 +pageRemover.header=PDF 页面移除器 pageRemover.pagesToDelete=要删除的页面(输入一个用逗号分隔的页码列表): pageRemover.submit=删除页面 pageRemover.placeholder=(例如:1,2,6 或 1-10,15-30) #rotate -rotate.title=旋转PDF -rotate.header=旋转PDF -rotate.selectAngle=选择旋转角度(以90度的倍数): +rotate.title=旋转 PDF +rotate.header=旋转 PDF +rotate.selectAngle=选择旋转角度(以 90 度的倍数): rotate.submit=旋转 #split-pdfs -split.title=拆分PDF -split.header=拆分PDF +split.title=拆分 PDF +split.header=拆分 PDF split.desc.1=选择希望进行分割的页数 -split.desc.2=如选择1,3,7-9将把一个10页的文件分割成6个独立的PDF: -split.desc.3=文档 #1:第1页 -split.desc.4=文档 #2:第2页和第3页 -split.desc.5=文档 #3:第4页、第5页、第6页和第7页 -split.desc.6=文档 #4:第7页 -split.desc.7=文档 #5:第8页 -split.desc.8=文档 #6:第9页和第10页 +split.desc.2=如选择1,3,7-9将把一个 10 页的文件分割成6个独立的PDF: +split.desc.3=文档 #1:第 1 页 +split.desc.4=文档 #2:第 2 页和第 3 页 +split.desc.5=文档 #3:第 4 页、第 5 页、第 6 页和第 7 页 +split.desc.6=文档 #4:第 7 页 +split.desc.7=文档 #5:第 8 页 +split.desc.8=文档 #6:第 9 页和第 10 页 split.splitPages=输入要分割的页面: split.submit=拆分 #merge -imageToPDF.title=图片转PDF -imageToPDF.header=将图片转换为PDF +imageToPDF.title=图片转 PDF +imageToPDF.header=将图片转换为 PDF imageToPDF.submit=转换 imageToPDF.selectLabel=图片适应选项 imageToPDF.fillPage=填充页面 imageToPDF.fitDocumentToImage=适应图片大小 imageToPDF.maintainAspectRatio=保持纵横比例 -imageToPDF.selectText.2=自动旋转PDF +imageToPDF.selectText.2=自动旋转 PDF imageToPDF.selectText.3=多文件逻辑(仅在处理多个图像时启用) -imageToPDF.selectText.4=合并成一个PDF文件 -imageToPDF.selectText.5=转换为独立的PDF文件 +imageToPDF.selectText.4=合并成一个 PDF 文件 +imageToPDF.selectText.5=转换为独立的 PDF 文件 #pdfToImage -pdfToImage.title=PDF转图片 -pdfToImage.header=将PDF转换为图片 +pdfToImage.title=PDF 转图片 +pdfToImage.header=将 PDF 转换为图片 pdfToImage.selectText=图像格式 pdfToImage.singleOrMultiple=图像结果类型 pdfToImage.single=单张图片 @@ -1038,13 +1038,13 @@ pdfToImage.color=颜色 pdfToImage.grey=灰度 pdfToImage.blackwhite=黑白(可能会丢失数据!)。 pdfToImage.submit=转换 -pdfToImage.info=WebP转换需要安装Python +pdfToImage.info=WebP 转换需要安装 Python #addPassword addPassword.title=添加密码 addPassword.header=添加密码(加密)。 -addPassword.selectText.1=选择要加密的PDF。 +addPassword.selectText.1=选择要加密的 PDF。 addPassword.selectText.2=密码 addPassword.selectText.3=加密密钥长度 addPassword.selectText.4=值越高越强,但值越低兼容性越好。 @@ -1067,7 +1067,7 @@ addPassword.submit=加密 watermark.title=添加水印 watermark.header=添加水印 watermark.customColor=自定义文本颜色 -watermark.selectText.1=选择要添加水印的PDF: +watermark.selectText.1=选择要添加水印的 PDF: watermark.selectText.2=水印文本: watermark.selectText.3=字体大小: watermark.selectText.4=旋转(0-360): @@ -1076,7 +1076,7 @@ watermark.selectText.6=垂直间距(每个水印之间的垂直距离): watermark.selectText.7=透明度(0% - 100%): watermark.selectText.8=水印类型: watermark.selectText.9=水印图片: -watermark.selectText.10=将PDF转换为PDF-Image +watermark.selectText.10=将 PDF 转换为 PDF-Image watermark.submit=添加水印 watermark.type.1=文字 watermark.type.2=图片 @@ -1086,7 +1086,7 @@ watermark.type.2=图片 permissions.title=更改权限 permissions.header=改变权限 permissions.warning=警告,为了使这些权限不能被改变,建议通过添加密码页面设置密码。 -permissions.selectText.1=选择PDF来改变权限 +permissions.selectText.1=选择 PDF 来改变权限 permissions.selectText.2=要设置的权限 permissions.selectText.3=防止文件的拼接 permissions.selectText.4=防止内容提取 @@ -1102,7 +1102,7 @@ permissions.submit=改变 #remove password removePassword.title=删除密码 removePassword.header=移除密码(解密)。 -removePassword.selectText.1=选择要解密的PDF +removePassword.selectText.1=选择要解密的 PDF removePassword.selectText.2=密码 removePassword.submit=删除 @@ -1127,9 +1127,9 @@ changeMetadata.submit=更改 #pdfToPDFA -pdfToPDFA.title=PDF转PDF/A -pdfToPDFA.header=将PDF转换为PDF/A -pdfToPDFA.credit=此服务使用qpdf进行PDF/A转换 +pdfToPDFA.title=PDF 转 PDF/A +pdfToPDFA.header=将 PDF 转换为 PDF/A +pdfToPDFA.credit=此服务使用 qpdf 进行 PDF/A 转换 pdfToPDFA.submit=转换 pdfToPDFA.tip=目前不支持上传多个 pdfToPDFA.outputFormat=输出格式 @@ -1137,51 +1137,51 @@ pdfToPDFA.pdfWithDigitalSignature=该PDF包含数字签名,下一步将移除 #PDFToWord -PDFToWord.title=PDF转Word -PDFToWord.header=将PDF转换为Word +PDFToWord.title=PDF 转 Word +PDFToWord.header=将 PDF 转换为 Word PDFToWord.selectText.1=输出文件格式 -PDFToWord.credit=此服务使用LibreOffice进行文件转换。 +PDFToWord.credit=此服务使用 LibreOffice 进行文件转换。 PDFToWord.submit=转换 #PDFToPresentation -PDFToPresentation.title=PDF转演示文稿 -PDFToPresentation.header=将PDF转换为演示文稿 +PDFToPresentation.title=PDF 转演示文稿 +PDFToPresentation.header=将 PDF 转换为演示文稿 PDFToPresentation.selectText.1=输出文件格式 -PDFToPresentation.credit=此服务使用LibreOffice进行文件转换。 +PDFToPresentation.credit=此服务使用 LibreOffice 进行文件转换。 PDFToPresentation.submit=转换 #PDFToText -PDFToText.title=PDF转文本/RTF -PDFToText.header=将PDF转换为文本/RTF +PDFToText.title=PDF 转文本/RTF +PDFToText.header=将 PDF 转换为文本/RTF PDFToText.selectText.1=输出文件格式 -PDFToText.credit=此服务使用LibreOffice进行文件转换。 +PDFToText.credit=此服务使用 LibreOffice 进行文件转换。 PDFToText.submit=转换 #PDFToHTML -PDFToHTML.title=PDF转HTML -PDFToHTML.header=将PDF转换为HTML -PDFToHTML.credit=此服务使用pdftohtml进行文件转换。 +PDFToHTML.title=PDF 转 HTML +PDFToHTML.header=将 PDF 转换为 HTML +PDFToHTML.credit=此服务使用 pdftohtml 进行文件转换。 PDFToHTML.submit=转换 #PDFToXML -PDFToXML.title=PDF转XML -PDFToXML.header=将PDF转换为XML -PDFToXML.credit=此服务使用LibreOffice进行文件转换。 +PDFToXML.title=PDF 转 XML +PDFToXML.header=将 PDF 转换为 XML +PDFToXML.credit=此服务使用 LibreOffice 进行文件转换。 PDFToXML.submit=转换 #PDFToCSV -PDFToCSV.title=PDF转CSV -PDFToCSV.header=将PDF转换为CSV +PDFToCSV.title=PDF 转 CSV +PDFToCSV.header=将 PDF 转换为 CSV PDFToCSV.prompt=选择需要提取表格的页面 PDFToCSV.submit=提取 #split-by-size-or-count -split-by-size-or-count.title=按照大小或数目拆分PDF -split-by-size-or-count.header=按照大小或数目拆分PDF +split-by-size-or-count.title=按照大小或数目拆分 PDF +split-by-size-or-count.header=按照大小或数目拆分 PDF split-by-size-or-count.type.label=选择拆分类型 split-by-size-or-count.type.size=按照大小 split-by-size-or-count.type.pageCount=按照页数 @@ -1192,9 +1192,9 @@ split-by-size-or-count.submit=提交 #overlay-pdfs -overlay-pdfs.header=叠加PDF文件 -overlay-pdfs.baseFile.label=选择基础PDF文件 -overlay-pdfs.overlayFiles.label=选择需要叠加在基础上的PDF文件 +overlay-pdfs.header=叠加 PDF 文件 +overlay-pdfs.baseFile.label=选择基础 PDF 文件 +overlay-pdfs.overlayFiles.label=选择需要叠加在基础上的 PDF 文件 overlay-pdfs.mode.label=选择叠加模式 overlay-pdfs.mode.sequential=按顺序叠加 overlay-pdfs.mode.interleaved=交错叠加 @@ -1208,14 +1208,14 @@ overlay-pdfs.submit=提交 #split-by-sections -split-by-sections.title=按照块(Section)拆分PDF -split-by-sections.header=将PDF拆分成块 +split-by-sections.title=按照块(Section)拆分 PDF +split-by-sections.header=将 PDF 拆分成块 split-by-sections.horizontal.label=水平分割 split-by-sections.vertical.label=垂直分割 split-by-sections.horizontal.placeholder=输入水平分割数 split-by-sections.vertical.placeholder=输入垂直分割数 -split-by-sections.submit=分割PDF -split-by-sections.merge=是否合并为一个pdf +split-by-sections.submit=分割 PDF +split-by-sections.merge=是否合并为一个 pdf #printFile @@ -1235,11 +1235,11 @@ licenses.version=版本 licenses.license=许可证 #survey -survey.nav=调查 -survey.title=Stirling-PDF调查 -survey.description=Stirling-PDF没有跟踪器,所以我们希望听取用户的意见来改进Stirling-PDF! -survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here: -survey.changes2=With these changes we are getting paid business support and funding +survey.nav=问卷调查 +survey.title=Stirling-PDF 问卷调查 +survey.description=Stirling-PDF 没有跟踪器,所以我们希望听取用户的意见来改进 Stirling-PDF! +survey.changes=自上次调查以来,Stirling-PDF 已经发生了变化!要了解更多信息,请在此处查看我们的博客文章: +survey.changes2=通过这些变化,我们得到了商业支持和资金援助。 survey.please=请考虑参加我们的调查! survey.disabled=(调查弹出窗口将在后续更新中被禁用,但可在页脚处查看) survey.button=参与调查 @@ -1249,11 +1249,11 @@ survey.dontShowAgain=不再显示 #error error.sorry=对此问题感到抱歉! error.needHelp=需要帮助 / 发现问题? -error.contactTip=如果你仍然遇到问题,不要犹豫,向我们寻求帮助。你可以在我们的GitHub页面上提交工单,或者通过Discord与我们联系: +error.contactTip=如果你仍然遇到问题,不要犹豫,向我们寻求帮助。你可以在我们的 GitHub 页面上提交工单,或者通过 Discord 与我们联系: error.404.head=404 - 页面未找到 | 哎呀,我们在代码中触发了错误! error.404.1=我们似乎找不到你寻找的页面。 error.404.2=出了些问题 -error.github=在GitHub上提交工单 +error.github=在 GitHub 上提交工单 error.showStack=显示堆栈跟踪 error.copyStack=复制堆栈跟踪 error.githubSubmit=GitHub - 提交工单 @@ -1267,29 +1267,29 @@ removeImage.removeImage=删除图像 removeImage.submit=删除图像 -splitByChapters.title=Split PDF by Chapters -splitByChapters.header=Split PDF by Chapters -splitByChapters.bookmarkLevel=Bookmark Level -splitByChapters.includeMetadata=Include Metadata -splitByChapters.allowDuplicates=Allow Duplicates +splitByChapters.title=按章节拆分 PDF +splitByChapters.header=按章节拆分 PDF +splitByChapters.bookmarkLevel=书签级别 +splitByChapters.includeMetadata=包含元数据 +splitByChapters.allowDuplicates=允许重复 splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure. splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.). splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. -splitByChapters.submit=Split PDF +splitByChapters.submit=拆分 PDF #File Chooser -fileChooser.click=Click -fileChooser.or=or -fileChooser.dragAndDrop=Drag & Drop -fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here +fileChooser.click=单击 +fileChooser.or=或 +fileChooser.dragAndDrop=拖放文件 +fileChooser.hoveredDragAndDrop=拖放文件到此处 #release notes -releases.footer=Releases -releases.title=Release Notes -releases.header=Release Notes -releases.current.version=Current Release -releases.note=Release notes are only available in English +releases.footer=版本 +releases.title=版本说明 +releases.header=版本说明 +releases.current.version=当前版本 +releases.note=版本说明仅提供英文版本 #Validate Signature validateSignature.title=Validate PDF Signatures From 2437460c7341714f8d1dac6c8e536860b1451c32 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:00:26 +0100 Subject: [PATCH 21/49] Update messages_it_IT.properties --- src/main/resources/messages_it_IT.properties | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index ef184cb48..d2b422c25 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -966,14 +966,14 @@ multiTool.undo=Annulla multiTool.redo=Rifai #decrypt -decrypt.passwordPrompt=This file is password-protected. Please enter the password: -decrypt.cancelled=Operation cancelled for PDF: {0} -decrypt.noPassword=No password provided for encrypted PDF: {0} -decrypt.invalidPassword=Please try again with the correct password. -decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} -decrypt.unexpectedError=There was an error processing the file. Please try again. -decrypt.serverError=Server error while decrypting: {0} -decrypt.success=File decrypted successfully. +decrypt.passwordPrompt=Questo file è protetto da password. Inserisci la password: +decrypt.cancelled=Operazione annullata per il PDF: {0} +decrypt.noPassword=Nessuna password fornita per il PDF crittografato: {0} +decrypt.invalidPassword=Riprova con la password corretta. +decrypt.invalidPasswordHeader=Password errata o crittografia non supportata per il PDF: {0} +decrypt.unexpectedError=Si è verificato un errore durante l'elaborazione del file. Riprova.. +decrypt.serverError=Errore del server durante la decrittazione: {0} +decrypt.success=File decrittografato con successo. #multiTool-advert multiTool-advert.message=Questa funzione è disponibile anche nella nostra pagina multi-strumento. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive! From 86662d9cf6bdfc6a50bb62e2f9019daf93dbad4b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:03:42 +0000 Subject: [PATCH 22/49] more testing --- .github/workflows/multiOSReleases.yml | 86 +++++++++++++++++++ build.gradle | 109 +++++++++++++++++++++++++ src/main/resources/static/favicon.icns | Bin 0 -> 62003 bytes 3 files changed, 195 insertions(+) create mode 100644 .github/workflows/multiOSReleases.yml create mode 100644 src/main/resources/static/favicon.icns diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml new file mode 100644 index 000000000..6b610ad09 --- /dev/null +++ b/.github/workflows/multiOSReleases.yml @@ -0,0 +1,86 @@ +name: Test Installers Build + +on: + push: + branches: + - testStuff + workflow_dispatch: + +permissions: + contents: write + packages: write + +jobs: + build-installers: + strategy: + matrix: + include: + - os: windows-latest + platform: win + ext: exe + - os: macos-latest + platform: mac + ext: dmg + - os: ubuntu-latest + platform: linux + ext: deb + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.7 + + # Install Windows dependencies + - name: Install WiX Toolset + if: matrix.os == 'windows-latest' + run: | + curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe + .\wix.exe /install /quiet + + # Install Linux dependencies + - name: Install Linux Dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y fakeroot rpm + + # Get version number + - name: Get version number + id: versionNumber + run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + shell: bash + + # Build installer + - name: Build Installer + run: ./gradlew jpackage + + # Rename and collect artifacts based on OS + - name: Prepare artifacts + id: prepare + shell: bash + run: | + if [ "${{ matrix.os }}" = "windows-latest" ]; then + mv Stirling-PDF.exe "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + mv build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.dmg "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + else + mv build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + fi + + # Upload installer as artifact for testing + - name: Upload Installer Artifact + uses: actions/upload-artifact@v4 + with: + name: Stirling-PDF-${{ matrix.platform }}.${{ matrix.ext }} + path: Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }} + retention-days: 1 + if-no-files-found: error diff --git a/build.gradle b/build.gradle index 5363a4cc4..ed557e065 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { id "com.diffplug.spotless" version "6.25.0" id "com.github.jk1.dependency-license-report" version "2.9" //id "nebula.lint" version "19.0.3" + id("org.panteleyev.jpackageplugin") version "1.6.0" } @@ -87,6 +88,114 @@ openApi { outputFileName = "SwaggerDoc.json" } +jpackage { + // Input directory containing the jar + input = "build/libs" + + // Application details + appName = "Stirling-PDF" + appVersion = "0.36.3" + vendor = "Stirling-Software" + + // Main application configuration + mainJar = "Stirling-PDF-${project.version}.jar" + mainClass = "stirling.software.SPDF.StirlingPdfApplication" + + // Default icon configuration + icon = "src/main/resources/static/favicon.ico" + + // Application description + appDescription = "Stirling PDF - Your Local PDF Editor" + + // JVM Options + javaOptions = [ + "-DBROWSER_OPEN=true", + "-DSTIRLING_PDF_DESKTOP_UI=true" + ] + + // Enable verbose output + verbose = true + + // Windows-specific configuration + windows { + winConsole = false + winDirChooser = true + winMenu = true + winShortcut = true + winPerUserInstall = true + winMenuGroup = "Stirling Software" + winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates + winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF" + winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases" + type = "exe" + } + + // macOS-specific configuration + mac { + icon = "src/main/resources/static/favicon.icns" + type = "dmg" + macPackageIdentifier = "com.stirling.software.pdf" + macPackageName = "Stirling-PDF" + macAppCategory = "public.app-category.productivity" + macSign = false // Enable signing + macAppStore = false // Not targeting App Store initially + + // Add license and other documentation to DMG + macDmgContent = [ + "README.md", + "LICENSE", + "CHANGELOG.md" + ] + + // Enable Mac-specific entitlements + //macEntitlements = "entitlements.plist" // You'll need to create this file + } + + // Linux-specific configuration + linux { + icon = "src/main/resources/static/favicon.png" + type = "deb" // Can also use "rpm" for Red Hat-based systems + + // Debian package configuration + linuxPackageName = "stirling-pdf" + linuxDebMaintainer = "support@stirling-software.com" + linuxMenuGroup = "Office;PDF;Productivity" + linuxAppCategory = "Office" + linuxAppRelease = "1" + linuxPackageDeps = true + linuxShortcut = true + + // RPM-specific settings + linuxRpmLicenseType = "MIT" + } + + // Common additional options + //jLinkOptions = [ + // "--strip-debug", + // "--compress=2", + // "--no-header-files", + // "--no-man-pages" + //] + + // Add any additional modules required + /*addModules = [ + "java.base", + "java.desktop", + "java.logging", + "java.sql", + "java.xml", + "jdk.crypto.ec" + ]*/ + + // Add copyright and license information + copyright = "Copyright © 2024 Stirling Software" + licenseFile = "LICENSE" + + // Set installation directory + installDir = "Stirling-PDF" +} + + launch4j { icon = "${projectDir}/src/main/resources/static/favicon.ico" diff --git a/src/main/resources/static/favicon.icns b/src/main/resources/static/favicon.icns new file mode 100644 index 0000000000000000000000000000000000000000..7b281937e8f796dfc6d06c2266f0e36db48c462b GIT binary patch literal 62003 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 Date: Thu, 12 Dec 2024 23:05:59 +0000 Subject: [PATCH 23/49] build --- .github/workflows/multiOSReleases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 6b610ad09..bb75aefcd 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -61,7 +61,7 @@ jobs: # Build installer - name: Build Installer - run: ./gradlew jpackage + run: ./gradlew build jpackage # Rename and collect artifacts based on OS - name: Prepare artifacts From 7f4134f52dee85777f4c28340160a1d6b216a9d9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:08:28 +0000 Subject: [PATCH 24/49] info --- .github/workflows/multiOSReleases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index bb75aefcd..d0f56c5e3 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -61,7 +61,7 @@ jobs: # Build installer - name: Build Installer - run: ./gradlew build jpackage + run: ./gradlew build jpackage --info # Rename and collect artifacts based on OS - name: Prepare artifacts From 65a86a6e97facd47ef7683e3fcb8c084c69aed24 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:16:27 +0000 Subject: [PATCH 25/49] remove mac 18+ --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ed557e065..47b14d15d 100644 --- a/build.gradle +++ b/build.gradle @@ -141,11 +141,11 @@ jpackage { macAppStore = false // Not targeting App Store initially // Add license and other documentation to DMG - macDmgContent = [ + /*macDmgContent = [ "README.md", "LICENSE", "CHANGELOG.md" - ] + ]*/ // Enable Mac-specific entitlements //macEntitlements = "entitlements.plist" // You'll need to create this file From f950e25dadd6550da0526831a1a4cabb11cb2c07 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:22:19 +0000 Subject: [PATCH 26/49] versioning --- build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 47b14d15d..cd674ddaf 100644 --- a/build.gradle +++ b/build.gradle @@ -88,13 +88,20 @@ openApi { outputFileName = "SwaggerDoc.json" } +def getMacVersion(String version) { + def currentYear = java.time.Year.now().getValue() + // Extract everything after the first dot (or the whole string if no dot) + def versionParts = version.split("\\.", 2) + return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}" +} + jpackage { // Input directory containing the jar input = "build/libs" // Application details appName = "Stirling-PDF" - appVersion = "0.36.3" + appVersion = project.version vendor = "Stirling-Software" // Main application configuration @@ -132,6 +139,7 @@ jpackage { // macOS-specific configuration mac { + appVersion = getMacVersion(project.version.toString()) icon = "src/main/resources/static/favicon.icns" type = "dmg" macPackageIdentifier = "com.stirling.software.pdf" From 446cedf26edb1fdedf5f4c817f562ef7a15411e9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:26:23 +0000 Subject: [PATCH 27/49] unix name --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index cd674ddaf..e5e8aefab 100644 --- a/build.gradle +++ b/build.gradle @@ -165,8 +165,8 @@ jpackage { type = "deb" // Can also use "rpm" for Red Hat-based systems // Debian package configuration - linuxPackageName = "stirling-pdf" - linuxDebMaintainer = "support@stirling-software.com" + linuxPackageName = "stirlingpdf" + linuxDebMaintainer = "support@stirlingpdf.com" linuxMenuGroup = "Office;PDF;Productivity" linuxAppCategory = "Office" linuxAppRelease = "1" From 2f23eb69c6cf4d8d2044300b5646c4d6ed565a58 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:40:49 +0000 Subject: [PATCH 28/49] naming --- .github/workflows/multiOSReleases.yml | 15 +++++++++------ build.gradle | 3 +++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index d0f56c5e3..6b834a80f 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -54,11 +54,14 @@ jobs: sudo apt-get install -y fakeroot rpm # Get version number - - name: Get version number - id: versionNumber - run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + - name: Get version numbers + id: versions + run: | + echo "version=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + echo "macversion=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT shell: bash + # Build installer - name: Build Installer run: ./gradlew build jpackage --info @@ -69,11 +72,11 @@ jobs: shell: bash run: | if [ "${{ matrix.os }}" = "windows-latest" ]; then - mv Stirling-PDF.exe "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "Stirling-PDF-${{ steps.versions.outputs.version }}.exe" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" elif [ "${{ matrix.os }}" = "macos-latest" ]; then - mv build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.dmg "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "Stirling-PDF-${{ steps.versions.outputs.macversion }}.dmg" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" else - mv build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "stirlingpdf_${{ steps.versions.outputs.version }}-1_amd64.deb" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" fi # Upload installer as artifact for testing diff --git a/build.gradle b/build.gradle index e5e8aefab..fe13370c3 100644 --- a/build.gradle +++ b/build.gradle @@ -425,3 +425,6 @@ tasks.named("test") { task printVersion { println project.version } +task printMacVersion { + println getMacVersion(project.version.toString()) +} From 23888c5d2c3c1d03622cc4cf867360d610eaae75 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:50:51 +0000 Subject: [PATCH 29/49] test --- .github/workflows/multiOSReleases.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 6b834a80f..4238ccb48 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -54,14 +54,16 @@ jobs: sudo apt-get install -y fakeroot rpm # Get version number - - name: Get version numbers - id: versions - run: | - echo "version=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - echo "macversion=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + - name: Get version number + id: versionNumber + run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT shell: bash - + - name: Get version number + id: versionNumberMac + run: echo "versionNumberMac=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + shell: bash + # Build installer - name: Build Installer run: ./gradlew build jpackage --info @@ -72,11 +74,11 @@ jobs: shell: bash run: | if [ "${{ matrix.os }}" = "windows-latest" ]; then - mv "Stirling-PDF-${{ steps.versions.outputs.version }}.exe" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" elif [ "${{ matrix.os }}" = "macos-latest" ]; then - mv "Stirling-PDF-${{ steps.versions.outputs.macversion }}.dmg" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" else - mv "stirlingpdf_${{ steps.versions.outputs.version }}-1_amd64.deb" "Stirling-PDF-${{ steps.versions.outputs.version }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "stirlingpdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" fi # Upload installer as artifact for testing From 32aa623c8b613e0961610b1acf1e20c0e00b7887 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Thu, 12 Dec 2024 23:57:12 +0000 Subject: [PATCH 30/49] test --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fe13370c3..94a1a2c73 100644 --- a/build.gradle +++ b/build.gradle @@ -165,7 +165,7 @@ jpackage { type = "deb" // Can also use "rpm" for Red Hat-based systems // Debian package configuration - linuxPackageName = "stirlingpdf" + //linuxPackageName = "stirlingpdf" linuxDebMaintainer = "support@stirlingpdf.com" linuxMenuGroup = "Office;PDF;Productivity" linuxAppCategory = "Office" From e6c2ad8c9c98230ecb57ae5f9bdcb112c6d604a9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:00:56 +0000 Subject: [PATCH 31/49] test --- .github/workflows/multiOSReleases.yml | 3 +++ build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 4238ccb48..a0c276b20 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -67,6 +67,9 @@ jobs: # Build installer - name: Build Installer run: ./gradlew build jpackage --info + env: + DOCKER_ENABLE_SECURITY: false + STIRLING_PDF_DESKTOP_UI: true # Rename and collect artifacts based on OS - name: Prepare artifacts diff --git a/build.gradle b/build.gradle index 94a1a2c73..3f3a4ee65 100644 --- a/build.gradle +++ b/build.gradle @@ -174,7 +174,7 @@ jpackage { linuxShortcut = true // RPM-specific settings - linuxRpmLicenseType = "MIT" + //linuxRpmLicenseType = "MIT" } // Common additional options From ccdcb05e65192e7289b8fd288876d92e2e55bfbd Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:13:47 +0000 Subject: [PATCH 32/49] x test --- .github/workflows/multiOSReleases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index a0c276b20..100396d09 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -66,7 +66,7 @@ jobs: # Build installer - name: Build Installer - run: ./gradlew build jpackage --info + run: ./gradlew build jpackage -x test --info env: DOCKER_ENABLE_SECURITY: false STIRLING_PDF_DESKTOP_UI: true From d55e007f93c8541666958476204da5d561fc1a7a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:24:51 +0000 Subject: [PATCH 33/49] test dirs --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3f3a4ee65..5b4a4e780 100644 --- a/build.gradle +++ b/build.gradle @@ -135,6 +135,7 @@ jpackage { winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF" winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases" type = "exe" + installDir = "C:/Program Files/Stirling-PDF" } // macOS-specific configuration @@ -148,6 +149,8 @@ jpackage { macSign = false // Enable signing macAppStore = false // Not targeting App Store initially + installDir = "/Applications/Stirling-PDF.app" + // Add license and other documentation to DMG /*macDmgContent = [ "README.md", @@ -173,6 +176,8 @@ jpackage { linuxPackageDeps = true linuxShortcut = true + installDir = "/opt/Stirling-PDF" + // RPM-specific settings //linuxRpmLicenseType = "MIT" } @@ -198,9 +203,6 @@ jpackage { // Add copyright and license information copyright = "Copyright © 2024 Stirling Software" licenseFile = "LICENSE" - - // Set installation directory - installDir = "Stirling-PDF" } From f899088c75bb641361692b4b5768137135707d9d Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:29:03 +0000 Subject: [PATCH 34/49] test --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5b4a4e780..c0e2b2970 100644 --- a/build.gradle +++ b/build.gradle @@ -174,7 +174,7 @@ jpackage { linuxAppCategory = "Office" linuxAppRelease = "1" linuxPackageDeps = true - linuxShortcut = true + linuxPackageName = "stirlingpdf" installDir = "/opt/Stirling-PDF" From f1272717095976d0e11a4f45c30a1e79e6a89c1e Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:31:39 +0000 Subject: [PATCH 35/49] test --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index c0e2b2970..d2eee9a9e 100644 --- a/build.gradle +++ b/build.gradle @@ -174,7 +174,6 @@ jpackage { linuxAppCategory = "Office" linuxAppRelease = "1" linuxPackageDeps = true - linuxPackageName = "stirlingpdf" installDir = "/opt/Stirling-PDF" From 4ae7c833577e5005981ef258d73f1754b3209b83 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:44:32 +0000 Subject: [PATCH 36/49] tests --- .github/workflows/multiOSReleases.yml | 10 +++++----- build.gradle | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 100396d09..6aa20977d 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -59,9 +59,9 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT shell: bash - - name: Get version number + - name: Get version number mac id: versionNumberMac - run: echo "versionNumberMac=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT + run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT shell: bash # Build installer @@ -77,11 +77,11 @@ jobs: shell: bash run: | if [ "${{ matrix.os }}" = "windows-latest" ]; then - mv "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" elif [ "${{ matrix.os }}" = "macos-latest" ]; then - mv "Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" else - mv "stirlingpdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" + mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}" fi # Upload installer as artifact for testing diff --git a/build.gradle b/build.gradle index d2eee9a9e..c6b736553 100644 --- a/build.gradle +++ b/build.gradle @@ -122,6 +122,8 @@ jpackage { // Enable verbose output verbose = true + + destinationDir = file("${projectDir}/build/jpackage") // Windows-specific configuration windows { From de3f59cf44eb9dcea6e31e60bcbecc8474c39b6d Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:47:37 +0000 Subject: [PATCH 37/49] destination = "${projectDir}/build/jpackage" --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c6b736553..122a67965 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,7 @@ jpackage { // Enable verbose output verbose = true - destinationDir = file("${projectDir}/build/jpackage") + destination = "${projectDir}/build/jpackage" // Windows-specific configuration windows { From 40b5904726eda26445acd8ef3e2a863dc5ef2bf7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:52:12 +0000 Subject: [PATCH 38/49] test --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 122a67965..325741a17 100644 --- a/build.gradle +++ b/build.gradle @@ -127,6 +127,7 @@ jpackage { // Windows-specific configuration windows { + appVersion = project.version winConsole = false winDirChooser = true winMenu = true @@ -166,6 +167,7 @@ jpackage { // Linux-specific configuration linux { + appVersion = project.version icon = "src/main/resources/static/favicon.png" type = "deb" // Can also use "rpm" for Red Hat-based systems From 9870e6ad7c737f3482f3fb3835511a1dced28fb4 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 00:59:42 +0000 Subject: [PATCH 39/49] fix --- build.gradle | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 325741a17..9116f1310 100644 --- a/build.gradle +++ b/build.gradle @@ -426,10 +426,14 @@ jar { tasks.named("test") { useJUnitPlatform() } - task printVersion { - println project.version + doLast { + println project.version + } } + task printMacVersion { - println getMacVersion(project.version.toString()) + doLast { + println getMacVersion(project.version.toString()) + } } From 378aca44600e3119061a0ec84f35c52b6f6d8003 Mon Sep 17 00:00:00 2001 From: tkymmm <136296842+tkymmm@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:23:26 +0900 Subject: [PATCH 40/49] Update messages_ja_JP.properties --- src/main/resources/messages_ja_JP.properties | 308 +++++++++---------- 1 file changed, 154 insertions(+), 154 deletions(-) diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index 709b40b88..efaae9571 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -56,12 +56,12 @@ userNotFoundMessage=ユーザーが見つかりません。 incorrectPasswordMessage=現在のパスワードが正しくありません。 usernameExistsMessage=新しいユーザー名はすでに存在します。 invalidUsernameMessage=ユーザー名が無効です。ユーザー名には文字、数字、およびそれに続く特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。 -invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end. +invalidPasswordMessage=パスワードは空にすることはできません。また、先頭・末尾にスペースを含めることもできません。 confirmPasswordErrorMessage=新しいパスワードと新しいパスワードの確認は一致する必要があります。 deleteCurrentUserMessage=現在ログインしているユーザーは削除できません。 deleteUsernameExistsMessage=そのユーザー名は存在しないため削除できません。 downgradeCurrentUserMessage=現在のユーザーの役割をダウングレードできません -disabledCurrentUserMessage=The current user cannot be disabled +disabledCurrentUserMessage=現在のユーザーを無効にすることはできません downgradeCurrentUserLongMessage=現在のユーザーの役割をダウングレードできません。したがって、現在のユーザーは表示されません。 userAlreadyExistsOAuthMessage=ユーザーは既にOAuth2ユーザーとして存在します。 userAlreadyExistsWebMessage=ユーザーは既にWebユーザーとして存在します。 @@ -76,12 +76,12 @@ donate=寄付する color=色 sponsor=スポンサー info=Info -pro=Pro -page=Page -pages=Pages -loading=Loading... -addToDoc=Add to Document -reset=Reset +pro=pro +page=ページ +pages=ページ +loading=読込中... +addToDoc=ドキュメントに追加 +reset=リセット legal.privacy=プライバシーポリシー legal.terms=利用規約 @@ -92,7 +92,7 @@ legal.impressum=著作権利者情報 ############### # Pipeline # ############### -pipeline.header=パイプラインメニュー (Alpha) +pipeline.header=パイプラインメニュー (Beta) pipeline.uploadButton=カスタムのアップロード pipeline.configureButton=設定 pipeline.defaultOption=カスタム @@ -117,21 +117,21 @@ pipelineOptions.validateButton=検証 ######################## # ENTERPRISE EDITION # ######################## -enterpriseEdition.button=Upgrade to Pro -enterpriseEdition.warning=This feature is only available to Pro users. -enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. -enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro +enterpriseEdition.button=Proにアップグレード +enterpriseEdition.warning=この機能はProユーザーのみが利用できます。 +enterpriseEdition.yamlAdvert=Stirling PDF Proは、YAML構成ファイルやその他のSSO機能をサポートしています。 +enterpriseEdition.ssoAdvert=より多くのユーザー管理機能をお探しですか? Stirling PDF Proをご覧ください ################# # Analytics # ################# -analytics.title=Do you want make Stirling PDF better? -analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents. -analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better. -analytics.enable=Enable analytics -analytics.disable=Disable analytics -analytics.settings=You can change the settings for analytics in the config/settings.yml file +analytics.title=Stirling PDFをもっと良くしたいですか? +analytics.paragraph1=Stirling PDFでは、製品の改善に役立つ分析機能をオプトインしています。個人情報やファイルの内容を追跡することはありません。 +analytics.paragraph2=Stirling-PDFの成長を支援しユーザーをより深く理解できるように分析を有効にすることを検討してください。 +analytics.enable=分析を有効にする +analytics.disable=分析を無効にする +analytics.settings=config/settings.ymlファイルでアナリティクスの設定を変更できます。 ############# # NAVBAR # @@ -142,14 +142,14 @@ navbar.language=言語 navbar.settings=設定 navbar.allTools=ツール navbar.multiTool=マルチツール -navbar.search=Search +navbar.search=検索 navbar.sections.organize=整理 navbar.sections.convertTo=PDFへ変換 navbar.sections.convertFrom=PDFから変換 navbar.sections.security=署名とセキュリティ navbar.sections.advance=アドバンスド navbar.sections.edit=閲覧と編集 -navbar.sections.popular=Popular +navbar.sections.popular=人気 ############# # SETTINGS # @@ -208,7 +208,7 @@ adminUserSettings.user=ユーザー adminUserSettings.addUser=新しいユーザを追加 adminUserSettings.deleteUser=ユーザの削除 adminUserSettings.confirmDeleteUser=ユーザを本当に削除しますか? -adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled? +adminUserSettings.confirmChangeUserStatus=ユーザーを無効/有効にする必要がありますか? adminUserSettings.usernameInfo=ユーザー名には、文字、数字、および次の特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。 adminUserSettings.roles=役割 adminUserSettings.role=役割 @@ -247,8 +247,8 @@ database.fileNotFound=ファイルが見つかりません database.fileNullOrEmpty=ファイルは null または空であってはなりません database.failedImportFile=ファイルのインポートに失敗 -session.expired=Your session has expired. Please refresh the page and try again. -session.refreshPage=Refresh Page +session.expired=セッションが期限切れです。ページを更新してもう一度お試しください。 +session.refreshPage=ページを更新 ############# # HOME-PAGE # @@ -488,52 +488,52 @@ overlay-pdfs.tags=Overlay home.split-by-sections.title=PDFをセクションで分割 home.split-by-sections.desc=PDFの各ページを縦横に分割します。 -split-by-sections.tags=Section Split, Divide, Customize +split-by-sections.tags=Section Split, Divide, Customize,Customise home.AddStampRequest.title=PDFにスタンプを追加 home.AddStampRequest.desc=設定した位置にテキストや画像のスタンプを追加できます -AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize +AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize,Customise home.PDFToBook.title=PDFを書籍に変換 home.PDFToBook.desc=calibreを使用してPDFを書籍/コミック形式に変換します -PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle +PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf home.BookToPDF.title=PDFを書籍に変換 home.BookToPDF.desc=calibreを使用してPDFを書籍/コミック形式に変換します -BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle +BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf home.removeImagePdf.title=画像の削除 home.removeImagePdf.desc=PDFから画像を削除してファイルサイズを小さくします removeImagePdf.tags=Remove Image,Page operations,Back end,server side -home.splitPdfByChapters.title=Split PDF by Chapters -home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. +home.splitPdfByChapters.title=PDFをチャプターごとに分割 +home.splitPdfByChapters.desc=チャプターの構造に基づいてPDFを複数のファイルに分割します splitPdfByChapters.tags=split,chapters,bookmarks,organize -home.validateSignature.title=Validate PDF Signature -home.validateSignature.desc=Verify digital signatures and certificates in PDF documents +home.validateSignature.title=PDF署名の検証 +home.validateSignature.desc=PDF文書のデジタル署名と証明書を検証します validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate #replace-invert-color -replace-color.title=Replace-Invert-Color -replace-color.header=Replace-Invert Color PDF -home.replaceColorPdf.title=Replace and Invert Color -home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size +replace-color.title=色の置換・反転 +replace-color.header=PDFの色の置換・反転 +home.replaceColorPdf.title=色の置換と反転 +home.replaceColorPdf.desc=PDF内のテキストと背景の色を置き換え、PDFのフルカラーを反転してファイルサイズを縮小します。 replaceColorPdf.tags=Replace Color,Page operations,Back end,server side -replace-color.selectText.1=Replace or Invert color Options -replace-color.selectText.2=Default(Default high contrast colors) -replace-color.selectText.3=Custom(Customized colors) -replace-color.selectText.4=Full-Invert(Invert all colors) -replace-color.selectText.5=High contrast color options -replace-color.selectText.6=white text on black background -replace-color.selectText.7=Black text on white background -replace-color.selectText.8=Yellow text on black background -replace-color.selectText.9=Green text on black background -replace-color.selectText.10=Choose text Color -replace-color.selectText.11=Choose background Color -replace-color.submit=Replace +replace-color.selectText.1=色の置換または反転オプション +replace-color.selectText.2=デフォルト(デフォルトの高コントラスト色) +replace-color.selectText.3=カスタム(カスタマイズされた色) +replace-color.selectText.4=フル反転(すべての色を反転) +replace-color.selectText.5=高コントラストカラーオプション +replace-color.selectText.6=黒背景に白文字 +replace-color.selectText.7=白背景に黒文字 +replace-color.selectText.8=黒背景に黄色文字 +replace-color.selectText.9=黒背景に緑文字 +replace-color.selectText.10=テキストの色を選択 +replace-color.selectText.11=背景色を選択 +replace-color.submit=置換 @@ -560,9 +560,9 @@ login.oauth2AccessDenied=アクセス拒否 login.oauth2InvalidTokenResponse=無効なトークン応答 login.oauth2InvalidIdToken=無効なIDトークン login.userIsDisabled=ユーザーは非アクティブ化されており、現在このユーザー名でのログインはブロックされています。管理者に連絡してください。 -login.alreadyLoggedIn=You are already logged in to -login.alreadyLoggedIn2=devices. Please log out of the devices and try again. -login.toManySessions=You have too many active sessions +login.alreadyLoggedIn=すでにログインしています +login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。 +login.toManySessions=アクティブなセッションが多すぎます #auto-redact autoRedact.title=自動塗りつぶし @@ -578,8 +578,8 @@ autoRedact.submitButton=送信 #showJS -showJS.title=JavaScriptを表示 -showJS.header=JavaScriptを表示 +showJS.title=Javascriptを表示 +showJS.header=Javascriptを表示 showJS.downloadJS=Javascriptをダウンロード showJS.submit=表示 @@ -757,7 +757,7 @@ certSign.showSig=署名を表示 certSign.reason=理由 certSign.location=場所 certSign.name=名前 -certSign.showLogo=Show Logo +certSign.showLogo=ロゴを表示 certSign.submit=PDFに署名 @@ -792,9 +792,9 @@ compare.highlightColor.2=ハイライトカラー 2: compare.document.1=ドキュメント 1 compare.document.2=ドキュメント 2 compare.submit=比較 -compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced -compare.large.file.message=One or Both of the provided documents are too large to process -compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison. +compare.complex.message=提供された文書の一方または両方が大きなファイルであるため、比較の精度が低下する可能性があります。 +compare.large.file.message=提供された文書の1つまたは両方が大きすぎて処理できません +compare.no.text.message=選択したPDFの1つまたは両方にテキストコンテンツがありません。比較するには、テキストを含むPDFを選択してください。 #BookToPDF BookToPDF.title=書籍やコミックをPDFに変換 @@ -803,8 +803,8 @@ BookToPDF.credit=calibreを使用 BookToPDF.submit=変換 #PDFToBook -PDFToBook.title=書籍をPDFに変換 -PDFToBook.header=書籍をPDFに変換 +PDFToBook.title=PDFを書籍に変換 +PDFToBook.header=PDFを書籍に変換 PDFToBook.selectText.1=フォーマット PDFToBook.credit=calibreを使用 PDFToBook.submit=変換 @@ -817,17 +817,17 @@ sign.draw=署名を書く sign.text=テキスト入力 sign.clear=クリア sign.add=追加 -sign.saved=Saved Signatures -sign.save=Save Signature -sign.personalSigs=Personal Signatures -sign.sharedSigs=Shared Signatures -sign.noSavedSigs=No saved signatures found -sign.addToAll=Add to all pages -sign.delete=Delete -sign.first=First page -sign.last=Last page -sign.next=Next page -sign.previous=Previous page +sign.saved=保存された署名 +sign.save=署名を保存 +sign.personalSigs=個人署名 +sign.sharedSigs=共有署名 +sign.noSavedSigs=保存された署名が見つかりません +sign.addToAll=すべてのページに追加 +sign.delete=削除 +sign.first=最初のページ +sign.last=最後のページ +sign.next=次のページ +sign.previous=前のページ #repair repair.title=修復 @@ -944,39 +944,39 @@ pdfOrganiser.placeholder=(例:1,3,2または4-8,2,10-12または2n-1) multiTool.title=PDFマルチツール multiTool.header=PDFマルチツール multiTool.uploadPrompts=ファイル名 -multiTool.selectAll=Select All -multiTool.deselectAll=Deselect All -multiTool.selectPages=Page Select -multiTool.selectedPages=Selected Pages -multiTool.page=Page -multiTool.deleteSelected=Delete Selected -multiTool.downloadAll=Export -multiTool.downloadSelected=Export Selected +multiTool.selectAll=すべて選択 +multiTool.deselectAll=選択を解除 +multiTool.selectPages=ページ選択 +multiTool.selectedPages=選択したページ +multiTool.page=ページ +multiTool.deleteSelected=選択項目を削除 +multiTool.downloadAll=エクスポート +multiTool.downloadSelected=選択項目をエクスポート -multiTool.insertPageBreak=Insert Page Break -multiTool.addFile=Add File -multiTool.rotateLeft=Rotate Left -multiTool.rotateRight=Rotate Right -multiTool.split=Split -multiTool.moveLeft=Move Left -multiTool.moveRight=Move Right -multiTool.delete=Delete -multiTool.dragDropMessage=Page(s) Selected -multiTool.undo=Undo -multiTool.redo=Redo +multiTool.insertPageBreak=改ページを挿入 +multiTool.addFile=ファイルを追加 +multiTool.rotateLeft=左回転 +multiTool.rotateRight=右回転 +multiTool.split=分割 +multiTool.moveLeft=左に移動 +multiTool.moveRight=右に移動 +multiTool.delete=削除 +multiTool.dragDropMessage=選択されたページ +multiTool.undo=元に戻す +multiTool.redo=やり直す #decrypt -decrypt.passwordPrompt=This file is password-protected. Please enter the password: -decrypt.cancelled=Operation cancelled for PDF: {0} -decrypt.noPassword=No password provided for encrypted PDF: {0} -decrypt.invalidPassword=Please try again with the correct password. -decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} -decrypt.unexpectedError=There was an error processing the file. Please try again. -decrypt.serverError=Server error while decrypting: {0} -decrypt.success=File decrypted successfully. +decrypt.passwordPrompt=このファイルはパスワードで保護されています。パスワードを入力してください: +decrypt.cancelled=PDFの操作がキャンセルされました: {0} +decrypt.noPassword=暗号化されたPDFにパスワードが指定されていません: {0} +decrypt.invalidPassword=正しいパスワードでもう一度お試しください。 +decrypt.invalidPasswordHeader=PDFのパスワードが正しくないか、暗号化がサポートされていません: {0} +decrypt.unexpectedError=ファイルの処理中にエラーが発生しました。もう一度お試しください。 +decrypt.serverError=復号化中にサーバーエラーが発生しました: {0} +decrypt.success=ファイルの暗号化が正常に完了しました。 #multiTool-advert -multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! +multiTool-advert.message=この機能は、マルチツールでもご利用いただけます。強化されたページごとのUIと追加機能についてはこちらをご覧ください。 #view pdf viewPdf.title=PDFを表示 @@ -1132,8 +1132,8 @@ pdfToPDFA.header=PDFをPDF/Aに変換 pdfToPDFA.credit=本サービスはPDF/Aの変換にqpdfを使用しています。 pdfToPDFA.submit=変換 pdfToPDFA.tip=現在、一度に複数の入力に対して機能しません -pdfToPDFA.outputFormat=Output format -pdfToPDFA.pdfWithDigitalSignature=PDF にはデジタル署名が含まれています。これは次の手順で削除されます。 +pdfToPDFA.outputFormat=出力形式 +pdfToPDFA.pdfWithDigitalSignature=PDFにはデジタル署名が含まれています。これは次の手順で削除されます。 #PDFToWord @@ -1238,8 +1238,8 @@ licenses.license=ライセンス survey.nav=アンケート survey.title=Stirling-PDFのアンケート survey.description=Stirling-PDFには追跡機能がないため、Stirling-PDFをより良くするために皆様の意見を聞かせてください! -survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here: -survey.changes2=With these changes we are getting paid business support and funding +survey.changes=Stirling-PDFは前回の調査から変更されました。詳細についてはこちらのブログ投稿をご覧ください。 +survey.changes2=これらの変更により私たちは有償のビジネスサポートと資金援助を受けています survey.please=アンケートにご協力ください! survey.disabled=(アンケートのポップアップは、次の更新では無効になりますが、ページの下部に表示されます。) survey.button=アンケートに答える @@ -1267,61 +1267,61 @@ removeImage.removeImage=画像の削除 removeImage.submit=画像を削除 -splitByChapters.title=Split PDF by Chapters -splitByChapters.header=Split PDF by Chapters -splitByChapters.bookmarkLevel=Bookmark Level -splitByChapters.includeMetadata=Include Metadata -splitByChapters.allowDuplicates=Allow Duplicates -splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure. -splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.). -splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. -splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. -splitByChapters.submit=Split PDF +splitByChapters.title=PDFをチャプターごとに分割 +splitByChapters.header=PDFをチャプターごとに分割 +splitByChapters.bookmarkLevel=ブックマークレベル +splitByChapters.includeMetadata=メタデータを含める +splitByChapters.allowDuplicates=重複を許可する +splitByChapters.desc.1=このツールは、チャプター構造に基づいてPDFファイルを複数のPDFに分割します。 +splitByChapters.desc.2=ブックマークレベル:分割に使用するブックマークのレベルを選択します(最上位レベルの場合は0、第2レベルの場合は1など)。 +splitByChapters.desc.3=メタデータを含める:チェックすると、元のPDFのメタデータが各分割PDFに含まれます。 +splitByChapters.desc.4=重複を許可:チェックすると同じページ上の複数のブックマークから個別のPDFを作成できます。 +splitByChapters.submit=PDFを分割 #File Chooser -fileChooser.click=Click -fileChooser.or=or -fileChooser.dragAndDrop=Drag & Drop -fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here +fileChooser.click=クリック +fileChooser.or=または +fileChooser.dragAndDrop=ドラッグ&ドロップ +fileChooser.hoveredDragAndDrop=ファイルをここにドラッグ&ドロップ #release notes -releases.footer=Releases -releases.title=Release Notes -releases.header=Release Notes -releases.current.version=Current Release -releases.note=Release notes are only available in English +releases.footer=リリース +releases.title=リリースノート +releases.header=リリースノート +releases.current.version=現在のリリース +releases.note=リリースノートは英語でのみで提供されています #Validate Signature -validateSignature.title=Validate PDF Signatures -validateSignature.header=Validate Digital Signatures -validateSignature.selectPDF=Select signed PDF file -validateSignature.submit=Validate Signatures -validateSignature.results=Validation Results -validateSignature.status=Status -validateSignature.signer=Signer -validateSignature.date=Date -validateSignature.reason=Reason -validateSignature.location=Location -validateSignature.noSignatures=No digital signatures found in this document -validateSignature.status.valid=Valid -validateSignature.status.invalid=Invalid -validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity -validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified -validateSignature.cert.expired=Certificate has expired -validateSignature.cert.revoked=Certificate has been revoked -validateSignature.signature.info=Signature Information -validateSignature.signature=Signature -validateSignature.signature.mathValid=Signature is mathematically valid BUT: -validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional) -validateSignature.cert.info=Certificate Details -validateSignature.cert.issuer=Issuer -validateSignature.cert.subject=Subject -validateSignature.cert.serialNumber=Serial Number -validateSignature.cert.validFrom=Valid From -validateSignature.cert.validUntil=Valid Until -validateSignature.cert.algorithm=Algorithm -validateSignature.cert.keySize=Key Size -validateSignature.cert.version=Version -validateSignature.cert.keyUsage=Key Usage -validateSignature.cert.selfSigned=Self-Signed -validateSignature.cert.bits=bits +validateSignature.title=PDF署名の検証 +validateSignature.header=デジタル署名の検証 +validateSignature.selectPDF=署名済みPDFファイルを選択 +validateSignature.submit=署名の検証 +validateSignature.results=検証結果 +validateSignature.status=状態 +validateSignature.signer=署名者 +validateSignature.date=日付 +validateSignature.reason=理由 +validateSignature.location=場所 +validateSignature.noSignatures=この文書にはデジタル署名が見つかりません +validateSignature.status.valid=有効 +validateSignature.status.invalid=無効 +validateSignature.chain.invalid=証明書チェーンの検証に失敗しました - 署名者の身元を確認できません +validateSignature.trust.invalid=証明書が信頼ストアにありません - ソースを検証できません +validateSignature.cert.expired=証明書の有効期限が切れています +validateSignature.cert.revoked=証明書は取り消されました +validateSignature.signature.info=署名情報 +validateSignature.signature=署名 +validateSignature.signature.mathValid=署名は数学的には有効ですが: +validateSignature.selectCustomCert=カスタム証明書ファイル X.509 (オプション) +validateSignature.cert.info=証明書の詳細 +validateSignature.cert.issuer=発行者 +validateSignature.cert.subject=主題 +validateSignature.cert.serialNumber=シリアルナンバー +validateSignature.cert.validFrom=有効開始日 +validateSignature.cert.validUntil=有効期限 +validateSignature.cert.algorithm=アルゴリズム +validateSignature.cert.keySize=キーサイズ +validateSignature.cert.version=バージョン +validateSignature.cert.keyUsage=キーの使用法 +validateSignature.cert.selfSigned=自己署名 +validateSignature.cert.bits=ビット From fe198458ca9344b6ffd55b929ced6a8dd1f0552d Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 10:28:03 +0000 Subject: [PATCH 41/49] fix main class --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9116f1310..1c32c450a 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,7 @@ jpackage { // Main application configuration mainJar = "Stirling-PDF-${project.version}.jar" - mainClass = "stirling.software.SPDF.StirlingPdfApplication" + mainClass = "stirling.software.SPDF.SPdfApplication" // Default icon configuration icon = "src/main/resources/static/favicon.ico" From 1ccdc1697bce5c5edbbff10a4abef705ad7e09f1 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 10:49:04 +0000 Subject: [PATCH 42/49] dif main class --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1c32c450a..24e235237 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,7 @@ jpackage { // Main application configuration mainJar = "Stirling-PDF-${project.version}.jar" - mainClass = "stirling.software.SPDF.SPdfApplication" + mainClass = "org.springframework.boot.loader.launch.JarLauncher" // Default icon configuration icon = "src/main/resources/static/favicon.ico" @@ -128,7 +128,7 @@ jpackage { // Windows-specific configuration windows { appVersion = project.version - winConsole = false + winConsole = true winDirChooser = true winMenu = true winShortcut = true From 43c4ec10897d6c78cd7ffcc070d1332af4a11322 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 11:31:49 +0000 Subject: [PATCH 43/49] fixes! --- build.gradle | 11 ++++++++--- .../stirling/software/SPDF/SPdfApplication.java | 14 +++++++++----- .../software/SPDF/UI/impl/DesktopBrowser.java | 14 +++++++++----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 24e235237..8d841aa20 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { } group = "stirling.software" -version = "0.36.0" +version = "0.36.1" java { @@ -117,7 +117,11 @@ jpackage { // JVM Options javaOptions = [ "-DBROWSER_OPEN=true", - "-DSTIRLING_PDF_DESKTOP_UI=true" + "-DSTIRLING_PDF_DESKTOP_UI=true", + "-Djava.awt.headless=false", + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED", + "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED" ] // Enable verbose output @@ -127,8 +131,9 @@ jpackage { // Windows-specific configuration windows { + launcherAsService = false appVersion = project.version - winConsole = true + winConsole = false winDirChooser = true winMenu = true winShortcut = true diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index f39c0e188..bb977c6b1 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -79,11 +79,11 @@ public class SPdfApplication { Properties props = new Properties(); - if ("true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { System.setProperty("java.awt.headless", "false"); app.setHeadless(false); - // props.put("java.awt.headless", "false"); - // props.put("spring.main.web-application-type", "servlet"); + props.put("java.awt.headless", "false"); + props.put("spring.main.web-application-type", "servlet"); } app.setAdditionalProfiles("default"); @@ -118,7 +118,7 @@ public class SPdfApplication { propertyFiles.get("spring.config.additional-location"))); } - if (props.isEmpty()) { + if (!props.isEmpty()) { finalProps.putAll(props); } app.setDefaultProperties(finalProps); @@ -147,9 +147,13 @@ public class SPdfApplication { @PostConstruct public void init() { + log.info( + "1 STIRLING_PDF_DESKTOP_UI={}", + Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))); baseUrlStatic = this.baseUrl; String url = baseUrl + ":" + getStaticPort(); - if (webBrowser != null && "true".equals(System.getenv("STIRLING_PDF_DESKTOP_UI"))) { + if (webBrowser != null + && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { webBrowser.initWebUI(url); } diff --git a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java index 835c36a99..1090103de 100644 --- a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java +++ b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java @@ -58,10 +58,12 @@ public class DesktopBrowser implements WebBrowser { private static SystemTray systemTray; public DesktopBrowser() { + log.info("DesktopBrowser 1"); SwingUtilities.invokeLater( () -> { loadingWindow = new LoadingWindow(null, "Initializing..."); loadingWindow.setVisible(true); + log.info("DesktopBrowser 2"); }); } @@ -69,6 +71,7 @@ public class DesktopBrowser implements WebBrowser { CompletableFuture.runAsync( () -> { try { + log.info("DesktopBrowser 4"); CefAppBuilder builder = new CefAppBuilder(); configureCefSettings(builder); @@ -91,7 +94,7 @@ public class DesktopBrowser implements WebBrowser { // Show the frame immediately but transparent frame.setVisible(true); }); - + log.info("DesktopBrowser 5"); } catch (Exception e) { log.error("Error initializing JCEF browser: ", e); cleanup(); @@ -153,27 +156,28 @@ public class DesktopBrowser implements WebBrowser { Objects.requireNonNull(state, "state cannot be null"); SwingUtilities.invokeLater( () -> { + log.info("state {}", state.name()); if (loadingWindow != null) { switch (state) { case LOCATING: - loadingWindow.setStatus("Locating Chromium..."); + loadingWindow.setStatus("Locating Files..."); loadingWindow.setProgress(0); break; case DOWNLOADING: if (percent >= 0) { loadingWindow.setStatus( String.format( - "Downloading Chromium: %.0f%%", + "Downloading additional files: %.0f%%", percent)); loadingWindow.setProgress((int) percent); } break; case EXTRACTING: - loadingWindow.setStatus("Extracting Chromium..."); + loadingWindow.setStatus("Extracting files..."); loadingWindow.setProgress(60); break; case INITIALIZING: - loadingWindow.setStatus("Initializing browser..."); + loadingWindow.setStatus("Initializing UI..."); loadingWindow.setProgress(80); break; case INITIALIZED: From ebd0ddc6ad3c9b0227bd43dfbe58ca7b99a229c1 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 12:14:21 +0000 Subject: [PATCH 44/49] test --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8d841aa20..74373b704 100644 --- a/build.gradle +++ b/build.gradle @@ -119,9 +119,11 @@ jpackage { "-DBROWSER_OPEN=true", "-DSTIRLING_PDF_DESKTOP_UI=true", "-Djava.awt.headless=false", + "-Dapple.awt.UIElement=true", "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED", "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED" + ] // Enable verbose output @@ -157,7 +159,7 @@ jpackage { macSign = false // Enable signing macAppStore = false // Not targeting App Store initially - installDir = "/Applications/Stirling-PDF.app" + //installDir = "Applications" // Add license and other documentation to DMG /*macDmgContent = [ From 13572a7f18f326d2acb053b3693fe3a1771eccdd Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 16:04:45 +0000 Subject: [PATCH 45/49] remove non windows for now --- .github/workflows/multiOSReleases.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 6aa20977d..fc1a7635a 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -18,12 +18,12 @@ jobs: - os: windows-latest platform: win ext: exe - - os: macos-latest - platform: mac - ext: dmg - - os: ubuntu-latest - platform: linux - ext: deb + #- os: macos-latest + # platform: mac + # ext: dmg + #- os: ubuntu-latest + # platform: linux + # ext: deb runs-on: ${{ matrix.os }} steps: @@ -63,7 +63,7 @@ jobs: id: versionNumberMac run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT shell: bash - + # Build installer - name: Build Installer run: ./gradlew build jpackage -x test --info @@ -91,4 +91,4 @@ jobs: name: Stirling-PDF-${{ matrix.platform }}.${{ matrix.ext }} path: Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }} retention-days: 1 - if-no-files-found: error + if-no-files-found: error \ No newline at end of file From 509a3059857b99eb6d687dfa454b8bb1ae23ddbc Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 16:58:34 +0000 Subject: [PATCH 46/49] logs and cleanup --- build.gradle | 5 +--- .../software/SPDF/SPdfApplication.java | 25 ++++++++++++++++--- .../software/SPDF/UI/impl/DesktopBrowser.java | 8 ------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 74373b704..909b8ffde 100644 --- a/build.gradle +++ b/build.gradle @@ -33,9 +33,6 @@ version = "0.36.1" java { // 17 is lowest but we support and recommend 21 sourceCompatibility = JavaVersion.VERSION_17 - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } } repositories { @@ -88,9 +85,9 @@ openApi { outputFileName = "SwaggerDoc.json" } +//0.11.5 to 2024.11.5 def getMacVersion(String version) { def currentYear = java.time.Year.now().getValue() - // Extract everything after the first dot (or the whole string if no dot) def versionParts = version.split("\\.", 2) return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}" } diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index bb977c6b1..0d3299c84 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableScheduling; +import io.github.pixee.security.SystemCommand; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; @@ -147,16 +148,32 @@ public class SPdfApplication { @PostConstruct public void init() { - log.info( - "1 STIRLING_PDF_DESKTOP_UI={}", - Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))); baseUrlStatic = this.baseUrl; String url = baseUrl + ":" + getStaticPort(); if (webBrowser != null && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { - webBrowser.initWebUI(url); + } else { + String browserOpenEnv = env.getProperty("BROWSER_OPEN"); + boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); + if (browserOpen) { + try { + String os = System.getProperty("os.name").toLowerCase(); + Runtime rt = Runtime.getRuntime(); + if (os.contains("win")) { + // For Windows + SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); + } else if (os.contains("mac")) { + SystemCommand.runCommand(rt, "open " + url); + } else if (os.contains("nix") || os.contains("nux")) { + SystemCommand.runCommand(rt, "xdg-open " + url); + } + } catch (Exception e) { + logger.error("Error opening browser: {}", e.getMessage()); + } + } } + logger.info("Running configs {}", applicationProperties.toString()); } @PreDestroy diff --git a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java index 1090103de..a5509e1b7 100644 --- a/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java +++ b/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java @@ -58,12 +58,10 @@ public class DesktopBrowser implements WebBrowser { private static SystemTray systemTray; public DesktopBrowser() { - log.info("DesktopBrowser 1"); SwingUtilities.invokeLater( () -> { loadingWindow = new LoadingWindow(null, "Initializing..."); loadingWindow.setVisible(true); - log.info("DesktopBrowser 2"); }); } @@ -71,10 +69,8 @@ public class DesktopBrowser implements WebBrowser { CompletableFuture.runAsync( () -> { try { - log.info("DesktopBrowser 4"); CefAppBuilder builder = new CefAppBuilder(); configureCefSettings(builder); - builder.setProgressHandler(createProgressHandler()); // Build and initialize CEF @@ -94,7 +90,6 @@ public class DesktopBrowser implements WebBrowser { // Show the frame immediately but transparent frame.setVisible(true); }); - log.info("DesktopBrowser 5"); } catch (Exception e) { log.error("Error initializing JCEF browser: ", e); cleanup(); @@ -156,7 +151,6 @@ public class DesktopBrowser implements WebBrowser { Objects.requireNonNull(state, "state cannot be null"); SwingUtilities.invokeLater( () -> { - log.info("state {}", state.name()); if (loadingWindow != null) { switch (state) { case LOCATING: @@ -226,7 +220,6 @@ public class DesktopBrowser implements WebBrowser { boolean isLoading, boolean canGoBack, boolean canGoForward) { - log.info("Loading state changed: " + isLoading); if (!isLoading && !browserInitialized) { browserInitialized = true; SwingUtilities.invokeLater( @@ -343,7 +336,6 @@ public class DesktopBrowser implements WebBrowser { if (icon != null) { frame.setIconImage(icon); setupTrayIcon(icon); - log.info("Successfully set frame icon"); } else { log.warn("Could not load icon from any source"); } From 24717dde19befcbb6115dd72323b08726cad7152 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 13 Dec 2024 18:20:54 +0000 Subject: [PATCH 47/49] finish --- .github/workflows/multiOSReleases.yml | 11 ++++------- build.gradle | 11 +++-------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index fc1a7635a..cd6206cd7 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -1,15 +1,12 @@ name: Test Installers Build on: - push: - branches: - - testStuff workflow_dispatch: - + release: + types: [created] permissions: contents: write packages: write - jobs: build-installers: strategy: @@ -87,8 +84,8 @@ jobs: # Upload installer as artifact for testing - name: Upload Installer Artifact uses: actions/upload-artifact@v4 - with: - name: Stirling-PDF-${{ matrix.platform }}.${{ matrix.ext }} + with: + name: Stirling-PDF-${{ matrix.platform }}-installer.{{ matrix.ext }} path: Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }} retention-days: 1 if-no-files-found: error \ No newline at end of file diff --git a/build.gradle b/build.gradle index 909b8ffde..422fd5bc7 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,6 @@ repositories { url 'https://build.shibboleth.net/maven/releases' } maven { url "https://build.shibboleth.net/maven/releases" } - // Add Maven repository for JCEF maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" } } @@ -93,23 +92,19 @@ def getMacVersion(String version) { } jpackage { - // Input directory containing the jar input = "build/libs" - // Application details appName = "Stirling-PDF" appVersion = project.version vendor = "Stirling-Software" + appDescription = "Stirling PDF - Your Local PDF Editor" - // Main application configuration mainJar = "Stirling-PDF-${project.version}.jar" mainClass = "org.springframework.boot.loader.launch.JarLauncher" - // Default icon configuration icon = "src/main/resources/static/favicon.ico" - // Application description - appDescription = "Stirling PDF - Your Local PDF Editor" + // JVM Options javaOptions = [ @@ -123,7 +118,7 @@ jpackage { ] - // Enable verbose output + verbose = true destination = "${projectDir}/build/jpackage" From dd2aae60adf326bb0c3ab7c2811d10fee6a158b9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:07:30 +0000 Subject: [PATCH 48/49] Update Dockerfile-fat --- Dockerfile-fat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-fat b/Dockerfile-fat index e96f635ab..d34c7daa4 100644 --- a/Dockerfile-fat +++ b/Dockerfile-fat @@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \ ./gradlew clean build # Main stage -FROM alpine:3.21.0 +FROM alpine:3.20.3 # Copy necessary files COPY scripts /scripts From bae83a281cd64f28f16343b9a162647ea9540604 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:07:45 +0000 Subject: [PATCH 49/49] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 82530a885..08ef76644 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Main stage -FROM alpine:3.21.0 +FROM alpine:3.20.3 # Copy necessary files COPY scripts /scripts