lots of improvements

This commit is contained in:
Anthony Stirling 2025-09-16 15:27:30 +01:00
parent a949019d5d
commit ff9c0e9bd4
8 changed files with 107 additions and 52 deletions

View File

@ -56,7 +56,8 @@ public class BookletImpositionController {
// Validate pages per sheet for booklet // Validate pages per sheet for booklet
if (pagesPerSheet != 2 && pagesPerSheet != 4) { if (pagesPerSheet != 2 && pagesPerSheet != 4) {
throw new IllegalArgumentException("pagesPerSheet must be 2 or 4 for booklet imposition"); throw new IllegalArgumentException(
"pagesPerSheet must be 2 or 4 for booklet imposition");
} }
PDDocument sourceDocument = pdfDocumentFactory.load(file); PDDocument sourceDocument = pdfDocumentFactory.load(file);
@ -65,8 +66,11 @@ public class BookletImpositionController {
// Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController) // Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController)
List<Integer> bookletOrder = getBookletPageOrder(bookletType, totalPages); List<Integer> bookletOrder = getBookletPageOrder(bookletType, totalPages);
// Step 2: Create new document with multi-page layout (reusing logic from MultiPageLayoutController) // Step 2: Create new document with multi-page layout (reusing logic from
PDDocument newDocument = createBookletWithLayout(sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation); // MultiPageLayoutController)
PDDocument newDocument =
createBookletWithLayout(
sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation);
sourceDocument.close(); sourceDocument.close();
@ -112,10 +116,16 @@ public class BookletImpositionController {
} }
// Reused and adapted logic from MultiPageLayoutController // Reused and adapted logic from MultiPageLayoutController
private PDDocument createBookletWithLayout(PDDocument sourceDocument, List<Integer> pageOrder, private PDDocument createBookletWithLayout(
int pagesPerSheet, boolean addBorder, String pageOrientation) throws IOException { PDDocument sourceDocument,
List<Integer> pageOrder,
int pagesPerSheet,
boolean addBorder,
String pageOrientation)
throws IOException {
PDDocument newDocument = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); PDDocument newDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
int cols = pagesPerSheet == 2 ? 2 : 2; // 2x1 for 2 pages, 2x2 for 4 pages int cols = pagesPerSheet == 2 ? 2 : 2; // 2x1 for 2 pages, 2x2 for 4 pages
int rows = pagesPerSheet == 2 ? 1 : 2; int rows = pagesPerSheet == 2 ? 1 : 2;
@ -125,16 +135,23 @@ public class BookletImpositionController {
while (currentPageIndex < totalOrderedPages) { while (currentPageIndex < totalOrderedPages) {
// Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded) // Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded)
PDRectangle pageSize = "LANDSCAPE".equals(pageOrientation) ? PDRectangle pageSize =
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) : PDRectangle.A4; "LANDSCAPE".equals(pageOrientation)
? new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth())
: PDRectangle.A4;
PDPage newPage = new PDPage(pageSize); PDPage newPage = new PDPage(pageSize);
newDocument.addPage(newPage); newDocument.addPage(newPage);
float cellWidth = newPage.getMediaBox().getWidth() / cols; float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows; float cellHeight = newPage.getMediaBox().getHeight() / rows;
PDPageContentStream contentStream = new PDPageContentStream( PDPageContentStream contentStream =
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); new PDPageContentStream(
newDocument,
newPage,
PDPageContentStream.AppendMode.APPEND,
true,
true);
LayerUtility layerUtility = new LayerUtility(newDocument); LayerUtility layerUtility = new LayerUtility(newDocument);
if (addBorder) { if (addBorder) {
@ -143,7 +160,9 @@ public class BookletImpositionController {
} }
// Place pages on the current sheet // Place pages on the current sheet
for (int sheetPosition = 0; sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages; sheetPosition++) { for (int sheetPosition = 0;
sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages;
sheetPosition++) {
int sourcePageIndex = pageOrder.get(currentPageIndex); int sourcePageIndex = pageOrder.get(currentPageIndex);
PDPage sourcePage = sourceDocument.getPage(sourcePageIndex); PDPage sourcePage = sourceDocument.getPage(sourcePageIndex);
PDRectangle rect = sourcePage.getMediaBox(); PDRectangle rect = sourcePage.getMediaBox();
@ -156,14 +175,17 @@ public class BookletImpositionController {
int colIndex = sheetPosition % cols; int colIndex = sheetPosition % cols;
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = newPage.getMediaBox().getHeight() float y =
- ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); newPage.getMediaBox().getHeight()
- ((rowIndex + 1) * cellHeight
- (cellHeight - rect.getHeight() * scale) / 2);
contentStream.saveGraphicsState(); contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y)); contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale)); contentStream.transform(Matrix.getScaleInstance(scale, scale));
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, sourcePageIndex); PDFormXObject formXObject =
layerUtility.importPageAsForm(sourceDocument, sourcePageIndex);
contentStream.drawForm(formXObject); contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();

View File

@ -13,8 +13,6 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.configuration.AppConfig; import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
@ -76,7 +74,8 @@ public class ConfigController {
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled()); configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
// Server certificate settings // Server certificate settings
configData.put("serverCertificateEnabled", configData.put(
"serverCertificateEnabled",
serverCertificateService != null && serverCertificateService.isEnabled()); serverCertificateService != null && serverCertificateService.isEnabled());
// Legal settings // Legal settings

View File

@ -53,6 +53,7 @@ import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException; import org.bouncycastle.pkcs.PKCSException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -70,12 +71,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.common.service.ServerCertificateServiceInterface;
import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.ServerCertificateServiceInterface;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@ -106,7 +105,8 @@ public class CertSignController {
public CertSignController( public CertSignController(
CustomPDFDocumentFactory pdfDocumentFactory, CustomPDFDocumentFactory pdfDocumentFactory,
@Autowired(required = false) ServerCertificateServiceInterface serverCertificateService) { @Autowired(required = false)
ServerCertificateServiceInterface serverCertificateService) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
this.serverCertificateService = serverCertificateService; this.serverCertificateService = serverCertificateService;
} }

View File

@ -7,14 +7,14 @@ import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.service.ServerCertificateService; import stirling.software.common.service.ServerCertificateServiceInterface;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class ServerCertificateInitializer { public class ServerCertificateInitializer {
private final ServerCertificateService serverCertificateService; private final ServerCertificateServiceInterface serverCertificateService;
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void initializeServerCertificate() { public void initializeServerCertificate() {

View File

@ -112,8 +112,7 @@ public class ServerCertificateController {
@GetMapping("/certificate") @GetMapping("/certificate")
@Operation( @Operation(
summary = "Download server certificate", summary = "Download server certificate",
description = description = "Download the server certificate in DER format for validation purposes")
"Download the server certificate in DER format for validation purposes")
public ResponseEntity<byte[]> getServerCertificate() { public ResponseEntity<byte[]> getServerCertificate() {
try { try {
if (!serverCertificateService.hasServerCertificate()) { if (!serverCertificateService.hasServerCertificate()) {

View File

@ -11,8 +11,14 @@ import java.security.cert.X509Certificate;
import java.util.Date; import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentSigner;
@ -186,6 +192,36 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
new JcaX509v3CertificateBuilder( new JcaX509v3CertificateBuilder(
subject, serialNumber, notBefore, notAfter, subject, keyPair.getPublic()); subject, serialNumber, notBefore, notAfter, subject, keyPair.getPublic());
// Add PDF-specific certificate extensions for optimal PDF signing compatibility
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
// 1) End-entity certificate, not a CA (critical)
certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
// 2) Key usage for PDF digital signatures (critical)
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation));
// 3) Extended key usage for document signing (non-critical, widely accepted)
certBuilder.addExtension(
Extension.extendedKeyUsage,
false,
new ExtendedKeyUsage(KeyPurposeId.id_kp_codeSigning));
// 4) Subject Key Identifier for chain building (non-critical)
certBuilder.addExtension(
Extension.subjectKeyIdentifier,
false,
extUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
// 5) Authority Key Identifier for self-signed cert (non-critical)
certBuilder.addExtension(
Extension.authorityKeyIdentifier,
false,
extUtils.createAuthorityKeyIdentifier(keyPair.getPublic()));
// Sign certificate // Sign certificate
ContentSigner signer = ContentSigner signer =
new JcaContentSignerBuilder("SHA256WithRSA") new JcaContentSignerBuilder("SHA256WithRSA")
@ -213,5 +249,4 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
keyStore.store(fos, DEFAULT_PASSWORD.toCharArray()); keyStore.store(fos, DEFAULT_PASSWORD.toCharArray());
} }
} }
} }