mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
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:
co-authored by
Anthony Stirling
Balázs
parent
0c46f77179
commit
fd1b7abc83
@@ -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: 
|
||||
// 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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+160
-156
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-64
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-99
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-60
@@ -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 {
|
||||
|
||||
+29
-22
@@ -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")
|
||||
|
||||
+8
-3
@@ -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(" ") + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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<>();
|
||||
|
||||
|
||||
+100
-87
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+1
-1
@@ -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)) {
|
||||
|
||||
+52
-42
@@ -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;
|
||||
|
||||
+1
-1
@@ -254,7 +254,7 @@ public class PipelineDirectoryProcessor {
|
||||
String extension =
|
||||
filename.contains(".")
|
||||
? filename.substring(
|
||||
filename.lastIndexOf(".")
|
||||
filename.lastIndexOf('.')
|
||||
+ 1)
|
||||
.toLowerCase(Locale.ROOT)
|
||||
: "";
|
||||
|
||||
+19
-5
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
-3
@@ -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;
|
||||
}
|
||||
|
||||
+157
-163
@@ -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
|
||||
|
||||
+19
-19
@@ -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()
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
+12
-8
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -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);
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user