refactor(merge,split,json): adopt streaming approach and standardize types, address gradle warnings (#5803)

Co-authored-by: Anthony Stirling <[email protected]>
Co-authored-by: Balázs <[email protected]>
This commit is contained in:
Balázs Szücs
2026-03-02 21:55:07 +00:00
committed by GitHub
co-authored by Anthony Stirling Balázs
parent 0c46f77179
commit fd1b7abc83
66 changed files with 1425 additions and 1430 deletions
@@ -103,8 +103,9 @@ public class EndpointConfiguration {
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
// group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
// Skip tool groups (qpdf, OCRmyPDF, Ghostscript, LibreOffice, etc.)
if (!isToolGroup(group)) {
log.debug(
@@ -131,10 +132,11 @@ public class EndpointConfiguration {
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
// to any disabled tool groups
for (String group : endpointGroups.keySet()) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (isToolGroup(group)
&& disabledGroups.contains(group)
&& endpointGroups.get(group).contains(endpoint)) {
&& entry.getValue().contains(endpoint)) {
log.debug(
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
original,
@@ -645,8 +647,9 @@ public class EndpointConfiguration {
}
// Check if endpoint belongs to any disabled functional group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
if (!isToolGroup(group)) {
return false;
}
@@ -365,11 +365,11 @@ public class ApplicationProperties {
}
public boolean isSettingsValid() {
return !ValidationUtils.isStringEmpty(this.getIssuer())
&& !ValidationUtils.isStringEmpty(this.getClientId())
&& !ValidationUtils.isStringEmpty(this.getClientSecret())
&& !ValidationUtils.isCollectionEmpty(this.getScopes())
&& !ValidationUtils.isStringEmpty(this.getUseAsUsername());
return !ValidationUtils.isStringEmpty(this.issuer)
&& !ValidationUtils.isStringEmpty(this.clientId)
&& !ValidationUtils.isStringEmpty(this.clientSecret)
&& !ValidationUtils.isCollectionEmpty(this.scopes)
&& !ValidationUtils.isStringEmpty(this.useAsUsername);
}
@Data
@@ -575,19 +575,17 @@ public class ApplicationProperties {
}
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
return this.enableAnalytics != null && this.enableAnalytics;
}
public boolean isPosthogEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
return this.isAnalyticsEnabled() && (this.enablePosthog == null || this.enablePosthog);
}
public boolean isScarfEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnableScarf() == null || this.getEnableScarf());
return this.isAnalyticsEnabled() && (this.enableScarf == null || this.enableScarf);
}
}
@@ -42,7 +42,7 @@ public enum Role {
// Using the fromString method to get the Role enum based on the roleId
Role role = fromString(roleId);
// Return the roleName of the found Role enum
return role.getRoleName();
return role.roleName;
}
// Method to retrieve all role IDs and role names
@@ -50,14 +50,14 @@ public enum Role {
// Using LinkedHashMap to preserve order
Map<String, String> roleDetails = new LinkedHashMap<>();
for (Role role : Role.values()) {
roleDetails.put(role.getRoleId(), role.getRoleName());
roleDetails.put(role.roleId, role.roleName);
}
return roleDetails;
}
public static Role fromString(String roleId) {
for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) {
if (role.roleId.equalsIgnoreCase(roleId)) {
return role;
}
}
@@ -117,13 +117,13 @@ public class Provider {
+ ", clientName="
+ getClientName()
+ ", clientId="
+ getClientId()
+ clientId
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ (clientSecret != null && !clientSecret.isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ useAsUsername
+ "]";
}
}
@@ -1,6 +1,8 @@
package stirling.software.common.service;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
@@ -21,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FileStorage {
/** Holds the result of a stream-to-disk store operation: the file ID and the bytes written. */
public record StoredFile(String fileId, long size) {}
@Value("${stirling.tempDir:/tmp/stirling-files}")
private String tempDirPath;
@@ -104,6 +109,40 @@ public class FileStorage {
return Files.readAllBytes(filePath);
}
/**
* Retrieve a file by its ID as a streaming InputStream. The caller is responsible for closing
* the returned stream.
*
* @param fileId The ID of the file to retrieve
* @return A buffered InputStream for the file
* @throws IOException If the file doesn't exist or can't be read
*/
public InputStream retrieveInputStream(String fileId) throws IOException {
Path filePath = getFilePath(fileId);
// Let Files.newInputStream throw NoSuchFileException naturally — avoids TOCTOU race
// between exists-check and open when another thread may delete concurrently.
return new BufferedInputStream(Files.newInputStream(filePath));
}
/**
* Store data from an InputStream as a file and return its unique ID and byte count. Streams
* directly to disk without buffering the entire content in heap.
*
* @param inputStream The input stream to read from
* @param originalName The original name of the file (unused, kept for API symmetry)
* @return A {@link StoredFile} containing the file ID and the number of bytes written
* @throws IOException If there is an error storing the file
*/
public StoredFile storeInputStream(InputStream inputStream, String originalName)
throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
long size = Files.copy(inputStream, filePath);
log.debug("Stored input stream with ID: {}", fileId);
return new StoredFile(fileId, size);
}
/**
* Delete a file by its ID
*
@@ -316,7 +316,7 @@ public class JobExecutorService {
filename =
disposition.substring(
disposition.indexOf("filename=") + 9,
disposition.lastIndexOf("\""));
disposition.lastIndexOf('"'));
}
}
@@ -129,7 +129,7 @@ public class MobileScannerService {
FILE_EXTENSION_PATTERN.matcher(safeFilename).replaceFirst("");
String ext =
safeFilename.contains(".")
? safeFilename.substring(safeFilename.lastIndexOf("."))
? safeFilename.substring(safeFilename.lastIndexOf('.'))
: "";
safeFilename = nameWithoutExt + "-" + counter + ext;
filePath = sessionDir.resolve(safeFilename).normalize().toAbsolutePath();
@@ -1,8 +1,8 @@
package stirling.software.common.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -19,7 +19,6 @@ import java.util.zip.ZipInputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
@@ -363,39 +362,29 @@ public class TaskManager {
String zipFileId, String originalZipFileName) throws IOException {
List<ResultFile> extractedFiles = new ArrayList<>();
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(zipFile.getBytes()))) {
try (InputStream fileStream = fileStorage.retrieveInputStream(zipFileId);
ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new BufferedInputStream(fileStream))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
// Use buffered reading for memory safety
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = zipIn.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
byte[] fileContent = out.toByteArray();
String contentType = determineContentType(entry.getName());
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
// storeInputStream returns the fileId and byte count — no extra stat needed
FileStorage.StoredFile stored =
fileStorage.storeInputStream(zipIn, entry.getName());
ResultFile resultFile =
ResultFile.builder()
.fileId(individualFileId)
.fileId(stored.fileId())
.fileName(entry.getName())
.contentType(contentType)
.fileSize(fileContent.length)
.fileSize(stored.size())
.build();
extractedFiles.add(resultFile);
log.debug(
"Extracted file: {} (size: {} bytes)",
entry.getName(),
fileContent.length);
"Extracted file: {} (size: {} bytes)", entry.getName(), stored.size());
}
zipIn.closeEntry();
}
@@ -4,6 +4,7 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
@@ -30,15 +31,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
@UtilityClass
public class CbzUtils {
public byte[] convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false);
}
public byte[] convertCbzToPdf(
public TempFile convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager,
@@ -64,70 +57,90 @@ public class CbzUtils {
try (PDDocument document = pdfDocumentFactory.createNewDocument();
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
// Pass 1: collect sorted image names (cheap just strings, no image data)
List<String> sortedImageNames = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<ImageEntryData> imageEntries = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && isImageFile(entry.getName())) {
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
is.transferTo(baos);
imageEntries.add(
new ImageEntryData(entry.getName(), baos.toByteArray()));
} catch (IOException e) {
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
}
sortedImageNames.add(entry.getName());
}
}
sortedImageNames.sort(new NaturalOrderComparator());
imageEntries.sort(
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
if (imageEntries.isEmpty()) {
if (sortedImageNames.isEmpty()) {
throw ExceptionUtils.createCbzNoImagesException();
}
for (ImageEntryData imageEntry : imageEntries) {
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageEntry.data(), imageEntry.name());
PDPage page =
new PDPage(
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
// Pass 2: load ONE image at a time peak memory = max(single image)
for (String imageName : sortedImageNames) {
ZipEntry entry = zipFile.getEntry(imageName);
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream imgBaos = new ByteArrayOutputStream();
is.transferTo(imgBaos);
byte[] imageBytes = imgBaos.toByteArray();
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageBytes, imageName);
PDPage page =
new PDPage(
new PDRectangle(
pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
log.warn("Error processing image {}: {}", imageName, e.getMessage());
}
// imageBytes eligible for GC after each iteration
} catch (IOException e) {
log.warn(
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
log.warn("Error reading image {}: {}", imageName, e.getMessage());
}
}
if (document.getNumberOfPages() == 0) {
throw ExceptionUtils.createCbzCorruptedImagesException();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] pdfBytes = baos.toByteArray();
// Apply Ghostscript optimization if requested
if (optimizeForEbook) {
try {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
// Write to TempFile (not BAOS)
TempFile pdfTempFile = new TempFile(tempFileManager, ".pdf");
try {
document.save(pdfTempFile.getFile());
if (optimizeForEbook) {
try {
byte[] pdfBytes = Files.readAllBytes(pdfTempFile.getPath());
byte[] optimized = GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
pdfTempFile.close();
TempFile optimizedFile = new TempFile(tempFileManager, ".pdf");
try {
Files.write(optimizedFile.getPath(), optimized);
return optimizedFile;
} catch (Exception e) {
optimizedFile.close();
throw e;
}
} catch (IOException e) {
log.warn(
"Ghostscript optimization failed, returning unoptimized PDF",
e);
}
}
}
return pdfBytes;
return pdfTempFile;
} catch (Exception e) {
pdfTempFile.close();
throw e;
}
}
}
}
@@ -175,8 +188,6 @@ public class CbzUtils {
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
}
private record ImageEntryData(String name, byte[] data) {}
private class NaturalOrderComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
@@ -44,9 +44,7 @@ public class FileToPdf {
sanitizeHtmlContent(
new String(fileBytes, StandardCharsets.UTF_8),
customHtmlSanitizer);
Files.write(
tempInputFile.getPath(),
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
Files.writeString(tempInputFile.getPath(), sanitizedHtml);
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
Files.write(tempInputFile.getPath(), fileBytes);
sanitizeHtmlFilesInZip(
@@ -115,8 +113,7 @@ public class FileToPdf {
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
String sanitizedContent =
sanitizeHtmlContent(content, customHtmlSanitizer);
Files.write(
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
Files.writeString(filePath, sanitizedContent);
} else {
Files.copy(zipIn, filePath);
}
@@ -17,6 +17,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
@@ -74,6 +75,10 @@ public class FormUtils {
*/
private static final float SAME_LINE_THRESHOLD_PT = 10.0f;
private static final Pattern HEX_UUID_PATTERN =
Pattern.compile("^[0-9a-fA-F]{8}[0-9a-fA-F]{24,}$");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
/**
* Returns a normalized logical type string for the supplied PDFBox field instance. Centralized
* so all callers share identical mapping logic.
@@ -1357,7 +1362,7 @@ public class FormUtils {
if (da != null && !da.isBlank()) {
// Standard DA looks like: /Helv 12 Tf 0 g
// We want the number before 'Tf'
String[] tokens = da.split("\\s+");
String[] tokens = WHITESPACE_PATTERN.split(da);
for (int i = 0; i < tokens.length; i++) {
if ("Tf".equals(tokens[i]) && i > 0) {
try {
@@ -1457,9 +1462,8 @@ public class FormUtils {
// Detect UUID-like hex strings (e.g. "cdc47b7041524571 7b2d93017fe77bf7")
// Standard UUIDs are 32 hex characters; require at least that to avoid
// false positives on short hex-like field names.
String nospaces = simplified.replaceAll("\\s+", "");
if (nospaces.length() >= 32 && nospaces.matches("^[0-9a-fA-F]{8}[0-9a-fA-F]{24,}$"))
return true;
String nospaces = WHITESPACE_PATTERN.matcher(simplified).replaceAll("");
if (nospaces.length() >= 32 && HEX_UUID_PATTERN.matcher(nospaces).matches()) return true;
return patterns.getGenericFieldNamePattern().matcher(simplified).matches()
|| patterns.getSimpleFormFieldPattern().matcher(simplified).matches()
@@ -10,6 +10,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -33,6 +34,8 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@Slf4j
public class PDFToFile {
private static final Pattern PATTERN =
Pattern.compile("(!\\[.*?\\])\\((?!images/)([^/)][^)]*?)\\)");
private final TempFileManager tempFileManager;
private final RuntimePathConfig runtimePathConfig;
@@ -163,7 +166,7 @@ public class PDFToFile {
private String updateImageReferences(String markdown) {
// Match markdown image syntax: ![alt text](image.png)
// Only update if the path doesn't already start with images/
return markdown.replaceAll("(!\\[.*?\\])\\((?!images/)([^/)][^)]*?)\\)", "$1(images/$2)");
return PATTERN.matcher(markdown).replaceAll("$1(images/$2)");
}
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
@@ -1,8 +1,8 @@
package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -22,8 +22,11 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
public class PdfToCbzUtils {
public static byte[] convertPdfToCbz(
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
public static TempFile convertPdfToCbz(
MultipartFile pdfFile,
int dpi,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
validatePdfFile(pdfFile);
@@ -33,7 +36,7 @@ public class PdfToCbzUtils {
throw ExceptionUtils.createPdfNoPages();
}
return createCbzFromPdf(document, dpi);
return createCbzFromPdf(document, dpi, tempFileManager);
}
}
@@ -53,46 +56,49 @@ public class PdfToCbzUtils {
}
}
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
private static TempFile createCbzFromPdf(
PDDocument document, int dpi, TempFileManager tempFileManager) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
TempFile cbzTempFile = new TempFile(tempFileManager, ".cbz");
try {
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(cbzTempFile.getPath()))) {
int totalPages = document.getNumberOfPages();
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
final int currentPage = pageIndex;
try {
BufferedImage image =
ExceptionUtils.handleOomRendering(
currentPage + 1,
dpi,
() ->
pdfRenderer.renderImageWithDPI(
currentPage, dpi, ImageType.RGB));
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
String imageFilename =
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
zipOut.putNextEntry(new ZipEntry(imageFilename));
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBZ creation for page " + (currentPage + 1), e);
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
// Re-throw OOM exceptions without wrapping
throw e;
} catch (IOException e) {
// Wrap other IOExceptions with context
throw ExceptionUtils.createFileProcessingException(
"CBZ creation for page " + (currentPage + 1), e);
}
}
}
zipOut.finish();
return cbzOutputStream.toByteArray();
return cbzTempFile;
} catch (Exception e) {
cbzTempFile.close();
throw e;
}
}
@@ -219,58 +219,31 @@ public class PdfUtils {
int maxWidth = 0;
int totalHeight = 0;
BufferedImage pdfSizeImage = null;
int pdfSizeImageIndex = -1;
// Using a map to store the rendered dimensions of each page size
// to avoid rendering the same page sizes multiple times
// Using a map to store the calculated dimensions of each page size
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
new HashMap<>();
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
PDPage page = document.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
PDRectangle cropBox = page.getCropBox();
int rotation = page.getRotation();
PdfRenderSettingsKey settings =
new PdfRenderSettingsKey(
mediaBox.getWidth(), mediaBox.getHeight(), rotation);
cropBox.getWidth(), cropBox.getHeight(), rotation);
PdfImageDimensionValue dimension = pageSizes.get(settings);
if (dimension == null) {
// Render the image to get the dimensions
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
page, pageIndex + 1, DPI);
pdfSizeImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
.contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigExceedsArray",
"PDF page {0} is too large to render at {1} DPI. The"
+ " resulting image would exceed Java's maximum"
+ " array size. Please try a lower DPI value"
+ " (recommended: 150 or less).",
i + 1,
DPI);
}
throw e;
float scale = DPI / 72f;
int widthPx = (int) Math.max(Math.floor(cropBox.getWidth() * scale), 1);
int heightPx =
(int) Math.max(Math.floor(cropBox.getHeight() * scale), 1);
if (rotation == 90 || rotation == 270) {
int tmp = widthPx;
widthPx = heightPx;
heightPx = tmp;
}
pdfSizeImageIndex = i;
dimension =
new PdfImageDimensionValue(
pdfSizeImage.getWidth(), pdfSizeImage.getHeight());
dimension = new PdfImageDimensionValue(widthPx, heightPx);
pageSizes.put(settings, dimension);
if (pdfSizeImage.getWidth() > maxWidth) {
maxWidth = pdfSizeImage.getWidth();
if (widthPx > maxWidth) {
maxWidth = widthPx;
}
}
totalHeight += dimension.height();
@@ -284,40 +257,32 @@ public class PdfUtils {
int currentHeight = 0;
BufferedImage pageImage;
// Check if the first image is the last rendered image
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
for (int i = 0; i < pageCount; ++i) {
final int pageIndex = i;
if (firstImageAlreadyRendered && i == 0) {
pageImage = pdfSizeImage;
} else {
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
try {
// Validate dimensions before rendering
ExceptionUtils.validateRenderingDimensions(
document.getPage(pageIndex), pageIndex + 1, DPI);
pageImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
.contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigForDpi",
"PDF page {0} is too large to render at {1} DPI. Please"
+ " try a lower DPI value (recommended: 150 or"
+ " less).",
i + 1,
DPI);
}
throw e;
pageImage =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
DPI,
() ->
pdfRenderer.renderImageWithDPI(
pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage().contains("Maximum size of image exceeded")) {
throw ExceptionUtils.createIllegalArgumentException(
"error.pageTooBigForDpi",
"PDF page {0} is too large to render at {1} DPI. Please"
+ " try a lower DPI value (recommended: 150 or"
+ " less).",
i + 1,
DPI);
}
throw e;
}
// Calculate the x-coordinate to center the image
@@ -191,7 +191,7 @@ public class YamlHelper {
mappingNode.getValue().clear();
mappingNode.getValue().addAll(updatedTuples);
}
setNewNode(node);
updatedRootNode = node;
return updated;
}
@@ -19,7 +19,7 @@ public abstract class ReplaceAndInvertColorStrategy extends PDFFile {
public ReplaceAndInvertColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
setFileInput(file);
setReplaceAndInvert(replaceAndInvert);
this.replaceAndInvert = replaceAndInvert;
}
public abstract InputStreamResource replace() throws IOException;
@@ -88,10 +88,10 @@ public abstract class CreateSignatureBase implements SignatureInterface {
Certificate cert = null;
while (cert == null && aliases.hasMoreElements()) {
alias = aliases.nextElement();
setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
privateKey = (PrivateKey) keystore.getKey(alias, pin);
Certificate[] certChain = keystore.getCertificateChain(alias);
if (certChain != null) {
setCertificateChain(certChain);
certificateChain = certChain;
cert = certChain[0];
if (cert instanceof X509Certificate) {
// avoid expired certificate
@@ -134,7 +134,7 @@ public class SPDFApplication {
baseUrlStatic = normalizeBackendUrl(backendUrl, serverPort);
contextPathStatic = contextPath;
serverPortStatic = serverPort;
String url = buildFullUrl(baseUrlStatic, getStaticPort(), contextPathStatic);
String url = buildFullUrl(baseUrlStatic, serverPortStatic, contextPathStatic);
// Log Tauri mode information
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) {
@@ -188,7 +188,7 @@ public class SPDFApplication {
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = buildFullUrl(baseUrlStatic, getStaticPort(), contextPathStatic);
String url = buildFullUrl(baseUrlStatic, serverPortStatic, contextPathStatic);
log.info("Navigate to {}", url);
}
@@ -104,7 +104,7 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
String infoString = mappingInfo.toString();
if (infoString.contains("{")) {
String patternsSection =
infoString.substring(infoString.indexOf("{") + 1, infoString.indexOf("}"));
infoString.substring(infoString.indexOf('{') + 1, infoString.indexOf('}'));
for (String pattern : patternsSection.split(",")) {
pattern = pattern.trim();
@@ -1,7 +1,7 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
@@ -14,10 +14,10 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -31,6 +31,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -39,10 +41,9 @@ import stirling.software.common.util.WebResponseUtils;
public class PosterPdfController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
value = "/split-for-poster-print",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@AutoJobPostMapping(value = "/split-for-poster-print", consumes = "multipart/form-data")
@MultiFileResponse
@Operation(
summary = "Split large PDF pages into smaller printable chunks",
@@ -51,7 +52,7 @@ public class PosterPdfController {
+ "suitable for printing on standard paper sizes (e.g., A4, Letter). "
+ "Divides each page into a grid of smaller pages using Apache PDFBox. "
+ "Input: PDF Output: ZIP-PDF Type: SISO")
public ResponseEntity<byte[]> posterPdf(@ModelAttribute PosterPdfRequest request)
public ResponseEntity<StreamingResponseBody> posterPdf(@ModelAttribute PosterPdfRequest request)
throws Exception {
log.debug("Starting PDF poster split process with request: {}", request);
@@ -60,184 +61,187 @@ public class PosterPdfController {
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "");
log.debug("Base filename for output: {}", filename);
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument outputDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream()) {
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try {
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument outputDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
TempFile pdfTempFile = new TempFile(tempFileManager, ".pdf")) {
// Get target page size
PDRectangle targetPageSize = getTargetPageSize(request.getPageSize());
log.debug(
"Target page size: {} ({}x{})",
request.getPageSize(),
targetPageSize.getWidth(),
targetPageSize.getHeight());
// Get target page size
PDRectangle targetPageSize = getTargetPageSize(request.getPageSize());
log.debug(
"Target page size: {} ({}x{})",
request.getPageSize(),
targetPageSize.getWidth(),
targetPageSize.getHeight());
// Create LayerUtility for importing pages as forms
LayerUtility layerUtility = new LayerUtility(outputDocument);
// Create LayerUtility for importing pages as forms
LayerUtility layerUtility = new LayerUtility(outputDocument);
int totalPages = sourceDocument.getNumberOfPages();
int xFactor = request.getXFactor();
int yFactor = request.getYFactor();
boolean rightToLeft = request.isRightToLeft();
log.debug(
"Processing {} pages with grid {}x{}, RTL={}",
totalPages,
xFactor,
yFactor,
rightToLeft);
// Process each page
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
PDPage sourcePage = sourceDocument.getPage(pageIndex);
// Get both MediaBox and CropBox
PDRectangle mediaBox = sourcePage.getMediaBox();
PDRectangle cropBox = sourcePage.getCropBox();
// If no CropBox is set, use MediaBox
if (cropBox == null) {
cropBox = mediaBox;
}
// Save original boxes for restoration
PDRectangle originalMediaBox = sourcePage.getMediaBox();
PDRectangle originalCropBox = sourcePage.getCropBox();
// Normalize the page: set MediaBox to CropBox
// This ensures the form's coordinate space starts at (0, 0)
// instead of having an offset from the original MediaBox
sourcePage.setMediaBox(cropBox);
sourcePage.setCropBox(cropBox);
// Handle page rotation
int rotation = sourcePage.getRotation();
float sourceWidth = cropBox.getWidth();
float sourceHeight = cropBox.getHeight();
// Swap dimensions if rotated 90 or 270 degrees
if (rotation == 90 || rotation == 270) {
float temp = sourceWidth;
sourceWidth = sourceHeight;
sourceHeight = temp;
}
int totalPages = sourceDocument.getNumberOfPages();
int xFactor = request.getXFactor();
int yFactor = request.getYFactor();
boolean rightToLeft = request.isRightToLeft();
log.debug(
"Page {}: Normalized to CropBox dimensions {}x{}, rotation {}",
pageIndex,
sourceWidth,
sourceHeight,
rotation);
"Processing {} pages with grid {}x{}, RTL={}",
totalPages,
xFactor,
yFactor,
rightToLeft);
// Import source page as form (now with normalized coordinate space)
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
// Process each page
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
PDPage sourcePage = sourceDocument.getPage(pageIndex);
// Restore original boxes
sourcePage.setMediaBox(originalMediaBox);
sourcePage.setCropBox(originalCropBox);
// Get both MediaBox and CropBox
PDRectangle mediaBox = sourcePage.getMediaBox();
PDRectangle cropBox = sourcePage.getCropBox();
// Calculate cell dimensions in source page coordinates
float cellWidth = sourceWidth / xFactor;
float cellHeight = sourceHeight / yFactor;
// If no CropBox is set, use MediaBox
if (cropBox == null) {
cropBox = mediaBox;
}
// Create grid cells (rows × columns)
for (int row = 0; row < yFactor; row++) {
for (int col = 0; col < xFactor; col++) {
// Apply RTL ordering for columns if enabled
int actualCol = rightToLeft ? (xFactor - 1 - col) : col;
// Save original boxes for restoration
PDRectangle originalMediaBox = sourcePage.getMediaBox();
PDRectangle originalCropBox = sourcePage.getCropBox();
// Calculate crop rectangle in source coordinates
// PDF coordinates start at bottom-left
float cropX = actualCol * cellWidth;
// For Y: invert so row 0 shows TOP (following SplitPdfBySectionsController
// pattern)
float cropY = (yFactor - 1 - row) * cellHeight;
// Normalize the page: set MediaBox to CropBox
// This ensures the form's coordinate space starts at (0, 0)
// instead of having an offset from the original MediaBox
sourcePage.setMediaBox(cropBox);
sourcePage.setCropBox(cropBox);
// Create new output page with target size
PDPage outputPage = new PDPage(targetPageSize);
outputDocument.addPage(outputPage);
// Handle page rotation
int rotation = sourcePage.getRotation();
float sourceWidth = cropBox.getWidth();
float sourceHeight = cropBox.getHeight();
try (PDPageContentStream contentStream =
new PDPageContentStream(
outputDocument,
outputPage,
PDPageContentStream.AppendMode.APPEND,
true,
true)) {
// Swap dimensions if rotated 90 or 270 degrees
if (rotation == 90 || rotation == 270) {
float temp = sourceWidth;
sourceWidth = sourceHeight;
sourceHeight = temp;
}
// Calculate uniform scale to fit cell into target page
// Scale UP if cell is smaller than target, scale DOWN if larger
float scaleX = targetPageSize.getWidth() / cellWidth;
float scaleY = targetPageSize.getHeight() / cellHeight;
float scale = Math.min(scaleX, scaleY);
log.debug(
"Page {}: Normalized to CropBox dimensions {}x{}, rotation {}",
pageIndex,
sourceWidth,
sourceHeight,
rotation);
// Center the scaled content on the target page
float scaledCellWidth = cellWidth * scale;
float scaledCellHeight = cellHeight * scale;
float offsetX = (targetPageSize.getWidth() - scaledCellWidth) / 2;
float offsetY = (targetPageSize.getHeight() - scaledCellHeight) / 2;
// Import source page as form (now with normalized coordinate space)
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
// Apply transformations
contentStream.saveGraphicsState();
// Restore original boxes
sourcePage.setMediaBox(originalMediaBox);
sourcePage.setCropBox(originalCropBox);
// Translate to center position
contentStream.transform(Matrix.getTranslateInstance(offsetX, offsetY));
// Calculate cell dimensions in source page coordinates
float cellWidth = sourceWidth / xFactor;
float cellHeight = sourceHeight / yFactor;
// Scale uniformly
contentStream.transform(Matrix.getScaleInstance(scale, scale));
// Create grid cells (rows × columns)
for (int row = 0; row < yFactor; row++) {
for (int col = 0; col < xFactor; col++) {
// Apply RTL ordering for columns if enabled
int actualCol = rightToLeft ? (xFactor - 1 - col) : col;
// Translate to show only the desired grid cell
// IMPORTANT: The PDFormXObject's BBox already matches the CropBox
// (including its offset), so we only need to translate by cropX/cropY
// relative to the CropBox origin, NOT the MediaBox origin
contentStream.transform(Matrix.getTranslateInstance(-cropX, -cropY));
// Calculate crop rectangle in source coordinates
// PDF coordinates start at bottom-left
float cropX = actualCol * cellWidth;
// For Y: invert so row 0 shows TOP (following
// SplitPdfBySectionsController
// pattern)
float cropY = (yFactor - 1 - row) * cellHeight;
// Draw the form
contentStream.drawForm(form);
// Create new output page with target size
PDPage outputPage = new PDPage(targetPageSize);
outputDocument.addPage(outputPage);
contentStream.restoreGraphicsState();
try (PDPageContentStream contentStream =
new PDPageContentStream(
outputDocument,
outputPage,
PDPageContentStream.AppendMode.APPEND,
true,
true)) {
// Calculate uniform scale to fit cell into target page
// Scale UP if cell is smaller than target, scale DOWN if larger
float scaleX = targetPageSize.getWidth() / cellWidth;
float scaleY = targetPageSize.getHeight() / cellHeight;
float scale = Math.min(scaleX, scaleY);
// Center the scaled content on the target page
float scaledCellWidth = cellWidth * scale;
float scaledCellHeight = cellHeight * scale;
float offsetX = (targetPageSize.getWidth() - scaledCellWidth) / 2;
float offsetY = (targetPageSize.getHeight() - scaledCellHeight) / 2;
// Apply transformations
contentStream.saveGraphicsState();
// Translate to center position
contentStream.transform(
Matrix.getTranslateInstance(offsetX, offsetY));
// Scale uniformly
contentStream.transform(Matrix.getScaleInstance(scale, scale));
// Translate to show only the desired grid cell
// IMPORTANT: The PDFormXObject's BBox already matches the CropBox
// (including its offset), so we only need to translate by
// cropX/cropY
// relative to the CropBox origin, NOT the MediaBox origin
contentStream.transform(
Matrix.getTranslateInstance(-cropX, -cropY));
// Draw the form
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
}
log.trace(
"Created output page for grid cell [{},{}] of page {}: cropX={}, cropY={}, translate=({}, {})",
row,
actualCol,
pageIndex,
cropX,
cropY,
-cropX,
-cropY);
}
log.trace(
"Created output page for grid cell [{},{}] of page {}: cropX={}, cropY={}, translate=({}, {})",
row,
actualCol,
pageIndex,
cropX,
cropY,
-cropX,
-cropY);
}
}
// Save output PDF to intermediate TempFile
outputDocument.save(pdfTempFile.getFile());
log.debug("Generated output PDF with {} pages", outputDocument.getNumberOfPages());
// Create ZIP from the PDF TempFile, streaming directly to zip TempFile
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
zipOut.putNextEntry(new ZipEntry(filename + "_poster.pdf"));
Files.copy(pdfTempFile.getPath(), zipOut);
zipOut.closeEntry();
}
// pdfTempFile auto-closed and deleted here (end of inner try-with-resources)
}
// Save output PDF
outputDocument.save(pdfOutputStream);
byte[] pdfData = pdfOutputStream.toByteArray();
log.debug(
"Generated output PDF with {} pages ({} bytes)",
outputDocument.getNumberOfPages(),
pdfData.length);
// Create ZIP file with the result
try (ZipOutputStream zipOut = new ZipOutputStream(zipOutputStream)) {
ZipEntry zipEntry = new ZipEntry(filename + "_poster.pdf");
zipOut.putNextEntry(zipEntry);
zipOut.write(pdfData);
zipOut.closeEntry();
}
byte[] zipData = zipOutputStream.toByteArray();
log.debug("Successfully created ZIP with {} bytes", zipData.length);
return WebResponseUtils.bytesToWebResponse(
zipData, filename + "_poster.zip", MediaType.APPLICATION_OCTET_STREAM);
log.debug("Successfully created ZIP");
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + "_poster.zip");
} catch (IOException e) {
ExceptionUtils.logException("PDF poster split process", e);
zipTempFile.close();
throw e;
} catch (Exception e) {
zipTempFile.close();
throw e;
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -10,11 +9,11 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -49,83 +48,60 @@ public class SplitPDFController {
+ " specified page numbers or ranges. Users can specify pages using"
+ " individual numbers, ranges, or 'all' for every page. Input:PDF"
+ " Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
public ResponseEntity<StreamingResponseBody> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException {
MultipartFile file = request.getFileInput();
try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip");
PDDocument document = pdfDocumentFactory.load(file)) {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
log.debug(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
splitDocumentsBoas = new ArrayList<>(pageNumbers.size());
int previousPageNumber = 0;
for (int splitPoint : pageNumbers) {
try (PDDocument splitDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
for (int i = previousPageNumber; i <= splitPoint; i++) {
PDPage page = document.getPage(i);
splitDocument.addPage(page);
log.debug("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
// Transfer metadata to split pdf
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
} catch (Exception e) {
ExceptionUtils.logException("document splitting and saving", e);
throw e;
TempFile outputTempFile = new TempFile(tempFileManager, ".zip");
try {
try (PDDocument document = pdfDocumentFactory.load(file)) {
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
}
String baseFilename = GeneralUtils.removeExtension(file.getOriginalFilename());
log.debug(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) {
int splitDocumentsSize = splitDocumentsBoas.size();
for (int i = 0; i < splitDocumentsSize; i++) {
StringBuilder sb = new StringBuilder(baseFilename.length() + 10);
sb.append(baseFilename).append('_').append(i + 1).append(".pdf");
String fileName = sb.toString();
String baseFilename = GeneralUtils.removeExtension(file.getOriginalFilename());
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) {
int previousPageNumber = 0;
for (int splitIndex = 0; splitIndex < pageNumbers.size(); splitIndex++) {
int splitPoint = pageNumbers.get(splitIndex);
try (PDDocument splitDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
for (int i = previousPageNumber; i <= splitPoint; i++) {
splitDocument.addPage(document.getPage(i));
log.debug("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
log.debug("Wrote split document {} to zip file", fileName);
String fileName = baseFilename + "_" + (splitIndex + 1) + ".pdf";
zipOut.putNextEntry(new ZipEntry(fileName));
splitDocument.save(zipOut);
zipOut.closeEntry();
log.debug("Wrote split document {} to zip file", fileName);
} catch (Exception e) {
ExceptionUtils.logException("document splitting and saving", e);
throw e;
}
}
}
}
log.debug(
"Successfully created zip file with split documents: {}",
outputTempFile.getPath().toString());
byte[] data = Files.readAllBytes(outputTempFile.getPath());
String zipFilename =
GeneralUtils.generateFilename(file.getOriginalFilename(), "_split.zip");
return WebResponseUtils.bytesToWebResponse(
data, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(outputTempFile, zipFilename);
} catch (Exception e) {
outputTempFile.close();
throw e;
}
}
}
@@ -1,8 +1,6 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -17,6 +15,7 @@ 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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -36,6 +35,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -47,6 +48,8 @@ public class SplitPdfByChaptersController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static List<Bookmark> extractOutlineItems(
PDDocument sourceDocument,
PDOutlineItem current,
@@ -122,22 +125,19 @@ public class SplitPdfByChaptersController {
@Operation(
summary = "Split PDFs by Chapters",
description = "Splits a PDF into chapters and returns a ZIP file.")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> splitPdf(
@ModelAttribute SplitPdfByChaptersRequest request) throws Exception {
MultipartFile file = request.getFileInput();
PDDocument sourceDocument = null;
Path zipFile = null;
try {
boolean includeMetadata = Boolean.TRUE.equals(request.getIncludeMetadata());
Integer bookmarkLevel =
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
if (bookmarkLevel < 0) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
}
sourceDocument = pdfDocumentFactory.load(file);
boolean includeMetadata = Boolean.TRUE.equals(request.getIncludeMetadata());
Integer bookmarkLevel =
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
if (bookmarkLevel < 0) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
}
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
@@ -157,12 +157,10 @@ public class SplitPdfByChaptersController {
bookmarkLevel);
// to handle last page edge case
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
} catch (Exception e) {
ExceptionUtils.logException("outline extraction", e);
return ResponseEntity.internalServerError()
.body("Unable to extract outline items".getBytes());
throw e;
}
boolean allowDuplicates = Boolean.TRUE.equals(request.getAllowDuplicates());
@@ -181,29 +179,10 @@ public class SplitPdfByChaptersController {
bookmark.getStartPage(),
bookmark.getEndPage());
}
List<ByteArrayOutputStream> splitDocumentsBoas =
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
zipFile = createZipFile(bookmarks, splitDocumentsBoas);
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
TempFile zipTempFile = createZipFile(sourceDocument, bookmarks, includeMetadata);
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "");
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
try {
if (sourceDocument != null) {
sourceDocument.close();
}
if (zipFile != null) {
Files.deleteIfExists(zipFile);
}
} catch (Exception e) {
log.error("Error while cleaning up resources", e);
}
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip");
}
}
@@ -232,72 +211,55 @@ public class SplitPdfByChaptersController {
return bookmarks;
}
private Path createZipFile(
List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas)
throws Exception {
Path zipFile = Files.createTempFile("split_documents", ".zip");
String fileNumberFormatter = "%0" + (Integer.toString(bookmarks.size()).length()) + "d ";
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
// split files will be named as "[FILE_NUMBER] [BOOKMARK_TITLE].pdf"
String fileName =
String.format(Locale.ROOT, fileNumberFormatter, i)
+ bookmarks.get(i).getTitle()
+ ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
log.debug("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) {
log.error("Failed writing to zip", e);
throw e;
}
log.info("Successfully created zip file with split documents: {}", zipFile);
return zipFile;
}
public List<ByteArrayOutputStream> getSplitDocumentsBoas(
private TempFile createZipFile(
PDDocument sourceDocument, List<Bookmark> bookmarks, boolean includeMetadata)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
PdfMetadata metadata = null;
if (includeMetadata) {
metadata = pdfMetadataService.extractMetadataFromPdf(sourceDocument);
}
for (Bookmark bookmark : bookmarks) {
try (PDDocument splitDocument = new PDDocument()) {
boolean isSinglePage = (bookmark.getStartPage() == bookmark.getEndPage());
PdfMetadata metadata =
includeMetadata ? pdfMetadataService.extractMetadataFromPdf(sourceDocument) : null;
String fileNumberFormatter = "%0" + (Integer.toString(bookmarks.size()).length()) + "d ";
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try {
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
for (int i = 0; i < bookmarks.size(); i++) {
Bookmark bookmark = bookmarks.get(i);
try (PDDocument splitDocument = new PDDocument()) {
boolean isSinglePage = (bookmark.getStartPage() == bookmark.getEndPage());
for (int i = bookmark.getStartPage();
i < bookmark.getEndPage() + (isSinglePage ? 1 : 0);
i++) {
PDPage page = sourceDocument.getPage(i);
splitDocument.addPage(page);
log.debug("Adding page {} to split document", i);
for (int pg = bookmark.getStartPage();
pg < bookmark.getEndPage() + (isSinglePage ? 1 : 0);
pg++) {
PDPage page = sourceDocument.getPage(pg);
splitDocument.addPage(page);
log.debug("Adding page {} to split document", pg);
}
if (includeMetadata) {
pdfMetadataService.setMetadataToPdf(splitDocument, metadata);
}
// split files will be named as "[FILE_NUMBER] [BOOKMARK_TITLE].pdf"
String fileName =
String.format(Locale.ROOT, fileNumberFormatter, i)
+ bookmark.getTitle()
+ ".pdf";
zipOut.putNextEntry(new ZipEntry(fileName));
splitDocument.save(zipOut);
zipOut.closeEntry();
log.debug("Wrote split document {} to zip file", fileName);
} catch (Exception e) {
ExceptionUtils.logException("document splitting and saving", e);
throw e;
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (includeMetadata) {
pdfMetadataService.setMetadataToPdf(splitDocument, metadata);
}
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
} catch (Exception e) {
ExceptionUtils.logException("document splitting and saving", e);
throw e;
}
log.info(
"Successfully created zip file with split documents: {}",
zipTempFile.getPath());
return zipTempFile;
} catch (Exception e) {
zipTempFile.close();
throw e;
}
return splitDocumentsBoas;
}
}
@@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -13,9 +12,11 @@ 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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -50,8 +51,8 @@ public class SplitPdfBySizeController {
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB"
+ " (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF"
+ " Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> autoSplitPdf(
@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput();
@@ -59,63 +60,51 @@ public class SplitPdfBySizeController {
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "");
log.debug("Base filename for output: {}", filename);
try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) {
Path managedZipPath = zipTempFile.getPath();
log.debug("Created temporary managed zip file: {}", managedZipPath);
try {
log.debug("Reading input file bytes");
byte[] pdfBytes = file.getBytes();
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try {
log.debug("Created temporary managed zip file: {}", zipTempFile.getPath());
log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()));
PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
log.debug(
"Successfully loaded PDF with {} pages", sourceDocument.getNumberOfPages());
log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(managedZipPath))) {
log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.debug(
"Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages());
int type = request.getSplitType();
String value = request.getSplitValue();
log.debug("Split type: {}, Split value: {}", type, value);
int type = request.getSplitType();
String value = request.getSplitValue();
log.debug("Split type: {}, Split value: {}", type, value);
if (type == 0) {
log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value);
log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) {
log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value);
log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"split type: " + type);
}
log.debug("PDF splitting completed successfully");
}
if (type == 0) {
log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value);
log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) {
log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value);
log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"split type: " + type);
}
byte[] data = Files.readAllBytes(managedZipPath);
log.debug("Successfully read {} bytes from ZIP file", data.length);
log.debug("Returning response with {} bytes of data", data.length);
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
ExceptionUtils.logException("PDF splitting process", e);
throw e; // Re-throw to ensure proper error response
log.debug("PDF splitting completed successfully");
}
log.debug("Returning streaming response for zip file");
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip");
} catch (Exception e) {
ExceptionUtils.logException("PDF splitting process", e);
zipTempFile.close();
throw e;
}
}
@@ -124,6 +113,7 @@ public class SplitPdfBySizeController {
throws IOException {
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
@Getter
class DocHolder implements AutoCloseable {
private PDDocument doc;
@@ -131,10 +121,6 @@ public class SplitPdfBySizeController {
this.doc = doc;
}
public PDDocument getDoc() {
return doc;
}
public void setDoc(PDDocument doc) {
if (this.doc != null) {
try {
@@ -22,6 +22,7 @@ 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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -51,6 +52,7 @@ import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -78,7 +80,7 @@ public class ConvertImgPDFController {
"This endpoint converts a PDF file to image(s) with the specified image format,"
+ " color type, and DPI. Users can choose to get a single image or multiple"
+ " images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
public ResponseEntity<?> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
String imageFormat = request.getImageFormat();
@@ -180,28 +182,33 @@ public class ConvertImgPDFController {
"No WebP files were created. " + resultProcess.getMessages());
}
byte[] bodyBytes = new byte[0];
if (webpFiles.size() == 1) {
// Return the single WebP file directly
Path webpFilePath = webpFiles.get(0);
bodyBytes = Files.readAllBytes(webpFilePath);
byte[] webpBytes = Files.readAllBytes(webpFilePath);
Files.deleteIfExists(tempFile);
tempFile = null;
FileUtils.deleteDirectory(tempOutputDir.toFile());
tempOutputDir = null;
String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
return WebResponseUtils.bytesToWebResponse(webpBytes, docName, mediaType);
} else {
// Create a ZIP file containing all WebP images
try (ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
ByteArrayOutputStream zipBAOS = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipBAOS)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
bodyBytes = zipOutputStream.toByteArray();
}
Files.deleteIfExists(tempFile);
tempFile = null;
FileUtils.deleteDirectory(tempOutputDir.toFile());
tempOutputDir = null;
String zipFilename = filename + "_convertedToImages.zip";
return WebResponseUtils.bytesToWebResponse(
zipBAOS.toByteArray(), zipFilename, MediaType.APPLICATION_OCTET_STREAM);
}
// Clean up the temporary files
Files.deleteIfExists(tempFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
result = bodyBytes;
}
if (singleImage) {
@@ -267,8 +274,8 @@ public class ConvertImgPDFController {
description =
"This endpoint converts a CBZ (ZIP) comic book archive to a PDF file. "
+ "Input:CBZ Output:PDF Type:SISO")
public ResponseEntity<?> convertCbzToPdf(@ModelAttribute ConvertCbzToPdfRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> convertCbzToPdf(
@ModelAttribute ConvertCbzToPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput();
boolean optimizeForEbook = request.isOptimizeForEbook();
@@ -278,13 +285,13 @@ public class ConvertImgPDFController {
optimizeForEbook = false;
}
byte[] pdfBytes =
TempFile pdfFile =
CbzUtils.convertCbzToPdf(
file, pdfDocumentFactory, tempFileManager, optimizeForEbook);
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf");
return WebResponseUtils.bytesToWebResponse(pdfBytes, filename);
return WebResponseUtils.pdfFileToWebResponse(pdfFile, filename);
}
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz")
@@ -293,8 +300,8 @@ public class ConvertImgPDFController {
description =
"This endpoint converts a PDF file to a CBZ (ZIP) comic book archive. "
+ "Input:PDF Output:CBZ Type:SISO")
public ResponseEntity<?> convertPdfToCbz(@ModelAttribute ConvertPdfToCbzRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> convertPdfToCbz(
@ModelAttribute ConvertPdfToCbzRequest request) throws IOException {
MultipartFile file = request.getFileInput();
int dpi = request.getDpi();
@@ -302,12 +309,12 @@ public class ConvertImgPDFController {
dpi = 300;
}
byte[] cbzBytes = PdfToCbzUtils.convertPdfToCbz(file, dpi, pdfDocumentFactory);
TempFile cbzFile =
PdfToCbzUtils.convertPdfToCbz(file, dpi, pdfDocumentFactory, tempFileManager);
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbz");
return WebResponseUtils.bytesToWebResponse(
cbzBytes, filename, MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(cbzFile, filename);
}
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf")
@@ -39,6 +39,8 @@ import stirling.software.common.util.WebResponseUtils;
public class ConvertPdfJsonController {
private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\r\\n\\t]+");
private static final Pattern NON_PRINTABLE_PATTERN = Pattern.compile("[^\\x20-\\x7E]");
private final PdfJsonConversionService pdfJsonConversionService;
@Autowired(required = false)
@@ -259,7 +261,10 @@ public class ConvertPdfJsonController {
if (length > 0) {
int start = Math.max(0, length - 64);
tail = new String(jsonBytes, start, length - start, StandardCharsets.UTF_8);
tail = tail.replaceAll("[\\r\\n\\t]+", " ").replaceAll("[^\\x20-\\x7E]", "?");
tail =
NON_PRINTABLE_PATTERN
.matcher(WHITESPACE_PATTERN.matcher(tail).replaceAll(" "))
.replaceAll("?");
}
log.debug(
"Returning {} JSON response ({} bytes, endsWithJson={}, tail='{}')",
@@ -421,9 +426,9 @@ public class ConvertPdfJsonController {
private String truncateForLog(String value) {
int max = 64;
if (value.length() <= max) {
return value.replaceAll("[\\r\\n\\t]+", " ");
return WHITESPACE_PATTERN.matcher(value).replaceAll(" ");
}
return value.substring(0, max).replaceAll("[\\r\\n\\t]+", " ") + "...";
return WHITESPACE_PATTERN.matcher(value.substring(0, max)).replaceAll(" ") + "...";
}
/**
@@ -67,7 +67,7 @@ public class ExtractImageScansController {
MultipartFile inputFile = request.getFileInput();
String fileName = inputFile.getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
String extension = fileName.substring(fileName.lastIndexOf('.') + 1);
List<String> images = new ArrayList<>();
@@ -5,6 +5,7 @@ import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -28,6 +29,7 @@ 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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -42,6 +44,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ImageProcessingUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -50,6 +54,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ExtractImagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/extract-images")
@MultiFileResponse
@@ -59,111 +64,115 @@ public class ExtractImagesController {
"This endpoint extracts images from a given PDF file and returns them in a zip"
+ " file. Users can specify the output image format. Input:PDF"
+ " Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request)
public ResponseEntity<StreamingResponseBody> extractImages(
@ModelAttribute PDFExtractImagesRequest request)
throws IOException, InterruptedException, ExecutionException {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
boolean allowDuplicates = Boolean.TRUE.equals(request.getAllowDuplicates());
PDDocument document = pdfDocumentFactory.load(file);
// Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document);
// Create ByteArrayOutputStream to write zip file to byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Create ZipOutputStream to create zip file
ZipOutputStream zos = new ZipOutputStream(baos);
// Set compression level
zos.setLevel(Deflater.BEST_COMPRESSION);
String filename = GeneralUtils.removeExtension(file.getOriginalFilename());
Set<byte[]> processedImages = new HashSet<>();
if (useMultithreading) {
// Virtual thread executor lightweight threads ideal for I/O-bound image extraction
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Set<Future<Void>> futures = new HashSet<>();
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try (ZipOutputStream zos =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()));
PDDocument document = pdfDocumentFactory.load(file)) {
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
try {
int pageCount = document.getPages().getCount();
log.debug("Document reports {} pages", pageCount);
// Set compression level
zos.setLevel(Deflater.BEST_COMPRESSION);
int consecutiveFailures = 0;
// Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document);
for (int pgNum = 0; pgNum < pageCount; pgNum++) {
try {
PDPage page = document.getPage(pgNum);
consecutiveFailures = 0; // Reset on success
final int currentPageNum = pgNum + 1; // Convert to 1-based page numbering
Future<Void> future =
executor.submit(
() -> {
try {
// Call the image extraction method for each page
extractImagesFromPage(
page,
format,
filename,
currentPageNum,
processedImages,
zos,
allowDuplicates);
} catch (Exception e) {
// Log the error and continue processing other pages
ExceptionUtils.logException(
"image extraction from page "
+ currentPageNum,
e);
}
if (useMultithreading) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Set<Future<Void>> futures = new HashSet<>();
return null; // Callable requires a return type
});
try {
int pageCount = document.getPages().getCount();
log.debug("Document reports {} pages", pageCount);
// Add the Future object to the list to track completion
futures.add(future);
} catch (Exception e) {
consecutiveFailures++;
ExceptionUtils.logException("page access for page " + (pgNum + 1), e);
int consecutiveFailures = 0;
if (consecutiveFailures >= 3) {
log.warn("Stopping page iteration after 3 consecutive failures");
break;
for (int pgNum = 0; pgNum < pageCount; pgNum++) {
try {
PDPage page = document.getPage(pgNum);
consecutiveFailures = 0; // Reset on success
final int currentPageNum =
pgNum + 1; // Convert to 1-based page numbering
Future<Void> future =
executor.submit(
() -> {
try {
// Call the image extraction method for each
// page
extractImagesFromPage(
page,
format,
filename,
currentPageNum,
processedImages,
zos,
allowDuplicates);
} catch (Exception e) {
// Log the error and continue processing other
// pages
ExceptionUtils.logException(
"image extraction from page "
+ currentPageNum,
e);
}
return null; // Callable requires a return type
});
// Add the Future object to the list to track completion
futures.add(future);
} catch (Exception e) {
consecutiveFailures++;
ExceptionUtils.logException("page access for page " + (pgNum + 1), e);
if (consecutiveFailures >= 3) {
log.warn("Stopping page iteration after 3 consecutive failures");
break;
}
}
}
} catch (Exception e) {
ExceptionUtils.logException("page count determination", e);
throw e;
}
} catch (Exception e) {
ExceptionUtils.logException("page count determination", e);
throw e;
}
// Wait for all tasks to complete
for (Future<Void> future : futures) {
future.get();
}
// Wait for all tasks to complete
for (Future<Void> future : futures) {
future.get();
}
// Close executor service
executor.shutdown();
} else {
// Single-threaded extraction
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
PDPage page = document.getPage(pgNum);
extractImagesFromPage(
page, format, filename, pgNum + 1, processedImages, zos, allowDuplicates);
// Close executor service
executor.shutdown();
} else {
// Single-threaded extraction
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
PDPage page = document.getPage(pgNum);
extractImagesFromPage(
page,
format,
filename,
pgNum + 1,
processedImages,
zos,
allowDuplicates);
}
}
// document and zos closed by try-with-resources
} catch (Exception e) {
zipTempFile.close();
throw e;
}
// Close PDDocument and ZipOutputStream
document.close();
zos.close();
// Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray();
return WebResponseUtils.baosToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(
zipTempFile, filename + "_extracted-images.zip");
}
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
@@ -214,13 +223,17 @@ public class ExtractImagesController {
// Convert to standard RGB colorspace if needed
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
// Write image to zip file
// Encode image outside the lock to allow parallel encoding across threads
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
byte[] imageData = imageBaos.toByteArray();
// Write encoded bytes to zip under lock (ZipOutputStream requires
// serialization)
synchronized (zos) {
zos.putNextEntry(new ZipEntry(imageName));
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.write(imageData);
zos.closeEntry();
}
}
@@ -94,7 +94,7 @@ public class OCRController {
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar();
boolean sidecar = request.isSidecar();
Boolean deskew = request.isDeskew();
Boolean clean = request.isClean();
Boolean cleanFinal = request.isCleanFinal();
@@ -233,7 +233,7 @@ public class StampController {
};
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf('.'));
// Use TempFile with try-with-resources for automatic cleanup
try (TempFile tempFileWrapper = new TempFile(tempFileManager, fileExtension)) {
@@ -1,7 +1,7 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -13,6 +13,7 @@ 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.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +29,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.PipelineApi;
import stirling.software.common.service.PostHogService;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.JacksonException;
@@ -45,6 +48,8 @@ public class PipelineController {
private final PostHogService postHogService;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@MultiFileResponse
@Operation(
@@ -53,8 +58,8 @@ public class PipelineController {
"This endpoint processes multiple PDF files through a configurable pipeline of operations. "
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
throws DatabindException, JacksonException {
public ResponseEntity<StreamingResponseBody> handleData(
@ModelAttribute HandleDataRequest request) throws DatabindException, JacksonException {
MultipartFile[] files = request.getFileInput();
String jsonString = request.getJson();
if (files == null) {
@@ -80,51 +85,56 @@ public class PipelineController {
PipelineResult result = processor.runPipelineAgainstFiles(inputFiles, config);
List<Resource> outputFiles = result.getOutputFiles();
if (outputFiles != null && outputFiles.size() == 1) {
// If there is only one file, return it directly
// If there is only one file, return it directly stream without int-overflow
Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int) singleFile.contentLength()];
is.read(bytes);
is.close();
log.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse(
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
TempFile singleTempFile = new TempFile(tempFileManager, ".out");
try {
try (InputStream is = singleFile.getInputStream()) {
is.transferTo(Files.newOutputStream(singleTempFile.getPath()));
}
log.info("Returning single file response...");
return WebResponseUtils.fileToWebResponse(
singleTempFile,
singleFile.getFilename(),
MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
singleTempFile.close();
throw e;
}
} else if (outputFiles == null) {
return null;
}
// Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos);
// A map to keep track of filenames and their counts
Map<String, Integer> filenameCount = new HashMap<>();
// Loop through each file and add it to the zip
for (Resource file : outputFiles) {
String originalFilename = file.getFilename();
String filename = originalFilename;
// Check if the filename already exists, and modify it if necessary
if (filenameCount.containsKey(originalFilename)) {
int count = filenameCount.get(originalFilename);
assert originalFilename != null;
filename = GeneralUtils.generateFilename(originalFilename, "(" + count + ")");
filenameCount.put(originalFilename, count + 1);
} else {
filenameCount.put(originalFilename, 1);
// Multiple files: stream into a zip TempFile
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try {
Map<String, Integer> filenameCount = new HashMap<>();
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
for (Resource file : outputFiles) {
String originalFilename = file.getFilename();
String filename = originalFilename;
if (filenameCount.containsKey(originalFilename)) {
int count = filenameCount.get(originalFilename);
filename =
GeneralUtils.generateFilename(
originalFilename, "(" + count + ")");
filenameCount.put(originalFilename, count + 1);
} else {
filenameCount.put(originalFilename, 1);
}
zipOut.putNextEntry(new ZipEntry(filename));
try (InputStream is = file.getInputStream()) {
is.transferTo(zipOut);
}
zipOut.closeEntry();
}
}
ZipEntry zipEntry = new ZipEntry(filename);
zipOut.putNextEntry(zipEntry);
// Read the file into a byte array
InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()];
is.read(bytes);
// Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry();
is.close();
log.info("Returning zipped file response...");
return WebResponseUtils.zipFileToWebResponse(zipTempFile, "output.zip");
} catch (Exception e) {
zipTempFile.close();
throw e;
}
zipOut.close();
log.info("Returning zipped file response...");
return WebResponseUtils.baosToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
log.error("Error handling data: ", e);
return null;
@@ -254,7 +254,7 @@ public class PipelineDirectoryProcessor {
String extension =
filename.contains(".")
? filename.substring(
filename.lastIndexOf(".")
filename.lastIndexOf('.')
+ 1)
.toLowerCase(Locale.ROOT)
: "";
@@ -67,7 +67,7 @@ public class PipelineProcessor {
public static String removeTrailingNaming(String filename) {
// Splitting filename into name and extension
int dotIndex = filename.lastIndexOf(".");
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1) {
// No extension found
return filename;
@@ -75,7 +75,7 @@ public class PipelineProcessor {
String name = filename.substring(0, dotIndex);
String extension = filename.substring(dotIndex);
// Finding the last underscore
int underscoreIndex = name.lastIndexOf("_");
int underscoreIndex = name.lastIndexOf('_');
if (underscoreIndex == -1) {
// No underscore found
return filename;
@@ -173,7 +173,7 @@ public class PipelineProcessor {
String providedExtension = "no extension";
if (filename != null && filename.contains(".")) {
providedExtension =
filename.substring(filename.lastIndexOf("."))
filename.substring(filename.lastIndexOf('.'))
.toLowerCase(Locale.ROOT);
}
@@ -248,7 +248,7 @@ public class PipelineProcessor {
String filename = file.getFilename();
if (filename != null && filename.contains(".")) {
return filename.substring(
filename.lastIndexOf("."))
filename.lastIndexOf('.'))
.toLowerCase(Locale.ROOT);
}
return "no extension";
@@ -450,7 +450,21 @@ public class PipelineProcessor {
return isZip(data, null);
}
private static final int MAX_UNZIP_DEPTH = 10;
private List<Resource> unzip(Resource data, PipelineResult result) throws IOException {
return unzip(data, result, 0);
}
private List<Resource> unzip(Resource data, PipelineResult result, int depth)
throws IOException {
if (depth > MAX_UNZIP_DEPTH) {
log.warn(
"ZIP nesting depth {} exceeds limit {}, treating as file",
depth,
MAX_UNZIP_DEPTH);
return List.of(data);
}
log.info("Unzipping data of length: {}", data.contentLength());
List<Resource> unzippedFiles = new ArrayList<>();
try (InputStream bais = data.getInputStream();
@@ -481,7 +495,7 @@ public class PipelineProcessor {
// If the unzipped file is a zip file, unzip it
if (isZip(fileResource, filename)) {
log.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(fileResource, result));
unzippedFiles.addAll(unzip(fileResource, result, depth + 1));
} else {
unzippedFiles.add(fileResource);
}
@@ -188,7 +188,7 @@ public class WatermarkController {
};
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf('.'));
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) {
@@ -3,6 +3,7 @@ package stirling.software.SPDF.exception;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import org.springframework.context.MessageSource;
@@ -23,8 +24,6 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -35,6 +34,8 @@ import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ExceptionUtils.*;
import stirling.software.common.util.RegexPatternUtils;
import tools.jackson.databind.ObjectMapper;
/**
* Returns RFC 7807 Problem Details for HTTP APIs, ensuring consistent error responses across the
* application.
@@ -870,7 +871,7 @@ public class GlobalExceptionHandler {
// Use ObjectMapper to properly escape JSON values and prevent XSS
ObjectMapper mapper = new ObjectMapper();
java.util.Map<String, Object> errorMap = new java.util.LinkedHashMap<>();
java.util.Map<String, Object> errorMap = new LinkedHashMap<>();
errorMap.put("type", "about:blank");
errorMap.put("title", "Not Acceptable");
errorMap.put("status", 406);
@@ -1,7 +1,5 @@
package stirling.software.SPDF.model.json;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
@@ -27,13 +25,13 @@ public class PdfJsonAnnotation {
private String contents;
/** Annotation rectangle [x1, y1, x2, y2] */
private List<Float> rect;
private float[] rect;
/** Annotation appearance characteristics */
private String appearanceState;
/** Color components (e.g., [r, g, b] for RGB) */
private List<Float> color;
private float[] color;
/** Annotation flags (print, hidden, etc.) */
private Integer flags;
@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PdfJsonDocument {
private PdfJsonMetadata metadata;
@@ -23,7 +23,7 @@ public class PdfJsonDocument {
private String xmpMetadata;
/** Indicates that images should be loaded lazily via API rather than embedded in the JSON. */
private Boolean lazyImages;
private boolean lazyImages;
@Builder.Default private List<PdfJsonFont> fonts = new ArrayList<>();
@@ -1,7 +1,5 @@
package stirling.software.SPDF.model.json;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
@@ -65,5 +63,5 @@ public class PdfJsonFontConversionCandidate {
private String diagnostics;
/** Known unicode/codepoint coverage derived from the conversion strategy. */
private List<Integer> glyphCoverage;
private int[] glyphCoverage;
}
@@ -45,13 +45,13 @@ public class PdfJsonFormField {
private Integer pageNumber;
/** Field rectangle [x1, y1, x2, y2] on the page */
private List<Float> rect;
private float[] rect;
/** For choice fields: list of options */
private List<String> options;
/** For choice fields: selected indices */
private List<Integer> selectedIndices;
private int[] selectedIndices;
/** For button fields: whether it's checked */
private Boolean checked;
@@ -1,8 +1,5 @@
package stirling.software.SPDF.model.json;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
@@ -30,7 +27,7 @@ public class PdfJsonImageElement {
private Float right;
private Float top;
private Float bottom;
@Builder.Default private List<Float> transform = new ArrayList<>();
private float[] transform;
private Integer zOrder;
private String imageData;
private String imageFormat;
@@ -11,10 +11,10 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PdfJsonPageDimension {
private Integer pageNumber;
private Float width;
private Float height;
private Integer rotation;
private int pageNumber;
private float width;
private float height;
private int rotation;
}
@@ -1,7 +1,5 @@
package stirling.software.SPDF.model.json;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
@@ -17,5 +15,5 @@ import lombok.NoArgsConstructor;
public class PdfJsonTextColor {
private String colorSpace;
private List<Float> components;
private float[] components;
}
@@ -1,7 +1,5 @@
package stirling.software.SPDF.model.json;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
@@ -32,10 +30,10 @@ public class PdfJsonTextElement {
private Float y;
private Float width;
private Float height;
private List<Float> textMatrix;
private float[] textMatrix;
private PdfJsonTextColor fillColor;
private PdfJsonTextColor strokeColor;
private Integer renderingMode;
private Boolean fallbackUsed;
private List<Integer> charCodes;
private int[] charCodes;
}
@@ -273,11 +273,11 @@ public class PdfJsonConversionService {
// Get job ID from request context if running in async mode
String contextJobId = getJobIdFromRequest();
boolean isRealJobId = (contextJobId != null && !contextJobId.isEmpty());
boolean useLazyImages = (contextJobId != null && !contextJobId.isEmpty());
// Generate synthetic jobId for synchronous conversions to prevent cache collisions
final String jobId;
if (!isRealJobId) {
if (!useLazyImages) {
jobId = "pdf2json:" + java.util.UUID.randomUUID().toString();
log.debug("Generated synthetic jobId for synchronous conversion: {}", jobId);
} else {
@@ -302,7 +302,7 @@ public class PdfJsonConversionService {
: "");
progressCallback.accept(p);
}
: isRealJobId
: useLazyImages
? (p) -> {
log.debug(
"Progress: [{}%] {} - {}{}",
@@ -369,7 +369,6 @@ public class PdfJsonConversionService {
int totalPages = document.getNumberOfPages();
// Always enable lazy mode for real async jobs so cache is available regardless of
// page count. Synchronous calls with synthetic jobId still do full extraction.
boolean useLazyImages = isRealJobId;
Map<COSBase, FontModelCacheEntry> fontCache = new IdentityHashMap<>();
Map<COSBase, EncodedImage> imageCache = new IdentityHashMap<>();
log.debug(
@@ -377,7 +376,7 @@ public class PdfJsonConversionService {
totalPages,
useLazyImages ? "lazy image" : "standard",
jobId,
isRealJobId);
useLazyImages);
Map<String, PdfJsonFont> fonts = new LinkedHashMap<>();
Map<Integer, List<PdfJsonTextElement>> textByPage = new LinkedHashMap<>();
Map<Integer, Map<PDFont, String>> pageFontResources = new HashMap<>();
@@ -431,7 +430,7 @@ public class PdfJsonConversionService {
progress.accept(
PdfJsonConversionProgress.of(
80, "annotations", "Collecting annotations and form fields"));
boolean includeAnnotationRawData = !(lightweight && isRealJobId);
boolean includeAnnotationRawData = !(lightweight && useLazyImages);
Map<Integer, List<PdfJsonAnnotation>> annotationsByPage =
collectAnnotations(
document, totalPages, progress, includeAnnotationRawData);
@@ -463,8 +462,8 @@ public class PdfJsonConversionService {
textByPage,
imagesByPage,
annotationsByPage,
lightweight && isRealJobId));
if (lightweight && isRealJobId) {
lightweight && useLazyImages));
if (lightweight && useLazyImages) {
// Lightweight async editor flow does not use form fields and this payload can
// be
// very large due nested raw dictionaries.
@@ -474,12 +473,11 @@ public class PdfJsonConversionService {
}
// Only cache for real async jobIds, not synthetic synchronous ones
if (useLazyImages && isRealJobId) {
if (useLazyImages) {
log.debug(
"Creating cache for jobId: {} (useLazyImages={}, isRealJobId={})",
"Creating cache for jobId: {} (useLazyImages={})",
jobId,
useLazyImages,
isRealJobId);
useLazyImages);
PdfJsonDocumentMetadata docMetadata = new PdfJsonDocumentMetadata();
docMetadata.setMetadata(pdfJson.getMetadata());
docMetadata.setXmpMetadata(pdfJson.getXmpMetadata());
@@ -529,16 +527,15 @@ public class PdfJsonConversionService {
scheduleDocumentCleanup(jobId);
} else {
log.warn(
"Skipping cache creation: useLazyImages={}, isRealJobId={}, jobId={}",
"Skipping cache creation: useLazyImages={}, jobId={}",
useLazyImages,
isRealJobId,
jobId);
}
if (lightweight) {
applyLightweightTransformations(pdfJson);
}
if (lightweight && isRealJobId) {
if (lightweight && useLazyImages) {
stripFontProgramPayloads(responseFonts);
stripFontCosStreamData(responseFonts);
}
@@ -2503,11 +2500,12 @@ public class PdfJsonConversionService {
PDRectangle rect = annotation.getRectangle();
if (rect != null) {
ann.setRect(
List.of(
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()));
new float[] {
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()
});
}
COSName appearanceState = annotation.getAppearanceState();
@@ -2516,12 +2514,7 @@ public class PdfJsonConversionService {
}
if (annotation.getColor() != null) {
float[] colorComponents = annotation.getColor().getComponents();
List<Float> colorList = new ArrayList<>(colorComponents.length);
for (float c : colorComponents) {
colorList.add(c);
}
ann.setColor(colorList);
ann.setColor(annotation.getColor().getComponents());
}
COSDictionary annotDict = annotation.getCOSObject();
@@ -2636,11 +2629,12 @@ public class PdfJsonConversionService {
PDRectangle rect = widget.getRectangle();
if (rect != null) {
formField.setRect(
List.of(
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()));
new float[] {
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()
});
}
}
}
@@ -3351,7 +3345,7 @@ public class PdfJsonConversionService {
PDFont currentFont = baseFont;
String currentFontId = baseFontId;
List<Integer> elementCodes = element.getCharCodes();
int[] elementCodes = element.getCharCodes();
int codeIndex = 0;
boolean rawType3CodesUsed = false;
int rawType3GlyphCount = 0;
@@ -3363,8 +3357,8 @@ public class PdfJsonConversionService {
PDFont targetFont = baseFont;
String targetFontId = baseFontId;
Integer rawCode = null;
if (elementCodes != null && codeIndex < elementCodes.size()) {
rawCode = elementCodes.get(codeIndex);
if (elementCodes != null && codeIndex < elementCodes.length) {
rawCode = elementCodes[codeIndex];
}
codeIndex++;
@@ -3667,10 +3661,7 @@ public class PdfJsonConversionService {
if (color == null || color.getComponents() == null) {
return;
}
float[] components = new float[color.getComponents().size()];
for (int i = 0; i < components.length; i++) {
components[i] = color.getComponents().get(i);
}
float[] components = color.getComponents();
String space = color.getColorSpace();
if (space == null) {
// Infer color space from component count
@@ -4287,8 +4278,11 @@ public class PdfJsonConversionService {
List<Integer> combinedCodes = new ArrayList<>();
for (PdfJsonTextElement element : elements) {
builder.append(Objects.toString(element.getText(), ""));
if (element.getCharCodes() != null && !element.getCharCodes().isEmpty()) {
combinedCodes.addAll(element.getCharCodes());
int[] codes = element.getCharCodes();
if (codes != null && codes.length > 0) {
for (int code : codes) {
combinedCodes.add(code);
}
}
}
return new MergedText(builder.toString(), combinedCodes.isEmpty() ? null : combinedCodes);
@@ -4354,9 +4348,9 @@ public class PdfJsonConversionService {
}
private int countGlyphs(PdfJsonTextElement element) {
List<Integer> codes = element.getCharCodes();
if (codes != null && !codes.isEmpty()) {
return codes.size();
int[] codes = element.getCharCodes();
if (codes != null && codes.length > 0) {
return codes.length;
}
String text = element.getText();
if (text != null && !text.isEmpty()) {
@@ -4920,15 +4914,15 @@ public class PdfJsonConversionService {
private void applyTextMatrix(PDPageContentStream contentStream, PdfJsonTextElement element)
throws IOException {
List<Float> matrix = element.getTextMatrix();
if (matrix != null && matrix.size() == 6) {
float[] matrix = element.getTextMatrix();
if (matrix != null && matrix.length == 6) {
float fontScale = resolveFontMatrixSize(element);
float a = matrix.get(0);
float b = matrix.get(1);
float c = matrix.get(2);
float d = matrix.get(3);
float e = matrix.get(4);
float f = matrix.get(5);
float a = matrix[0];
float b = matrix[1];
float c = matrix[2];
float d = matrix[3];
float e = matrix[4];
float f = matrix[5];
if (fontScale != 0f) {
a /= fontScale;
@@ -4950,12 +4944,12 @@ public class PdfJsonConversionService {
if (fromElement != null && fromElement > 0f) {
return fromElement;
}
List<Float> matrix = element.getTextMatrix();
if (matrix != null && matrix.size() >= 4) {
float a = matrix.get(0);
float b = matrix.get(1);
float c = matrix.get(2);
float d = matrix.get(3);
float[] matrix = element.getTextMatrix();
if (matrix != null && matrix.length >= 4) {
float a = matrix[0];
float b = matrix[1];
float c = matrix[2];
float d = matrix[3];
float verticalScale = (float) Math.hypot(b, d);
if (verticalScale > 0f) {
return verticalScale;
@@ -5048,7 +5042,7 @@ public class PdfJsonConversionService {
}
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
Bounds bounds = computeBounds(ctm);
List<Float> matrixValues = toMatrixValues(ctm);
float[] matrixValues = toMatrixValues(ctm);
PdfJsonImageElement element =
PdfJsonImageElement.builder()
@@ -5222,15 +5216,15 @@ public class PdfJsonConversionService {
private record EncodedImage(String base64, String format) {}
private List<Float> toMatrixValues(Matrix matrix) {
List<Float> values = new ArrayList<>(6);
values.add(matrix.getValue(0, 0));
values.add(matrix.getValue(0, 1));
values.add(matrix.getValue(1, 0));
values.add(matrix.getValue(1, 1));
values.add(matrix.getValue(2, 0));
values.add(matrix.getValue(2, 1));
return values;
private float[] toMatrixValues(Matrix matrix) {
return new float[] {
matrix.getValue(0, 0),
matrix.getValue(0, 1),
matrix.getValue(1, 0),
matrix.getValue(1, 1),
matrix.getValue(2, 0),
matrix.getValue(2, 1)
};
}
private EncodedImage encodeImage(PDImage image) {
@@ -5341,16 +5335,16 @@ public class PdfJsonConversionService {
cache.put(cacheKey, image);
}
List<Float> transform = element.getTransform();
if (transform != null && transform.size() == 6) {
float[] transform = element.getTransform();
if (transform != null && transform.length == 6) {
Matrix matrix =
new Matrix(
safeFloat(transform.get(0), 1f),
safeFloat(transform.get(1), 0f),
safeFloat(transform.get(2), 0f),
safeFloat(transform.get(3), 1f),
safeFloat(transform.get(4), 0f),
safeFloat(transform.get(5), 0f));
safeFloat(transform[0], 1f),
safeFloat(transform[1], 0f),
safeFloat(transform[2], 0f),
safeFloat(transform[3], 1f),
safeFloat(transform[4], 0f),
safeFloat(transform[5], 0f));
contentStream.drawImage(image, matrix);
return;
}
@@ -5516,14 +5510,17 @@ public class PdfJsonConversionService {
if (pdfont instanceof PDType3Font) {
int[] codes = position.getCharacterCodes();
if (codes != null && codes.length > 0) {
List<Integer> codeList = new ArrayList<>(codes.length);
int count = 0;
for (int code : codes) {
if (code >= 0) {
codeList.add(code);
}
if (code >= 0) count++;
}
if (!codeList.isEmpty()) {
element.setCharCodes(codeList);
if (count > 0) {
int[] filtered = new int[count];
int idx = 0;
for (int code : codes) {
if (code >= 0) filtered[idx++] = code;
}
element.setCharCodes(filtered);
}
}
}
@@ -5552,11 +5549,11 @@ public class PdfJsonConversionService {
return;
}
List<Float> matrix = element.getTextMatrix();
float[] matrix = element.getTextMatrix();
if (matrix != null) {
if (matrix.isEmpty()) {
if (matrix.length == 0) {
element.setTextMatrix(null);
} else if (matrix.size() == 6) {
} else if (matrix.length == 6) {
element.setX(null);
element.setY(null);
}
@@ -5597,29 +5594,29 @@ public class PdfJsonConversionService {
if (color == null || color.getComponents() == null) {
return true;
}
List<Float> components = color.getComponents();
if (components.isEmpty()) {
float[] components = color.getComponents();
if (components.length == 0) {
return true;
}
String space = color.getColorSpace();
if (space == null || "DeviceRGB".equals(space)) {
if (components.size() < 3) {
if (components.length < 3) {
return false;
}
return Math.abs(components.get(0)) < FLOAT_EPSILON
&& Math.abs(components.get(1)) < FLOAT_EPSILON
&& Math.abs(components.get(2)) < FLOAT_EPSILON;
return Math.abs(components[0]) < FLOAT_EPSILON
&& Math.abs(components[1]) < FLOAT_EPSILON
&& Math.abs(components[2]) < FLOAT_EPSILON;
}
if ("DeviceGray".equals(space)) {
return Math.abs(components.get(0)) < FLOAT_EPSILON;
return Math.abs(components[0]) < FLOAT_EPSILON;
}
return false;
}
private Float baselineFrom(PdfJsonTextElement element) {
List<Float> matrix = element.getTextMatrix();
if (matrix != null && matrix.size() >= 6) {
return matrix.get(5);
float[] matrix = element.getTextMatrix();
if (matrix != null && matrix.length >= 6) {
return matrix[5];
}
return element.getY();
}
@@ -5648,11 +5645,12 @@ public class PdfJsonConversionService {
private final float orientationC;
private final float orientationD;
private final Float baseline;
private final List<Float> baseMatrix;
private final float[] baseMatrix;
private final float startXCoord;
private final float startYCoord;
private final StringBuilder textBuilder = new StringBuilder();
private final List<Integer> charCodeBuffer = new ArrayList<>();
private int[] charCodeBuf = new int[16];
private int charCodeLen = 0;
private float totalWidth;
private float maxHeight;
private float endXCoord;
@@ -5660,17 +5658,15 @@ public class PdfJsonConversionService {
TextRunAccumulator(PdfJsonTextElement element, TextPosition position) {
this.baseElement = element;
this.styleKey = buildStyleKey(element);
this.baseMatrix =
element.getTextMatrix() != null
? new ArrayList<>(element.getTextMatrix())
: null;
if (baseMatrix != null && baseMatrix.size() >= 6) {
orientationA = baseMatrix.get(0);
orientationB = baseMatrix.get(1);
orientationC = baseMatrix.get(2);
orientationD = baseMatrix.get(3);
startXCoord = baseMatrix.get(4);
startYCoord = baseMatrix.get(5);
float[] tm = element.getTextMatrix();
this.baseMatrix = tm != null ? tm.clone() : null;
if (baseMatrix != null && baseMatrix.length >= 6) {
orientationA = baseMatrix[0];
orientationB = baseMatrix[1];
orientationC = baseMatrix[2];
orientationD = baseMatrix[3];
startXCoord = baseMatrix[4];
startYCoord = baseMatrix[5];
} else {
orientationA = 1f;
orientationB = 0f;
@@ -5684,25 +5680,23 @@ public class PdfJsonConversionService {
this.maxHeight = element.getHeight() != null ? element.getHeight() : 0f;
this.endXCoord = position.getXDirAdj() + position.getWidthDirAdj();
this.textBuilder.append(element.getText());
if (element.getCharCodes() != null) {
charCodeBuffer.addAll(element.getCharCodes());
}
appendCharCodes(element.getCharCodes());
}
boolean canAppend(PdfJsonTextElement element, TextPosition position) {
if (!styleKey.equals(buildStyleKey(element))) {
return false;
}
List<Float> matrix = element.getTextMatrix();
float[] matrix = element.getTextMatrix();
float a = 1f;
float b = 0f;
float c = 0f;
float d = 1f;
if (matrix != null && matrix.size() >= 4) {
a = matrix.get(0);
b = matrix.get(1);
c = matrix.get(2);
d = matrix.get(3);
if (matrix != null && matrix.length >= 4) {
a = matrix[0];
b = matrix[1];
c = matrix[2];
d = matrix[3];
}
if (Math.abs(a - orientationA) > ORIENTATION_TOLERANCE
|| Math.abs(b - orientationB) > ORIENTATION_TOLERANCE
@@ -5734,9 +5728,19 @@ public class PdfJsonConversionService {
maxHeight = height;
}
endXCoord = position.getXDirAdj() + position.getWidthDirAdj();
if (element.getCharCodes() != null) {
charCodeBuffer.addAll(element.getCharCodes());
appendCharCodes(element.getCharCodes());
}
private void appendCharCodes(int[] codes) {
if (codes == null) return;
int needed = charCodeLen + codes.length;
if (needed > charCodeBuf.length) {
charCodeBuf =
java.util.Arrays.copyOf(
charCodeBuf, Math.max(needed, charCodeBuf.length * 2));
}
System.arraycopy(codes, 0, charCodeBuf, charCodeLen, codes.length);
charCodeLen += codes.length;
}
PdfJsonTextElement build() {
@@ -5748,22 +5752,21 @@ public class PdfJsonConversionService {
}
result.setWidth(totalWidth);
result.setHeight(maxHeight);
if (baseMatrix != null && baseMatrix.size() == 6) {
List<Float> matrix = new ArrayList<>(baseMatrix);
matrix.set(0, orientationA);
matrix.set(1, orientationB);
matrix.set(2, orientationC);
matrix.set(3, orientationD);
matrix.set(4, startXCoord);
matrix.set(5, startYCoord);
if (baseMatrix != null && baseMatrix.length == 6) {
float[] matrix =
new float[] {
orientationA, orientationB,
orientationC, orientationD,
startXCoord, startYCoord
};
result.setTextMatrix(matrix);
result.setX(null);
result.setY(null);
}
if (charCodeBuffer.isEmpty()) {
if (charCodeLen == 0) {
result.setCharCodes(null);
} else {
result.setCharCodes(new ArrayList<>(charCodeBuffer));
result.setCharCodes(java.util.Arrays.copyOf(charCodeBuf, charCodeLen));
}
compactTextElement(result);
return result;
@@ -5784,29 +5787,25 @@ public class PdfJsonConversionService {
Integer renderingMode,
Float spaceWidth) {}
private List<Float> extractMatrix(TextPosition position) {
float[] values = new float[6];
values[0] = position.getTextMatrix().getValue(0, 0);
values[1] = position.getTextMatrix().getValue(0, 1);
values[2] = position.getTextMatrix().getValue(1, 0);
values[3] = position.getTextMatrix().getValue(1, 1);
values[4] = position.getTextMatrix().getValue(2, 0);
values[5] = position.getTextMatrix().getValue(2, 1);
List<Float> matrix = new ArrayList<>(6);
for (float value : values) {
matrix.add(value);
}
return matrix;
private float[] extractMatrix(TextPosition position) {
return new float[] {
position.getTextMatrix().getValue(0, 0),
position.getTextMatrix().getValue(0, 1),
position.getTextMatrix().getValue(1, 0),
position.getTextMatrix().getValue(1, 1),
position.getTextMatrix().getValue(2, 0),
position.getTextMatrix().getValue(2, 1)
};
}
private Float computeFontMatrixSize(List<Float> matrix) {
if (matrix == null || matrix.size() < 4) {
private Float computeFontMatrixSize(float[] matrix) {
if (matrix == null || matrix.length < 4) {
return null;
}
float a = matrix.get(0);
float b = matrix.get(1);
float c = matrix.get(2);
float d = matrix.get(3);
float a = matrix[0];
float b = matrix[1];
float c = matrix[2];
float d = matrix[3];
float scaleX = (float) Math.hypot(a, c);
float scaleY = (float) Math.hypot(b, d);
float scale = Math.max(scaleX, scaleY);
@@ -5850,11 +5849,10 @@ public class PdfJsonConversionService {
colorSpaceName,
ex.getMessage());
}
List<Float> values = new ArrayList<>(effective.length);
for (float component : effective) {
values.add(component);
}
return PdfJsonTextColor.builder().colorSpace(colorSpaceName).components(values).build();
return PdfJsonTextColor.builder()
.colorSpace(colorSpaceName)
.components(effective)
.build();
}
private String sanitizeForLog(String value) {
@@ -6241,11 +6239,12 @@ public class PdfJsonConversionService {
PDRectangle rect = annotation.getRectangle();
if (rect != null) {
ann.setRect(
List.of(
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()));
new float[] {
rect.getLowerLeftX(),
rect.getLowerLeftY(),
rect.getUpperRightX(),
rect.getUpperRightY()
});
}
COSName appearanceState = annotation.getAppearanceState();
@@ -6254,12 +6253,7 @@ public class PdfJsonConversionService {
}
if (annotation.getColor() != null) {
float[] colorComponents = annotation.getColor().getComponents();
List<Float> colorList = new ArrayList<>(colorComponents.length);
for (float c : colorComponents) {
colorList.add(c);
}
ann.setColor(colorList);
ann.setColor(annotation.getColor().getComponents());
}
COSDictionary annotDict = annotation.getCOSObject();
@@ -30,7 +30,7 @@ public class SharedSignatureService {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper;
public SharedSignatureService(ObjectMapper objectMapper) {
@@ -159,12 +159,12 @@ public class SharedSignatureService {
String dataUrl = request.getDataUrl();
if (dataUrl != null && dataUrl.startsWith("data:image/")) {
// Extract base64 data
String base64Data = dataUrl.substring(dataUrl.indexOf(",") + 1);
String base64Data = dataUrl.substring(dataUrl.indexOf(',') + 1);
byte[] imageBytes = Base64.getDecoder().decode(base64Data);
// Determine and validate file extension from data URL
String mimeType = dataUrl.substring(dataUrl.indexOf(":") + 1, dataUrl.indexOf(";"));
String rawExtension = mimeType.substring(mimeType.indexOf("/") + 1);
String mimeType = dataUrl.substring(dataUrl.indexOf(':') + 1, dataUrl.indexOf(';'));
String rawExtension = mimeType.substring(mimeType.indexOf('/') + 1);
String extension = validateAndNormalizeExtension(rawExtension);
// Save image file only
@@ -134,16 +134,16 @@ public class PdfJsonImageService {
cache.put(cacheKey, image);
}
List<Float> transform = element.getTransform();
if (transform != null && transform.size() == 6) {
float[] transform = element.getTransform();
if (transform != null && transform.length == 6) {
Matrix matrix =
new Matrix(
safeFloat(transform.get(0), 1f),
safeFloat(transform.get(1), 0f),
safeFloat(transform.get(2), 0f),
safeFloat(transform.get(3), 1f),
safeFloat(transform.get(4), 0f),
safeFloat(transform.get(5), 0f));
safeFloat(transform[0], 1f),
safeFloat(transform[1], 0f),
safeFloat(transform[2], 0f),
safeFloat(transform[3], 1f),
safeFloat(transform[4], 0f),
safeFloat(transform[5], 0f));
contentStream.drawImage(image, matrix);
return;
}
@@ -269,18 +269,18 @@ public class PdfJsonImageService {
return 0f;
}
private List<Float> toMatrixValues(Matrix matrix) {
List<Float> values = new ArrayList<>(6);
values.add(matrix.getValue(0, 0));
values.add(matrix.getValue(0, 1));
values.add(matrix.getValue(1, 0));
values.add(matrix.getValue(1, 1));
values.add(matrix.getValue(2, 0));
values.add(matrix.getValue(2, 1));
return values;
private float[] toMatrixValues(Matrix matrix) {
return new float[] {
matrix.getValue(0, 0),
matrix.getValue(0, 1),
matrix.getValue(1, 0),
matrix.getValue(1, 1),
matrix.getValue(2, 0),
matrix.getValue(2, 1)
};
}
private float safeFloat(Float value, float defaultValue) {
private static float safeFloat(Float value, float defaultValue) {
if (value == null || Float.isNaN(value) || Float.isInfinite(value)) {
return defaultValue;
}
@@ -324,7 +324,7 @@ public class PdfJsonImageService {
}
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
Bounds bounds = computeBounds(ctm);
List<Float> matrixValues = toMatrixValues(ctm);
float[] matrixValues = toMatrixValues(ctm);
PdfJsonImageElement element =
PdfJsonImageElement.builder()
@@ -110,7 +110,12 @@ public class Type3LibraryStrategy implements Type3ConversionStrategy {
.webProgramFormat(toFormat(entry.getWebProgram()))
.pdfProgram(toBase64(entry.getPdfProgram()))
.pdfProgramFormat(toFormat(entry.getPdfProgram()))
.glyphCoverage(entry.getGlyphCoverage())
.glyphCoverage(
entry.getGlyphCoverage() != null
? entry.getGlyphCoverage().stream()
.mapToInt(Integer::intValue)
.toArray()
: null)
.message(message)
.build();
}
@@ -1,4 +1,9 @@
multipart.enabled=true
# Jackson 3 defaults FAIL_ON_NULL_FOR_PRIMITIVES to true (was false in Jackson 2).
# Restore Jackson 2 behaviour so absent/null JSON fields map to Java primitive defaults.
spring.jackson.deserialization.fail-on-null-for-primitives=false
logging.level.org.springframework=WARN
logging.level.org.springframework.security=WARN
logging.level.org.hibernate=WARN
@@ -25,14 +30,16 @@ server.compression.enabled=true
server.compression.min-response-size=1024
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript
server.error.path=/error
server.error.whitelabel.enabled=false
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always
spring.web.error.path=/error
spring.web.error.whitelabel.enabled=false
spring.web.error.include-stacktrace=always
spring.web.error.include-exception=true
spring.web.error.include-message=always
# Enable RFC 7807 Problem Details for HTTP APIs
spring.mvc.problemdetails.enabled=true
# Disable Spring's built-in ProblemDetailsExceptionHandler (@Order(0)) so that
# GlobalExceptionHandler runs first and provides detailed, logged error responses.
# GlobalExceptionHandler already produces RFC 7807 ProblemDetail objects.
spring.mvc.problemdetails.enabled=false
#logging.level.org.springframework.web=DEBUG
#logging.level.org.springframework=DEBUG
@@ -25,6 +25,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
@@ -69,16 +70,15 @@ class SplitPdfBySizeControllerTest {
request.setSplitType(1); // Page count
request.setSplitValue("2");
when(pdfDocumentFactory.load(any(byte[].class)))
.thenAnswer(inv -> Loader.loadPDF((byte[]) inv.getArgument(0)));
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
.thenAnswer(inv -> new PDDocument());
ResponseEntity<byte[]> response = controller.autoSplitPdf(request);
ResponseEntity<?> response = controller.autoSplitPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(response.getHeaders().getContentType())
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
}
@@ -104,15 +104,19 @@ class SplitPdfBySizeControllerTest {
request.setSplitType(2); // Document count
request.setSplitValue("3"); // Split into 3 docs (2 pages each)
when(pdfDocumentFactory.load(any(byte[].class)))
.thenAnswer(inv -> Loader.loadPDF((byte[]) inv.getArgument(0)));
when(pdfDocumentFactory.load(any(org.springframework.web.multipart.MultipartFile.class)))
.thenAnswer(
inv ->
Loader.loadPDF(
((org.springframework.web.multipart.MultipartFile)
inv.getArgument(0))
.getBytes()));
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
.thenAnswer(inv -> new PDDocument());
ResponseEntity<byte[]> response = controller.autoSplitPdf(request);
ResponseEntity<?> response = controller.autoSplitPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
}
}
@@ -13,10 +13,12 @@ import org.springframework.mock.web.MockMultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.PdfToCbzUtils;
import stirling.software.common.util.TempFileManager;
public class PdfToCbzUtilsTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@BeforeEach
public void setUp() {
@@ -42,7 +44,9 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> PdfToCbzUtils.convertPdfToCbz(null, 300, pdfDocumentFactory));
() ->
PdfToCbzUtils.convertPdfToCbz(
null, 300, pdfDocumentFactory, tempFileManager));
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@@ -54,7 +58,9 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> PdfToCbzUtils.convertPdfToCbz(emptyFile, 300, pdfDocumentFactory));
() ->
PdfToCbzUtils.convertPdfToCbz(
emptyFile, 300, pdfDocumentFactory, tempFileManager));
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@@ -66,7 +72,9 @@ public class PdfToCbzUtilsTest {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> PdfToCbzUtils.convertPdfToCbz(nonPdfFile, 300, pdfDocumentFactory));
() ->
PdfToCbzUtils.convertPdfToCbz(
nonPdfFile, 300, pdfDocumentFactory, tempFileManager));
Assertions.assertEquals("File must be in PDF format", exception.getMessage());
}
@@ -84,7 +92,9 @@ public class PdfToCbzUtilsTest {
// structure
Assertions.assertThrows(
Exception.class,
() -> PdfToCbzUtils.convertPdfToCbz(pdfFile, 300, pdfDocumentFactory));
() ->
PdfToCbzUtils.convertPdfToCbz(
pdfFile, 300, pdfDocumentFactory, tempFileManager));
// Verify that load was called
Mockito.verify(pdfDocumentFactory).load(pdfFile);
@@ -27,7 +27,7 @@ import stirling.software.common.service.UserServiceInterface;
class PdfMetadataServiceBasicTest {
private PdfMetadataService pdfMetadataService;
private final String STIRLING_PDF_LABEL = "Stirling PDF";
private static final String STIRLING_PDF_LABEL = "Stirling PDF";
@BeforeEach
void setUp() {
@@ -33,7 +33,7 @@ class PdfMetadataServiceTest {
@Mock private ApplicationProperties applicationProperties;
@Mock private UserServiceInterface userService;
private PdfMetadataService pdfMetadataService;
private final String STIRLING_PDF_LABEL = "Stirling PDF";
private static final String STIRLING_PDF_LABEL = "Stirling PDF";
@BeforeEach
void setUp() {
@@ -28,8 +28,8 @@ class SignatureServiceTest {
private SharedSignatureService signatureService;
private Path personalSignatureFolder;
private Path sharedSignatureFolder;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final String TEST_USER = "testUser";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
private static final String TEST_USER = "testUser";
@BeforeEach
void setUp() throws IOException {
@@ -49,7 +49,7 @@ public enum AuditEventType {
// If the exact enum name doesn't match, try finding a similar one
for (AuditEventType eventType : values()) {
if (eventType.name().equalsIgnoreCase(type)
|| eventType.getDescription().equalsIgnoreCase(type)) {
|| eventType.description.equalsIgnoreCase(type)) {
return eventType;
}
}
@@ -35,7 +35,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper;
// Storage limits per user
@@ -115,7 +115,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
}
// Extract base64 data
String base64Data = dataUrl.substring(dataUrl.indexOf(",") + 1);
String base64Data = dataUrl.substring(dataUrl.indexOf(',') + 1);
byte[] imageBytes = Base64.getDecoder().decode(base64Data);
// Validate decoded size
@@ -127,8 +127,8 @@ public class SignatureService implements PersonalSignatureServiceInterface {
}
// Determine and validate file extension from data URL
String mimeType = dataUrl.substring(dataUrl.indexOf(":") + 1, dataUrl.indexOf(";"));
String rawExtension = mimeType.substring(mimeType.indexOf("/") + 1);
String mimeType = dataUrl.substring(dataUrl.indexOf(':') + 1, dataUrl.indexOf(';'));
String rawExtension = mimeType.substring(mimeType.indexOf('/') + 1);
String extension = validateAndNormalizeExtension(rawExtension);
// Save image file
+29 -38
View File
@@ -85,49 +85,40 @@ allprojects {
}
}
def writeIfChanged(File targetFile, String newContent) {
if (targetFile.getText('UTF-8') != newContent) {
targetFile.write(newContent, 'UTF-8')
}
}
def updateTauriConfigVersion(String version) {
File tauriConfig = file('frontend/src-tauri/tauri.conf.json')
def parsed = new JsonSlurper().parse(tauriConfig)
parsed.version = version
def formatted = JsonOutput.prettyPrint(JsonOutput.toJson(parsed)) + System.lineSeparator()
writeIfChanged(tauriConfig, formatted)
}
def updateSimulationVersion(File fileToUpdate, String version) {
def content = fileToUpdate.getText('UTF-8')
def matcher = content =~ /(appVersion:\s*')([^']*)(')/
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${fileToUpdate} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${version}${matcher.group(3)}")
writeIfChanged(fileToUpdate, updatedContent)
}
def rootProjectRef = project
def appVersionStr = project.version.toString()
def tauriConfigPath = layout.projectDirectory.file('frontend/src-tauri/tauri.conf.json').asFile.path
def sim1Path = layout.projectDirectory.file('frontend/src/core/testing/serverExperienceSimulations.ts').asFile.path
def sim2Path = layout.projectDirectory.file('frontend/src/proprietary/testing/serverExperienceSimulations.ts').asFile.path
tasks.register('syncAppVersion') {
group = 'versioning'
description = 'Synchronizes app version across desktop and simulation configs.'
doLast {
def appVersion = rootProjectRef.version.toString()
println "Synchronizing application version to ${appVersion}"
updateTauriConfigVersion(appVersion)
println "Synchronizing application version to ${appVersionStr}"
[
'frontend/src/core/testing/serverExperienceSimulations.ts',
'frontend/src/proprietary/testing/serverExperienceSimulations.ts'
].each { path ->
updateSimulationVersion(file(path), appVersion)
def tauriConfigFile = new File(tauriConfigPath)
if (tauriConfigFile.exists()) {
def parsed = new groovy.json.JsonSlurper().parse(tauriConfigFile)
parsed.version = appVersionStr
def formatted = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(parsed)) + System.lineSeparator()
if (tauriConfigFile.getText('UTF-8') != formatted) {
tauriConfigFile.write(formatted, 'UTF-8')
}
}
[new File(sim1Path), new File(sim2Path)].each { f ->
if (f.exists()) {
def content = f.getText('UTF-8')
def matcher = (content =~ /(appVersion:\s*')([^']*)(')/)
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${f} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(3)}")
if (content != updatedContent) {
f.write(updatedContent, 'UTF-8')
}
}
}
}
}
@@ -279,7 +270,7 @@ subprojects {
}
def thresholds = [
LINE : 0.16,
LINE : 0.13,
INSTRUCTION: 0.14,
BRANCH : 0.09
]
@@ -372,7 +363,7 @@ subprojects {
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.16
minimum = 0.13
}
// Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
limit {
+323 -359
View File
@@ -1032,37 +1032,6 @@
"vue": ">=3.2.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
"integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@@ -1890,9 +1859,9 @@
}
},
"node_modules/@iconify-json/material-symbols": {
"version": "1.2.57",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.57.tgz",
"integrity": "sha512-jt3umOT1AyRimDKogZs6ygp5HQy2YhYz0RMCBcgSl+f7O2sjcAABqrFGqmqNGKIlDxzh2+MdQKu0KX9z6dgpKQ==",
"version": "1.2.58",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.58.tgz",
"integrity": "sha512-yPDXwGFNZ4Fq6O8NGbMGP7N4lVk8uX+oMwF3rIb6WRv6lID1W+pd9GN/KiM20rxZR36FjrG6TI5+x2LKLHdOnA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1950,91 +1919,6 @@
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@@ -2173,9 +2057,9 @@
}
},
"node_modules/@maxim_mazurok/gapi.client.drive-v3": {
"version": "0.1.20260215",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.1.20260215.tgz",
"integrity": "sha512-u2FW7c54o1aA4HDXiQSc28urNQwLfegS9FzfD3TnLfAHcTC8OO83Dq03nAUJFuWbThQNdo7QmRskZjB9EIx7Sw==",
"version": "0.1.20260220",
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.1.20260220.tgz",
"integrity": "sha512-ySN46cAYsMw6IiZ7a3eKeUqyH++eL4sPIFlgwu33l0mJHLevK4Qd5VxJOgMS8nBp44xKssCCBLRuRq091rp1WA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2675,18 +2559,6 @@
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -2998,9 +2870,9 @@
}
},
"node_modules/@posthog/types": {
"version": "1.353.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.353.0.tgz",
"integrity": "sha512-wG5zMdr80QNV+0XkJFCX5ZxcpdER1TtYe21YlLAuI4i0G1wjQDi/FDCjh4b0QGiV8KljQMHfC2xPZ/SXnPBlBQ==",
"version": "1.354.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.354.0.tgz",
"integrity": "sha512-sfH1PiThX1YWkrZSls6zMuZcJWnvboCnZEJ3Z/OI8WgBmLDJfQpficbuLM3tgSLIchI22TPAkpwdT987iW6XIA==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
@@ -4595,16 +4467,6 @@
"node": ">=18"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -5524,9 +5386,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"version": "10.4.26",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.26.tgz",
"integrity": "sha512-c6Hxv5eR12gQmANICaAGM967LGOXZ4SVAuwkiDrqPqZ5oReOnj/ZBtj3dyfwAnEV5qbzspNzjMM8lZENDK8f5A==",
"funding": [
{
"type": "opencollective",
@@ -5544,7 +5406,7 @@
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"caniuse-lite": "^1.0.30001774",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -5636,9 +5498,9 @@
}
},
"node_modules/bare-fs": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
"integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
@@ -5662,9 +5524,9 @@
}
},
"node_modules/bare-os": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz",
"integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
@@ -6164,6 +6026,13 @@
"node": ">=12"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -6179,6 +6048,19 @@
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -6949,9 +6831,9 @@
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
@@ -7564,34 +7446,6 @@
"node": ">=18"
}
},
"node_modules/filing-cabinet/node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/filing-cabinet/node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -7698,19 +7552,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -7915,28 +7756,6 @@
"node": ">= 14"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -7950,37 +7769,6 @@
"node": ">=10.13.0"
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/globals": {
"version": "17.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz",
@@ -8749,6 +8537,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
@@ -9794,6 +9595,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
@@ -10482,9 +10296,9 @@
}
},
"node_modules/posthog-js": {
"version": "1.353.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.353.0.tgz",
"integrity": "sha512-1ifN9CDMavZwRvm+6UYAaEbvzJPvC6q0warmNGyqUcmtDe3SZh4PfvftSJrucH5Oeh6r+ihWtLS7ESIUS0pYuw==",
"version": "1.354.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.354.0.tgz",
"integrity": "sha512-qrpToz7mN1PmEfo+Ob4Z8euX4z2p17LA0EAtFeyod3IVnlwnu+Ybea/oxVsPiq5YAPo+p5z73FcjF2yEJ7oZnA==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
@@ -10493,7 +10307,7 @@
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.23.1",
"@posthog/types": "1.353.0",
"@posthog/types": "1.354.0",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",
@@ -10866,15 +10680,6 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/re-resizable": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
@@ -11202,6 +11007,59 @@
"npm-normalize-package-bin": "^1.0.0"
}
},
"node_modules/read-package-json/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/read-package-json/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/read-package-json/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/read-package-json/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -11386,6 +11244,13 @@
"node": ">=8"
}
},
"node_modules/restore-cursor/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
@@ -11542,11 +11407,17 @@
"license": "ISC"
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC"
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/signature_pad": {
"version": "5.1.3",
@@ -11776,16 +11647,18 @@
}
},
"node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=20"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -11807,31 +11680,24 @@
"node": ">=8"
}
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
"node": ">=8"
}
},
"node_modules/stringify-object": {
@@ -11850,16 +11716,18 @@
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
@@ -11876,6 +11744,18 @@
"node": ">=8"
}
},
"node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -11899,6 +11779,15 @@
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
@@ -12000,9 +11889,9 @@
}
},
"node_modules/svelte": {
"version": "5.53.6",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.6.tgz",
"integrity": "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA==",
"version": "5.53.5",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz",
"integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -12078,6 +11967,74 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/tablemark/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/tablemark/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/tablemark/node_modules/string-width": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
"integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tablemark/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/tablemark/node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tailwindcss": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
@@ -12452,6 +12409,21 @@
}
}
},
"node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -13165,17 +13137,18 @@
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=18"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@@ -13200,6 +13173,13 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -13215,22 +13195,24 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
"dependencies": {
"ansi-regex": "^5.0.1"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -13239,44 +13221,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -13377,6 +13321,13 @@
"node": ">=12"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -13392,6 +13343,19 @@
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@@ -38,7 +38,7 @@ const SplitAutomationSettings = ({ parameters, onParameterChange, disabled = fal
label={t("split.steps.chooseMethod", "Choose Method")}
placeholder={t("split.selectMethod", "Select a split method")}
value={parameters.method}
onChange={(value) => onParameterChange('method', value as (SplitMethod | '') || '')}
onChange={(value) => onParameterChange('method', value as SplitMethod | null)}
data={methodSelectOptions}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
import { TooltipContent } from '@app/types/tips';
import { SPLIT_METHODS, type SplitMethod } from '@app/constants/splitConstants';
export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | null => {
export const useSplitSettingsTips = (method: SplitMethod | null): TooltipContent | null => {
const { t } = useTranslation();
if (!method) return null;
@@ -68,6 +68,7 @@ export const getSplitEndpoint = (parameters: SplitParameters): string => {
}
switch (parameters.method) {
case null:
case SPLIT_METHODS.BY_PAGES:
return "/api/v1/general/split-pages";
case SPLIT_METHODS.BY_SECTIONS:
@@ -3,7 +3,7 @@ import { BaseParameters } from '@app/types/parameters';
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
export interface SplitParameters extends BaseParameters {
method: SplitMethod | '';
method: SplitMethod | null;
pages: string;
hDiv: string;
vDiv: string;
@@ -24,7 +24,7 @@ export interface SplitParameters extends BaseParameters {
export type SplitParametersHook = BaseParametersHook<SplitParameters>;
export const defaultParameters: SplitParameters = {
method: '',
method: null,
pages: '',
hDiv: '2',
vDiv: '2',
@@ -45,13 +45,8 @@ export const defaultParameters: SplitParameters = {
export const useSplitParameters = (): SplitParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: (params) => {
if (!params.method) return ENDPOINTS[SPLIT_METHODS.BY_PAGES];
return ENDPOINTS[params.method as SplitMethod];
},
endpointName: (params) => params.method ? ENDPOINTS[params.method] : ENDPOINTS[SPLIT_METHODS.BY_PAGES],
validateFn: (params) => {
if (!params.method) return false;
switch (params.method) {
case SPLIT_METHODS.BY_PAGES:
return params.pages.trim() !== "";
+1 -1
View File
@@ -60,7 +60,7 @@ const Split = (props: BaseToolProps) => {
{
title: t("split.steps.chooseMethod", "Choose Method"),
isCollapsed: !!base.params.parameters.method, // Collapse when method is selected
onCollapsedClick: () => base.params.updateParameter('method', ''),
onCollapsedClick: () => base.params.updateParameter('method', null),
tooltip: methodTips,
content: (
<CardSelector<SplitMethod, MethodOption>
+13 -1
View File
@@ -26,4 +26,16 @@ declare module 'axios' {
}
}
export {};
declare module 'posthog-js/react' {
import { ReactNode } from 'react';
import posthogJs, { PostHogConfig } from 'posthog-js';
export const PostHogProvider: React.FC<{
client?: typeof posthogJs;
options?: Partial<PostHogConfig>;
apiKey?: string;
children?: ReactNode;
}>;
}
export { };
+11 -3
View File
@@ -5,14 +5,22 @@
"@app/*": [
"src/core/*"
],
"@core/*": ["src/core/*"],
"@proprietary/*": ["src/core/*"]
"@core/*": [
"src/core/*"
],
"@proprietary/*": [
"src/core/*"
],
"posthog-js/react": [
"node_modules/posthog-js/react/dist/types/index.d.ts"
]
}
},
"include": [
"src/global.d.ts",
"src/*.js",
"src/*.ts",
"src/*.tsx",
"src/core"
]
}
}
+11 -3
View File
@@ -7,15 +7,23 @@
"src/proprietary/*",
"src/core/*"
],
"@proprietary/*": ["src/proprietary/*"],
"@core/*": ["src/core/*"]
"@proprietary/*": [
"src/proprietary/*"
],
"@core/*": [
"src/core/*"
],
"posthog-js/react": [
"node_modules/posthog-js/react/dist/types/index.d.ts"
]
}
},
"include": [
"src/global.d.ts",
"src/*.js",
"src/*.ts",
"src/*.tsx",
"src/core/setupTests.ts",
"src/desktop"
]
}
}
+11 -3
View File
@@ -6,15 +6,23 @@
"src/proprietary/*",
"src/core/*"
],
"@core/*": ["src/core/*"],
"@proprietary/*": ["src/proprietary/*"]
"@core/*": [
"src/core/*"
],
"@proprietary/*": [
"src/proprietary/*"
],
"posthog-js/react": [
"node_modules/posthog-js/react/dist/types/index.d.ts"
]
}
},
"include": [
"src/global.d.ts",
"src/*.js",
"src/*.ts",
"src/*.tsx",
"src/core/setupTests.ts",
"src/proprietary"
]
}
}
+6
View File
@@ -2,6 +2,7 @@ import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import path from 'path';
export default defineConfig(({ mode }) => {
@@ -116,5 +117,10 @@ export default defineConfig(({ mode }) => {
},
},
base: env.RUN_SUBPATH ? `/${env.RUN_SUBPATH}` : './',
resolve: {
alias: {
'posthog-js/react': path.resolve(__dirname, 'node_modules/posthog-js/react/dist/esm/index.js'),
},
},
};
});