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