mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
line art (#5052)
## Summary - introduce a shared line art conversion interface and proprietary ImageMagick-backed implementation - have the compress controller optionally autowire the enterprise service before running per-image line art processing - remove ImageMagick command details from core by delegating conversions through the proprietary service ## Testing - not run (not requested) ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_6928aecceaf083289a9269b1ca99307e) --------- Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
33188815da
commit
5f72c05623
@@ -491,6 +491,9 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Ghostscript", "repair");
|
addEndpointToGroup("Ghostscript", "repair");
|
||||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||||
|
|
||||||
|
/* ImageMagick */
|
||||||
|
addEndpointToGroup("ImageMagick", "compress-pdf");
|
||||||
|
|
||||||
/* tesseract */
|
/* tesseract */
|
||||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||||
|
|
||||||
@@ -574,6 +577,7 @@ public class EndpointConfiguration {
|
|||||||
|| "Javascript".equals(group)
|
|| "Javascript".equals(group)
|
||||||
|| "Weasyprint".equals(group)
|
|| "Weasyprint".equals(group)
|
||||||
|| "Pdftohtml".equals(group)
|
|| "Pdftohtml".equals(group)
|
||||||
|
|| "ImageMagick".equals(group)
|
||||||
|| "rar".equals(group);
|
|| "rar".equals(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -653,6 +653,7 @@ public class ApplicationProperties {
|
|||||||
private int weasyPrintSessionLimit;
|
private int weasyPrintSessionLimit;
|
||||||
private int installAppSessionLimit;
|
private int installAppSessionLimit;
|
||||||
private int calibreSessionLimit;
|
private int calibreSessionLimit;
|
||||||
|
private int imageMagickSessionLimit;
|
||||||
private int qpdfSessionLimit;
|
private int qpdfSessionLimit;
|
||||||
private int tesseractSessionLimit;
|
private int tesseractSessionLimit;
|
||||||
private int ghostscriptSessionLimit;
|
private int ghostscriptSessionLimit;
|
||||||
@@ -690,6 +691,10 @@ public class ApplicationProperties {
|
|||||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getImageMagickSessionLimit() {
|
||||||
|
return imageMagickSessionLimit > 0 ? imageMagickSessionLimit : 4;
|
||||||
|
}
|
||||||
|
|
||||||
public int getGhostscriptSessionLimit() {
|
public int getGhostscriptSessionLimit() {
|
||||||
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||||
}
|
}
|
||||||
@@ -719,6 +724,8 @@ public class ApplicationProperties {
|
|||||||
@JsonProperty("calibretimeoutMinutes")
|
@JsonProperty("calibretimeoutMinutes")
|
||||||
private long calibreTimeoutMinutes;
|
private long calibreTimeoutMinutes;
|
||||||
|
|
||||||
|
private long imageMagickTimeoutMinutes;
|
||||||
|
|
||||||
private long tesseractTimeoutMinutes;
|
private long tesseractTimeoutMinutes;
|
||||||
private long qpdfTimeoutMinutes;
|
private long qpdfTimeoutMinutes;
|
||||||
private long ghostscriptTimeoutMinutes;
|
private long ghostscriptTimeoutMinutes;
|
||||||
@@ -756,6 +763,10 @@ public class ApplicationProperties {
|
|||||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getImageMagickTimeoutMinutes() {
|
||||||
|
return imageMagickTimeoutMinutes > 0 ? imageMagickTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
|
|
||||||
public long getGhostscriptTimeoutMinutes() {
|
public long getGhostscriptTimeoutMinutes() {
|
||||||
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
|
||||||
|
public interface LineArtConversionService {
|
||||||
|
PDImageXObject convertImageToLineArt(
|
||||||
|
PDDocument doc, PDImageXObject originalImage, double threshold, int edgeLevel)
|
||||||
|
throws IOException;
|
||||||
|
}
|
||||||
@@ -86,6 +86,11 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getCalibreSessionLimit();
|
.getCalibreSessionLimit();
|
||||||
|
case IMAGEMAGICK ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getImageMagickSessionLimit();
|
||||||
case GHOSTSCRIPT ->
|
case GHOSTSCRIPT ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
@@ -141,6 +146,11 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getCalibreTimeoutMinutes();
|
.getCalibreTimeoutMinutes();
|
||||||
|
case IMAGEMAGICK ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getImageMagickTimeoutMinutes();
|
||||||
case GHOSTSCRIPT ->
|
case GHOSTSCRIPT ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
@@ -301,6 +311,7 @@ public class ProcessExecutor {
|
|||||||
WEASYPRINT,
|
WEASYPRINT,
|
||||||
INSTALL_APP,
|
INSTALL_APP,
|
||||||
CALIBRE,
|
CALIBRE,
|
||||||
|
IMAGEMAGICK,
|
||||||
TESSERACT,
|
TESSERACT,
|
||||||
QPDF,
|
QPDF,
|
||||||
GHOSTSCRIPT,
|
GHOSTSCRIPT,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public class ExternalAppDepConfig {
|
|||||||
put("qpdf", List.of("qpdf"));
|
put("qpdf", List.of("qpdf"));
|
||||||
put("tesseract", List.of("tesseract"));
|
put("tesseract", List.of("tesseract"));
|
||||||
put("rar", List.of("rar")); // Required for real CBR output
|
put("rar", List.of("rar")); // Required for real CBR output
|
||||||
|
put("magick", List.of("ImageMagick"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -128,6 +129,7 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup(unoconvPath);
|
checkDependencyAndDisableGroup(unoconvPath);
|
||||||
checkDependencyAndDisableGroup("rar");
|
checkDependencyAndDisableGroup("rar");
|
||||||
|
checkDependencyAndDisableGroup("magick");
|
||||||
// Special handling for Python/OpenCV dependencies
|
// Special handling for Python/OpenCV dependencies
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||||
if (!pythonAvailable) {
|
if (!pythonAvailable) {
|
||||||
|
|||||||
+103
@@ -28,10 +28,13 @@ import org.apache.pdfbox.pdmodel.PDResources;
|
|||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
|||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.annotations.api.MiscApi;
|
import stirling.software.common.annotations.api.MiscApi;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.service.LineArtConversionService;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
@@ -58,6 +62,9 @@ public class CompressController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private LineArtConversionService lineArtConversionService;
|
||||||
|
|
||||||
private boolean isQpdfEnabled() {
|
private boolean isQpdfEnabled() {
|
||||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||||
}
|
}
|
||||||
@@ -66,6 +73,10 @@ public class CompressController {
|
|||||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isImageMagickEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("ImageMagick");
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -660,6 +671,9 @@ public class CompressController {
|
|||||||
Integer optimizeLevel = request.getOptimizeLevel();
|
Integer optimizeLevel = request.getOptimizeLevel();
|
||||||
String expectedOutputSizeString = request.getExpectedOutputSize();
|
String expectedOutputSizeString = request.getExpectedOutputSize();
|
||||||
Boolean convertToGrayscale = request.getGrayscale();
|
Boolean convertToGrayscale = request.getGrayscale();
|
||||||
|
Boolean convertToLineArt = request.getLineArt();
|
||||||
|
Double lineArtThreshold = request.getLineArtThreshold();
|
||||||
|
Integer lineArtEdgeLevel = request.getLineArtEdgeLevel();
|
||||||
if (expectedOutputSizeString == null && optimizeLevel == null) {
|
if (expectedOutputSizeString == null && optimizeLevel == null) {
|
||||||
throw new Exception("Both expected output size and optimize level are not specified");
|
throw new Exception("Both expected output size and optimize level are not specified");
|
||||||
}
|
}
|
||||||
@@ -689,6 +703,26 @@ public class CompressController {
|
|||||||
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
|
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(convertToLineArt)) {
|
||||||
|
if (lineArtConversionService == null) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
"Line art conversion is unavailable - ImageMagick service not found");
|
||||||
|
}
|
||||||
|
if (!isImageMagickEnabled()) {
|
||||||
|
throw new IOException(
|
||||||
|
"ImageMagick is not enabled but line art conversion was requested");
|
||||||
|
}
|
||||||
|
double thresholdValue =
|
||||||
|
lineArtThreshold == null
|
||||||
|
? 55d
|
||||||
|
: Math.min(100d, Math.max(0d, lineArtThreshold));
|
||||||
|
int edgeLevel =
|
||||||
|
lineArtEdgeLevel == null ? 1 : Math.min(3, Math.max(1, lineArtEdgeLevel));
|
||||||
|
currentFile =
|
||||||
|
applyLineArtConversion(currentFile, tempFiles, thresholdValue, edgeLevel);
|
||||||
|
}
|
||||||
|
|
||||||
boolean sizeMet = false;
|
boolean sizeMet = false;
|
||||||
boolean imageCompressionApplied = false;
|
boolean imageCompressionApplied = false;
|
||||||
boolean externalCompressionApplied = false;
|
boolean externalCompressionApplied = false;
|
||||||
@@ -810,6 +844,75 @@ public class CompressController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path applyLineArtConversion(
|
||||||
|
Path currentFile, List<Path> tempFiles, double threshold, int edgeLevel)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Path lineArtFile = Files.createTempFile("lineart_output_", ".pdf");
|
||||||
|
tempFiles.add(lineArtFile);
|
||||||
|
|
||||||
|
try (PDDocument doc = pdfDocumentFactory.load(currentFile.toFile())) {
|
||||||
|
Map<String, List<ImageReference>> uniqueImages = findImages(doc);
|
||||||
|
CompressionStats stats = new CompressionStats();
|
||||||
|
stats.uniqueImagesCount = uniqueImages.size();
|
||||||
|
calculateImageStats(uniqueImages, stats);
|
||||||
|
|
||||||
|
Map<String, PDImageXObject> convertedImages =
|
||||||
|
createLineArtImages(doc, uniqueImages, stats, threshold, edgeLevel);
|
||||||
|
|
||||||
|
replaceImages(doc, uniqueImages, convertedImages, stats);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Applied line art conversion to {} unique images ({} total references)",
|
||||||
|
stats.uniqueImagesCount,
|
||||||
|
stats.totalImages);
|
||||||
|
|
||||||
|
doc.save(lineArtFile.toString());
|
||||||
|
return lineArtFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PDImageXObject> createLineArtImages(
|
||||||
|
PDDocument doc,
|
||||||
|
Map<String, List<ImageReference>> uniqueImages,
|
||||||
|
CompressionStats stats,
|
||||||
|
double threshold,
|
||||||
|
int edgeLevel)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Map<String, PDImageXObject> convertedImages = new HashMap<>();
|
||||||
|
|
||||||
|
for (Entry<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
|
||||||
|
String imageHash = entry.getKey();
|
||||||
|
List<ImageReference> references = entry.getValue();
|
||||||
|
if (references.isEmpty()) continue;
|
||||||
|
|
||||||
|
PDImageXObject originalImage = getOriginalImage(doc, references.get(0));
|
||||||
|
|
||||||
|
int originalSize = (int) originalImage.getCOSObject().getLength();
|
||||||
|
stats.totalOriginalBytes += originalSize;
|
||||||
|
|
||||||
|
PDImageXObject converted =
|
||||||
|
lineArtConversionService.convertImageToLineArt(
|
||||||
|
doc, originalImage, threshold, edgeLevel);
|
||||||
|
convertedImages.put(imageHash, converted);
|
||||||
|
stats.compressedImages++;
|
||||||
|
|
||||||
|
int convertedSize = (int) converted.getCOSObject().getLength();
|
||||||
|
stats.totalCompressedBytes += convertedSize * references.size();
|
||||||
|
|
||||||
|
double reductionPercentage = 100.0 - ((convertedSize * 100.0) / originalSize);
|
||||||
|
log.info(
|
||||||
|
"Image hash {}: Line art conversion {} → {} (reduced by {}%)",
|
||||||
|
imageHash,
|
||||||
|
GeneralUtils.formatBytes(originalSize),
|
||||||
|
GeneralUtils.formatBytes(convertedSize),
|
||||||
|
String.format("%.1f", reductionPercentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedImages;
|
||||||
|
}
|
||||||
|
|
||||||
// Run Ghostscript compression
|
// Run Ghostscript compression
|
||||||
private void applyGhostscriptCompression(
|
private void applyGhostscriptCompression(
|
||||||
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
||||||
|
|||||||
+6
@@ -230,4 +230,10 @@ public class ConfigController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/group-enabled")
|
||||||
|
public ResponseEntity<Boolean> isGroupEnabled(@RequestParam(name = "group") String group) {
|
||||||
|
boolean enabled = endpointConfiguration.isGroupEnabled(group);
|
||||||
|
return ResponseEntity.ok(enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -74,13 +74,13 @@ public class ReactRoutingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(
|
@GetMapping(
|
||||||
"/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}")
|
"/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|pdfium|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}")
|
||||||
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request) throws IOException {
|
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request) throws IOException {
|
||||||
return serveIndexHtml(request);
|
return serveIndexHtml(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(
|
@GetMapping(
|
||||||
"/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
|
"/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|pdfium|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*}/{subpath:^(?!.*\\.).*$}")
|
||||||
public ResponseEntity<String> forwardNestedPaths(HttpServletRequest request)
|
public ResponseEntity<String> forwardNestedPaths(HttpServletRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return serveIndexHtml(request);
|
return serveIndexHtml(request);
|
||||||
|
|||||||
@@ -45,4 +45,26 @@ public class OptimizePdfRequest extends PDFFile {
|
|||||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
defaultValue = "false")
|
defaultValue = "false")
|
||||||
private Boolean grayscale = false;
|
private Boolean grayscale = false;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Whether to convert images to high-contrast line art using ImageMagick. Default is false.",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean lineArt = false;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Threshold to use for line art conversion (0-100).",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
|
defaultValue = "55")
|
||||||
|
private Double lineArtThreshold = 55d;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Edge detection strength to use for line art conversion (1-3). This maps to"
|
||||||
|
+ " ImageMagick's -edge radius.",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
|
defaultValue = "1",
|
||||||
|
allowableValues = {"1", "2", "3"})
|
||||||
|
private Integer lineArtEdgeLevel = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ processExecutor:
|
|||||||
weasyPrintSessionLimit: 16
|
weasyPrintSessionLimit: 16
|
||||||
installAppSessionLimit: 1
|
installAppSessionLimit: 1
|
||||||
calibreSessionLimit: 1
|
calibreSessionLimit: 1
|
||||||
|
imageMagickSessionLimit: 4
|
||||||
ghostscriptSessionLimit: 8
|
ghostscriptSessionLimit: 8
|
||||||
ocrMyPdfSessionLimit: 2
|
ocrMyPdfSessionLimit: 2
|
||||||
timeoutMinutes: # Process executor timeout in minutes
|
timeoutMinutes: # Process executor timeout in minutes
|
||||||
@@ -233,6 +234,7 @@ processExecutor:
|
|||||||
weasyPrinttimeoutMinutes: 30
|
weasyPrinttimeoutMinutes: 30
|
||||||
installApptimeoutMinutes: 60
|
installApptimeoutMinutes: 60
|
||||||
calibretimeoutMinutes: 30
|
calibretimeoutMinutes: 30
|
||||||
|
imageMagickTimeoutMinutes: 30
|
||||||
tesseractTimeoutMinutes: 30
|
tesseractTimeoutMinutes: 30
|
||||||
qpdfTimeoutMinutes: 30
|
qpdfTimeoutMinutes: 30
|
||||||
ghostscriptTimeoutMinutes: 30
|
ghostscriptTimeoutMinutes: 30
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package stirling.software.proprietary.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.LineArtConversionService;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ImageMagickLineArtConversionService implements LineArtConversionService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PDImageXObject convertImageToLineArt(
|
||||||
|
PDDocument doc, PDImageXObject originalImage, double threshold, int edgeLevel)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Path inputImage = Files.createTempFile("lineart_image_input_", ".png");
|
||||||
|
Path outputImage = Files.createTempFile("lineart_image_output_", ".tiff");
|
||||||
|
|
||||||
|
try {
|
||||||
|
ImageIO.write(originalImage.getImage(), "png", inputImage.toFile());
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("magick");
|
||||||
|
command.add(inputImage.toString());
|
||||||
|
command.add("-colorspace");
|
||||||
|
command.add("Gray");
|
||||||
|
|
||||||
|
// Edge-aware line art conversion using ImageMagick's built-in operators.
|
||||||
|
// -edge/-negate/-normalize are standard convert options (IM v6+/v7) that
|
||||||
|
// accentuate outlines before thresholding to a bilevel image.
|
||||||
|
command.add("-edge");
|
||||||
|
command.add(String.valueOf(edgeLevel));
|
||||||
|
command.add("-negate");
|
||||||
|
command.add("-normalize");
|
||||||
|
|
||||||
|
command.add("-type");
|
||||||
|
command.add("Bilevel");
|
||||||
|
command.add("-threshold");
|
||||||
|
command.add(String.format(Locale.ROOT, "%.1f%%", threshold));
|
||||||
|
command.add("-compress");
|
||||||
|
command.add("Group4");
|
||||||
|
command.add(outputImage.toString());
|
||||||
|
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.IMAGEMAGICK)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
log.warn(
|
||||||
|
"ImageMagick line art conversion failed with return code: {}",
|
||||||
|
result.getRc());
|
||||||
|
throw new IOException("ImageMagick line art conversion failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] convertedBytes = Files.readAllBytes(outputImage);
|
||||||
|
return PDImageXObject.createFromByteArray(
|
||||||
|
doc, convertedBytes, originalImage.getCOSObject().toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("ImageMagick line art conversion failed", e);
|
||||||
|
throw new IOException("ImageMagick line art conversion failed", e);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(inputImage);
|
||||||
|
Files.deleteIfExists(outputImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
gcompat \
|
gcompat \
|
||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
|
imagemagick \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF
|
# OCR MY PDF
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
imagemagick \
|
||||||
fontforge \
|
fontforge \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
imagemagick \
|
||||||
fontforge \
|
fontforge \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
imagemagick \
|
||||||
fontforge \
|
fontforge \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
|
imagemagick \
|
||||||
fontforge \
|
fontforge \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
|
|||||||
@@ -3729,6 +3729,16 @@ filesize = "File Size"
|
|||||||
[compress.grayscale]
|
[compress.grayscale]
|
||||||
label = "Apply Grayscale for Compression"
|
label = "Apply Grayscale for Compression"
|
||||||
|
|
||||||
|
[compress.lineArt]
|
||||||
|
label = "Convert images to line art"
|
||||||
|
description = "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction."
|
||||||
|
unavailable = "ImageMagick is not installed or enabled on this server"
|
||||||
|
detailLevel = "Detail level"
|
||||||
|
edgeEmphasis = "Edge emphasis"
|
||||||
|
edgeLow = "Gentle"
|
||||||
|
edgeMedium = "Balanced"
|
||||||
|
edgeHigh = "Strong"
|
||||||
|
|
||||||
[compress.tooltip.header]
|
[compress.tooltip.header]
|
||||||
title = "Compress Settings Overview"
|
title = "Compress Settings Overview"
|
||||||
|
|
||||||
@@ -3746,6 +3756,10 @@ bullet2 = "Higher values reduce file size"
|
|||||||
title = "Grayscale"
|
title = "Grayscale"
|
||||||
text = "Select this option to convert all images to black and white, which can significantly reduce file size especially for scanned PDFs or image-heavy documents."
|
text = "Select this option to convert all images to black and white, which can significantly reduce file size especially for scanned PDFs or image-heavy documents."
|
||||||
|
|
||||||
|
[compress.tooltip.lineArt]
|
||||||
|
title = "Line Art"
|
||||||
|
text = "Convert pages to high-contrast black and white using ImageMagick. Use detail level to control how much content becomes black, and edge emphasis to control how aggressively edges are detected."
|
||||||
|
|
||||||
[compress.error]
|
[compress.error]
|
||||||
failed = "An error occurred while compressing the PDF."
|
failed = "An error occurred while compressing the PDF."
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +1,82 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Stirling-PDF",
|
"productName": "Stirling-PDF",
|
||||||
"version": "2.0.0",
|
"version": "2.1.3",
|
||||||
"identifier": "stirling.pdf.dev",
|
"identifier": "stirling.pdf.dev",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
"devUrl": "http://localhost:5173",
|
"devUrl": "http://localhost:5173",
|
||||||
"beforeDevCommand": "npm run dev -- --mode desktop",
|
"beforeDevCommand": "npm run dev -- --mode desktop",
|
||||||
"beforeBuildCommand": "npm run build -- --mode desktop"
|
"beforeBuildCommand": "npm run build -- --mode desktop"
|
||||||
},
|
|
||||||
"app": {
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"title": "Stirling-PDF",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 800,
|
|
||||||
"resizable": true,
|
|
||||||
"fullscreen": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": true,
|
|
||||||
"targets": ["deb", "rpm", "dmg", "app", "msi"],
|
|
||||||
"icon": [
|
|
||||||
"icons/icon.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico",
|
|
||||||
"icons/16x16.png",
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/64x64.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/192x192.png"
|
|
||||||
],
|
|
||||||
"resources": [
|
|
||||||
"libs/*.jar",
|
|
||||||
"runtime/jre/**/*"
|
|
||||||
],
|
|
||||||
"fileAssociations": [
|
|
||||||
{
|
|
||||||
"ext": ["pdf"],
|
|
||||||
"name": "PDF Document",
|
|
||||||
"description": "Open PDF files with Stirling-PDF",
|
|
||||||
"role": "Editor",
|
|
||||||
"mimeType": "application/pdf"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"linux": {
|
|
||||||
"deb": {
|
|
||||||
"desktopTemplate": "stirling-pdf.desktop"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"windows": {
|
"app": {
|
||||||
"certificateThumbprint": null,
|
"windows": [
|
||||||
"digestAlgorithm": "sha256",
|
{
|
||||||
"timestampUrl": "http://timestamp.digicert.com"
|
"title": "Stirling-PDF",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"macOS": {
|
"bundle": {
|
||||||
"minimumSystemVersion": "10.15",
|
"active": true,
|
||||||
"signingIdentity": null,
|
"targets": [
|
||||||
"entitlements": null,
|
"deb",
|
||||||
"providerShortName": null
|
"rpm",
|
||||||
|
"dmg",
|
||||||
|
"app",
|
||||||
|
"msi"
|
||||||
|
],
|
||||||
|
"icon": [
|
||||||
|
"icons/icon.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico",
|
||||||
|
"icons/16x16.png",
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/64x64.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/192x192.png"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
"libs/*.jar",
|
||||||
|
"runtime/jre/**/*"
|
||||||
|
],
|
||||||
|
"fileAssociations": [
|
||||||
|
{
|
||||||
|
"ext": [
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"name": "PDF Document",
|
||||||
|
"description": "Open PDF files with Stirling-PDF",
|
||||||
|
"role": "Editor",
|
||||||
|
"mimeType": "application/pdf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"linux": {
|
||||||
|
"deb": {
|
||||||
|
"desktopTemplate": "stirling-pdf.desktop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "http://timestamp.digicert.com"
|
||||||
|
},
|
||||||
|
"macOS": {
|
||||||
|
"minimumSystemVersion": "10.15",
|
||||||
|
"signingIdentity": null,
|
||||||
|
"entitlements": null,
|
||||||
|
"providerShortName": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"shell": {
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"requireLiteralLeadingDot": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"shell": {
|
|
||||||
"open": true
|
|
||||||
},
|
|
||||||
"fs": {
|
|
||||||
"requireLiteralLeadingDot": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Stack, Text, NumberInput, Select, Divider, Checkbox } from "@mantine/core";
|
import { Stack, Text, NumberInput, Select, Divider, Checkbox, Slider, SegmentedControl } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CompressParameters } from "@app/hooks/tools/compress/useCompressParameters";
|
import { CompressParameters } from "@app/hooks/tools/compress/useCompressParameters";
|
||||||
import ButtonSelector from "@app/components/shared/ButtonSelector";
|
import ButtonSelector from "@app/components/shared/ButtonSelector";
|
||||||
|
import apiClient from "@app/services/apiClient";
|
||||||
|
|
||||||
interface CompressSettingsProps {
|
interface CompressSettingsProps {
|
||||||
parameters: CompressParameters;
|
parameters: CompressParameters;
|
||||||
@@ -13,6 +14,20 @@ interface CompressSettingsProps {
|
|||||||
const CompressSettings = ({ parameters, onParameterChange, disabled = false }: CompressSettingsProps) => {
|
const CompressSettings = ({ parameters, onParameterChange, disabled = false }: CompressSettingsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isSliding, setIsSliding] = useState(false);
|
const [isSliding, setIsSliding] = useState(false);
|
||||||
|
const [imageMagickAvailable, setImageMagickAvailable] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkImageMagick = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<boolean>('/api/v1/config/group-enabled?group=ImageMagick');
|
||||||
|
setImageMagickAvailable(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check ImageMagick availability:', error);
|
||||||
|
setImageMagickAvailable(true); // Optimistic fallback
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkImageMagick();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@@ -129,6 +144,62 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("compress.grayscale.label", "Apply Grayscale for compression")}
|
label={t("compress.grayscale.label", "Apply Grayscale for compression")}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={parameters.lineArt}
|
||||||
|
onChange={(event) => onParameterChange('lineArt', event.currentTarget.checked)}
|
||||||
|
disabled={disabled || imageMagickAvailable === false}
|
||||||
|
label={t("compress.lineArt.label", "Convert images to line art (bilevel)")}
|
||||||
|
description={
|
||||||
|
imageMagickAvailable === false
|
||||||
|
? t("compress.lineArt.unavailable", "ImageMagick is not installed or enabled on this server")
|
||||||
|
: t("compress.lineArt.description", "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction.")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{parameters.lineArt && (
|
||||||
|
<Stack gap="xs" style={{ opacity: (disabled || imageMagickAvailable === false) ? 0.6 : 1 }}>
|
||||||
|
<Text size="sm" fw={600}>{t('compress.lineArt.detailLevel', 'Detail level')}</Text>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
step={1}
|
||||||
|
value={(() => {
|
||||||
|
// Map threshold to slider position
|
||||||
|
const thresholdMap = [20, 35, 50, 65, 80];
|
||||||
|
const closest = thresholdMap.reduce((prev, curr, idx) =>
|
||||||
|
Math.abs(curr - parameters.lineArtThreshold) < Math.abs(thresholdMap[prev] - parameters.lineArtThreshold)
|
||||||
|
? idx : prev, 0);
|
||||||
|
return closest + 1;
|
||||||
|
})()}
|
||||||
|
onChange={(value) => {
|
||||||
|
// Map slider position to threshold: 1=20%, 2=35%, 3=50%, 4=65%, 5=80%
|
||||||
|
const thresholdMap = [20, 35, 50, 65, 80];
|
||||||
|
onParameterChange('lineArtThreshold', thresholdMap[value - 1]);
|
||||||
|
}}
|
||||||
|
disabled={disabled || imageMagickAvailable === false}
|
||||||
|
label={null}
|
||||||
|
marks={[
|
||||||
|
{ value: 1 },
|
||||||
|
{ value: 2 },
|
||||||
|
{ value: 3 },
|
||||||
|
{ value: 4 },
|
||||||
|
{ value: 5 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size="sm" fw={600}>{t('compress.lineArt.edgeEmphasis', 'Edge emphasis')}</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
fullWidth
|
||||||
|
disabled={disabled || imageMagickAvailable === false}
|
||||||
|
data={[
|
||||||
|
{ value: '1', label: t('compress.lineArt.edgeLow', 'Gentle') },
|
||||||
|
{ value: '2', label: t('compress.lineArt.edgeMedium', 'Balanced') },
|
||||||
|
{ value: '3', label: t('compress.lineArt.edgeHigh', 'Strong') },
|
||||||
|
]}
|
||||||
|
value={parameters.lineArtEdgeLevel.toString()}
|
||||||
|
onChange={(value) => onParameterChange('lineArtEdgeLevel', parseInt(value) as 1 | 2 | 3)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ export const useCompressTips = (): TooltipContent => {
|
|||||||
{
|
{
|
||||||
title: t("compress.tooltip.grayscale.title", "Grayscale"),
|
title: t("compress.tooltip.grayscale.title", "Grayscale"),
|
||||||
description: t("compress.tooltip.grayscale.text", "Select this option to convert all images to black and white, which can significantly reduce file size especially for scanned PDFs or image-heavy documents.")
|
description: t("compress.tooltip.grayscale.text", "Select this option to convert all images to black and white, which can significantly reduce file size especially for scanned PDFs or image-heavy documents.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("compress.tooltip.lineArt.title", "Line Art"),
|
||||||
|
description: t(
|
||||||
|
"compress.tooltip.lineArt.text",
|
||||||
|
"Convert pages to high-contrast black and white using ImageMagick. Use line thickness to control the threshold percentage and detection strength to choose how aggressively edges are outlined."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export const buildCompressFormData = (parameters: CompressParameters, file: File
|
|||||||
}
|
}
|
||||||
|
|
||||||
formData.append("grayscale", parameters.grayscale.toString());
|
formData.append("grayscale", parameters.grayscale.toString());
|
||||||
|
formData.append("lineArt", parameters.lineArt.toString());
|
||||||
|
if (parameters.lineArt) {
|
||||||
|
formData.append("lineArtThreshold", parameters.lineArtThreshold.toString());
|
||||||
|
formData.append("lineArtEdgeLevel", parameters.lineArtEdgeLevel.toString());
|
||||||
|
}
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/u
|
|||||||
export interface CompressParameters extends BaseParameters {
|
export interface CompressParameters extends BaseParameters {
|
||||||
compressionLevel: number;
|
compressionLevel: number;
|
||||||
grayscale: boolean;
|
grayscale: boolean;
|
||||||
|
lineArt: boolean;
|
||||||
|
lineArtThreshold: number;
|
||||||
|
lineArtEdgeLevel: 1 | 2 | 3;
|
||||||
expectedSize: string;
|
expectedSize: string;
|
||||||
compressionMethod: 'quality' | 'filesize';
|
compressionMethod: 'quality' | 'filesize';
|
||||||
fileSizeValue: string;
|
fileSizeValue: string;
|
||||||
@@ -13,6 +16,9 @@ export interface CompressParameters extends BaseParameters {
|
|||||||
export const defaultParameters: CompressParameters = {
|
export const defaultParameters: CompressParameters = {
|
||||||
compressionLevel: 5,
|
compressionLevel: 5,
|
||||||
grayscale: false,
|
grayscale: false,
|
||||||
|
lineArt: false,
|
||||||
|
lineArtThreshold: 50,
|
||||||
|
lineArtEdgeLevel: 3,
|
||||||
expectedSize: '',
|
expectedSize: '',
|
||||||
compressionMethod: 'quality',
|
compressionMethod: 'quality',
|
||||||
fileSizeValue: '',
|
fileSizeValue: '',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
|
|||||||
|
|
||||||
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
||||||
enableAnalytics: true,
|
enableAnalytics: true,
|
||||||
appVersion: '2.0.0',
|
appVersion: '2.1.3',
|
||||||
serverCertificateEnabled: false,
|
serverCertificateEnabled: false,
|
||||||
enableAlphaFunctionality: false,
|
enableAlphaFunctionality: false,
|
||||||
serverPort: 8080,
|
serverPort: 8080,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
|
|||||||
|
|
||||||
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
||||||
enableAnalytics: true,
|
enableAnalytics: true,
|
||||||
appVersion: '2.0.0',
|
appVersion: '2.1.3',
|
||||||
serverCertificateEnabled: false,
|
serverCertificateEnabled: false,
|
||||||
enableAlphaFunctionality: false,
|
enableAlphaFunctionality: false,
|
||||||
enableDesktopInstallSlide: true,
|
enableDesktopInstallSlide: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user