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

@ -43,8 +43,8 @@ public class BookletImpositionController {
summary = "Create a booklet with proper page imposition",
description =
"This operation combines page reordering for booklet printing with multi-page layout. "
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> createBookletImposition(
@ModelAttribute BookletImpositionRequest request) throws IOException {
@ -56,7 +56,8 @@ public class BookletImpositionController {
// Validate pages per sheet for booklet
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);
@ -65,8 +66,11 @@ public class BookletImpositionController {
// Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController)
List<Integer> bookletOrder = getBookletPageOrder(bookletType, totalPages);
// Step 2: Create new document with multi-page layout (reusing logic from MultiPageLayoutController)
PDDocument newDocument = createBookletWithLayout(sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation);
// Step 2: Create new document with multi-page layout (reusing logic from
// MultiPageLayoutController)
PDDocument newDocument =
createBookletWithLayout(
sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation);
sourceDocument.close();
@ -112,10 +116,16 @@ public class BookletImpositionController {
}
// Reused and adapted logic from MultiPageLayoutController
private PDDocument createBookletWithLayout(PDDocument sourceDocument, List<Integer> pageOrder,
int pagesPerSheet, boolean addBorder, String pageOrientation) throws IOException {
private PDDocument createBookletWithLayout(
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 rows = pagesPerSheet == 2 ? 1 : 2;
@ -125,16 +135,23 @@ public class BookletImpositionController {
while (currentPageIndex < totalOrderedPages) {
// Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded)
PDRectangle pageSize = "LANDSCAPE".equals(pageOrientation) ?
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) : PDRectangle.A4;
PDRectangle pageSize =
"LANDSCAPE".equals(pageOrientation)
? new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth())
: PDRectangle.A4;
PDPage newPage = new PDPage(pageSize);
newDocument.addPage(newPage);
float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows;
PDPageContentStream contentStream = new PDPageContentStream(
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
PDPageContentStream contentStream =
new PDPageContentStream(
newDocument,
newPage,
PDPageContentStream.AppendMode.APPEND,
true,
true);
LayerUtility layerUtility = new LayerUtility(newDocument);
if (addBorder) {
@ -143,7 +160,9 @@ public class BookletImpositionController {
}
// 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);
PDPage sourcePage = sourceDocument.getPage(sourcePageIndex);
PDRectangle rect = sourcePage.getMediaBox();
@ -156,14 +175,17 @@ public class BookletImpositionController {
int colIndex = sheetPosition % cols;
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
float y = newPage.getMediaBox().getHeight()
- ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
float y =
newPage.getMediaBox().getHeight()
- ((rowIndex + 1) * cellHeight
- (cellHeight - rect.getHeight() * scale) / 2);
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, sourcePageIndex);
PDFormXObject formXObject =
layerUtility.importPageAsForm(sourceDocument, sourcePageIndex);
contentStream.drawForm(formXObject);
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.tags.Tag;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
@ -36,7 +34,7 @@ public class ConfigController {
ApplicationContext applicationContext,
EndpointConfiguration endpointConfiguration,
@org.springframework.beans.factory.annotation.Autowired(required = false)
ServerCertificateServiceInterface serverCertificateService) {
ServerCertificateServiceInterface serverCertificateService) {
this.applicationProperties = applicationProperties;
this.applicationContext = applicationContext;
this.endpointConfiguration = endpointConfiguration;
@ -76,7 +74,8 @@ public class ConfigController {
configData.put("premiumEnabled", applicationProperties.getPremium().isEnabled());
// Server certificate settings
configData.put("serverCertificateEnabled",
configData.put(
"serverCertificateEnabled",
serverCertificateService != null && serverCertificateService.isEnabled());
// Legal settings

View File

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

View File

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

View File

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

View File

@ -11,8 +11,14 @@ import java.security.cert.X509Certificate;
import java.util.Date;
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.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
@ -186,6 +192,36 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
new JcaX509v3CertificateBuilder(
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
ContentSigner signer =
new JcaContentSignerBuilder("SHA256WithRSA")
@ -213,5 +249,4 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
keyStore.store(fos, DEFAULT_PASSWORD.toCharArray());
}
}
}