diff --git a/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java b/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java index f9cbaace1..1f421375a 100644 --- a/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java +++ b/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java @@ -4,4 +4,5 @@ public enum ReplaceAndInvert { HIGH_CONTRAST_COLOR, CUSTOM_COLOR, FULL_INVERSION, + COLOR_SPACE_CONVERSION, } diff --git a/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java new file mode 100644 index 000000000..c784bbed6 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java @@ -0,0 +1,94 @@ +package stirling.software.common.util.misc; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.web.multipart.MultipartFile; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; + +@Slf4j +public class ColorSpaceConversionStrategy extends ReplaceAndInvertColorStrategy { + + public ColorSpaceConversionStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) { + super(file, replaceAndInvert); + } + + @Override + public InputStreamResource replace() throws IOException { + Path tempInputFile = null; + Path tempOutputFile = null; + + try { + tempInputFile = Files.createTempFile("colorspace_input_", ".pdf"); + tempOutputFile = Files.createTempFile("colorspace_output_", ".pdf"); + + Files.write(tempInputFile, getFileInput().getBytes()); + + log.info("Starting CMYK color space conversion"); + + List command = new ArrayList<>(); + command.add("gs"); + command.add("-sDEVICE=pdfwrite"); + command.add("-dCompatibilityLevel=1.5"); + command.add("-dPDFSETTINGS=/prepress"); + command.add("-dNOPAUSE"); + command.add("-dQUIET"); + command.add("-dBATCH"); + command.add("-sProcessColorModel=DeviceCMYK"); + command.add("-sColorConversionStrategy=CMYK"); + command.add("-sColorConversionStrategyForImages=CMYK"); + command.add("-sOutputFile=" + tempOutputFile.toString()); + command.add(tempInputFile.toString()); + + log.debug("Executing Ghostscript command for CMYK conversion: {}", command); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); + + if (result.getRc() != 0) { + log.error( + "Ghostscript CMYK conversion failed with return code: {}. Output: {}", + result.getRc(), + result.getMessages()); + throw new IOException( + "CMYK color space conversion failed: " + result.getMessages()); + } + + log.info("CMYK color space conversion completed successfully"); + + byte[] pdfBytes = Files.readAllBytes(tempOutputFile); + return new InputStreamResource(new ByteArrayInputStream(pdfBytes)); + + } catch (Exception e) { + log.warn("CMYK color space conversion failed", e); + throw new IOException( + "Failed to convert PDF to CMYK color space: " + e.getMessage(), e); + } finally { + if (tempInputFile != null) { + try { + Files.deleteIfExists(tempInputFile); + } catch (IOException e) { + log.warn("Failed to delete temporary input file: {}", tempInputFile, e); + } + } + if (tempOutputFile != null) { + try { + Files.deleteIfExists(tempOutputFile); + } catch (IOException e) { + log.warn("Failed to delete temporary output file: {}", tempOutputFile, e); + } + } + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java b/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java index 49be7fd42..e53850ff8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java +++ b/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java @@ -5,6 +5,7 @@ import org.springframework.web.multipart.MultipartFile; import stirling.software.common.model.api.misc.HighContrastColorCombination; import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.misc.ColorSpaceConversionStrategy; import stirling.software.common.util.misc.CustomColorReplaceStrategy; import stirling.software.common.util.misc.InvertFullColorStrategy; import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy; @@ -19,21 +20,17 @@ public class ReplaceAndInvertColorFactory { String backGroundColor, String textColor) { - if (replaceAndInvertOption == ReplaceAndInvert.CUSTOM_COLOR - || replaceAndInvertOption == ReplaceAndInvert.HIGH_CONTRAST_COLOR) { - - return new CustomColorReplaceStrategy( - file, - replaceAndInvertOption, - textColor, - backGroundColor, - highContrastColorCombination); - - } else if (replaceAndInvertOption == ReplaceAndInvert.FULL_INVERSION) { - - return new InvertFullColorStrategy(file, replaceAndInvertOption); - } - - return null; + return switch (replaceAndInvertOption) { + case CUSTOM_COLOR, HIGH_CONTRAST_COLOR -> + new CustomColorReplaceStrategy( + file, + replaceAndInvertOption, + textColor, + backGroundColor, + highContrastColorCombination); + case FULL_INVERSION -> new InvertFullColorStrategy(file, replaceAndInvertOption); + case COLOR_SPACE_CONVERSION -> + new ColorSpaceConversionStrategy(file, replaceAndInvertOption); + }; } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 3e876a556..acee3db0c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -400,6 +400,7 @@ public class EndpointConfiguration { /* Ghostscript */ addEndpointToGroup("Ghostscript", "repair"); addEndpointToGroup("Ghostscript", "compress-pdf"); + addEndpointToGroup("Ghostscript", "replace-invert-pdf"); /* tesseract */ addEndpointToGroup("tesseract", "ocr-pdf"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java index 0b6fbf23c..1c7b589c0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java @@ -31,8 +31,8 @@ public class ReplaceAndInvertColorController { @Operation( summary = "Replace-Invert Color PDF", description = - "This endpoint accepts a PDF file and option of invert all colors or replace" - + " text and background colors. Input:PDF Output:PDF Type:SISO") + "This endpoint accepts a PDF file and provides options to invert all colors, replace" + + " text and background colors, or convert to CMYK color space for printing. Input:PDF Output:PDF Type:SISO") public ResponseEntity replaceAndInvertColor( @ModelAttribute ReplaceAndInvertColorRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java index 50ef14b1e..02bbcd6ad 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java @@ -17,7 +17,12 @@ public class ReplaceAndInvertColorRequest extends PDFFile { description = "Replace and Invert color options of a pdf.", requiredMode = Schema.RequiredMode.REQUIRED, defaultValue = "HIGH_CONTRAST_COLOR", - allowableValues = {"HIGH_CONTRAST_COLOR", "CUSTOM_COLOR", "FULL_INVERSION"}) + allowableValues = { + "HIGH_CONTRAST_COLOR", + "CUSTOM_COLOR", + "FULL_INVERSION", + "COLOR_SPACE_CONVERSION" + }) private ReplaceAndInvert replaceAndInvertOption; @Schema( diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index c76c0c785..e52e9cc45 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -878,6 +878,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Colour replace-color.selectText.11=Choose background Colour +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace diff --git a/app/core/src/main/resources/templates/misc/replace-color.html b/app/core/src/main/resources/templates/misc/replace-color.html index 4defcff72..18b8c9f0c 100644 --- a/app/core/src/main/resources/templates/misc/replace-color.html +++ b/app/core/src/main/resources/templates/misc/replace-color.html @@ -30,6 +30,7 @@ + @@ -56,6 +57,18 @@ + + @@ -74,12 +87,15 @@ $('#high-contrast-options').hide(); $('#custom-color-1').hide(); $('#custom-color-2').hide(); + $('#color-space-info').hide(); if (selectedOption === "HIGH_CONTRAST_COLOR") { $('#high-contrast-options').show(); } else if (selectedOption === "CUSTOM_COLOR") { $('#custom-color-1').show(); $('#custom-color-2').show(); + } else if (selectedOption === "COLOR_SPACE_CONVERSION") { + $('#color-space-info').show(); } }); });