mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
Merge branch 'main' into pdfCache
This commit is contained in:
@@ -491,6 +491,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Ghostscript", "repair");
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
|
||||
/* ImageMagick */
|
||||
addEndpointToGroup("ImageMagick", "compress-pdf");
|
||||
|
||||
/* tesseract */
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
|
||||
@@ -574,6 +577,7 @@ public class EndpointConfiguration {
|
||||
|| "Javascript".equals(group)
|
||||
|| "Weasyprint".equals(group)
|
||||
|| "Pdftohtml".equals(group)
|
||||
|| "ImageMagick".equals(group)
|
||||
|| "rar".equals(group);
|
||||
}
|
||||
|
||||
|
||||
@@ -398,6 +398,7 @@ public class ApplicationProperties {
|
||||
private Boolean enableAnalytics;
|
||||
private Boolean enablePosthog;
|
||||
private Boolean enableScarf;
|
||||
private Boolean enableDesktopInstallSlide;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private int maxDPI;
|
||||
@@ -693,6 +694,7 @@ public class ApplicationProperties {
|
||||
private int weasyPrintSessionLimit;
|
||||
private int installAppSessionLimit;
|
||||
private int calibreSessionLimit;
|
||||
private int imageMagickSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
private int ghostscriptSessionLimit;
|
||||
@@ -730,6 +732,10 @@ public class ApplicationProperties {
|
||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getImageMagickSessionLimit() {
|
||||
return imageMagickSessionLimit > 0 ? imageMagickSessionLimit : 4;
|
||||
}
|
||||
|
||||
public int getGhostscriptSessionLimit() {
|
||||
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||
}
|
||||
@@ -759,6 +765,8 @@ public class ApplicationProperties {
|
||||
@JsonProperty("calibretimeoutMinutes")
|
||||
private long calibreTimeoutMinutes;
|
||||
|
||||
private long imageMagickTimeoutMinutes;
|
||||
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
private long ghostscriptTimeoutMinutes;
|
||||
@@ -796,6 +804,10 @@ public class ApplicationProperties {
|
||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getImageMagickTimeoutMinutes() {
|
||||
return imageMagickTimeoutMinutes > 0 ? imageMagickTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getGhostscriptTimeoutMinutes() {
|
||||
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
.getSessionLimit()
|
||||
.getCalibreSessionLimit();
|
||||
case IMAGEMAGICK ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getImageMagickSessionLimit();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -141,6 +146,11 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getCalibreTimeoutMinutes();
|
||||
case IMAGEMAGICK ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getImageMagickTimeoutMinutes();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -301,6 +311,7 @@ public class ProcessExecutor {
|
||||
WEASYPRINT,
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
IMAGEMAGICK,
|
||||
TESSERACT,
|
||||
QPDF,
|
||||
GHOSTSCRIPT,
|
||||
|
||||
@@ -26,6 +26,7 @@ public class RequestUriUtils {
|
||||
|| normalizedUri.startsWith("/public/")
|
||||
|| normalizedUri.startsWith("/pdfjs/")
|
||||
|| normalizedUri.startsWith("/pdfjs-legacy/")
|
||||
|| normalizedUri.startsWith("/pdfium/")
|
||||
|| normalizedUri.startsWith("/assets/")
|
||||
|| normalizedUri.startsWith("/locales/")
|
||||
|| normalizedUri.startsWith("/Login/")
|
||||
@@ -61,7 +62,8 @@ public class RequestUriUtils {
|
||||
|| normalizedUri.endsWith(".css")
|
||||
|| normalizedUri.endsWith(".mjs")
|
||||
|| normalizedUri.endsWith(".html")
|
||||
|| normalizedUri.endsWith(".toml");
|
||||
|| normalizedUri.endsWith(".toml")
|
||||
|| normalizedUri.endsWith(".wasm");
|
||||
}
|
||||
|
||||
public static boolean isFrontendRoute(String contextPath, String requestURI) {
|
||||
@@ -125,11 +127,13 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|| requestURI.endsWith(".js")
|
||||
|| requestURI.endsWith(".toml")
|
||||
|| requestURI.endsWith(".wasm")
|
||||
|| requestURI.contains("swagger")
|
||||
|| requestURI.startsWith("/api/v1/info")
|
||||
|| requestURI.startsWith("/site.webmanifest")
|
||||
|| requestURI.startsWith("/fonts")
|
||||
|| requestURI.startsWith("/pdfjs"));
|
||||
|| requestURI.startsWith("/pdfjs")
|
||||
|| requestURI.startsWith("/pdfium"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,9 @@ public class RequestUriUtilsTest {
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"),
|
||||
"PDF.js files should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/pdfium/pdfium.wasm"),
|
||||
"PDFium wasm should be static");
|
||||
assertTrue(
|
||||
RequestUriUtils.isStaticResource("/api/v1/info/status"),
|
||||
"API status should be static");
|
||||
@@ -110,7 +113,8 @@ public class RequestUriUtilsTest {
|
||||
"/downloads/document.png",
|
||||
"/assets/brand.ico",
|
||||
"/any/path/with/image.svg",
|
||||
"/deep/nested/folder/icon.png"
|
||||
"/deep/nested/folder/icon.png",
|
||||
"/pdfium/pdfium.wasm"
|
||||
})
|
||||
void testIsStaticResourceWithFileExtensions(String path) {
|
||||
assertTrue(
|
||||
@@ -148,6 +152,9 @@ public class RequestUriUtilsTest {
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/script.js"),
|
||||
"JS files should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/pdfium/pdfium.wasm"),
|
||||
"PDFium wasm should not be trackable");
|
||||
assertFalse(
|
||||
RequestUriUtils.isTrackableResource("/swagger/index.html"),
|
||||
"Swagger files should not be trackable");
|
||||
@@ -224,7 +231,8 @@ public class RequestUriUtilsTest {
|
||||
"/api/v1/info/health",
|
||||
"/site.webmanifest",
|
||||
"/fonts/roboto.woff",
|
||||
"/pdfjs/viewer.js"
|
||||
"/pdfjs/viewer.js",
|
||||
"/pdfium/pdfium.wasm"
|
||||
})
|
||||
void testNonTrackableResources(String path) {
|
||||
assertFalse(
|
||||
|
||||
@@ -46,6 +46,7 @@ public class ExternalAppDepConfig {
|
||||
put("qpdf", List.of("qpdf"));
|
||||
put("tesseract", List.of("tesseract"));
|
||||
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(unoconvPath);
|
||||
checkDependencyAndDisableGroup("rar");
|
||||
checkDependencyAndDisableGroup("magick");
|
||||
// Special handling for Python/OpenCV dependencies
|
||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||
if (!pythonAvailable) {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -25,6 +29,20 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Cache hashed assets (JS/CSS with content hashes) for 1 year
|
||||
// These files have names like index-ChAS4tCC.js that change when content changes
|
||||
registry.addResourceHandler("/assets/**")
|
||||
.addResourceLocations("classpath:/static/assets/")
|
||||
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());
|
||||
|
||||
// Don't cache index.html - it needs to be fresh to reference latest hashed assets
|
||||
registry.addResourceHandler("/index.html")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.setCacheControl(CacheControl.noCache().mustRevalidate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// Check if running in Tauri mode
|
||||
|
||||
@@ -28,10 +28,13 @@ import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
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.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
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.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.LineArtConversionService;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -58,6 +62,9 @@ public class CompressController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@Autowired(required = false)
|
||||
private LineArtConversionService lineArtConversionService;
|
||||
|
||||
private boolean isQpdfEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||
}
|
||||
@@ -66,6 +73,10 @@ public class CompressController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
private boolean isImageMagickEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("ImageMagick");
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@@ -660,6 +671,9 @@ public class CompressController {
|
||||
Integer optimizeLevel = request.getOptimizeLevel();
|
||||
String expectedOutputSizeString = request.getExpectedOutputSize();
|
||||
Boolean convertToGrayscale = request.getGrayscale();
|
||||
Boolean convertToLineArt = request.getLineArt();
|
||||
Double lineArtThreshold = request.getLineArtThreshold();
|
||||
Integer lineArtEdgeLevel = request.getLineArtEdgeLevel();
|
||||
if (expectedOutputSizeString == null && optimizeLevel == null) {
|
||||
throw new Exception("Both expected output size and optimize level are not specified");
|
||||
}
|
||||
@@ -689,6 +703,26 @@ public class CompressController {
|
||||
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 imageCompressionApplied = 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
|
||||
private void applyGhostscriptCompression(
|
||||
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
||||
|
||||
@@ -124,6 +124,9 @@ public class ConfigController {
|
||||
"enableAnalytics", applicationProperties.getSystem().getEnableAnalytics());
|
||||
configData.put("enablePosthog", applicationProperties.getSystem().getEnablePosthog());
|
||||
configData.put("enableScarf", applicationProperties.getSystem().getEnableScarf());
|
||||
configData.put(
|
||||
"enableDesktopInstallSlide",
|
||||
applicationProperties.getSystem().getEnableDesktopInstallSlide());
|
||||
|
||||
// Premium/Enterprise settings
|
||||
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
|
||||
@@ -227,4 +230,10 @@ public class ConfigController {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,12 @@ public class CertSignController {
|
||||
|
||||
switch (certType) {
|
||||
case "PEM":
|
||||
privateKeyFile =
|
||||
validateFilePresent(
|
||||
privateKeyFile, "PEM private key", "private key file is required");
|
||||
certFile =
|
||||
validateFilePresent(
|
||||
certFile, "PEM certificate", "certificate file is required");
|
||||
ks = KeyStore.getInstance("JKS");
|
||||
ks.load(null);
|
||||
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
|
||||
@@ -200,10 +206,16 @@ public class CertSignController {
|
||||
break;
|
||||
case "PKCS12":
|
||||
case "PFX":
|
||||
p12File =
|
||||
validateFilePresent(
|
||||
p12File, "PKCS12 keystore", "PKCS12/PFX keystore file is required");
|
||||
ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(p12File.getInputStream(), password.toCharArray());
|
||||
break;
|
||||
case "JKS":
|
||||
jksfile =
|
||||
validateFilePresent(
|
||||
jksfile, "JKS keystore", "JKS keystore file is required");
|
||||
ks = KeyStore.getInstance("JKS");
|
||||
ks.load(jksfile.getInputStream(), password.toCharArray());
|
||||
break;
|
||||
@@ -251,6 +263,17 @@ public class CertSignController {
|
||||
GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf"));
|
||||
}
|
||||
|
||||
private MultipartFile validateFilePresent(
|
||||
MultipartFile file, String argumentName, String errorDescription) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument",
|
||||
"Invalid argument: {0}",
|
||||
argumentName + " - " + errorDescription);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
|
||||
throws IOException, OperatorCreationException, PKCSException {
|
||||
try (PEMParser pemParser =
|
||||
|
||||
@@ -74,13 +74,13 @@ public class ReactRoutingController {
|
||||
}
|
||||
|
||||
@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 {
|
||||
return serveIndexHtml(request);
|
||||
}
|
||||
|
||||
@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)
|
||||
throws IOException {
|
||||
return serveIndexHtml(request);
|
||||
|
||||
@@ -45,4 +45,26 @@ public class OptimizePdfRequest extends PDFFile {
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
defaultValue = "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;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ system:
|
||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
||||
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
||||
enableAnalytics: null # Master toggle for analytics: set to 'true' to enable all analytics, 'false' to disable all analytics, or leave as 'null' to prompt admin on first launch
|
||||
enableDesktopInstallSlide: true # Set to 'false' to hide the desktop app installation slide in the onboarding flow
|
||||
enablePosthog: null # Enable PostHog analytics (open-source product analytics): set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
|
||||
enableScarf: null # Enable Scarf tracking pixel: set to 'true' to enable, 'false' to disable, or 'null' to enable by default when analytics is enabled
|
||||
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
|
||||
@@ -206,6 +207,7 @@ processExecutor:
|
||||
weasyPrintSessionLimit: 16
|
||||
installAppSessionLimit: 1
|
||||
calibreSessionLimit: 1
|
||||
imageMagickSessionLimit: 4
|
||||
ghostscriptSessionLimit: 8
|
||||
ocrMyPdfSessionLimit: 2
|
||||
timeoutMinutes: # Process executor timeout in minutes
|
||||
@@ -215,6 +217,7 @@ processExecutor:
|
||||
weasyPrinttimeoutMinutes: 30
|
||||
installApptimeoutMinutes: 60
|
||||
calibretimeoutMinutes: 30
|
||||
imageMagickTimeoutMinutes: 30
|
||||
tesseractTimeoutMinutes: 30
|
||||
qpdfTimeoutMinutes: 30
|
||||
ghostscriptTimeoutMinutes: 30
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
@@ -107,7 +108,8 @@ class CertSignControllerTest {
|
||||
derCertBytes = baos.toByteArray();
|
||||
}
|
||||
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
MultipartFile file = invocation.getArgument(0);
|
||||
@@ -167,6 +169,31 @@ class CertSignControllerTest {
|
||||
assertTrue(response.getBody().length > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSignPdfWithMissingPkcs12FileThrowsError() {
|
||||
MockMultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
|
||||
SignPDFWithCertRequest request = new SignPDFWithCertRequest();
|
||||
request.setFileInput(pdfFile);
|
||||
request.setCertType("PFX");
|
||||
request.setPassword("password");
|
||||
request.setShowSignature(false);
|
||||
request.setReason("test");
|
||||
request.setLocation("test");
|
||||
request.setName("tester");
|
||||
request.setPageNumber(1);
|
||||
request.setShowLogo(false);
|
||||
|
||||
IllegalArgumentException exception =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> certSignController.signPDFWithCert(request));
|
||||
|
||||
assertTrue(exception.getMessage().contains("PKCS12 keystore"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSignPdfWithJks() throws Exception {
|
||||
MockMultipartFile pdfFile =
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.api.UserApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
|
||||
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
|
||||
@@ -34,7 +33,6 @@ import stirling.software.proprietary.service.SignatureService;
|
||||
* authentication and enforces per-user storage limits. All endpoints require authentication
|
||||
* via @PreAuthorize("isAuthenticated()").
|
||||
*/
|
||||
@UserApi
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/signatures")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user