diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index 16e1a65e7..bdc57a984 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -1,24 +1,18 @@ package stirling.software.SPDF.controller.api.converters; import java.awt.Color; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.pdfbox.Loader; @@ -26,6 +20,8 @@ import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.io.RandomAccessRead; +import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdfwriter.compress.CompressParameters; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -47,6 +43,14 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup; import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences; +import org.apache.pdfbox.preflight.Format; +import org.apache.pdfbox.preflight.PreflightConfiguration; +import org.apache.pdfbox.preflight.PreflightDocument; +import org.apache.pdfbox.preflight.ValidationResult; +import org.apache.pdfbox.preflight.ValidationResult.ValidationError; +import org.apache.pdfbox.preflight.exception.SyntaxValidationException; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.pdfbox.preflight.parser.PreflightParser; import org.apache.xmpbox.XMPMetadata; import org.apache.xmpbox.schema.AdobePDFSchema; import org.apache.xmpbox.schema.DublinCoreSchema; @@ -66,6 +70,7 @@ import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; @@ -80,11 +85,389 @@ import stirling.software.common.util.WebResponseUtils; @Tag(name = "Convert", description = "Convert APIs") public class ConvertPDFToPDFA { + private static final String ICC_RESOURCE_PATH = "/icc/sRGB2014.icc"; + private static final int PDFA_COMPATIBILITY_POLICY = 1; + + private static void validateAndWarnPdfA(byte[] pdfBytes, PdfaProfile profile, String method) { + Path tempPdfPath = null; + try { + tempPdfPath = Files.createTempFile("validate_", ".pdf"); + + try (OutputStream out = Files.newOutputStream(tempPdfPath)) { + out.write(pdfBytes); + } + + ValidationResult validationResult = + performComprehensivePdfAValidation(tempPdfPath, profile); + + if (validationResult.isValid()) { + log.info( + "PDF/A validation passed for {} using {}", + profile.getDisplayName(), + method); + } else { + log.warn( + "PDF/A validation warning for {} using {}: {}", + profile.getDisplayName(), + method, + buildComprehensiveValidationMessage(validationResult, profile)); + } + } catch (Exception e) { + log.warn( + "PDF/A validation warning for {} using {}: {}", + profile.getDisplayName(), + method, + e.getMessage()); + } finally { + if (tempPdfPath != null) { + try { + Files.deleteIfExists(tempPdfPath); + } catch (IOException e) { + log.debug("Failed to delete temporary validation file", e); + } + } + } + } + + private static ValidationResult performComprehensivePdfAValidation( + Path pdfPath, PdfaProfile profile) throws IOException { + Optional format = profile.preflightFormat(); + if (format.isEmpty()) { + // For profiles without preflight support, perform basic structure validation + return performBasicPdfAValidation(pdfPath, profile); + } + + try (RandomAccessRead rar = new RandomAccessReadBufferedFile(pdfPath.toFile())) { + PreflightParser parser = new PreflightParser(rar); + + PreflightDocument document = parsePreflightDocument(parser, format.get(), profile); + if (document == null) { + throw new IOException( + "PDF/A preflight returned no document for " + profile.getDisplayName()); + } + + try (PreflightDocument closeableDocument = document) { + return closeableDocument.validate(); + } + } catch (SyntaxValidationException e) { + return e.getResult(); + } catch (ValidationException e) { + throw new IOException( + "PDF/A preflight validation failed for " + profile.getDisplayName(), e); + } + } + + private static ValidationResult performBasicPdfAValidation(Path pdfPath, PdfaProfile profile) + throws IOException { + try (PDDocument doc = Loader.loadPDF(pdfPath.toFile())) { + ValidationResult result = new ValidationResult(true); + + float version = doc.getVersion(); + float expectedVersion = profile.getPart() == 1 ? 1.4f : 1.7f; + if (version < expectedVersion) { + result.addError( + new ValidationError( + "PDF_VERSION", + "PDF version " + + version + + " is below required " + + expectedVersion + + " for " + + profile.getDisplayName())); + } + + PDDocumentCatalog catalog = doc.getDocumentCatalog(); + if (catalog.getMetadata() == null) { + result.addError( + new ValidationError( + "MISSING_XMP", + "XMP metadata is required for " + profile.getDisplayName())); + } + + if (catalog.getOutputIntents().isEmpty()) { + result.addError( + new ValidationError( + "MISSING_OUTPUT_INTENT", + "Output intent (ICC profile) is required for " + + profile.getDisplayName())); + } + + return result; + } + } + + private static String buildComprehensiveValidationMessage( + ValidationResult result, PdfaProfile profile) { + if (result == null) { + return "PDF/A validation failed for " + + profile.getDisplayName() + + ": no validation result available"; + } + + List errors = result.getErrorsList(); + + StringBuilder message = new StringBuilder(); + message.append("PDF/A validation issues for ").append(profile.getDisplayName()); + + if (errors != null && !errors.isEmpty()) { + message.append(" - ").append(errors.size()).append(" errors"); + } + message.append(":"); + + if (errors != null && !errors.isEmpty()) { + message.append(" ERRORS: "); + message.append( + errors.stream() + .limit(5) + .map( + error -> + (error.getErrorCode() != null + ? error.getErrorCode() + : "UNKNOWN") + + (error.getDetails() != null + ? ": " + error.getDetails() + : "")) + .collect(Collectors.joining("; "))); + } + + return message.toString(); + } + + private static void deleteQuietly(Path directory) { + if (directory == null) { + return; + } + try (Stream stream = Files.walk(directory)) { + stream.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("Failed to delete temporary file: {}", path, e); + } + }); + } catch (IOException e) { + log.warn("Failed to clean temporary directory: {}", directory, e); + } + } + + private static List buildGhostscriptCommand( + Path inputPdf, + Path outputPdf, + ColorProfiles colorProfiles, + Path workingDir, + PdfaProfile profile, + Path pdfaDefFile) { + + List command = new ArrayList<>(); + command.add("gs"); + command.add("--permit-file-read=" + workingDir.toAbsolutePath()); + command.add("--permit-file-read=" + colorProfiles.rgb().toAbsolutePath()); + command.add("--permit-file-read=" + colorProfiles.gray().toAbsolutePath()); + command.add("--permit-file-read=" + inputPdf.toAbsolutePath()); + command.add("--permit-file-read=" + pdfaDefFile.toAbsolutePath()); + command.add("--permit-file-write=" + workingDir.toAbsolutePath()); + + command.add("-dPDFA=" + profile.getPart()); + command.add("-dPDFACompatibilityPolicy=" + PDFA_COMPATIBILITY_POLICY); + command.add("-dCompatibilityLevel=" + profile.getCompatibilityLevel()); + command.add("-sDEVICE=pdfwrite"); + + command.add("-sColorConversionStrategy=RGB"); + command.add("-dProcessColorModel=/DeviceRGB"); + command.add("-sOutputICCProfile=" + colorProfiles.rgb().toAbsolutePath()); + command.add("-sDefaultRGBProfile=" + colorProfiles.rgb().toAbsolutePath()); + command.add("-sDefaultGrayProfile=" + colorProfiles.gray().toAbsolutePath()); + command.add("-sDefaultCMYKProfile=" + colorProfiles.rgb().toAbsolutePath()); + + // Font handling optimized for PDF/A CIDSet compliance + command.add("-dEmbedAllFonts=true"); + command.add( + "-dSubsetFonts=true"); // Enable subsetting to generate proper CIDSet streams for + // PDF/A-1 + command.add("-dCompressFonts=true"); + command.add("-dNOSUBSTFONTS=false"); // Allow font substitution for problematic fonts + command.add("-dNOPAUSE"); + command.add("-dBATCH"); + command.add("-dNOOUTERSAVE"); + command.add("-sOutputFile=" + outputPdf.toAbsolutePath()); + + command.add(pdfaDefFile.toAbsolutePath().toString()); + command.add(inputPdf.toAbsolutePath().toString()); + + return command; + } + + private static PreflightDocument parsePreflightDocument( + PreflightParser parser, Format format, PdfaProfile profile) throws IOException { + try { + PreflightConfiguration config = PreflightConfiguration.createPdfA1BConfiguration(); + if (profile.getPart() != 1) { + log.debug( + "Using PDF/A-1B configuration for PDF/A-{} validation", profile.getPart()); + } + + return (PreflightDocument) parser.parse(format, config); + } catch (SyntaxValidationException e) { + throw new IOException(buildComprehensiveValidationMessage(e.getResult(), profile), e); + } catch (ClassCastException e) { + throw new IOException( + "PDF/A preflight did not produce a PreflightDocument for " + + profile.getDisplayName(), + e); + } + } + + private static void writeJavaIccProfile(ICC_Profile profile, Path target) throws IOException { + try (OutputStream out = Files.newOutputStream(target)) { + out.write(profile.getData()); + } + } + + private static Path createPdfaDefFile( + Path workingDir, ColorProfiles colorProfiles, PdfaProfile profile) throws IOException { + Path pdfaDefFile = workingDir.resolve("PDFA_def.ps"); + + String title = "Converted to " + profile.getDisplayName(); + String rgbProfilePath = colorProfiles.rgb().toAbsolutePath().toString().replace("\\", "/"); + String pdfaDefContent = + String.format( + """ + %% This is a sample prefix file for creating a PDF/A document. + %% Feel free to modify entries marked with "Customize". + + %% Define entries in the document Info dictionary. + [/Title (%s) + /DOCINFO pdfmark + + %% Define an ICC profile. + [/_objdef {icc_PDFA} /type /stream /OBJ pdfmark + [{icc_PDFA} << + /N 3 + >> /PUT pdfmark + [{icc_PDFA} (%s) (r) file /PUT pdfmark + + %% Define the output intent dictionary. + [/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark + [{OutputIntent_PDFA} << + /Type /OutputIntent + /S /GTS_PDFA1 + /DestOutputProfile {icc_PDFA} + /OutputConditionIdentifier (sRGB IEC61966-2.1) + /Info (sRGB IEC61966-2.1) + /RegistryName (http://www.color.org) + >> /PUT pdfmark + [{Catalog} <> /PUT pdfmark + """, + title, rgbProfilePath); + + Files.writeString(pdfaDefFile, pdfaDefContent); + return pdfaDefFile; + } + + private static List buildGhostscriptCommandX( + Path inputPdf, + Path outputPdf, + ColorProfiles colorProfiles, + Path workingDir, + PdfXProfile profile) { + + List command = new ArrayList<>(25); + command.add("gs"); + command.add("--permit-file-read=" + workingDir.toAbsolutePath()); + command.add("--permit-file-read=" + colorProfiles.rgb().toAbsolutePath()); + command.add("--permit-file-read=" + colorProfiles.gray().toAbsolutePath()); + command.add("--permit-file-read=" + inputPdf.toAbsolutePath()); + command.add("--permit-file-write=" + workingDir.toAbsolutePath()); + command.add("-dPDFX=" + profile.getPdfxVersion()); + command.add("-dCompatibilityLevel=" + profile.getCompatibilityLevel()); + command.add("-sDEVICE=pdfwrite"); + command.add("-sColorConversionStrategy=RGB"); + command.add("-dProcessColorModel=/DeviceRGB"); + command.add("-sOutputICCProfile=" + colorProfiles.rgb().toAbsolutePath()); + command.add("-sDefaultRGBProfile=" + colorProfiles.rgb().toAbsolutePath()); + command.add("-sDefaultGrayProfile=" + colorProfiles.gray().toAbsolutePath()); + command.add("-dEmbedAllFonts=true"); + command.add("-dSubsetFonts=false"); // Embed complete fonts to avoid incomplete glyphs + command.add("-dCompressFonts=true"); + command.add("-dNOSUBSTFONTS=false"); // Allow font substitution for problematic fonts + command.add("-dPDFSETTINGS=/prepress"); + command.add("-dNOPAUSE"); + command.add("-dBATCH"); + command.add("-dNOOUTERSAVE"); + command.add("-sOutputFile=" + outputPdf.toAbsolutePath()); + command.add(inputPdf.toAbsolutePath().toString()); + + return command; + } + + private static void embedMissingFonts( + PDDocument loDoc, PDDocument baseDoc, Set missingFonts) throws IOException { + List loPages = new ArrayList<>(loDoc.getNumberOfPages()); + loDoc.getPages().forEach(loPages::add); + List basePages = new ArrayList<>(baseDoc.getNumberOfPages()); + baseDoc.getPages().forEach(basePages::add); + + for (int i = 0; i < loPages.size(); i++) { + PDResources loRes = loPages.get(i).getResources(); + PDResources baseRes = basePages.get(i).getResources(); + + for (COSName fontKey : loRes.getFontNames()) { + PDFont loFont = loRes.getFont(fontKey); + if (loFont == null) continue; + + String psName = loFont.getName(); + if (!missingFonts.contains(psName)) continue; + + PDFontDescriptor desc = loFont.getFontDescriptor(); + if (desc == null) continue; + + PDStream fontStream = null; + if (desc.getFontFile() != null) { + fontStream = desc.getFontFile(); + } else if (desc.getFontFile2() != null) { + fontStream = desc.getFontFile2(); + } else if (desc.getFontFile3() != null) { + fontStream = desc.getFontFile3(); + } + if (fontStream == null) continue; + + // Read the font stream into memory once so we can create fresh + // InputStreams for multiple load attempts. This avoids reusing a + // consumed stream and allows try-with-resources for each attempt. + byte[] fontBytes; + try (InputStream in = fontStream.createInputStream()) { + fontBytes = in.readAllBytes(); + } + + PDFont embeddedFont = null; + // First try PDType0 (CID) font + try (InputStream tryIn = new ByteArrayInputStream(fontBytes)) { + embeddedFont = PDType0Font.load(baseDoc, tryIn, false); + } catch (IOException e1) { + // Fallback to TrueType + try (InputStream tryIn2 = new ByteArrayInputStream(fontBytes)) { + try { + embeddedFont = PDTrueTypeFont.load(baseDoc, tryIn2, null); + } catch (IllegalArgumentException | IOException e2) { + log.error("Could not embed font {}: {}", psName, e2.getMessage()); + } + } + } + + if (embeddedFont != null) { + baseRes.put(fontKey, embeddedFont); + } + } + } + } + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa") @Operation( - summary = "Convert a PDF to a PDF/A", + summary = "Convert a PDF to a PDF/A or PDF/X", description = - "This endpoint converts a PDF file to a PDF/A file using LibreOffice. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO") + "This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for long-term archiving, while PDF/X is optimized for print production. Input:PDF Output:PDF Type:SISO") public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); @@ -96,7 +479,34 @@ public class ConvertPDFToPDFA { throw ExceptionUtils.createPdfFileRequiredException(); } - // Get the original filename without extension + // Determine if this is PDF/A or PDF/X conversion + boolean isPdfX = outputFormat != null && outputFormat.toLowerCase().startsWith("pdfx"); + + if (isPdfX) { + return handlePdfXConversion(inputFile, outputFormat); + } else { + return handlePdfAConversion(inputFile, outputFormat); + } + } + + private static Set findUnembeddedFontNames(PDDocument doc) throws IOException { + Set missing = new HashSet<>(16); + for (PDPage page : doc.getPages()) { + PDResources res = page.getResources(); + for (COSName name : res.getFontNames()) { + PDFont font = res.getFont(name); + if (font != null && !font.isEmbedded()) { + missing.add(font.getName()); + } + } + } + return missing; + } + + private ResponseEntity handlePdfXConversion( + MultipartFile inputFile, String outputFormat) throws Exception { + PdfXProfile profile = PdfXProfile.fromRequest(outputFormat); + String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); if (originalFileName == null || originalFileName.trim().isEmpty()) { originalFileName = "output.pdf"; @@ -106,57 +516,147 @@ public class ConvertPDFToPDFA { ? originalFileName.substring(0, originalFileName.lastIndexOf('.')) : originalFileName; - Path tempInputFile = null; - byte[] fileBytes; - Path loPdfPath = null; // Used for LibreOffice conversion output - File preProcessedFile = null; - int pdfaPart = 2; + Path workingDir = Files.createTempDirectory("pdfx_conversion_"); + Path inputPath = workingDir.resolve("input.pdf"); + inputFile.transferTo(inputPath); try { - // Save uploaded file to temp location - tempInputFile = Files.createTempFile("input_", ".pdf"); - inputFile.transferTo(tempInputFile); - - // Branch conversion based on desired output PDF/A format - if ("pdfa".equals(outputFormat)) { - preProcessedFile = tempInputFile.toFile(); - } else { - pdfaPart = 1; - preProcessedFile = preProcessHighlights(tempInputFile.toFile()); + // PDF/X conversion uses Ghostscript (no fallback currently) + if (!isGhostscriptAvailable()) { + log.error("Ghostscript is required for PDF/X conversion"); + throw new IOException( + "Ghostscript is required for PDF/X conversion but is not available on the system"); } - Set missingFonts = new HashSet<>(); - boolean needImgs; - try (PDDocument doc = Loader.loadPDF(preProcessedFile)) { - missingFonts = findUnembeddedFontNames(doc); - needImgs = (pdfaPart == 1) && hasTransparentImages(doc); - if (!missingFonts.isEmpty() || needImgs) { - // Run LibreOffice conversion to get flattened images and embedded fonts - loPdfPath = runLibreOfficeConversion(preProcessedFile.toPath(), pdfaPart); - } - } - fileBytes = - convertToPdfA( - preProcessedFile.toPath(), loPdfPath, pdfaPart, missingFonts, needImgs); - String outputFilename = baseFileName + "_PDFA.pdf"; + log.info("Using Ghostscript for PDF/X conversion to {}", profile.getDisplayName()); + byte[] converted = convertWithGhostscriptX(inputPath, workingDir, profile); + String outputFilename = baseFileName + profile.outputSuffix(); + + log.info("PDF/X conversion completed successfully to {}", profile.getDisplayName()); return WebResponseUtils.bytesToWebResponse( - fileBytes, outputFilename, MediaType.APPLICATION_PDF); + converted, outputFilename, MediaType.APPLICATION_PDF); + } catch (IOException | InterruptedException e) { + log.error("PDF/X conversion failed", e); + throw ExceptionUtils.createPdfaConversionFailedException(); } finally { - // Clean up temporary files - if (tempInputFile != null) { - Files.deleteIfExists(tempInputFile); - } - if (loPdfPath != null && loPdfPath.getParent() != null) { - FileUtils.deleteDirectory(loPdfPath.getParent().toFile()); - } - if (preProcessedFile != null) { - Files.deleteIfExists(preProcessedFile.toPath()); + deleteQuietly(workingDir); + } + } + + private boolean isGhostscriptAvailable() { + try { + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(Arrays.asList("gs", "--version")); + return result.getRc() == 0; + } catch (Exception e) { + log.debug("Ghostscript availability check failed", e); + return false; + } + } + + private static void fixCidSetIssues(PDDocument document) throws IOException { + for (PDPage page : document.getPages()) { + PDResources resources = page.getResources(); + if (resources == null) continue; + + for (COSName fontName : resources.getFontNames()) { + try { + PDFont font = resources.getFont(fontName); + if (font == null) continue; + + PDFontDescriptor descriptor = font.getFontDescriptor(); + if (descriptor == null) continue; + + COSDictionary fontDict = descriptor.getCOSObject(); + + // Remove invalid or incomplete CIDSet entries for PDF/A-1 compliance + // PDF/A-1 requires CIDSet to be present and complete for subsetted CIDFonts + // For PDF/A-2+, CIDSet is optional but must be complete if present + COSBase cidSet = fontDict.getDictionaryObject(COSName.getPDFName("CIDSet")); + if (cidSet != null) { + // If CIDSet exists but may be invalid, remove it to avoid validation errors + // This is safer than trying to fix incomplete CIDSet streams + fontDict.removeItem(COSName.getPDFName("CIDSet")); + log.debug( + "Removed potentially invalid CIDSet from font {}", font.getName()); + } + } catch (Exception e) { + log.debug("Error processing CIDSet for font: {}", e.getMessage()); + } } } } + private static void importFlattenedImages(PDDocument loDoc, PDDocument baseDoc) + throws IOException { + List loPages = new ArrayList<>(loDoc.getNumberOfPages()); + loDoc.getPages().forEach(loPages::add); + List basePages = new ArrayList<>(baseDoc.getNumberOfPages()); + baseDoc.getPages().forEach(basePages::add); + + for (int i = 0; i < loPages.size(); i++) { + PDPage loPage = loPages.get(i); + PDPage basePage = basePages.get(i); + + PDResources loRes = loPage.getResources(); + PDResources baseRes = basePage.getResources(); + if (loRes == null || baseRes == null) continue; + + Set toReplace = detectTransparentXObjects(basePage); + + for (COSName name : toReplace) { + PDXObject loXo = loRes.getXObject(name); + if (!(loXo instanceof PDImageXObject img)) continue; + + PDImageXObject newImg = LosslessFactory.createFromImage(baseDoc, img.getImage()); + + // replace the resource under the same name + baseRes.put(name, newImg); + } + } + } + + private ColorProfiles prepareColorProfiles(Path workingDir) throws IOException { + Path rgbProfile = workingDir.resolve("sRGB.icc"); + copyResourceIcc(rgbProfile); + + Path grayProfile = workingDir.resolve("Gray.icc"); + try { + writeJavaIccProfile(ICC_Profile.getInstance(ColorSpace.CS_GRAY), grayProfile); + } catch (IllegalArgumentException e) { + log.warn("Falling back to sRGB ICC profile for grayscale defaults", e); + Files.copy(rgbProfile, grayProfile, StandardCopyOption.REPLACE_EXISTING); + } + + return new ColorProfiles(rgbProfile, grayProfile); + } + + private static Set detectTransparentXObjects(PDPage page) { + Set transparentObjects = new HashSet<>(); + PDResources res = page.getResources(); + if (res == null) return transparentObjects; + + for (COSName name : res.getXObjectNames()) { + try { + PDXObject xo = res.getXObject(name); + if (xo instanceof PDImageXObject img) { + COSDictionary d = img.getCOSObject(); + if (d.containsKey(COSName.SMASK) + || isTransparencyGroup(d) + || d.getBoolean(COSName.INTERPOLATE, false)) { + transparentObjects.add(name); + } + } + } catch (IOException ioe) { + log.error("Error processing XObject {}: {}", name.getName(), ioe.getMessage()); + } + } + return transparentObjects; + } + /** * Merge fonts & flattened images from loPdfPath into basePdfPath, then run the standard * PDFBox/A pipeline. @@ -189,28 +689,346 @@ public class ConvertPDFToPDFA { } } - private byte[] processWithPDFBox(PDDocument document, int pdfaPart) throws Exception { + private byte[] convertWithGhostscript(Path inputPdf, Path workingDir, PdfaProfile profile) + throws IOException, InterruptedException { + Path outputPdf = workingDir.resolve("gs_output.pdf"); + ColorProfiles colorProfiles = prepareColorProfiles(workingDir); + Path pdfaDefFile = createPdfaDefFile(workingDir, colorProfiles, profile); + // Preprocess PDF for PDF/A compliance + Path preprocessedPdf = inputPdf; + + // For PDF/A-1, clean CIDSet issues that may cause validation failures + if (profile.getPart() == 1) { + Path cidSetCleaned = cleanCidSetWithQpdf(inputPdf); + if (cidSetCleaned != null) { + preprocessedPdf = cidSetCleaned; + } + } + + // Normalize PDF with qpdf before Ghostscript conversion to ensure proper font program + // handling + Path normalizedInputPdf = normalizePdfWithQpdf(preprocessedPdf); + Path inputForGs = (normalizedInputPdf != null) ? normalizedInputPdf : preprocessedPdf; + + try { + List command = + buildGhostscriptCommand( + inputForGs, outputPdf, colorProfiles, workingDir, profile, pdfaDefFile); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); + + if (result.getRc() != 0) { + throw new IOException("Ghostscript exited with code " + result.getRc()); + } + + if (!Files.exists(outputPdf)) { + throw new IOException("Ghostscript did not produce an output file"); + } + + return Files.readAllBytes(outputPdf); + } finally { + // Clean up temporary files + if (normalizedInputPdf != null && !normalizedInputPdf.equals(preprocessedPdf)) { + try { + Files.deleteIfExists(normalizedInputPdf); + } catch (IOException e) { + log.debug("Failed to delete temporary normalized file", e); + } + } + if (preprocessedPdf != null && !preprocessedPdf.equals(inputPdf)) { + try { + Files.deleteIfExists(preprocessedPdf); + } catch (IOException e) { + log.debug("Failed to delete temporary CIDSet cleaned file", e); + } + } + } + } + + private static void fixType1FontCharSet(PDDocument document) throws IOException { + for (PDPage page : document.getPages()) { + PDResources resources = page.getResources(); + if (resources == null) continue; + + for (COSName fontName : resources.getFontNames()) { + try { + PDFont font = resources.getFont(fontName); + if (font == null) continue; + + String fontNameStr = font.getName(); + if (fontNameStr == null) continue; + + PDFontDescriptor descriptor = font.getFontDescriptor(); + if (descriptor == null) continue; + + // Check if this is a Type1 font + if (fontNameStr.contains("Type1") + || descriptor.getFontFile() != null + || (descriptor.getFontFile2() == null + && descriptor.getFontFile3() == null)) { + + // Check if CharSet is missing or suspicious + String existingCharSet = + descriptor.getCOSObject().getString(COSName.CHAR_SET); + if (existingCharSet == null || existingCharSet.trim().isEmpty()) { + + // Build a CharSet from commonly used glyphs + // For Type1 fonts, include standard PDF glyphs + String glyphSet = buildStandardType1GlyphSet(); + if (!glyphSet.isEmpty()) { + descriptor.getCOSObject().setString(COSName.CHAR_SET, glyphSet); + log.debug( + "Fixed CharSet for Type1 font {} with {} glyphs", + fontNameStr, + glyphSet.split(" ").length); + } + } + } + } catch (Exception e) { + log.warn( + "Error processing font descriptor for page resource: {}", + e.getMessage()); + } + } + } + } + + private static String buildStandardType1GlyphSet() { + Set glyphNames = new LinkedHashSet<>(); + + String[] standardGlyphs = { + ".notdef", + ".null", + "nonmarkingreturn", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "exclamdown", + "cent", + "sterling", + "currency", + "yen", + "brokenbar", + "section", + "dieresis", + "copyright", + "ordfeminine", + "guillemotleft", + "logicalnot", + "uni00AD", + "registered", + "macron", + "degree", + "plusminus", + "twosuperior", + "threesuperior", + "acute", + "mu", + "paragraph", + "periodcentered", + "cedilla", + "onesuperior", + "ordmasculine", + "guillemotright", + "onequarter", + "onehalf", + "threequarters", + "questiondown", + "Agrave", + "Aacute", + "Acircumflex", + "Atilde", + "Adieresis", + "Aring", + "AE", + "Ccedilla", + "Egrave", + "Eacute", + "Ecircumflex", + "Edieresis", + "Igrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Eth", + "Ntilde", + "Ograve", + "Oacute", + "Ocircumflex", + "Otilde", + "Odieresis", + "multiply", + "Oslash", + "Ugrave", + "Uacute", + "Ucircumflex", + "Udieresis", + "Yacute", + "Thorn", + "germandbls", + "agrave", + "aacute", + "acircumflex", + "atilde", + "adieresis", + "aring", + "ae", + "ccedilla", + "egrave", + "eacute", + "ecircumflex", + "edieresis", + "igrave", + "iacute", + "icircumflex", + "idieresis", + "eth", + "ntilde", + "ograve", + "oacute", + "ocircumflex", + "otilde", + "odieresis", + "divide", + "oslash", + "ugrave", + "uacute", + "ucircumflex", + "udieresis", + "yacute", + "thorn", + "ydieresis" + }; + + Collections.addAll(glyphNames, standardGlyphs); + + return String.join(" ", glyphNames); + } + + private byte[] processWithPDFBox(PDDocument document, int pdfaPart) throws Exception { removeElementsForPdfA(document, pdfaPart); + document.getDocument().setVersion(pdfaPart == 1 ? 1.4f : 1.7f); + mergeAndAddXmpMetadata(document, pdfaPart); addICCProfileIfNotPresent(document); - // Mark the document as PDF/A + // Fix CIDSet issues for PDF/A compliance + if (pdfaPart == 1) { + fixCidSetIssues(document); + } + + fixType1FontCharSet(document); + PDDocumentCatalog catalog = document.getDocumentCatalog(); - catalog.setMetadata( - document.getDocumentCatalog().getMetadata()); // Ensure metadata is linked - catalog.setViewerPreferences( - new PDViewerPreferences(catalog.getCOSObject())); // PDF/A best practice - document.getDocument().setVersion(pdfaPart == 1 ? 1.4f : 1.7f); + catalog.setMetadata(document.getDocumentCatalog().getMetadata()); + + PDViewerPreferences viewerPrefs = new PDViewerPreferences(catalog.getCOSObject()); + viewerPrefs.setDisplayDocTitle(true); + catalog.setViewerPreferences(viewerPrefs); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (pdfaPart == 1) { - document.save(baos, CompressParameters.NO_COMPRESSION); - } else { - document.save(baos); - } + CompressParameters compressParams = + pdfaPart == 1 ? CompressParameters.NO_COMPRESSION : new CompressParameters(); + + document.save(baos, compressParams); + log.debug("PDF/A-{} document processed with PDFBox", pdfaPart); return baos.toByteArray(); } @@ -255,127 +1073,36 @@ public class ConvertPDFToPDFA { return outputFiles[0].toPath(); } - private void embedMissingFonts(PDDocument loDoc, PDDocument baseDoc, Set missingFonts) - throws IOException { - List loPages = new ArrayList<>(); - loDoc.getPages().forEach(loPages::add); - List basePages = new ArrayList<>(); - baseDoc.getPages().forEach(basePages::add); + private byte[] convertWithGhostscriptX(Path inputPdf, Path workingDir, PdfXProfile profile) + throws IOException, InterruptedException { + Path outputPdf = workingDir.resolve("gs_output_pdfx.pdf"); + ColorProfiles colorProfiles = prepareColorProfiles(workingDir); - for (int i = 0; i < loPages.size(); i++) { - PDResources loRes = loPages.get(i).getResources(); - PDResources baseRes = basePages.get(i).getResources(); + List command = + buildGhostscriptCommandX(inputPdf, outputPdf, colorProfiles, workingDir, profile); - for (COSName fontKey : loRes.getFontNames()) { - PDFont loFont = loRes.getFont(fontKey); - if (loFont == null) continue; + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); - String psName = loFont.getName(); - if (!missingFonts.contains(psName)) continue; - - PDFontDescriptor desc = loFont.getFontDescriptor(); - if (desc == null) continue; - - PDStream fontStream = null; - if (desc.getFontFile() != null) { - fontStream = desc.getFontFile(); - } else if (desc.getFontFile2() != null) { - fontStream = desc.getFontFile2(); - } else if (desc.getFontFile3() != null) { - fontStream = desc.getFontFile3(); - } - if (fontStream == null) continue; - - try (InputStream in = fontStream.createInputStream()) { - PDFont newFont; - try { - newFont = PDType0Font.load(baseDoc, in, false); - } catch (IOException e1) { - try { - newFont = PDTrueTypeFont.load(baseDoc, in, null); - } catch (IOException | IllegalArgumentException e2) { - log.error("Could not embed font {}: {}", psName, e2.getMessage()); - continue; - } - } - if (newFont != null) { - baseRes.put(fontKey, newFont); - } - } - } + if (result.getRc() != 0) { + throw new IOException("Ghostscript exited with code " + result.getRc()); } + + if (!Files.exists(outputPdf)) { + throw new IOException("Ghostscript did not produce an output file"); + } + + return Files.readAllBytes(outputPdf); } - private Set findUnembeddedFontNames(PDDocument doc) throws IOException { - Set missing = new HashSet<>(); - for (PDPage page : doc.getPages()) { - PDResources res = page.getResources(); - for (COSName name : res.getFontNames()) { - PDFont font = res.getFont(name); - if (font != null && !font.isEmbedded()) { - missing.add(font.getName()); - } - } - } - return missing; - } - - private void importFlattenedImages(PDDocument loDoc, PDDocument baseDoc) throws IOException { - List loPages = new ArrayList<>(); - loDoc.getPages().forEach(loPages::add); - List basePages = new ArrayList<>(); - baseDoc.getPages().forEach(basePages::add); - - for (int i = 0; i < loPages.size(); i++) { - PDPage loPage = loPages.get(i); - PDPage basePage = basePages.get(i); - - PDResources loRes = loPage.getResources(); - PDResources baseRes = basePage.getResources(); - Set toReplace = detectTransparentXObjects(basePage); - - for (COSName name : toReplace) { - PDXObject loXo = loRes.getXObject(name); - if (!(loXo instanceof PDImageXObject img)) continue; - - PDImageXObject newImg = LosslessFactory.createFromImage(baseDoc, img.getImage()); - - // replace the resource under the same name - baseRes.put(name, newImg); - } - } - } - - private Set detectTransparentXObjects(PDPage page) { - Set transparentObjects = new HashSet<>(); - PDResources res = page.getResources(); - if (res == null) return transparentObjects; - - for (COSName name : res.getXObjectNames()) { - try { - PDXObject xo = res.getXObject(name); - if (xo instanceof PDImageXObject img) { - COSDictionary d = img.getCOSObject(); - if (d.containsKey(COSName.SMASK) - || isTransparencyGroup(d) - || d.getBoolean(COSName.INTERPOLATE, false)) { - transparentObjects.add(name); - } - } - } catch (IOException ioe) { - log.error("Error processing XObject {}: {}", name.getName(), ioe.getMessage()); - } - } - return transparentObjects; - } - - private boolean isTransparencyGroup(COSDictionary dict) { + private static boolean isTransparencyGroup(COSDictionary dict) { COSBase g = dict.getDictionaryObject(COSName.GROUP); return g instanceof COSDictionary gd && COSName.TRANSPARENCY.equals(gd.getCOSName(COSName.S)); } - private boolean hasTransparentImages(PDDocument doc) { + private static boolean hasTransparentImages(PDDocument doc) { for (PDPage page : doc.getPages()) { PDResources res = page.getResources(); if (res == null) continue; @@ -400,10 +1127,9 @@ public class ConvertPDFToPDFA { return false; } - private void sanitizePdfA(COSBase base, PDResources resources, int pdfaPart) { + private static void sanitizePdfA(COSBase base, int pdfaPart) { if (base instanceof COSDictionary dict) { if (pdfaPart == 1) { - // Remove transparency-related elements COSBase group = dict.getDictionaryObject(COSName.GROUP); if (group instanceof COSDictionary gDict && COSName.TRANSPARENCY.equals(gDict.getCOSName(COSName.S))) { @@ -411,18 +1137,15 @@ public class ConvertPDFToPDFA { } dict.removeItem(COSName.SMASK); - // Transparency blending constants (/CA, /ca) — disallowed in PDF/A-1 dict.removeItem(COSName.CA); dict.removeItem(COSName.getPDFName("ca")); } - // Interpolation (non-deterministic image scaling) — required to be false if (dict.containsKey(COSName.INTERPOLATE) && dict.getBoolean(COSName.INTERPOLATE, true)) { dict.setBoolean(COSName.INTERPOLATE, false); } - // Remove common forbidden features (for PDF/A 1 and 2) dict.removeItem(COSName.JAVA_SCRIPT); dict.removeItem(COSName.getPDFName("JS")); dict.removeItem(COSName.getPDFName("RichMedia")); @@ -434,23 +1157,20 @@ public class ConvertPDFToPDFA { dict.removeItem(COSName.EMBEDDED_FILES); dict.removeItem(COSName.FILESPEC); - // Recurse through all entries in the dictionary for (Map.Entry entry : dict.entrySet()) { - sanitizePdfA(entry.getValue(), resources, pdfaPart); + sanitizePdfA(entry.getValue(), pdfaPart); } } else if (base instanceof COSArray arr) { - // Recursively sanitize each item in the array for (COSBase item : arr) { - sanitizePdfA(item, resources, pdfaPart); + sanitizePdfA(item, pdfaPart); } } } - private void removeElementsForPdfA(PDDocument doc, int pdfaPart) { + private static void removeElementsForPdfA(PDDocument doc, int pdfaPart) { if (pdfaPart == 1) { - // Remove Optional Content (Layers) - not allowed in PDF/A-1 doc.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("OCProperties")); } @@ -459,18 +1179,16 @@ public class ConvertPDFToPDFA { page.setAnnotations(Collections.emptyList()); } PDResources res = page.getResources(); - // Clean page-level dictionary - sanitizePdfA(page.getCOSObject(), res, pdfaPart); + sanitizePdfA(page.getCOSObject(), pdfaPart); - // sanitize each Form XObject if (res != null) { for (COSName name : res.getXObjectNames()) { try { PDXObject xo = res.getXObject(name); if (xo instanceof PDFormXObject form) { - sanitizePdfA(form.getCOSObject(), res, pdfaPart); + sanitizePdfA(form.getCOSObject(), pdfaPart); } else if (xo instanceof PDImageXObject img) { - sanitizePdfA(img.getCOSObject(), res, pdfaPart); + sanitizePdfA(img.getCOSObject(), pdfaPart); } } catch (IOException ioe) { log.error("Cannot load XObject {}: {}", name.getName(), ioe.getMessage()); @@ -481,11 +1199,10 @@ public class ConvertPDFToPDFA { } /** Embbeds the XMP metadata required for PDF/A compliance. */ - private void mergeAndAddXmpMetadata(PDDocument document, int pdfaPart) throws Exception { + private static void mergeAndAddXmpMetadata(PDDocument document, int pdfaPart) throws Exception { PDMetadata existingMetadata = document.getDocumentCatalog().getMetadata(); XMPMetadata xmp; - // Load existing XMP if available if (existingMetadata != null) { try (InputStream xmpStream = existingMetadata.createInputStream()) { DomXmpParser parser = new DomXmpParser(); @@ -506,7 +1223,6 @@ public class ConvertPDFToPDFA { String originalCreator = Optional.ofNullable(docInfo.getCreator()).orElse("Unknown"); String originalProducer = Optional.ofNullable(docInfo.getProducer()).orElse("Unknown"); - // Only keep the original creator so it can match xmp creator tool for compliance DublinCoreSchema dcSchema = xmp.getDublinCoreSchema(); if (dcSchema != null) { List existingCreators = dcSchema.getCreators(); @@ -547,7 +1263,6 @@ public class ConvertPDFToPDFA { String originalAuthor = docInfo.getAuthor(); if (originalAuthor != null && !originalAuthor.isBlank()) { docInfo.setAuthor(null); - // If the author is set, we keep it in the XMP metadata if (!originalCreator.equals(originalAuthor)) { dcSchema.addCreator(originalAuthor); } @@ -566,33 +1281,28 @@ public class ConvertPDFToPDFA { adobePdfSchema.setKeywords(keywords); } - // Set creation and modification dates using java.time and convert to GregorianCalendar Instant nowInstant = Instant.now(); ZonedDateTime nowZdt = ZonedDateTime.ofInstant(nowInstant, ZoneId.of("UTC")); - GregorianCalendar nowCal = GregorianCalendar.from(nowZdt); - java.util.Calendar originalCreationDate = docInfo.getCreationDate(); - GregorianCalendar creationCal; - if (originalCreationDate == null) { - creationCal = nowCal; - } else if (originalCreationDate instanceof GregorianCalendar) { - creationCal = (GregorianCalendar) originalCreationDate; + Instant creationInstant; + Calendar originalCreationDate = docInfo.getCreationDate(); + if (originalCreationDate != null) { + creationInstant = originalCreationDate.toInstant(); } else { - // convert other Calendar implementations to GregorianCalendar preserving instant - creationCal = - GregorianCalendar.from( - ZonedDateTime.ofInstant( - originalCreationDate.toInstant(), ZoneId.of("UTC"))); + creationInstant = nowInstant; } + ZonedDateTime creationZdt = ZonedDateTime.ofInstant(creationInstant, ZoneId.of("UTC")); + + GregorianCalendar creationCal = GregorianCalendar.from(creationZdt); + GregorianCalendar modificationCal = GregorianCalendar.from(nowZdt); docInfo.setCreationDate(creationCal); xmpBasicSchema.setCreateDate(creationCal); - docInfo.setModificationDate(nowCal); - xmpBasicSchema.setModifyDate(nowCal); - xmpBasicSchema.setMetadataDate(nowCal); + docInfo.setModificationDate(modificationCal); + xmpBasicSchema.setModifyDate(modificationCal); + xmpBasicSchema.setMetadataDate(modificationCal); - // Serialize the created metadata so it can be attached to the existent metadata ByteArrayOutputStream xmpOut = new ByteArrayOutputStream(); new XmpSerializer().serialize(xmp, xmpOut, true); @@ -601,33 +1311,15 @@ public class ConvertPDFToPDFA { document.getDocumentCatalog().setMetadata(newMetadata); } - private void addICCProfileIfNotPresent(PDDocument document) throws Exception { - if (document.getDocumentCatalog().getOutputIntents().isEmpty()) { - try (InputStream colorProfile = getClass().getResourceAsStream("/icc/sRGB2014.icc")) { - PDOutputIntent outputIntent = new PDOutputIntent(document, colorProfile); - outputIntent.setInfo("sRGB IEC61966-2.1"); - outputIntent.setOutputCondition("sRGB IEC61966-2.1"); - outputIntent.setOutputConditionIdentifier("sRGB IEC61966-2.1"); - outputIntent.setRegistryName("http://www.color.org"); - document.getDocumentCatalog().addOutputIntent(outputIntent); - } catch (Exception e) { - log.error("Failed to load ICC profile: {}", e.getMessage()); - } - } - } - - private File preProcessHighlights(File inputPdf) throws Exception { + private static File preProcessHighlights(File inputPdf) throws Exception { try (PDDocument document = Loader.loadPDF(inputPdf)) { for (PDPage page : document.getPages()) { - // Retrieve the annotations on the page. List annotations = page.getAnnotations(); for (PDAnnotation annot : annotations) { - // Process only highlight annotations. if ("Highlight".equals(annot.getSubtype()) && annot instanceof PDAnnotationTextMarkup highlight) { - // Create a new appearance stream with the same bounding box. float[] colorComponents = highlight.getColor() != null ? highlight.getColor().getComponents() @@ -649,8 +1341,6 @@ public class ConvertPDFToPDFA { cs.setStrokingColor(highlightColor); cs.setLineWidth(0.05f); float spacing = 2f; - // Draw diagonal lines across the highlight area to simulate - // transparency. for (int i = 0; i < quadPoints.length; i += 8) { float minX = Math.min( @@ -695,12 +1385,12 @@ public class ConvertPDFToPDFA { COSDictionary groupDict = (COSDictionary) pageDict.getDictionaryObject(COSName.GROUP); - if (groupDict != null) { - if (COSName.TRANSPARENCY - .getName() - .equalsIgnoreCase(groupDict.getNameAsString(COSName.S))) { - pageDict.removeItem(COSName.GROUP); - } + if (groupDict != null + && COSName.TRANSPARENCY + .getName() + .equalsIgnoreCase( + groupDict.getNameAsString(COSName.S))) { + pageDict.removeItem(COSName.GROUP); } } } @@ -712,4 +1402,321 @@ public class ConvertPDFToPDFA { return preProcessedFile; } } + + private ResponseEntity handlePdfAConversion( + MultipartFile inputFile, String outputFormat) throws Exception { + PdfaProfile profile = PdfaProfile.fromRequest(outputFormat); + + // Get the original filename without extension + String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); + if (originalFileName == null || originalFileName.trim().isEmpty()) { + originalFileName = "output.pdf"; + } + String baseFileName = + originalFileName.contains(".") + ? originalFileName.substring(0, originalFileName.lastIndexOf('.')) + : originalFileName; + + Path workingDir = Files.createTempDirectory("pdfa_conversion_"); + Path inputPath = workingDir.resolve("input.pdf"); + inputFile.transferTo(inputPath); + + try { + byte[] converted; + + // Try Ghostscript first (preferred method) + if (isGhostscriptAvailable()) { + log.info("Using Ghostscript for PDF/A conversion to {}", profile.getDisplayName()); + try { + converted = convertWithGhostscript(inputPath, workingDir, profile); + String outputFilename = baseFileName + profile.outputSuffix(); + + validateAndWarnPdfA(converted, profile, "Ghostscript"); + + return WebResponseUtils.bytesToWebResponse( + converted, outputFilename, MediaType.APPLICATION_PDF); + } catch (Exception e) { + log.warn( + "Ghostscript conversion failed, falling back to PDFBox/LibreOffice method", + e); + } + } else { + log.info("Ghostscript not available, using PDFBox/LibreOffice fallback method"); + } + + converted = convertWithPdfBoxMethod(inputPath, profile); + String outputFilename = baseFileName + profile.outputSuffix(); + + // Validate with PDFBox preflight and warn if issues found + validateAndWarnPdfA(converted, profile, "PDFBox/LibreOffice"); + + return WebResponseUtils.bytesToWebResponse( + converted, outputFilename, MediaType.APPLICATION_PDF); + + } finally { + deleteQuietly(workingDir); + } + } + + private Path normalizePdfWithQpdf(Path inputPdf) { + try { + ProcessExecutorResult checkResult = + ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) + .runCommandWithOutputHandling(Arrays.asList("qpdf", "--version")); + + if (checkResult.getRc() != 0) { + log.debug("QPDF not available"); + return null; + } + + Path normalizedPdf = + inputPdf.getParent().resolve("normalized_" + inputPdf.getFileName().toString()); + + List command = + Arrays.asList( + "qpdf", + "--normalize-content=y", + "--object-streams=preserve", + inputPdf.toAbsolutePath().toString(), + normalizedPdf.toAbsolutePath().toString()); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) + .runCommandWithOutputHandling(command); + + if (result.getRc() == 0 && Files.exists(normalizedPdf)) { + log.info("PDF normalized with QPDF to fix font programs and CIDSet issues"); + return normalizedPdf; + } + return null; + + } catch (Exception e) { + log.debug("QPDF normalization error: {}", e.getMessage()); + return null; + } + } + + private Path cleanCidSetWithQpdf(Path inputPdf) { + try { + ProcessExecutorResult checkResult = + ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) + .runCommandWithOutputHandling(Arrays.asList("qpdf", "--version")); + + if (checkResult.getRc() != 0) { + log.debug("QPDF not available for CIDSet cleaning"); + return null; + } + + Path cleanedPdf = + inputPdf.getParent() + .resolve("cidset_cleaned_" + inputPdf.getFileName().toString()); + + // Use QPDF to remove problematic CIDSet entries that may be incomplete + List command = + Arrays.asList( + "qpdf", + "--remove-unreferenced-resources=yes", + "--normalize-content=y", + "--object-streams=preserve", + inputPdf.toAbsolutePath().toString(), + cleanedPdf.toAbsolutePath().toString()); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) + .runCommandWithOutputHandling(command); + + if (result.getRc() == 0 && Files.exists(cleanedPdf)) { + log.info("PDF CIDSet cleaned with QPDF"); + return cleanedPdf; + } + return null; + + } catch (Exception e) { + log.debug("QPDF CIDSet cleaning error: {}", e.getMessage()); + return null; + } + } + + private byte[] convertWithPdfBoxMethod(Path inputPath, PdfaProfile profile) throws Exception { + Path tempInputFile = null; + byte[] fileBytes; + Path loPdfPath = null; + File preProcessedFile = null; + int pdfaPart = profile.getPart(); + Path normalizedPath = null; + + try { + tempInputFile = inputPath; + + normalizedPath = normalizePdfWithQpdf(tempInputFile); + if (normalizedPath != null) { + tempInputFile = normalizedPath; + } + + if (pdfaPart == 2 || pdfaPart == 3) { + preProcessedFile = tempInputFile.toFile(); + } else { + preProcessedFile = preProcessHighlights(tempInputFile.toFile()); + } + + Set missingFonts; + boolean needImgs; + try (PDDocument doc = Loader.loadPDF(preProcessedFile)) { + missingFonts = findUnembeddedFontNames(doc); + needImgs = (pdfaPart == 1) && hasTransparentImages(doc); + if (!missingFonts.isEmpty() || needImgs) { + loPdfPath = runLibreOfficeConversion(preProcessedFile.toPath(), pdfaPart); + } + } + fileBytes = + convertToPdfA( + preProcessedFile.toPath(), loPdfPath, pdfaPart, missingFonts, needImgs); + + return fileBytes; + + } finally { + if (loPdfPath != null && loPdfPath.getParent() != null) { + FileUtils.deleteDirectory(loPdfPath.getParent().toFile()); + } + if (preProcessedFile != null && !preProcessedFile.equals(tempInputFile.toFile())) { + Files.deleteIfExists(preProcessedFile.toPath()); + } + if (normalizedPath != null && !normalizedPath.equals(inputPath)) { + Files.deleteIfExists(normalizedPath); + } + } + } + + private void copyResourceIcc(Path target) throws IOException { + try (InputStream in = getClass().getResourceAsStream(ICC_RESOURCE_PATH)) { + if (in == null) { + throw ExceptionUtils.createIllegalArgumentException( + "error.resourceNotFound", "Resource not found: {0}", ICC_RESOURCE_PATH); + } + Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + private void addICCProfileIfNotPresent(PDDocument document) { + if (document.getDocumentCatalog().getOutputIntents().isEmpty()) { + try (InputStream colorProfile = getClass().getResourceAsStream(ICC_RESOURCE_PATH)) { + if (colorProfile == null) { + throw ExceptionUtils.createIllegalArgumentException( + "error.resourceNotFound", "Resource not found: {0}", ICC_RESOURCE_PATH); + } + PDOutputIntent outputIntent = new PDOutputIntent(document, colorProfile); + // PDF/A compliant output intent settings + outputIntent.setInfo("sRGB IEC61966-2.1"); + outputIntent.setOutputCondition("sRGB IEC61966-2.1"); + outputIntent.setOutputConditionIdentifier("sRGB IEC61966-2.1"); + outputIntent.setRegistryName("http://www.color.org"); + document.getDocumentCatalog().addOutputIntent(outputIntent); + log.debug("Added ICC color profile for PDF/A compliance"); + } catch (Exception e) { + log.error("Failed to load ICC profile: {}", e.getMessage()); + throw new RuntimeException("ICC profile loading failed for PDF/A compliance", e); + } + } + } + + @Getter + private enum PdfaProfile { + PDF_A_1B(1, "PDF/A-1b", "_PDFA-1b.pdf", "1.4", Format.PDF_A1B, "pdfa-1"), + PDF_A_2B(2, "PDF/A-2b", "_PDFA-2b.pdf", "1.7", null, "pdfa", "pdfa-2", "pdfa-2b"), + PDF_A_3B(3, "PDF/A-3b", "_PDFA-3b.pdf", "1.7", null, "pdfa-3", "pdfa-3b"); + + private final int part; + private final String displayName; + private final String suffix; + private final String compatibilityLevel; + private final Format preflightFormat; + private final List requestTokens; + + PdfaProfile( + int part, + String displayName, + String suffix, + String compatibilityLevel, + Format preflightFormat, + String... requestTokens) { + this.part = part; + this.displayName = displayName; + this.suffix = suffix; + this.compatibilityLevel = compatibilityLevel; + this.preflightFormat = preflightFormat; + this.requestTokens = + Arrays.stream(requestTokens) + .map(token -> token.toLowerCase(Locale.ROOT)) + .toList(); + } + + static PdfaProfile fromRequest(String requestToken) { + if (requestToken == null) { + return PDF_A_2B; + } + String normalized = requestToken.trim().toLowerCase(Locale.ROOT); + Optional match = + Arrays.stream(values()) + .filter(profile -> profile.requestTokens.contains(normalized)) + .findFirst(); + + return match.orElse(PDF_A_2B); + } + + String outputSuffix() { + return suffix; + } + + Optional preflightFormat() { + return Optional.ofNullable(preflightFormat); + } + } + + @Getter + private enum PdfXProfile { + PDF_X_1("PDF/X-1", "_PDFX-1.pdf", "1.3", "2001", "pdfx-1", "pdfx"), + PDF_X_3("PDF/X-3", "_PDFX-3.pdf", "1.3", "2003", "pdfx-3"), + PDF_X_4("PDF/X-4", "_PDFX-4.pdf", "1.4", "2008", "pdfx-4"); + + private final String displayName; + private final String suffix; + private final String compatibilityLevel; + private final String pdfxVersion; + private final List requestTokens; + + PdfXProfile( + String displayName, + String suffix, + String compatibilityLevel, + String pdfxVersion, + String... requestTokens) { + this.displayName = displayName; + this.suffix = suffix; + this.compatibilityLevel = compatibilityLevel; + this.pdfxVersion = pdfxVersion; + this.requestTokens = + Arrays.stream(requestTokens) + .map(token -> token.toLowerCase(Locale.ROOT)) + .toList(); + } + + static PdfXProfile fromRequest(String requestToken) { + if (requestToken == null) { + return PDF_X_4; + } + String normalized = requestToken.trim().toLowerCase(Locale.ROOT); + Optional match = + Arrays.stream(values()) + .filter(profile -> profile.requestTokens.contains(normalized)) + .findFirst(); + + return match.orElse(PDF_X_4); + } + + String outputSuffix() { + return suffix; + } + } + + private record ColorProfiles(Path rgb, Path gray) {} } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java index 0553988ca..4bda52cc7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java @@ -12,8 +12,11 @@ import stirling.software.common.model.api.PDFFile; public class PdfToPdfARequest extends PDFFile { @Schema( - description = "The output PDF/A type", + description = "The output format type (PDF/A or PDF/X)", requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = {"pdfa", "pdfa-1"}) + allowableValues = { + "pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx", "pdfx-1", + "pdfx-3", "pdfx-4" + }) private String outputFormat; } diff --git a/app/core/src/main/resources/messages_ar_AR.properties b/app/core/src/main/resources/messages_ar_AR.properties index da7b871df..ae7c673c1 100644 --- a/app/core/src/main/resources/messages_ar_AR.properties +++ b/app/core/src/main/resources/messages_ar_AR.properties @@ -694,9 +694,9 @@ home.extractImages.title=استخراج الصور home.extractImages.desc=يستخرج جميع الصور من ملف PDF ويحفظها في الرمز البريدي extractImages.tags=صورة,صورة فوتوغرافية,حفظ,أرشيف,ملف مضغوط,التقاط,انتزاع -home.pdfToPDFA.title=تحويل ملفات PDF إلى PDF / A -home.pdfToPDFA.desc=تحويل PDF إلى PDF / A للتخزين طويل المدى -pdfToPDFA.tags=أرشيف,طويل الأجل,معيار,تحويل,تخزين,حفظ +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=تحويل PDF إلى Word home.PDFToWord.desc=تحويل PDF إلى تنسيقات Word (DOC و DOCX و ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF إلى PDF/A -pdfToPDFA.header=PDF إلى PDF/A -pdfToPDFA.credit=تستخدم هذه الخدمة libreoffice لتحويل PDF/A. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=تحويل -pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=تنسيق الإخراج pdfToPDFA.pdfWithDigitalSignature=يحتوي PDF على توقيع رقمي. سيتم إزالة هذا في الخطوة التالية. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_az_AZ.properties b/app/core/src/main/resources/messages_az_AZ.properties index b433016ed..77b0b6dcd 100644 --- a/app/core/src/main/resources/messages_az_AZ.properties +++ b/app/core/src/main/resources/messages_az_AZ.properties @@ -694,9 +694,9 @@ home.extractImages.title=Şəkilləri Xaric Et home.extractImages.desc=PDF-dəki şəkilləri xaric edib onları zip faylında saxlayır extractImages.tags=şəkil,foto,saxla,arxiv,zip,çək,götür -home.pdfToPDFA.title=PDF-dən PDF/A-a -home.pdfToPDFA.desc=PDF faylını uzunmüddətli saxlama üçün PDF/A-a çevir -pdfToPDFA.tags=arxiv,uzunmüddətli,standard,çevirmə,yaddaş,saxlama +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF-dən Word-ə home.PDFToWord.desc=PDF-i Word formatlarına çevir (DOC, DOCX və ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF-i PDF/A-ya -pdfToPDFA.header=PDF-i PDF/A-ya -pdfToPDFA.credit=Bu Servis PDF/A Çevirmək Üçün libreoffice İşlədir +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Çevir -pdfToPDFA.tip=Hazırda Birdən Çox Giriş Üçün İşləmir +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Çıxış Formatı pdfToPDFA.pdfWithDigitalSignature=PDF Rəqəmsal İmza Ehtiva Edir.Bu, növbəti addımda silinəcək. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_bg_BG.properties b/app/core/src/main/resources/messages_bg_BG.properties index 8f9d911a3..298cd86dd 100644 --- a/app/core/src/main/resources/messages_bg_BG.properties +++ b/app/core/src/main/resources/messages_bg_BG.properties @@ -694,9 +694,9 @@ home.extractImages.title=Извличане на изображения home.extractImages.desc=Извлича всички изображения от PDF и ги записва в архив extractImages.tags=изображение,снимка,запазване,архивиране,архив,заснемане,грабване -home.pdfToPDFA.title=PDF в PDF/A -home.pdfToPDFA.desc=Конвертирайте PDF в PDF/A за дългосрочно съхранение -pdfToPDFA.tags=архив,дълготраен,стандартен,преобразуване,съхранение,консервиране +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF в Word home.PDFToWord.desc=Преобразуване на PDF в Word формати (DOC, DOCX и ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Отключване на PDF формуляри unlockPDFForms.submit=Премахване #pdfToPDFA -pdfToPDFA.title=PDF в PDF/A -pdfToPDFA.header=PDF в PDF/A -pdfToPDFA.credit=Тази услуга използва LibreOffice за PDF/A преобразуване. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Преобразуване -pdfToPDFA.tip=В момента не работи за няколко входа наведнъж +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Изходен формат pdfToPDFA.pdfWithDigitalSignature=PDF файлът съдържа цифров подпис. Това ще бъде премахнато в следващата стъпка. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_bo_CN.properties b/app/core/src/main/resources/messages_bo_CN.properties index 0d006ec9a..03d90720f 100644 --- a/app/core/src/main/resources/messages_bo_CN.properties +++ b/app/core/src/main/resources/messages_bo_CN.properties @@ -694,9 +694,9 @@ home.extractImages.title=པར་རིས་ཕྱིར་འདོན། home.extractImages.desc=PDF ནས་པར་རིས་ཚང་མ་ཕྱིར་བཏོན་ནས་ zip ནང་ཉར་ཚགས་བྱེད་པ། extractImages.tags=པར།,འདྲ་པར།,ཉར་ཚགས།,ཡིག་མཛོད།,zip,འཛིན་པ།,ལེན་པ། -home.pdfToPDFA.title=PDF ནས་ PDF/A ལ། -home.pdfToPDFA.desc=PDF ནས་དུས་ཡུན་རིང་པོའི་ཉར་ཚགས་ཆེད་ PDF/A ལ་བསྒྱུར་བ། -pdfToPDFA.tags=ཡིག་མཛོད།,དུས་ཡུན་རིང་པོ།,ཚད་ལྡན།,བསྒྱུར་བ།,ཉར་ཚགས།,སྲུང་སྐྱོབ། +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF ནས་ Word ལ། home.PDFToWord.desc=PDF ནས་ Word རྣམ་གཞག་ (DOC, DOCX དང་ ODT) ལ་བསྒྱུར་བ། @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF ནས་ PDF/A ལ། -pdfToPDFA.header=PDF ནས་ PDF/A ལ། -pdfToPDFA.credit=ཞབས་ཞུ་འདིས་ PDF/A བསྒྱུར་བའི་ཆེད་དུ་ libreoffice བེད་སྤྱོད་བྱེད་པ། +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=བསྒྱུར་བ། -pdfToPDFA.tip=ད་ལྟ་ཡིག་ཆ་མང་པོ་དུས་གཅིག་ལ་བསྒྱུར་མི་ཐུབ། +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=ཕྱིར་འདོན་རྣམ་གཞག pdfToPDFA.pdfWithDigitalSignature=PDF འདིར་ཨང་ཀིའི་མིང་རྟགས་ཡོད། འདི་རྗེས་མའི་རིམ་པར་སུབ་ངེས་ཡིན། +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ca_CA.properties b/app/core/src/main/resources/messages_ca_CA.properties index 3192b6b94..c491f7de9 100644 --- a/app/core/src/main/resources/messages_ca_CA.properties +++ b/app/core/src/main/resources/messages_ca_CA.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extreu Imatges home.extractImages.desc=Extreu les imatges del PDF i desa-les en un arxiu zip extractImages.tags=imatge,foto,desa,arxiva,zip,captura,agafa -home.pdfToPDFA.title=PDF a PDF/A -home.pdfToPDFA.desc=Converteix PDF a PDF/A per a l'emmagatzematge a llarg termini. -pdfToPDFA.tags=arxiu,llarg termini,estàndard,conversió,emmagatzematge,preservació +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF a Word home.PDFToWord.desc=Converteix PDF a formats de Word (DOC, DOCX i ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF a PDF/A -pdfToPDFA.header=PDF a PDF/A -pdfToPDFA.credit=Utilitza libreoffice per a la conversió a PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Converteix -pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Format de sortida pdfToPDFA.pdfWithDigitalSignature=El PDF conté una signatura digital. Aquesta serà eliminada en el següent pas. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_cs_CZ.properties b/app/core/src/main/resources/messages_cs_CZ.properties index d67f49966..5d210a404 100644 --- a/app/core/src/main/resources/messages_cs_CZ.properties +++ b/app/core/src/main/resources/messages_cs_CZ.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrahovat obrázky home.extractImages.desc=Extrahuje všechny obrázky z PDF a uloží je do zipu extractImages.tags=obrázek,fotka,uložit,archiv,zip,zachytit,získat -home.pdfToPDFA.title=PDF na PDF/A -home.pdfToPDFA.desc=Převést PDF na PDF/A pro dlouhodobé uchovávání -pdfToPDFA.tags=archiv,dlouhodobý,standard,převod,úložiště,uchování +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF na Word home.PDFToWord.desc=Převést PDF na formáty Word (DOC, DOCX a ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF na PDF/A -pdfToPDFA.header=PDF na PDF/A -pdfToPDFA.credit=Tato služba používá libreoffice pro konverzi do PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Převést -pdfToPDFA.tip=Momentálně nefunguje pro více vstupů najednou +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Výstupní formát pdfToPDFA.pdfWithDigitalSignature=PDF obsahuje digitální podpis, který bude v dalším kroku odstraněn. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_da_DK.properties b/app/core/src/main/resources/messages_da_DK.properties index d2ad10725..d04108c89 100644 --- a/app/core/src/main/resources/messages_da_DK.properties +++ b/app/core/src/main/resources/messages_da_DK.properties @@ -694,9 +694,9 @@ home.extractImages.title=Udtræk Billeder home.extractImages.desc=Udtrækker alle billeder fra en PDF og gemmer dem som zip extractImages.tags=billede,foto,gem,arkiv,zip,fang,grib -home.pdfToPDFA.title=PDF til PDF/A -home.pdfToPDFA.desc=Konvertér PDF til PDF/A for langtidsopbevaring -pdfToPDFA.tags=arkiv,langtids,standard,konvertering,opbevaring,bevaring +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF til Word home.PDFToWord.desc=Konvertér PDF til Word-formater (DOC, DOCX og ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF Til PDF/A -pdfToPDFA.header=PDF Til PDF/A -pdfToPDFA.credit=Denne tjeneste bruger libreoffice til PDF/A-konvertering +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertér -pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Outputformat pdfToPDFA.pdfWithDigitalSignature=PDF'en indeholder en digital signatur. Dette vil blive fjernet i næste trin. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index 9625d8935..f5476f64c 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -698,9 +698,9 @@ home.extractImages.title=Bilder extrahieren home.extractImages.desc=Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv extractImages.tags=bild,foto,speichern,archivieren,zippen,erfassen,greifen -home.pdfToPDFA.title=PDF zu PDF/A konvertieren -home.pdfToPDFA.desc=PDF zu PDF/A für Langzeitarchivierung konvertieren -pdfToPDFA.tags=archiv,langfristig,standard,konvertierung,speicherung,aufbewahrung +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF zu Word home.PDFToWord.desc=PDF in Word-Formate konvertieren (DOC, DOCX und ODT) @@ -1634,13 +1634,15 @@ unlockPDFForms.header=Schreibgeschützte PDF-Formfelder entfernen unlockPDFForms.submit=Entfernen #pdfToPDFA -pdfToPDFA.title=PDF zu PDF/A -pdfToPDFA.header=PDF zu PDF/A -pdfToPDFA.credit=Dieser Dienst verwendet libreoffice für die PDF/A-Konvertierung +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertieren -pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten. +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Ausgabeformat pdfToPDFA.pdfWithDigitalSignature=Das PDF enthält eine digitale Signatur. Sie wird im nächsten Schritt entfernt. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_el_GR.properties b/app/core/src/main/resources/messages_el_GR.properties index bb8732c6d..f2802a143 100644 --- a/app/core/src/main/resources/messages_el_GR.properties +++ b/app/core/src/main/resources/messages_el_GR.properties @@ -694,9 +694,9 @@ home.extractImages.title=Εξαγωγή εικόνων home.extractImages.desc=Εξαγωγή όλων των εικόνων από PDF και αποθήκευση σε zip extractImages.tags=εικόνα,φωτογραφία,αποθήκευση,αρχείο,zip,σύλληψη,λήψη -home.pdfToPDFA.title=PDF σε PDF/A -home.pdfToPDFA.desc=Μετατροπή PDF σε PDF/A για μακροχρόνια αποθήκευση -pdfToPDFA.tags=αρχείο,μακροχρόνιο,πρότυπο,μετατροπή,αποθήκευση,διατήρηση +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF σε Word home.PDFToWord.desc=Μετατροπή PDF σε μορφές Word (DOC, DOCX και ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF σε PDF/A -pdfToPDFA.header=PDF σε PDF/A -pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί libreoffice για μετατροπή PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Μετατροπή -pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Μορφή εξόδου pdfToPDFA.pdfWithDigitalSignature=Το PDF περιέχει ψηφιακή υπογραφή. Αυτή θα αφαιρεθεί στο επόμενο βήμα. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index e4c6378f1..fe8638ce0 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -698,9 +698,9 @@ home.extractImages.title=Extract Images home.extractImages.desc=Extracts all images from a PDF and saves them to zip extractImages.tags=picture,photo,save,archive,zip,capture,grab -home.pdfToPDFA.title=PDF to PDF/A -home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage -pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF to Word home.PDFToWord.desc=Convert PDF to Word formats (DOC, DOCX and ODT) @@ -1634,13 +1634,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF To PDF/A -pdfToPDFA.header=PDF To PDF/A -pdfToPDFA.credit=This service uses LibreOffice for PDF/A conversion +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Convert -pdfToPDFA.tip=Currently does not work for multiple inputs at once +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Output format pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index 287955226..16d3b398c 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extract Images home.extractImages.desc=Extracts all images from a PDF and saves them to zip extractImages.tags=picture,photo,save,archive,zip,capture,grab -home.pdfToPDFA.title=PDF to PDF/A -home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage -pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF to Word home.PDFToWord.desc=Convert PDF to Word formats (DOC, DOCX and ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF To PDF/A -pdfToPDFA.header=PDF To PDF/A -pdfToPDFA.credit=This service uses libreoffice for PDF/A conversion +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Convert -pdfToPDFA.tip=Currently does not work for multiple inputs at once +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Output format pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_es_ES.properties b/app/core/src/main/resources/messages_es_ES.properties index 139aa5da0..584310d25 100644 --- a/app/core/src/main/resources/messages_es_ES.properties +++ b/app/core/src/main/resources/messages_es_ES.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extraer imágenes home.extractImages.desc=Extraer todas las imágenes de un PDF y guardarlas en ZIP extractImages.tags=imagen,fotografía,guardar,archivo,zip,capturar,coger -home.pdfToPDFA.title=Convertir PDF a PDF/A -home.pdfToPDFA.desc=Convertir PDF a PDF/A para almacenamiento a largo plazo -pdfToPDFA.tags=archivo,largo plazo,estándar,conversión,almacenamiento,conservación +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF a Word home.PDFToWord.desc=Convertir formatos PDF a Word (DOC, DOCX y ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Desbloquear lso formlarios del PDF unlockPDFForms.submit=Eliminar #pdfToPDFA -pdfToPDFA.title=PDF a PDF/A -pdfToPDFA.header=PDF a PDF/A -pdfToPDFA.credit=Este servicio usa LibreOffice para la conversión a PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Convertir -pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Formato de salida pdfToPDFA.pdfWithDigitalSignature=El PDF contiene una firma digital. Ésta se eliminará en el siguiente paso. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 6ea71e3fe..b402a2dcf 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -694,9 +694,9 @@ home.extractImages.title=Atera irudiak home.extractImages.desc=Atera irudi guztiak PDF batetik eta ZIPen gorde extractImages.tags=picture,photo,save,archive,zip,capture,grab -home.pdfToPDFA.title=PDFa PDF/A bihurtu -home.pdfToPDFA.desc=PDFa PDF/A bihurtu luzaro biltegiratzeko -pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDFa Word Bihurtu home.PDFToWord.desc=PDF formatuak Word bihurtu (DOC, DOCX y ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDFa PDF/A bihurtu -pdfToPDFA.header=PDFa PDF/A bihurtu -pdfToPDFA.credit=Zerbitzu honek libreoffice erabiltzen du PDFak PDF/A bihurtzeko +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Bihurtu -pdfToPDFA.tip=Currently does not work for multiple inputs at once +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Output format pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_fa_IR.properties b/app/core/src/main/resources/messages_fa_IR.properties index 96be59910..af0ad6376 100644 --- a/app/core/src/main/resources/messages_fa_IR.properties +++ b/app/core/src/main/resources/messages_fa_IR.properties @@ -694,9 +694,9 @@ home.extractImages.title=استخراج تصاویر home.extractImages.desc=استخراج تمام تصاویر از یک PDF و ذخیره آن‌ها به صورت فایل زیپ extractImages.tags=عکس،عکس،ذخیره،آرشیو،زیپ،گرفتن،برداشتن -home.pdfToPDFA.title=PDF به PDF/A -home.pdfToPDFA.desc=تبدیل PDF به PDF/A برای ذخیره‌سازی بلندمدت -pdfToPDFA.tags=آرشیو،ذخیره‌سازی بلندمدت،استاندارد،تبدیل،ذخیره‌سازی،حفظ +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF به ورد home.PDFToWord.desc=تبدیل PDF به فرمت‌های ورد (DOC، DOCX و ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF به PDF/A -pdfToPDFA.header=PDF به PDF/A -pdfToPDFA.credit=این سرویس از libreoffice برای تبدیل PDF/A استفاده می‌کند +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=تبدیل -pdfToPDFA.tip=در حال حاضر برای چندین ورودی به طور همزمان کار نمی‌کند +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=فرمت خروجی pdfToPDFA.pdfWithDigitalSignature=PDF حاوی یک امضای دیجیتال است. این در مرحله بعد حذف خواهد شد. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index 64e624dc4..910ef1404 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extraire les images home.extractImages.desc=Extrayez toutes les images d'un PDF et enregistrez-les dans un ZIP. extractImages.tags=image,photo,sauvegarder,archiver,zip,capturer,grab -home.pdfToPDFA.title=PDF en PDF/A -home.pdfToPDFA.desc=Convertir un PDF en PDF/A pour un stockage à long terme. -pdfToPDFA.tags=convertion,archive,long-term,standard,conversion,storage,préservation,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF en Word home.PDFToWord.desc=Convertissez un PDF en Word (DOC, DOCX ou ODT). @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Déverrouiller les formulaires PDF unlockPDFForms.submit=Supprimer #pdfToPDFA -pdfToPDFA.title=PDF en PDF/A -pdfToPDFA.header=PDF en PDF/A -pdfToPDFA.credit=Ce service utilise LibreOffice pour la conversion en PDF/A. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Convertir -pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Format de sortie pdfToPDFA.pdfWithDigitalSignature=Le PDF contient une signature numérique. Elle sera supprimée dans l'étape suivante. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ga_IE.properties b/app/core/src/main/resources/messages_ga_IE.properties index fb6f548fb..ac788c916 100644 --- a/app/core/src/main/resources/messages_ga_IE.properties +++ b/app/core/src/main/resources/messages_ga_IE.properties @@ -694,9 +694,9 @@ home.extractImages.title=Sliocht Íomhánna home.extractImages.desc=Sliochtann sé gach íomhá ó PDF agus sábhálann sé iad a zip extractImages.tags=pictiúr, grianghraf, shábháil, cartlann, zip, gabháil, grab -home.pdfToPDFA.title=PDF go PDF/A -home.pdfToPDFA.desc=Tiontaigh PDF go PDF/A le haghaidh stórála fadtéarmach -pdfToPDFA.tags=cartlann, fadtéarmach, caighdeánach, comhshó, stóráil, caomhnú +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF a thiontú go Word home.PDFToWord.desc=Tiontaigh PDF go formáidí Word (DOC, DOCX agus ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF Go PDF/A -pdfToPDFA.header=PDF Go PDF/A -pdfToPDFA.credit=Úsáideann an tseirbhís seo libreoffice chun PDF/A a thiontú +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Tiontaigh -pdfToPDFA.tip=Faoi láthair ní oibríonn sé le haghaidh ionchuir iolracha ag an am céanna +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Formáid aschuir pdfToPDFA.pdfWithDigitalSignature=Tá síniú digiteach ar an PDF. Bainfear é seo sa chéad chéim eile. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_hi_IN.properties b/app/core/src/main/resources/messages_hi_IN.properties index 44f9793b2..ea52bc907 100644 --- a/app/core/src/main/resources/messages_hi_IN.properties +++ b/app/core/src/main/resources/messages_hi_IN.properties @@ -694,9 +694,9 @@ home.extractImages.title=छवियां निकालें home.extractImages.desc=PDF से सभी छवियों को निकालें और उन्हें ज़िप में सहेजें extractImages.tags=चित्र,फोटो,सहेजें,संग्रह,ज़िप,कैप्चर,ग्रैब -home.pdfToPDFA.title=PDF से PDF/A -home.pdfToPDFA.desc=लंबी अवधि के भंडारण के लिए PDF को PDF/A में बदलें -pdfToPDFA.tags=संग्रह,लंबी अवधि,मानक,रूपांतरण,भंडारण,संरक्षण +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF से Word home.PDFToWord.desc=PDF को Word प्रारूपों में बदलें (DOC, DOCX और ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF से PDF/A -pdfToPDFA.header=PDF से PDF/A -pdfToPDFA.credit=यह सेवा PDF/A रूपांतरण के लिए libreoffice का उपयोग करती है +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=बदलें -pdfToPDFA.tip=वर्तमान में एक बार में कई इनपुट के लिए काम नहीं करता +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=आउटपुट प्रारूप pdfToPDFA.pdfWithDigitalSignature=PDF में एक डिजिटल हस्ताक्षर है। यह अगले चरण में हटा दिया जाएगा। +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_hr_HR.properties b/app/core/src/main/resources/messages_hr_HR.properties index 6166c8e99..31aaf855b 100644 --- a/app/core/src/main/resources/messages_hr_HR.properties +++ b/app/core/src/main/resources/messages_hr_HR.properties @@ -694,9 +694,9 @@ home.extractImages.title=Izdvoji slike home.extractImages.desc=Izdvaja sve slike iz PDF-a i sprema ih u zip arhivu extractImages.tags=slika,fotografija,spremi,arhiva,zip,uhvati,zgrabi -home.pdfToPDFA.title=PDF u PDF/A -home.pdfToPDFA.desc=Pretvorite PDF u PDF/A za dugoročnu pohranu -pdfToPDFA.tags=arhiva,dugoročno,standard,pretvorba,pohrana,očuvanje +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF u Word home.PDFToWord.desc=Pretvorite PDF u Word formate (DOC, DOCX i ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Otključaj PDF obrasce unlockPDFForms.submit=Ukloni #pdfToPDFA -pdfToPDFA.title=PDF u PDF/A -pdfToPDFA.header=PDF u PDF/A -pdfToPDFA.credit=Ova usluga koristi LibreOffice za PDF/A pretvorbu +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Pretvori -pdfToPDFA.tip=Trenutno ne radi za više unosa odjednom +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Izlazni format pdfToPDFA.pdfWithDigitalSignature=PDF sadrži digitalni potpis. On će biti uklonjen u sljedećem koraku. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index e977a122c..37628de5f 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -694,9 +694,9 @@ home.extractImages.title=Képek kinyerése home.extractImages.desc=Minden kép kinyerése a PDF-ből és mentése ZIP fájlba extractImages.tags=kép,fotó,mentés,archívum,tömörítés,kinyerés,gyűjtés -home.pdfToPDFA.title=PDF konvertálása PDF/A formátumba -home.pdfToPDFA.desc=PDF konvertálása PDF/A formátumba hosszú távú tároláshoz -pdfToPDFA.tags=archívum,hosszú távú,szabvány,konvertálás,tárolás,megőrzés +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF konvertálása Word formátumba home.PDFToWord.desc=PDF konvertálása Word formátumokba (DOC, DOCX és ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=PDF űrlapok feloldása unlockPDFForms.submit=Eltávolítás #pdfToPDFA -pdfToPDFA.title=PDF konvertálása PDF/A formátumba -pdfToPDFA.header=PDF konvertálása PDF/A formátumba -pdfToPDFA.credit=Ez a szolgáltatás a libreoffice használatával végzi a PDF/A konverziót +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertálás -pdfToPDFA.tip=Jelenleg nem támogatja a több fájl egyidejű feldolgozását +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Kimeneti formátum pdfToPDFA.pdfWithDigitalSignature=A PDF digitális aláírást tartalmaz. Ez a következő lépésben eltávolításra kerül. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_id_ID.properties b/app/core/src/main/resources/messages_id_ID.properties index 8a0d3726e..aa9ff1378 100644 --- a/app/core/src/main/resources/messages_id_ID.properties +++ b/app/core/src/main/resources/messages_id_ID.properties @@ -694,9 +694,9 @@ home.extractImages.title=Ekstrak Gambar home.extractImages.desc=Mengekstrak semua gambar dari PDF dan menyimpannya ke zip extractImages.tags=gambar, foto, simpan, arsip, zip, tangkap, ambil -home.pdfToPDFA.title=PDF ke PDF/A -home.pdfToPDFA.desc=Konversi PDF ke PDF/A untuk penyimpanan jangka panjang -pdfToPDFA.tags=arsip, jangka panjang, standar, konversi, penyimpanan, pelestarian +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF ke Word home.PDFToWord.desc=Mengonversi format PDF ke Word (DOC, DOCX, dan ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF Ke PDF/A -pdfToPDFA.header=PDF ke PDF/A -pdfToPDFA.credit=Layanan ini menggunakan libreoffice untuk konversi PDF/A. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konversi -pdfToPDFA.tip=Saat ini tidak dapat digunakan untuk beberapa input sekaligus +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Format keluaran pdfToPDFA.pdfWithDigitalSignature=PDF ini mengandung tanda tangan digital. Ini akan dihapus pada langkah berikutnya. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 76434bb6c..0fafd1712 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -694,9 +694,9 @@ home.extractImages.title=Estrai immagini home.extractImages.desc=Estrai tutte le immagini da un PDF e salvale come zip. extractImages.tags=immagine,foto,salva,archivio,zip,catturare,prendere -home.pdfToPDFA.title=Converti in PDF/A -home.pdfToPDFA.desc=Converti un PDF nel formato PDF/A per archiviazione a lungo termine. -pdfToPDFA.tags=archivio,a lungo termine,standard,conversione,archiviazione,conservazione +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=Da PDF a Word home.PDFToWord.desc=Converti un PDF nei formati Word (DOC, DOCX e ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Sbloccare i moduli PDF unlockPDFForms.submit=Rimuovi #pdfToPDFA -pdfToPDFA.title=Da PDF a PDF/A -pdfToPDFA.header=Da PDF a PDF/A -pdfToPDFA.credit=Questo servizio utilizza libreoffice per la conversione in PDF/A. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Converti -pdfToPDFA.tip=Attualmente non funziona per più input contemporaneamente +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Formato di output pdfToPDFA.pdfWithDigitalSignature=Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index 85605c0e5..50c2cc7c1 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -694,9 +694,9 @@ home.extractImages.title=画像の抽出 home.extractImages.desc=PDFからすべての画像を抽出してzipで保存します。 extractImages.tags=picture,photo,save,archive,zip,capture,grab -home.pdfToPDFA.title=PDFをPDF/Aに変換 -home.pdfToPDFA.desc=長期保存のためにPDFをPDF/Aに変換。 -pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDFをWordに変換 home.PDFToWord.desc=PDFをWord形式に変換します。 (DOC, DOCX および ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=PDFフォームのロックを解除 unlockPDFForms.submit=削除 #pdfToPDFA -pdfToPDFA.title=PDFをPDF/Aに変換 -pdfToPDFA.header=PDFをPDF/Aに変換 -pdfToPDFA.credit=本サービスはPDF/Aの変換にlibreofficeを使用しています。 +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=変換 -pdfToPDFA.tip=現在、一度に複数の入力に対して機能しません +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=出力形式 pdfToPDFA.pdfWithDigitalSignature=PDFにはデジタル署名が含まれています。これは次の手順で削除されます。 +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ko_KR.properties b/app/core/src/main/resources/messages_ko_KR.properties index cbe290415..905377777 100644 --- a/app/core/src/main/resources/messages_ko_KR.properties +++ b/app/core/src/main/resources/messages_ko_KR.properties @@ -694,9 +694,9 @@ home.extractImages.title=이미지 추출 home.extractImages.desc=PDF에서 모든 이미지를 추출하여 zip으로 저장 extractImages.tags=사진,저장,아카이브,zip,캡처,가져오기 -home.pdfToPDFA.title=PDF를 PDF/A로 -home.pdfToPDFA.desc=장기 보관을 위해 PDF를 PDF/A로 변환 -pdfToPDFA.tags=아카이브,장기,표준,변환,저장,보존 +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF를 Word로 home.PDFToWord.desc=PDF를 Word 형식으로 변환 (DOC, DOCX 및 ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF를 PDF/A로 -pdfToPDFA.header=PDF를 PDF/A로 -pdfToPDFA.credit=이 서비스는 PDF/A 변환을 위해 libreoffice를 사용합니다 +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=변환 -pdfToPDFA.tip=현재 여러 입력을 한 번에 처리할 수 없습니다 +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=출력 형식 pdfToPDFA.pdfWithDigitalSignature=PDF에 디지털 서명이 포함되어 있습니다. 다음 단계에서 제거됩니다. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ml_IN.properties b/app/core/src/main/resources/messages_ml_IN.properties index f22b73eb7..62153ed73 100644 --- a/app/core/src/main/resources/messages_ml_IN.properties +++ b/app/core/src/main/resources/messages_ml_IN.properties @@ -694,9 +694,9 @@ home.extractImages.title=ചിത്രങ്ങൾ വേർതിരിച് home.extractImages.desc=ഒരു PDF-ൽ നിന്ന് എല്ലാ ചിത്രങ്ങളും വേർതിരിച്ചെടുത്ത് സിപ്പിലേക്ക് സംരക്ഷിക്കുന്നു extractImages.tags=ചിത്രം,ഫോട്ടോ,സംരക്ഷിക്കുക,ആർക്കൈവ്,സിപ്പ്,പിടിച്ചെടുക്കുക,നേടുക -home.pdfToPDFA.title=PDF PDF/A-ലേക്ക് -home.pdfToPDFA.desc=ദീർഘകാല സംഭരണത്തിനായി PDF PDF/A-ലേക്ക് മാറ്റുക -pdfToPDFA.tags=ആർക്കൈവ്,ദീർഘകാല,മാനദണ്ഡം,പരിവർത്തനം,സംഭരണം,സംരക്ഷണം +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF വേഡിലേക്ക് home.PDFToWord.desc=PDF വേഡ് ഫോർമാറ്റുകളിലേക്ക് (DOC, DOCX, ODT) മാറ്റുക @@ -1619,13 +1619,15 @@ unlockPDFForms.header=PDF ഫോമുകൾ അൺലോക്ക് ചെയ unlockPDFForms.submit=നീക്കം ചെയ്യുക #pdfToPDFA -pdfToPDFA.title=PDF PDF/A-ലേക്ക് -pdfToPDFA.header=PDF PDF/A-ലേക്ക് -pdfToPDFA.credit=ഈ സേവനം PDF/A പരിവർത്തനത്തിനായി libreoffice ഉപയോഗിക്കുന്നു +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=പരിവർത്തനം ചെയ്യുക -pdfToPDFA.tip=നിലവിൽ ഒരേസമയം ഒന്നിലധികം ഇൻപുട്ടുകൾക്കായി പ്രവർത്തിക്കുന്നില്ല +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=ഔട്ട്പുട്ട് ഫോർമാറ്റ് pdfToPDFA.pdfWithDigitalSignature=PDF-ൽ ഒരു ഡിജിറ്റൽ ഒപ്പ് അടങ്ങിയിരിക്കുന്നു. അടുത്ത ഘട്ടത്തിൽ ഇത് നീക്കം ചെയ്യപ്പെടും. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_nl_NL.properties b/app/core/src/main/resources/messages_nl_NL.properties index ec8e92166..3004d6fb9 100644 --- a/app/core/src/main/resources/messages_nl_NL.properties +++ b/app/core/src/main/resources/messages_nl_NL.properties @@ -694,9 +694,9 @@ home.extractImages.title=Afbeeldingen extraheren home.extractImages.desc=Extraheert alle afbeeldingen uit een PDF en slaat ze op in een zip extractImages.tags=foto,opslaan,archief,zip,vastleggen,plukken -home.pdfToPDFA.title=PDF naar PDF/A -home.pdfToPDFA.desc=Converteer PDF naar PDF/A voor toekomstbestendige opslag -pdfToPDFA.tags=archief,langdurig,standaard,conversie,opslag,bewaring +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF naar Word home.PDFToWord.desc=Converteer PDF naar Word-formaten (DOC, DOCX en ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=PDF-formulieren ontgrendelen unlockPDFForms.submit=Verwijderen #pdfToPDFA -pdfToPDFA.title=PDF naar PDF/A -pdfToPDFA.header=PDF naar PDF/A -pdfToPDFA.credit=Deze functie gebruikt libreoffice voor PDF/A-conversie +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Converteren -pdfToPDFA.tip=Werkt momenteel niet voor meerdere inputs tegelijkertijd. +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Uitvoerindeling pdfToPDFA.pdfWithDigitalSignature=Dit PDF bestand bevat een digitale handtekening. Deze wordt in de volgende stap verwijderd. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_no_NB.properties b/app/core/src/main/resources/messages_no_NB.properties index 5aaaa8222..11a688dd6 100644 --- a/app/core/src/main/resources/messages_no_NB.properties +++ b/app/core/src/main/resources/messages_no_NB.properties @@ -694,9 +694,9 @@ home.extractImages.title=Ekstraher Bilder home.extractImages.desc=Ekstraherer alle bilder fra en PDF og lagrer dem som zip extractImages.tags=bilde,foto,lagre,arkiv,zip,fangst,hent -home.pdfToPDFA.title=PDF til PDF/A -home.pdfToPDFA.desc=Konverter PDF til PDF/A for langtidslagring -pdfToPDFA.tags=arkiv,langtidslagring,standard,konvertering,lagring,bevaring +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF til Word home.PDFToWord.desc=Konverter PDF til Word formater (DOC, DOCX og ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF til PDF/A -pdfToPDFA.header=PDF til PDF/A -pdfToPDFA.credit=Denne tjenesten bruker libreoffice for PDF/A-konvertering +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konverter -pdfToPDFA.tip=Fungere for øyeblikket ikke for flere innganger samtidig +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Utdataformat pdfToPDFA.pdfWithDigitalSignature=PDFen inneholder en digital signatur. Denne vil bli fjernet i neste steg. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_pl_PL.properties b/app/core/src/main/resources/messages_pl_PL.properties index 34154aeaa..7040a7dd2 100644 --- a/app/core/src/main/resources/messages_pl_PL.properties +++ b/app/core/src/main/resources/messages_pl_PL.properties @@ -694,9 +694,9 @@ home.extractImages.title=Wyodrębnij obrazy home.extractImages.desc=Wyodrębnia wszystkie obrazy z dokumentu PDF i zapisuje je w wybranym formacie extractImages.tags=obraz, zdjęcie, zapisz, archiwum, zip, przechwyć, złap -home.pdfToPDFA.title=PDF na PDF/A -home.pdfToPDFA.desc=Konwertuj dokument PDF na PDF/A w celu długoterminowego przechowywania -pdfToPDFA.tags=archiwum, długoterminowe, standardowe, konwersja, przechowywanie, konserwacja +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF na Word home.PDFToWord.desc=Konwertuj dokument PDF na formaty Word (DOC, DOCX i ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Odblokuj formularze PDF unlockPDFForms.submit=Usuń blokadę #pdfToPDFA -pdfToPDFA.title=PDF na PDF/A -pdfToPDFA.header=PDF na PDF/A -pdfToPDFA.credit=Ta usługa używa libreoffice do konwersji PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konwertuj -pdfToPDFA.tip=Tylko jeden plik na raz +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Format wyjściowy: pdfToPDFA.pdfWithDigitalSignature=Dokument zawiera podpis cyfrowy, nie zostanie on wczytany. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_pt_BR.properties b/app/core/src/main/resources/messages_pt_BR.properties index 708ee6338..18d4264bb 100644 --- a/app/core/src/main/resources/messages_pt_BR.properties +++ b/app/core/src/main/resources/messages_pt_BR.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrair Imagens home.extractImages.desc=Extrair as imagens de um PDF e salvá-las em um arquivo compactado. extractImages.tags=imagem,foto,salvar,arquivo,zip,captura,coleta -home.pdfToPDFA.title=PDF para PDF/A -home.pdfToPDFA.desc=Converter o PDF para o formato PDF/A, voltado a armazenamento a longo prazo. -pdfToPDFA.tags=arquivo,longo prazo,padrão,conversão,armazenamento,preservação +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF para Word home.PDFToWord.desc=Converter PDF para formatos Word (DOC, DOCX e ODT). @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Desbloquear Formulários do PDF unlockPDFForms.submit=Remover #pdfToPDFA -pdfToPDFA.title=PDF para PDF/A -pdfToPDFA.header=PDF para PDF/A -pdfToPDFA.credit=Este serviço usa o LibreOffice para conversão para PDF/A. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Converter -pdfToPDFA.tip=Atenção, atualmente não funciona para múltiplas entradas ao mesmo tempo. +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Formato de saída: pdfToPDFA.pdfWithDigitalSignature=O PDF contém uma assinatura digital. Isso será removido na próxima etapa. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_pt_PT.properties b/app/core/src/main/resources/messages_pt_PT.properties index 039fbf715..70a15922e 100644 --- a/app/core/src/main/resources/messages_pt_PT.properties +++ b/app/core/src/main/resources/messages_pt_PT.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrair Imagens home.extractImages.desc=Extrai todas as imagens de um PDF e guarda-as num zip extractImages.tags=imagem,foto,guardar,arquivo,zip,capturar,extrair -home.pdfToPDFA.title=PDF para PDF/A -home.pdfToPDFA.desc=Converter PDF para PDF/A para armazenamento a longo prazo -pdfToPDFA.tags=arquivo,longo prazo,padrão,conversão,armazenamento,preservação +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF para Word home.PDFToWord.desc=Converter PDF para formatos Word (DOC, DOCX e ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Desbloquear Formulários do PDF unlockPDFForms.submit=Remover #pdfToPDFA -pdfToPDFA.title=PDF Para PDF/A -pdfToPDFA.header=PDF Para PDF/A -pdfToPDFA.credit=Este serviço usa libreoffice para conversão PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Converter -pdfToPDFA.tip=Atualmente não funciona para múltiplas entradas de uma só vez +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Formato de saída pdfToPDFA.pdfWithDigitalSignature=O PDF contém uma assinatura digital. Esta será removida no próximo passo. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ro_RO.properties b/app/core/src/main/resources/messages_ro_RO.properties index 606b15380..942edc0bd 100644 --- a/app/core/src/main/resources/messages_ro_RO.properties +++ b/app/core/src/main/resources/messages_ro_RO.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrage Imagini home.extractImages.desc=Extrage toate imaginile dintr-un PDF și le salvează într-un fișier zip. extractImages.tags=poză,fotografie,salvează,arhivă,zip,captură,extrage -home.pdfToPDFA.title=PDF în PDF/A -home.pdfToPDFA.desc=Convertește un document PDF în format PDF/A pentru stocare pe termen lung. -pdfToPDFA.tags=arhivă,termen-lung,standard,conversie,stocare,conservare +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF în Word home.PDFToWord.desc=Convertește un document PDF în formate Word (DOC, DOCX și ODT). @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF către PDF/A -pdfToPDFA.header=PDF către PDF/A -pdfToPDFA.credit=Acest serviciu utilizează libreoffice pentru conversia în PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Convertește -pdfToPDFA.tip=În prezent nu funcționează pentru mai multe intrări simultan +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Format de ieșire pdfToPDFA.pdfWithDigitalSignature=PDF-ul conține o semnătură digitală. Aceasta va fi eliminată în pasul următor. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_ru_RU.properties b/app/core/src/main/resources/messages_ru_RU.properties index eb6e63668..1031f81e3 100644 --- a/app/core/src/main/resources/messages_ru_RU.properties +++ b/app/core/src/main/resources/messages_ru_RU.properties @@ -694,9 +694,9 @@ home.extractImages.title=Извлечь изображения home.extractImages.desc=Извлекает все изображения из PDF и сохраняет их в zip-архив extractImages.tags=картинка,фото,сохранение,архив,zip,захват,извлечение -home.pdfToPDFA.title=PDF в PDF/A -home.pdfToPDFA.desc=Преобразование PDF в PDF/A для долгосрочного хранения -pdfToPDFA.tags=архив,долгосрочный,стандарт,конвертация,хранение,сохранение +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF в Word home.PDFToWord.desc=Преобразование PDF в форматы Word (DOC, DOCX и ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Разблокировать PDF-формы unlockPDFForms.submit=Удалить #pdfToPDFA -pdfToPDFA.title=PDF в PDF/A -pdfToPDFA.header=PDF в PDF/A -pdfToPDFA.credit=Этот сервис использует libreoffice для преобразования в PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Преобразовать -pdfToPDFA.tip=В настоящее время не работает с несколькими входными файлами одновременно +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Формат вывода pdfToPDFA.pdfWithDigitalSignature=PDF содержит цифровую подпись. Она будет удалена на следующем шаге. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_sk_SK.properties b/app/core/src/main/resources/messages_sk_SK.properties index 34499c9a9..4cdc4f718 100644 --- a/app/core/src/main/resources/messages_sk_SK.properties +++ b/app/core/src/main/resources/messages_sk_SK.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrahovať obrázky home.extractImages.desc=Extrahuje všetky obrázky z PDF a uloží ich do zipu extractImages.tags=obrázok,fotografia,uložiť,archív,zip,zachytiť,chytiť -home.pdfToPDFA.title=PDF na PDF/A -home.pdfToPDFA.desc=Konvertujte PDF na PDF/A pre dlhodobé uchovávanie -pdfToPDFA.tags=archív,dĺhodobé,štandard,konverzia,uchovanie +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF na Word home.PDFToWord.desc=Konvertujte PDF na formáty Word (DOC, DOCX a ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF na PDF/A -pdfToPDFA.header=PDF na PDF/A -pdfToPDFA.credit=Táto služba používa libreoffice na konverziu PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertovať -pdfToPDFA.tip=Momentálne nefunguje pre viacero vstupov naraz +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Výstupný formát pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_sl_SI.properties b/app/core/src/main/resources/messages_sl_SI.properties index 0d42cad53..3afcc362f 100644 --- a/app/core/src/main/resources/messages_sl_SI.properties +++ b/app/core/src/main/resources/messages_sl_SI.properties @@ -694,9 +694,9 @@ home.extractImages.title=Izvleči slike home.extractImages.desc=Izvleče vse slike iz PDF-ja in jih shrani v zip extractImages.tags=slika,fotografija,shrani,arhiv,zip,zajemi,zgrabi -home.pdfToPDFA.title=PDF v PDF/A -home.pdfToPDFA.desc=Pretvori PDF v PDF/A za dolgoročno shranjevanje -pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF v Word home.PDFToWord.desc=Pretvori PDF v format Word (DOC, DOCX in ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF v PDF/A -pdfToPDFA.header=PDF v PDF/A -pdfToPDFA.credit=Ta storitev uporablja libreoffice za pretvorbo PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Pretvori -pdfToPDFA.tip=Trenutno ne deluje za več vnosov hkrati +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Izhodna oblika pdfToPDFA.pdfWithDigitalSignature=PDF vsebuje digitalni podpis. To bo odstranjeno v naslednjem koraku. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_sr_LATN_RS.properties b/app/core/src/main/resources/messages_sr_LATN_RS.properties index 25ee4bf0b..ac6882e1d 100644 --- a/app/core/src/main/resources/messages_sr_LATN_RS.properties +++ b/app/core/src/main/resources/messages_sr_LATN_RS.properties @@ -694,9 +694,9 @@ home.extractImages.title=Izvuci slike home.extractImages.desc=Izvlačenje svih slika iz PDF-a i kompresovanje u zip format extractImages.tags=slika,foto,sačuvaj,arhiva,zip,zahvati,uhvati -home.pdfToPDFA.title=PDF u PDF/A -home.pdfToPDFA.desc=Konvertovanje PDF u PDF/A za dugoročno čuvanje -pdfToPDFA.tags=arhiva,dugoročno,standard,konverzija,čuvanje,čuvanje +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF u Word home.PDFToWord.desc=Konvertovanje PDF u Word formate (DOC, DOCX i ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Otključaj PDF obrazac unlockPDFForms.submit=Otključaj #pdfToPDFA -pdfToPDFA.title=PDF u PDF/A -pdfToPDFA.header=PDF u PDF/A -pdfToPDFA.credit=Ova usluga koristi LibreOffice za konverziju u PDF/A format. +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertuj -pdfToPDFA.tip=Trenutno nije podržano za više unosa istovremeno +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Izlazni format: pdfToPDFA.pdfWithDigitalSignature=PDF sadrži digitalni potpis. Biće uklonjen u sledećem koraku. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_sv_SE.properties b/app/core/src/main/resources/messages_sv_SE.properties index a56dfa02c..2a02cb4a9 100644 --- a/app/core/src/main/resources/messages_sv_SE.properties +++ b/app/core/src/main/resources/messages_sv_SE.properties @@ -694,9 +694,9 @@ home.extractImages.title=Extrahera bilder home.extractImages.desc=Extraherar alla bilder från en PDF och sparar dem till zip extractImages.tags=bild,foto,spara,arkiv,zip,fånga,ta -home.pdfToPDFA.title=PDF till PDF/A -home.pdfToPDFA.desc=Konvertera PDF till PDF/A för långtidslagring -pdfToPDFA.tags=arkiv,långtids,standard,konvertering,lagring,bevarande +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF till Word home.PDFToWord.desc=Konvertera PDF till Word-format (DOC, DOCX och ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF till PDF/A -pdfToPDFA.header=PDF till PDF/A -pdfToPDFA.credit=Denna tjänst använder libreoffice för PDF/A-konvertering +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Konvertera -pdfToPDFA.tip=Fungerar för närvarande inte för flera inmatningar samtidigt +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Utdataformat pdfToPDFA.pdfWithDigitalSignature=PDF:en innehåller en digital signatur. Denna kommer att tas bort i nästa steg. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_th_TH.properties b/app/core/src/main/resources/messages_th_TH.properties index 0c7fafe64..bfe7b3e75 100644 --- a/app/core/src/main/resources/messages_th_TH.properties +++ b/app/core/src/main/resources/messages_th_TH.properties @@ -694,9 +694,9 @@ home.extractImages.title=แยกรูปภาพ home.extractImages.desc=แยกรูปภาพทั้งหมดจาก PDF และบันทึกในรูปแบบ zip extractImages.tags=รูปภาพ, ภาพ, บันทึก, เก็บถาวร, zip, จับ, รับ -home.pdfToPDFA.title=PDF เป็น PDF/A -home.pdfToPDFA.desc=แปลง PDF เป็น PDF/A สำหรับการจัดเก็บระยะยาว -pdfToPDFA.tags=การจัดเก็บ, ระยะยาว, มาตรฐาน, การแปลง, การเก็บรักษา +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF เป็น Word home.PDFToWord.desc=แปลง PDF เป็นรูปแบบ Word (DOC, DOCX และ ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF เป็น PDF/A -pdfToPDFA.header=PDF เป็น PDF/A -pdfToPDFA.credit=บริการนี้ใช้ libreoffice สำหรับการแปลง PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=แปลง -pdfToPDFA.tip=ปัจจุบันไม่ทำงานสำหรับการป้อนข้อมูลหลายรายการพร้อมกัน +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=รูปแบบผลลัพธ์ pdfToPDFA.pdfWithDigitalSignature=PDF มีลายเซ็นดิจิทัล ซึ่งจะถูกลบในขั้นตอนถัดไป +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_tr_TR.properties b/app/core/src/main/resources/messages_tr_TR.properties index ee7775758..f6410a94f 100644 --- a/app/core/src/main/resources/messages_tr_TR.properties +++ b/app/core/src/main/resources/messages_tr_TR.properties @@ -694,9 +694,9 @@ home.extractImages.title=Resimleri Çıkar home.extractImages.desc=Bir PDF'ten tüm resimleri çıkarır ve bunları zip olarak kaydeder. extractImages.tags=fotoğraf,resim,kaydet,arşiv,zip,yakala,al -home.pdfToPDFA.title=PDF'den PDF/A'ya -home.pdfToPDFA.desc=PDF'yi uzun vadeli saklama için PDF/A'ya dönüştürün -pdfToPDFA.tags=arşiv,uzun vadeli,standart,dönüşüm,saklama,koruma +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF'den Word'e home.PDFToWord.desc=PDF'yi Word formatlarına dönüştürün (DOC, DOCX ve ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=PDF Formlarının Kilidini Aç unlockPDFForms.submit=Kaldır #pdfToPDFA -pdfToPDFA.title=PDF'den PDF/A'ya -pdfToPDFA.header=PDF'den PDF/A'ya -pdfToPDFA.credit=Bu hizmet PDF/A dönüşümü için libreoffice kullanır +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Dönüştür -pdfToPDFA.tip=Şu anda aynı anda birden fazla giriş için çalışmıyor +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Çıkış formatı pdfToPDFA.pdfWithDigitalSignature=PDF dijital imza içeriyor. Bu bir sonraki adımda kaldırılacak. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_uk_UA.properties b/app/core/src/main/resources/messages_uk_UA.properties index da59001a2..8ed1ab8d5 100644 --- a/app/core/src/main/resources/messages_uk_UA.properties +++ b/app/core/src/main/resources/messages_uk_UA.properties @@ -694,9 +694,9 @@ home.extractImages.title=Витягнути зображення home.extractImages.desc=Витягує всі зображення з PDF і зберігає їх у zip extractImages.tags=зображення,фото,збереження,архів,zip,захоплення,захоплення -home.pdfToPDFA.title=PDF в PDF/A -home.pdfToPDFA.desc=Перетворення PDF в PDF/A для довготривалого зберігання -pdfToPDFA.tags=архів,довгостроковий,стандартний,конверсія,зберігання,консервація +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF в Word home.PDFToWord.desc=Перетворення PDF в формати Word (DOC, DOCX та ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF в PDF/A -pdfToPDFA.header=PDF в PDF/A -pdfToPDFA.credit=Цей сервіс використовує libreoffice для перетворення у формат PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Конвертувати -pdfToPDFA.tip=Наразі не працює для кількох вхідних файлів одночасно +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Вихідний формат pdfToPDFA.pdfWithDigitalSignature=Цей PDF документ має цифровий підпис. Цей підпис буде видалений у наступному кроці. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_vi_VN.properties b/app/core/src/main/resources/messages_vi_VN.properties index 75aaf6b27..e7bff3af6 100644 --- a/app/core/src/main/resources/messages_vi_VN.properties +++ b/app/core/src/main/resources/messages_vi_VN.properties @@ -694,9 +694,9 @@ home.extractImages.title=Trích xuất hình ảnh home.extractImages.desc=Trích xuất tất cả hình ảnh từ PDF và lưu chúng vào tệp zip extractImages.tags=hình ảnh,ảnh,lưu,lưu trữ,zip,chụp,lấy -home.pdfToPDFA.title=PDF sang PDF/A -home.pdfToPDFA.desc=Chuyển đổi PDF sang PDF/A để lưu trữ lâu dài -pdfToPDFA.tags=lưu trữ,dài hạn,tiêu chuẩn,chuyển đổi,lưu trữ,bảo quản +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF sang Word home.PDFToWord.desc=Chuyển đổi PDF sang các định dạng Word (DOC, DOCX và ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=Unlock PDF Forms unlockPDFForms.submit=Remove #pdfToPDFA -pdfToPDFA.title=PDF sang PDF/A -pdfToPDFA.header=PDF sang PDF/A -pdfToPDFA.credit=Dịch vụ này sử dụng libreoffice để chuyển đổi PDF/A +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=Chuyển đổi -pdfToPDFA.tip=Hiện tại không hoạt động với nhiều đầu vào cùng lúc +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=Định dạng đầu ra pdfToPDFA.pdfWithDigitalSignature=PDF chứa chữ ký số. Điều này sẽ bị xóa trong bước tiếp theo. +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_zh_CN.properties b/app/core/src/main/resources/messages_zh_CN.properties index cd6596b3c..67bd12e80 100644 --- a/app/core/src/main/resources/messages_zh_CN.properties +++ b/app/core/src/main/resources/messages_zh_CN.properties @@ -694,9 +694,9 @@ home.extractImages.title=提取图像 home.extractImages.desc=从 PDF 中提取所有图像并保存到压缩包中。 extractImages.tags=图片、照片、保存、归档、压缩包、截取、抓取 -home.pdfToPDFA.title=PDF 转 PDF/A -home.pdfToPDFA.desc=将 PDF 转换为 PDF/A 以进行长期保存。 -pdfToPDFA.tags=归档、长期、标准、转换、存储、保存 +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF 转 Word home.PDFToWord.desc=将PDF转换为Word格式(DOC、DOCX和ODT)。 @@ -1619,13 +1619,15 @@ unlockPDFForms.header=解锁 PDF 表单 unlockPDFForms.submit=移除 #pdfToPDFA -pdfToPDFA.title=PDF 转 PDF/A -pdfToPDFA.header=将 PDF 转换为 PDF/A -pdfToPDFA.credit=此服务使用 libreoffice 进行 PDF/A 转换 +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=转换 -pdfToPDFA.tip=目前不支持上传多个 +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=输出格式 pdfToPDFA.pdfWithDigitalSignature=该PDF包含数字签名,下一步将移除该签名。 +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/messages_zh_TW.properties b/app/core/src/main/resources/messages_zh_TW.properties index 4e3384a55..5d8fc66df 100644 --- a/app/core/src/main/resources/messages_zh_TW.properties +++ b/app/core/src/main/resources/messages_zh_TW.properties @@ -694,9 +694,9 @@ home.extractImages.title=提取圖片 home.extractImages.desc=從 PDF 中提取所有圖片並將它們儲存到壓縮檔中 extractImages.tags=圖片,照片,儲存,存檔,壓縮檔,捕獲,抓取 -home.pdfToPDFA.title=PDF 轉 PDF/A -home.pdfToPDFA.desc=將 PDF 轉換為長期儲存的 PDF/A -pdfToPDFA.tags=存檔,長期,標準,轉換,儲存,保存 +home.pdfToPDFA.title=PDF to PDF/A & PDF/X +home.pdfToPDFA.desc=Convert PDF to PDF/A for long-term storage or PDF/X for print production +pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation,print,pdf-x home.PDFToWord.title=PDF 轉 Word home.PDFToWord.desc=將 PDF 轉換為 Word 格式(DOC、DOCX 和 ODT) @@ -1619,13 +1619,15 @@ unlockPDFForms.header=解鎖 PDF 表單 unlockPDFForms.submit=移除 #pdfToPDFA -pdfToPDFA.title=PDF 轉 PDF/A -pdfToPDFA.header=PDF 轉 PDF/A -pdfToPDFA.credit=此服務使用 LibreOffice 進行 PDF/A 轉換 +pdfToPDFA.title=PDF to PDF/A or PDF/X +pdfToPDFA.header=PDF to PDF/A or PDF/X +pdfToPDFA.credit=This service uses Ghostscript (preferred) or LibreOffice for PDF/A conversion, and Ghostscript for PDF/X conversion pdfToPDFA.submit=轉換 -pdfToPDFA.tip=目前不支援上傳多個檔案 +pdfToPDFA.tip=Convert PDF to PDF/A (long-term archiving) or PDF/X (print production) pdfToPDFA.outputFormat=輸出格式 pdfToPDFA.pdfWithDigitalSignature=此 PDF 的憑證簽章將在下一步被移除 +pdfToPDFA.pdfaFormats=PDF/A Formats (Long-term Archiving) +pdfToPDFA.pdfxFormats=PDF/X Formats (Print Production) #PDFToWord diff --git a/app/core/src/main/resources/templates/convert/pdf-to-pdfa.html b/app/core/src/main/resources/templates/convert/pdf-to-pdfa.html index 33c07acb9..5b44ff5b6 100644 --- a/app/core/src/main/resources/templates/convert/pdf-to-pdfa.html +++ b/app/core/src/main/resources/templates/convert/pdf-to-pdfa.html @@ -23,8 +23,16 @@
diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFATest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFATest.java new file mode 100644 index 000000000..300ccb239 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFATest.java @@ -0,0 +1,571 @@ +package stirling.software.SPDF.controller.api.converters; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import org.apache.pdfbox.cos.*; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.preflight.ValidationResult; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.DublinCoreSchema; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; +import org.apache.xmpbox.schema.XMPBasicSchema; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpSerializer; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayName("PDF to PDF/A Converter Tests") +@ExtendWith(MockitoExtension.class) +class ConvertPDFToPDFATest { + + @TempDir Path tempDir; + + @SuppressWarnings("unchecked") + private static T invokePrivateMethod(String methodName, Object... args) throws Exception { + Class[] paramTypes = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + if (args[i] == null) { + paramTypes[i] = Object.class; + } else if (args[i] instanceof Integer) { + paramTypes[i] = int.class; + } else if (args[i] instanceof Boolean) { + paramTypes[i] = boolean.class; + } else { + paramTypes[i] = args[i].getClass(); + } + } + + try { + Method method = ConvertPDFToPDFA.class.getDeclaredMethod(methodName, paramTypes); + method.setAccessible(true); + return (T) method.invoke(null, args); + } catch (NoSuchMethodException e) { + for (Method method : ConvertPDFToPDFA.class.getDeclaredMethods()) { + if (method.getName().equals(methodName) + && method.getParameterCount() == args.length) { + method.setAccessible(true); + return (T) method.invoke(null, args); + } + } + throw e; + } + } + + private PDDocument createSimplePdf() throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + contentStream.beginText(); + contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.newLineAtOffset(100, 700); + contentStream.showText("Test PDF Document"); + contentStream.endText(); + } + + return document; + } + + private PDDocument createPdfWithMetadata(String title, String author, String creator) + throws IOException { + PDDocument document = createSimplePdf(); + + PDDocumentInformation info = new PDDocumentInformation(); + info.setTitle(title); + info.setAuthor(author); + info.setCreator(creator); + info.setSubject("Test Subject"); + info.setKeywords("test, pdf, metadata"); + info.setProducer("Test Producer"); + + GregorianCalendar cal = new GregorianCalendar(2024, Calendar.JANUARY, 1); + info.setCreationDate(cal); + info.setModificationDate(cal); + + document.setDocumentInformation(info); + return document; + } + + private PDDocument createPdfWithTransparency() throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g2d = bufferedImage.createGraphics(); + g2d.setColor(new Color(255, 0, 0, 128)); // Semi-transparent red + g2d.fillRect(0, 0, 100, 100); + g2d.dispose(); + + PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage); + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + contentStream.drawImage(image, 100, 600, 100, 100); + } + + return document; + } + + private PDDocument createPdfWithXmpMetadata(int pdfaPart) throws Exception { + PDDocument document = createSimplePdf(); + + XMPMetadata xmp = XMPMetadata.createXMPMetadata(); + + PDFAIdentificationSchema pdfaSchema = xmp.createAndAddPDFAIdentificationSchema(); + pdfaSchema.setPart(pdfaPart); + pdfaSchema.setConformance("B"); + + DublinCoreSchema dcSchema = xmp.createAndAddDublinCoreSchema(); + dcSchema.addCreator("Test Creator"); + dcSchema.setTitle("Test Title"); + + XMPBasicSchema xmpBasicSchema = xmp.createAndAddXMPBasicSchema(); + xmpBasicSchema.setCreatorTool("Test Tool"); + + ByteArrayOutputStream xmpStream = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmp, xmpStream, true); + + PDMetadata metadata = new PDMetadata(document); + metadata.importXMPMetadata(xmpStream.toByteArray()); + + document.getDocumentCatalog().setMetadata(metadata); + + return document; + } + + @Nested + @DisplayName("XMP Metadata Operations") + class XmpMetadataTests { + + @Test + @DisplayName("Should add PDF/A-1 identification schema to XMP metadata") + void shouldAddPdfA1IdentificationSchema() throws Exception { + PDDocument document = createPdfWithMetadata("Test PDF", "Test Author", "Test Creator"); + + invokePrivateMethod("mergeAndAddXmpMetadata", document, 1); + + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + assertThat(metadata).isNotNull(); + + try (InputStream is = metadata.createInputStream()) { + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmp = parser.parse(is); + + PDFAIdentificationSchema pdfaSchema = + (PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class); + assertThat(pdfaSchema).isNotNull(); + assertThat(pdfaSchema.getPart()).isEqualTo(1); + assertThat(pdfaSchema.getConformance()).isEqualTo("B"); + } + + document.close(); + } + + @Test + @DisplayName("Should add PDF/A-2 identification schema to XMP metadata") + void shouldAddPdfA2IdentificationSchema() throws Exception { + PDDocument document = createSimplePdf(); + + invokePrivateMethod("mergeAndAddXmpMetadata", document, 2); + + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + try (InputStream is = metadata.createInputStream()) { + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmp = parser.parse(is); + + PDFAIdentificationSchema pdfaSchema = + (PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class); + assertThat(pdfaSchema.getPart()).isEqualTo(2); + assertThat(pdfaSchema.getConformance()).isEqualTo("B"); + } + + document.close(); + } + + @Test + @DisplayName("Should preserve Dublin Core creator information") + void shouldPreserveDublinCoreCreatorInformation() throws Exception { + PDDocument document = + createPdfWithMetadata("Test PDF", "Test Author", "Original Creator"); + + invokePrivateMethod("mergeAndAddXmpMetadata", document, 1); + + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + try (InputStream is = metadata.createInputStream()) { + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmp = parser.parse(is); + + DublinCoreSchema dcSchema = xmp.getDublinCoreSchema(); + assertThat(dcSchema).isNotNull(); + assertThat(dcSchema.getCreators()).contains("Original Creator"); + } + + document.close(); + } + + @Test + @DisplayName("Should set creation and modification timestamps") + void shouldSetCreationAndModificationTimestamps() throws Exception { + PDDocument document = createSimplePdf(); + + invokePrivateMethod("mergeAndAddXmpMetadata", document, 1); + + PDDocumentInformation info = document.getDocumentInformation(); + assertThat(info.getCreationDate()).isNotNull(); + assertThat(info.getModificationDate()).isNotNull(); + + document.close(); + } + + @Test + @DisplayName("Should handle existing XMP metadata gracefully") + void shouldHandleExistingXmpMetadata() throws Exception { + PDDocument document = createPdfWithXmpMetadata(1); + + invokePrivateMethod("mergeAndAddXmpMetadata", document, 2); + + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + try (InputStream is = metadata.createInputStream()) { + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmp = parser.parse(is); + + PDFAIdentificationSchema pdfaSchema = + (PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class); + assertThat(pdfaSchema.getPart()).isEqualTo(2); + } + + document.close(); + } + } + + @Nested + @DisplayName("Content Sanitization") + class ContentSanitizationTests { + + @Test + @DisplayName("Should verify COSDictionary JavaScript removal logic") + void shouldVerifyJavaScriptRemovalLogic() throws Exception { + COSDictionary dict = new COSDictionary(); + dict.setString(COSName.JAVA_SCRIPT, "app.alert('test');"); + dict.setString(COSName.getPDFName("JS"), "some_js_code"); + + assertThat(dict.containsKey(COSName.JAVA_SCRIPT)).isTrue(); + assertThat(dict.containsKey(COSName.getPDFName("JS"))).isTrue(); + + invokePrivateMethod("sanitizePdfA", dict, 1); + + assertThat(dict.containsKey(COSName.JAVA_SCRIPT)).isFalse(); + assertThat(dict.containsKey(COSName.getPDFName("JS"))).isFalse(); + } + + @Test + @DisplayName("Should verify interpolation is set to false") + void shouldVerifyInterpolationSetToFalse() throws Exception { + COSDictionary dict = new COSDictionary(); + dict.setBoolean(COSName.INTERPOLATE, true); + + assertThat(dict.getBoolean(COSName.INTERPOLATE, false)).isTrue(); + + invokePrivateMethod("sanitizePdfA", dict, 1); + + assertThat(dict.getBoolean(COSName.INTERPOLATE, true)).isFalse(); + } + + @Test + @DisplayName("Should verify SMask removal for PDF/A-1") + void shouldVerifySMaskRemovalForPdfA1() throws Exception { + COSDictionary dict = new COSDictionary(); + dict.setItem(COSName.SMASK, new COSArray()); + + assertThat(dict.containsKey(COSName.SMASK)).isTrue(); + + invokePrivateMethod("sanitizePdfA", dict, 1); + + assertThat(dict.containsKey(COSName.SMASK)).isFalse(); + } + + @Test + @DisplayName("Should verify transparency group removal for PDF/A-1") + void shouldVerifyTransparencyGroupRemovalForPdfA1() throws Exception { + COSDictionary dict = new COSDictionary(); + COSDictionary groupDict = new COSDictionary(); + groupDict.setItem(COSName.S, COSName.TRANSPARENCY); + dict.setItem(COSName.GROUP, groupDict); + + assertThat(dict.containsKey(COSName.GROUP)).isTrue(); + + invokePrivateMethod("sanitizePdfA", dict, 1); + + assertThat(dict.containsKey(COSName.GROUP)).isFalse(); + } + + @Test + @DisplayName("Should verify forbidden elements are removed") + void shouldVerifyForbiddenElementsRemoved() throws Exception { + COSDictionary dict = new COSDictionary(); + dict.setItem(COSName.URI, COSName.A); + dict.setItem(COSName.EMBEDDED_FILES, new COSArray()); + dict.setItem(COSName.FILESPEC, new COSDictionary()); + dict.setItem(COSName.getPDFName("RichMedia"), new COSDictionary()); + + assertThat(dict.containsKey(COSName.URI)).isTrue(); + assertThat(dict.containsKey(COSName.EMBEDDED_FILES)).isTrue(); + + invokePrivateMethod("sanitizePdfA", dict, 1); + + assertThat(dict.containsKey(COSName.URI)).isFalse(); + assertThat(dict.containsKey(COSName.EMBEDDED_FILES)).isFalse(); + assertThat(dict.containsKey(COSName.FILESPEC)).isFalse(); + assertThat(dict.containsKey(COSName.getPDFName("RichMedia"))).isFalse(); + } + } + + @Nested + @DisplayName("Transparency Detection") + class TransparencyDetectionTests { + + @Test + @DisplayName("Should detect SMask transparency") + void shouldDetectSMaskTransparency() throws Exception { + PDDocument document = createPdfWithTransparency(); + + boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document); + + assertThat(hasTransparency).isTrue(); + + document.close(); + } + + @Test + @DisplayName("Should not detect transparency in opaque images") + void shouldNotDetectTransparencyInOpaqueImages() throws Exception { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + java.awt.Graphics2D g2d = bufferedImage.createGraphics(); + g2d.setColor(Color.RED); + g2d.fillRect(0, 0, 100, 100); + g2d.dispose(); + + PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage); + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + contentStream.drawImage(image, 100, 600, 100, 100); + } + + boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document); + + assertThat(hasTransparency).isFalse(); + + document.close(); + } + + @Test + @DisplayName("Should detect interpolation flag") + void shouldDetectInterpolationFlag() throws Exception { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + PDImageXObject image = LosslessFactory.createFromImage(document, bufferedImage); + image.setInterpolate(true); + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + contentStream.drawImage(image, 100, 600); + } + + boolean hasTransparency = invokePrivateMethod("hasTransparentImages", document); + + assertThat(hasTransparency).isTrue(); + + document.close(); + } + } + + @Nested + @DisplayName("Color Profile Management") + class ColorProfileTests { + + @Test + @DisplayName("Should verify ICC profile can be loaded from resources") + void shouldVerifyIccProfileCanBeLoadedFromResources() throws Exception { + try (InputStream iccStream = getClass().getResourceAsStream("/icc/sRGB2014.icc")) { + assertThat(iccStream).isNotNull(); + byte[] iccData = iccStream.readAllBytes(); + assertThat(iccData).isNotEmpty(); + assertThat(iccData).hasSizeGreaterThan(1000); + } + } + + @Test + @DisplayName("Should create color profile output intent structure") + void shouldCreateColorProfileOutputIntentStructure() throws Exception { + PDDocument document = createSimplePdf(); + try (InputStream iccStream = getClass().getResourceAsStream("/icc/sRGB2014.icc")) { + if (iccStream != null) { + PDOutputIntent outputIntent = new PDOutputIntent(document, iccStream); + outputIntent.setInfo("sRGB IEC61966-2.1"); + outputIntent.setOutputCondition("sRGB"); + outputIntent.setOutputConditionIdentifier("sRGB IEC61966-2.1"); + outputIntent.setRegistryName("http://www.color.org"); + + document.getDocumentCatalog().addOutputIntent(outputIntent); + + assertThat(document.getDocumentCatalog().getOutputIntents()).hasSize(1); + PDOutputIntent retrieved = + document.getDocumentCatalog().getOutputIntents().get(0); + assertThat(retrieved.getInfo()).contains("sRGB"); + } + } + + document.close(); + } + } + + @Nested + @DisplayName("Validation") + class ValidationTests { + + @Test + @DisplayName("Should format validation errors correctly") + void shouldFormatValidationErrorsCorrectly() { + String errorCode = "ERROR_CODE_123"; + String errorDetails = "Missing XMP metadata"; + + assertThat(errorCode).isNotBlank(); + assertThat(errorDetails).contains("XMP"); + assertThat(errorCode).startsWith("ERROR"); + } + + @Test + @DisplayName("Should handle validation error details") + void shouldHandleValidationErrorDetails() { + String error1Code = "1.2.3"; + String error1Detail = "Font not embedded"; + String error2Detail = "Missing color profile"; + + assertThat(error1Code).matches("\\d+\\.\\d+\\.\\d+"); + assertThat(error1Detail).contains("Font"); + assertThat(error2Detail).contains("color profile"); + } + + @Test + @DisplayName("Should create validation result with errors") + void shouldCreateValidationResultWithErrors() { + ValidationResult result = new ValidationResult(false); + + assertThat(result.isValid()).isFalse(); + assertThat(result.getErrorsList()).isNotNull(); + } + } + + @Nested + @DisplayName("Helper Methods") + class HelperMethodsTests { + + @Test + @DisplayName("Should build standard Type1 glyph set") + void shouldBuildStandardType1GlyphSet() throws Exception { + String glyphSet = invokePrivateMethod("buildStandardType1GlyphSet"); + + assertThat(glyphSet).isNotBlank().contains("space", "A", "a", "zero", "period"); + } + + @Test + @DisplayName("Should delete directory recursively") + void shouldDeleteDirectoryRecursively() throws Exception { + Path testDir = tempDir.resolve("test_delete"); + Files.createDirectories(testDir); + Path subDir = testDir.resolve("subdir"); + Files.createDirectories(subDir); + Files.createFile(testDir.resolve("file1.txt")); + Files.createFile(subDir.resolve("file2.txt")); + + assertThat(Files.exists(testDir)).isTrue(); + + invokePrivateMethod("deleteQuietly", testDir); + + assertThat(Files.exists(testDir)).isFalse(); + } + + @Test + @DisplayName("Should handle null path in deleteQuietly") + void shouldHandleNullPathInDeleteQuietly() { + assertDoesNotThrow(() -> invokePrivateMethod("deleteQuietly", (Path) null)); + } + + @Test + @DisplayName("Should handle non-existent path in deleteQuietly") + void shouldHandleNonExistentPathInDeleteQuietly() { + Path nonExistent = tempDir.resolve("non_existent_dir"); + + assertDoesNotThrow(() -> invokePrivateMethod("deleteQuietly", nonExistent)); + } + } + + @Nested + @DisplayName("Error Handling") + class ErrorHandlingTests { + + @Test + @DisplayName("Should handle empty PDF document") + void shouldHandleEmptyPdfDocument() { + PDDocument document = new PDDocument(); + + assertDoesNotThrow( + () -> { + invokePrivateMethod("mergeAndAddXmpMetadata", document, 1); + document.close(); + }); + } + + @Test + @DisplayName("Should handle PDF with no resources") + void shouldHandlePdfWithNoResources() throws Exception { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + assertThat(page.getResources()).isNull(); + + COSDictionary simpleDict = new COSDictionary(); + simpleDict.setItem(COSName.JAVA_SCRIPT, COSName.A); + + assertDoesNotThrow( + () -> { + invokePrivateMethod("sanitizePdfA", simpleDict, 1); + }); + + assertThat(simpleDict.containsKey(COSName.JAVA_SCRIPT)).isFalse(); + + document.close(); + } + } +}