From a7cd6bfd2eee4df960fdbbac024199f233d4553c Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 2 Sep 2023 00:05:50 +0100 Subject: [PATCH] itext changes --- build.gradle | 1 + .../software/SPDF/SPdfApplication.java | 1 - .../software/SPDF/config/OpenApiConfig.java | 9 +- .../config/security/InitialSecuritySetup.java | 15 +- .../api/ToSinglePageController.java | 51 +- .../api/other/PageNumbersController.java | 95 ++- .../controller/api/other/ShowJavascript.java | 76 +-- .../api/security/CertSignController2.java | 258 ++++++++ .../controller/api/security/GetInfoOnPDF.java | 610 +++++++++--------- .../SPDF/model/ApplicationProperties.java | 42 +- .../software/SPDF/utils/GeneralUtils.java | 3 +- src/main/resources/settings.yml.template | 4 - 12 files changed, 668 insertions(+), 497 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java diff --git a/build.gradle b/build.gradle index c8cb5d3a..33d9e069 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,7 @@ dependencies { //general PDF implementation 'org.apache.pdfbox:pdfbox:2.0.29' + implementation 'org.apache.pdfbox:xmpbox:2.0.29' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' implementation 'com.itextpdf:itext7-core:7.2.5' diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index c26999d0..65542e17 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -66,7 +66,6 @@ public class SPdfApplication { GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/templates/"); - GeneralUtils.createDir("config"); diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 573873da..d3cade1a 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -18,14 +18,9 @@ public class OpenApiConfig { public OpenAPI customOpenAPI() { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { - Properties props = new Properties(); - try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { - props.load(input); - version = props.getProperty("version"); - } catch (IOException ex) { - ex.printStackTrace(); + version = "1.0.0"; // default version if all else fails - } + } return new OpenAPI().components(new Components()).info( diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 08b939b7..7bb13535 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -14,25 +14,26 @@ import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; - +import stirling.software.SPDF.model.*; @Component public class InitialSecuritySetup { @Autowired private UserService userService; + @Autowired ApplicationProperties applicationProperties; @PostConstruct public void init() { if (!userService.hasUsers()) { - String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); - String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); - if (initialUsername != null && initialPassword != null) { - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - } - + String initialUsername = "admin"; + String initialPassword = "stirling"; + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + + + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 42e18e24..4e4ea33f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,8 +1,14 @@ package stirling.software.SPDF.controller.api; +import java.awt.geom.AffineTransform; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -24,6 +30,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class ToSinglePageController { @@ -41,37 +49,34 @@ public class ToSinglePageController { @Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true) MultipartFile file) throws IOException { - PdfReader reader = new PdfReader(file.getInputStream()); - PdfDocument sourceDocument = new PdfDocument(reader); - + PDDocument sourceDocument = PDDocument.load(file.getInputStream()); float totalHeight = 0; float width = 0; - for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) { - Rectangle pageSize = sourceDocument.getPage(i).getPageSize(); + for (PDPage page : sourceDocument.getPages()) { + PDRectangle pageSize = page.getMediaBox(); totalHeight += pageSize.getHeight(); if(width < pageSize.getWidth()) - width = pageSize.getWidth(); + width = pageSize.getWidth(); + } + + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(new PDRectangle(width, totalHeight)); + newDocument.addPage(newPage); + + LayerUtility layerUtility = new LayerUtility(newDocument); + float yOffset = totalHeight; + + for (PDPage page : sourceDocument.getPages()) { + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); + AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); + layerUtility.appendFormAsLayer(newDocument.getPage(0), form, af, page.getResources().getCOSObject().toString()); + yOffset -= page.getMediaBox().getHeight(); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument newDocument = new PdfDocument(writer); - PageSize newPageSize = new PageSize(width, totalHeight); - newDocument.addNewPage(newPageSize); - - Document layoutDoc = new Document(newDocument); - float yOffset = totalHeight; - - for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) { - PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument); - Image copiedPage = new Image(pageCopy); - copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight()); - yOffset -= sourceDocument.getPage(i).getPageSize().getHeight(); - layoutDoc.add(copiedPage); - } - - layoutDoc.close(); + newDocument.save(baos); + newDocument.close(); sourceDocument.close(); byte[] result = baos.toByteArray(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index 14288c83..c48a77e9 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java @@ -6,6 +6,11 @@ import java.io.IOException; import java.net.URLEncoder; import java.util.List; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; @@ -15,18 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.io.font.constants.StandardFonts; -import com.itextpdf.kernel.font.PdfFont; -import com.itextpdf.kernel.font.PdfFontFactory; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.canvas.PdfCanvas; -import com.itextpdf.layout.Canvas; -import com.itextpdf.layout.element.Paragraph; -import com.itextpdf.layout.properties.TextAlignment; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -51,11 +45,10 @@ public class PageNumbersController { @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber, @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText) throws IOException { + int pageNumber = startingNumber; + byte[] fileBytes = file.getBytes(); + PDDocument document = PDDocument.load(fileBytes); - byte[] fileBytes = file.getBytes(); - ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes); - - int pageNumber = startingNumber; float marginFactor; switch (customMargin.toLowerCase()) { case "small": @@ -67,79 +60,67 @@ public class PageNumbersController { case "large": marginFactor = 0.05f; break; - case "x-large": - marginFactor = 0.1f; - break; default: marginFactor = 0.035f; break; } float fontSize = 12.0f; + PDType1Font font = PDType1Font.HELVETICA; - PdfReader reader = new PdfReader(bais); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - - PdfDocument pdfDoc = new PdfDocument(reader, writer); - - List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages()); + List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); for (int i : pagesToNumberList) { - PdfPage page = pdfDoc.getPage(i+1); - Rectangle pageSize = page.getPageSize(); - PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc); + PDPage page = document.getPage(i); + PDRectangle pageSize = page.getMediaBox(); - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); - - PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA); - float textWidth = font.getWidth(text, fontSize); - float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize); + String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); float x, y; - TextAlignment alignment; int xGroup = (position - 1) % 3; int yGroup = 2 - (position - 1) / 3; switch (xGroup) { case 0: // left - x = pageSize.getLeft() + marginFactor * pageSize.getWidth(); - alignment = TextAlignment.LEFT; + x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); break; case 1: // center - x = pageSize.getLeft() + (pageSize.getWidth()) / 2; - alignment = TextAlignment.CENTER; + x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); break; default: // right - x = pageSize.getRight() - marginFactor * pageSize.getWidth(); - alignment = TextAlignment.RIGHT; + x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth(); break; } switch (yGroup) { - case 0: // bottom - y = pageSize.getBottom() + marginFactor * pageSize.getHeight(); - break; - case 1: // middle - y = pageSize.getBottom() + (pageSize.getHeight() ) / 2; - break; - default: // top - y = pageSize.getTop() - marginFactor * pageSize.getHeight(); - break; - } + case 0: // bottom + y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); + break; + case 1: // middle + y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); + break; + default: // top + y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); + break; + } - new Canvas(pdfCanvas, page.getPageSize()) - .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment); + PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + contentStream.beginText(); + contentStream.setFont(font, fontSize); + contentStream.newLineAtOffset(x, y); + contentStream.showText(text); + contentStream.endText(); + contentStream.close(); pageNumber++; } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + document.close(); - pdfDoc.close(); - byte[] resultBytes = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF); + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java index 122a1871..e8f33144 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java @@ -1,7 +1,11 @@ package stirling.software.SPDF.controller.api.other; import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDNameTreeNode; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -10,16 +14,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.pdf.PdfArray; -import com.itextpdf.kernel.pdf.PdfDictionary; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfName; -import com.itextpdf.kernel.pdf.PdfObject; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfStream; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @@ -28,55 +22,33 @@ public class ShowJavascript { private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); @PostMapping(consumes = "multipart/form-data", value = "/show-javascript") - @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") public ResponseEntity extractHeader( - @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile) - throws Exception { + @RequestPart(value = "fileInput") MultipartFile inputFile) throws Exception { + + String script = ""; - try ( - PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream())) - ) { - - String name = ""; - String script = ""; - String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: "; - //Javascript - PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); - if (namesDict != null) { - PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript); - if (javascriptDict != null) { + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); - for (int i = 0; i < namesArray.size(); i += 2) { - if(namesArray.getAsString(i) != null) - name = namesArray.getAsString(i).toString(); + PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); - PdfObject jsCode = namesArray.get(i+1); - if (jsCode instanceof PdfStream) { - byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - script = "//" + entryName + name + "\n" +jsCodeStr; + if (jsTree != null) { + Map jsEntries = jsTree.getNames(); - } else if (jsCode instanceof PdfDictionary) { - // If the JS code is in a dictionary, you'll need to know the key to use. - // Assuming the key is PdfName.JS: - PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS); - if (jsCodeStream != null) { - byte[] jsCodeBytes = jsCodeStream.getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - script = "//" + entryName + name + "\n" +jsCodeStr; - } - } - } + for (Map.Entry entry : jsEntries.entrySet()) { + String name = entry.getKey(); + PDActionJavaScript jsAction = entry.getValue(); + String jsCodeStr = jsAction.getAction(); - } + script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; } - if(script.equals("")) { - script = "PDF '" +inputFile.getOriginalFilename() + "' does not contain Javascript"; - } - return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js"); - } - + } + + if (script.isEmpty()) { + script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + } + + return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java new file mode 100644 index 00000000..e159b2a3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java @@ -0,0 +1,258 @@ +package stirling.software.SPDF.controller.api.security; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.cms.CMSTypedData; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ContentDisposition; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.utils.WebResponseUtils; + +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +@RestController +@Tag(name = "Security", description = "Security APIs") +public class CertSignController2 { + + private static final Logger logger = LoggerFactory.getLogger(CertSignController2.class); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") + @Operation(summary = "Sign PDF with a Digital Certificate", + description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") + public ResponseEntity signPDF( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be signed") + MultipartFile pdf, + + @RequestParam(value = "certType", required = false) + @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) + String certType, + + @RequestParam(value = "key", required = false) + @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") + MultipartFile privateKeyFile, + + @RequestParam(value = "cert", required = false) + @Parameter(description = "The digital certificate (required for PEM type certificates)") + MultipartFile certFile, + + @RequestParam(value = "p12", required = false) + @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") + MultipartFile p12File, + + @RequestParam(value = "password", required = false) + @Parameter(description = "The password for the keystore or the private key") + String password, + + @RequestParam(value = "showSignature", required = false) + @Parameter(description = "Whether to visually show the signature in the PDF file") + Boolean showSignature, + + @RequestParam(value = "reason", required = false) + @Parameter(description = "The reason for signing the PDF") + String reason, + + @RequestParam(value = "location", required = false) + @Parameter(description = "The location where the PDF is signed") + String location, + + @RequestParam(value = "name", required = false) + @Parameter(description = "The name of the signer") + String name, + + @RequestParam(value = "pageNumber", required = false) + @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") + Integer pageNumber) throws Exception { + + BouncyCastleProvider provider = new BouncyCastleProvider(); + Security.addProvider(provider); + + PrivateKey privateKey = null; + X509Certificate cert = null; + + if (certType != null) { + switch (certType) { + case "PKCS12": + if (p12File != null) { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); + String alias = ks.aliases().nextElement(); + privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + cert = (X509Certificate) ks.getCertificate(alias); + } + break; + case "PEM": + if (privateKeyFile != null && certFile != null) { + // Load private key + KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider); + if (isPEM(privateKeyFile.getBytes())) { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); + } else { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); + } + + // Load certificate + CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); + if (isPEM(certFile.getBytes())) { + cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); + } else { + cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); + } + } + break; + } + } + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + + // Load the PDF + try (PDDocument document = PDDocument.load(pdf.getBytes())) { + SignatureOptions signatureOptions = new SignatureOptions(); + + // If you want to show the signature + if (showSignature != null && showSignature) { + // Calculate signature field position based on your requirements + + PDPage page = document.getPage(pageNumber - 1); // zero-based + + PDVisibleSignDesigner signDesigner = new PDVisibleSignDesigner(new ByteArrayInputStream(pdf.getBytes())); + //TODO signDesigner + + PDVisibleSigProperties sigProperties = new PDVisibleSigProperties(); + + //TODO sigProperties extra + signatureOptions.setVisualSignature(sigProperties); + signatureOptions.setPage(pageNumber - 1); + } + + document.addSignature(signature, signatureOptions); + + // External signing + ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); + + byte[] content = IOUtils.toByteArray(externalSigning.getContent()); + + // Using BouncyCastle to sign + CMSTypedData cmsData = new CMSProcessableByteArray(content); + + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider).build(privateKey); + + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(provider).build()).build(signer, cert)); + + gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); + CMSSignedData signedData = gen.generate(cmsData, false); + + byte[] cmsSignature = signedData.getEncoded(); + + externalSigning.setSignature(cmsSignature); + + + // After setting the signature, return the resultant PDF + try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { + document.save(signedPdfOutput); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDisposition(ContentDisposition.builder("attachment").filename("signed.pdf").build()); + + return new ResponseEntity<>(signedPdfOutput.toByteArray(), headers, HttpStatus.OK); + } + } + + + } + + private byte[] parsePEM(byte[] content) throws IOException { + PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + return pemReader.readPemObject().getContent(); + } + + private boolean isPEM(byte[] content) { + String contentStr = new String(content); + return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + } + + + + + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 393ac7ed..ef4187f7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -10,14 +10,52 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - +import java.io.ByteArrayOutputStream; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSDocument; +import org.apache.pdfbox.cos.COSInputStream; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSObject; +import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.COSObjectable; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -25,33 +63,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.xml.XmpParsingException; +import org.apache.xmpbox.xml.XmpSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.itextpdf.forms.PdfAcroForm; -import com.itextpdf.forms.fields.PdfFormField; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfArray; -import com.itextpdf.kernel.pdf.PdfCatalog; -import com.itextpdf.kernel.pdf.PdfDictionary; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfName; -import com.itextpdf.kernel.pdf.PdfObject; -import com.itextpdf.kernel.pdf.PdfOutline; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfResources; -import com.itextpdf.kernel.pdf.PdfStream; -import com.itextpdf.kernel.pdf.PdfString; -import com.itextpdf.kernel.pdf.annot.PdfAnnotation; -import com.itextpdf.kernel.pdf.annot.PdfFileAttachmentAnnotation; -import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation; -import com.itextpdf.kernel.pdf.layer.PdfLayer; -import com.itextpdf.kernel.pdf.layer.PdfOCProperties; -import com.itextpdf.kernel.xmp.XMPException; -import com.itextpdf.kernel.xmp.XMPMeta; -import com.itextpdf.kernel.xmp.XMPMetaFactory; -import com.itextpdf.kernel.xmp.options.SerializeOptions; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -72,7 +90,6 @@ public class GetInfoOnPDF { try ( PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); - PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream())) ) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode jsonOutput = objectMapper.createObjectNode(); @@ -120,21 +137,17 @@ public class GetInfoOnPDF { boolean hasCompression = false; String compressionType = "None"; - // Check for object streams - for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) { - PdfObject obj = itextDoc.getPdfObject(i); - if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) { - hasCompression = true; - compressionType = "Object Streams"; - break; + COSDocument cosDoc = pdfBoxDoc.getDocument(); + for (COSObject cosObject : cosDoc.getObjects()) { + if (cosObject.getObject() instanceof COSStream) { + COSStream cosStream = (COSStream) cosObject.getObject(); + if (COSName.OBJ_STM.equals(cosStream.getItem(COSName.TYPE))) { + hasCompression = true; + compressionType = "Object Streams"; + break; + } } } - - // If not compressed using object streams, check for compressed Xref tables - if (!hasCompression && itextDoc.getReader().hasRebuiltXref()) { - hasCompression = true; - compressionType = "Compressed Xref or Rebuilt Xref"; - } basicInfo.put("Compression", hasCompression); if(hasCompression) basicInfo.put("CompressionType", compressionType); @@ -144,9 +157,8 @@ public class GetInfoOnPDF { basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); - // Page Mode using iText7 - PdfCatalog catalog = itextDoc.getCatalog(); - PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode); + PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); + String pageMode = catalog.getPageMode().name(); // Document Information using PDFBox docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); @@ -157,49 +169,54 @@ public class GetInfoOnPDF { - PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false); + PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); + ObjectNode formFieldsNode = objectMapper.createObjectNode(); if (acroForm != null) { - for (Map.Entry entry : acroForm.getFormFields().entrySet()) { - formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString()); + for (PDField field : acroForm.getFieldTree()) { + formFieldsNode.put(field.getFullyQualifiedName(), field.getValueAsString()); } } jsonOutput.set("FormFields", formFieldsNode); + //embeed files TODO size + PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); + ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); - if(itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) != null) - { - PdfDictionary embeddedFiles = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) - .getAsDictionary(PdfName.EmbeddedFiles); - if (embeddedFiles != null) { - - PdfArray namesArray = embeddedFiles.getAsArray(PdfName.Names); - if(namesArray != null) { - for (int i = 0; i < namesArray.size(); i += 2) { - ObjectNode embeddedFileNode = objectMapper.createObjectNode(); - embeddedFileNode.put("Name", namesArray.getAsString(i).toString()); - // Add other details if required - embeddedFilesArray.add(embeddedFileNode); - } + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + for (Map.Entry entry : efMap.entrySet()) { + ObjectNode embeddedFileNode = objectMapper.createObjectNode(); + embeddedFileNode.put("Name", entry.getKey()); + PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); + if (embeddedFile != null) { + embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes + } + embeddedFilesArray.add(embeddedFileNode); + } } - - } } other.set("EmbeddedFiles", embeddedFilesArray); + + //attachments TODO size ArrayNode attachmentsArray = objectMapper.createArrayNode(); - for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { - for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) { - if (annotation instanceof PdfFileAttachmentAnnotation) { + for (PDPage page : pdfBoxDoc.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationFileAttachment) { + PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; + ObjectNode attachmentNode = objectMapper.createObjectNode(); - attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString()); - attachmentNode.put("Description", annotation.getContents().getValue()); + attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); + attachmentNode.put("Description", fileAttachmentAnnotation.getContents()); + attachmentsArray.add(attachmentNode); } } @@ -207,65 +224,54 @@ public class GetInfoOnPDF { other.set("Attachments", attachmentsArray); //Javascript - PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); + PDDocumentNameDictionary namesDict = catalog.getNames(); ArrayNode javascriptArray = objectMapper.createArrayNode(); + if (namesDict != null) { - PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript); + PDJavascriptNameTreeNode javascriptDict = namesDict.getJavaScript(); if (javascriptDict != null) { + try { + Map jsEntries = javascriptDict.getNames(); - PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); - for (int i = 0; i < namesArray.size(); i += 2) { - ObjectNode jsNode = objectMapper.createObjectNode(); - if(namesArray.getAsString(i) != null) - jsNode.put("JS Name", namesArray.getAsString(i).toString()); + for (Map.Entry entry : jsEntries.entrySet()) { + ObjectNode jsNode = objectMapper.createObjectNode(); + jsNode.put("JS Name", entry.getKey()); - // Here we check for a PdfStream object and retrieve the JS code from it - PdfObject jsCode = namesArray.get(i+1); - if (jsCode instanceof PdfStream) { - byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - jsNode.put("JS Script Length", jsCodeStr.length()); - } else if (jsCode instanceof PdfDictionary) { - // If the JS code is in a dictionary, you'll need to know the key to use. - // Assuming the key is PdfName.JS: - PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS); - if (jsCodeStream != null) { - byte[] jsCodeBytes = jsCodeStream.getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - jsNode.put("JS Script Character Length", jsCodeStr.length()); + PDActionJavaScript jsAction = entry.getValue(); + if (jsAction != null) { + String jsCodeStr = jsAction.getAction(); + if (jsCodeStr != null) { + jsNode.put("JS Script Length", jsCodeStr.length()); + } } + + javascriptArray.add(jsNode); } - - javascriptArray.add(jsNode); + } catch (IOException e) { + e.printStackTrace(); } - } } other.set("JavaScript", javascriptArray); + //TODO size - PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false); + PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); ArrayNode layersArray = objectMapper.createArrayNode(); + if (ocProperties != null) { - - for (PdfLayer layer : ocProperties.getLayers()) { + for (PDOptionalContentGroup ocg : ocProperties.getOptionalContentGroups()) { ObjectNode layerNode = objectMapper.createObjectNode(); - layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString()); + layerNode.put("Name", ocg.getName()); layersArray.add(layerNode); } - } + other.set("Layers", layersArray); //TODO Security - - - - - // Digital Signatures using iText7 TODO - @@ -282,13 +288,13 @@ public class GetInfoOnPDF { } - boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A"); - boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X"); - boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E"); - boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT"); - boolean isPdfUACompliant = checkForStandard(itextDoc, "PDF/UA"); - boolean isPdfBCompliant = checkForStandard(itextDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. - boolean isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. + boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); + boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); + boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); + boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); + boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); + boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. + boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant); @@ -302,27 +308,39 @@ public class GetInfoOnPDF { + PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); ArrayNode bookmarksArray = objectMapper.createArrayNode(); - PdfOutline root = itextDoc.getOutlines(false); + if (root != null) { - for (PdfOutline child : root.getAllChildren()) { + for (PDOutlineItem child : root.children()) { addOutlinesToArray(child, bookmarksArray); } } + other.set("Bookmarks/Outline/TOC", bookmarksArray); - byte[] xmpBytes = itextDoc.getXmpMetadata(); - String xmpString = null; - if (xmpBytes != null) { - try { - XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes); - xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions())); - } catch (XMPException e) { - e.printStackTrace(); - } - } - other.put("XMPMetadata", xmpString); - + + + PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); + + String xmpString = null; + + if (pdMetadata != null) { + try { + COSInputStream is = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(is); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, os, true); + xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); + } catch (XmpParsingException | IOException e) { + e.printStackTrace(); + } + } + + other.put("XMPMetadata", xmpString); + if (pdfBoxDoc.isEncrypted()) { @@ -356,23 +374,40 @@ public class GetInfoOnPDF { ObjectNode pageInfoParent = objectMapper.createObjectNode(); - for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { + for (int pageNum = 1; pageNum <= pdfBoxDoc.getNumberOfPages(); pageNum++) { ObjectNode pageInfo = objectMapper.createObjectNode(); + // Retrieve the page + PDPage page = pdfBoxDoc.getPage(pageNum); + // Page-level Information - Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize(); - pageInfo.put("Width", pageSize.getWidth()); - pageInfo.put("Height", pageSize.getHeight()); - pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation()); - pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight())); - pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight())); - + PDRectangle mediaBox = page.getMediaBox(); + + float width = mediaBox.getWidth(); + float height = mediaBox.getHeight(); + + pageInfo.put("Width", width); + pageInfo.put("Height", height); + pageInfo.put("Rotation", page.getRotation()); + + pageInfo.put("Page Orientation", getPageOrientation(width, height)); + pageInfo.put("Standard Size", getPageSize(width, height)); + // Boxes - pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString()); - pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString()); - pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString()); - pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString()); - pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString()); + pageInfo.put("MediaBox", mediaBox.toString()); + + // Assuming the following boxes are defined for your document; if not, you may get null values. + PDRectangle cropBox = page.getCropBox(); + pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); + + PDRectangle bleedBox = page.getBleedBox(); + pageInfo.put("BleedBox", bleedBox == null ? "Undefined" : bleedBox.toString()); + + PDRectangle trimBox = page.getTrimBox(); + pageInfo.put("TrimBox", trimBox == null ? "Undefined" : trimBox.toString()); + + PDRectangle artBox = page.getArtBox(); + pageInfo.put("ArtBox", artBox == null ? "Undefined" : artBox.toString()); // Content Extraction PDFTextStripper textStripper = new PDFTextStripper(); @@ -382,17 +417,18 @@ public class GetInfoOnPDF { pageInfo.put("Text Characters Count", pageText.length()); // - // Annotations - List annotations = itextDoc.getPage(pageNum).getAnnotations(); + // Annotations + + List annotations = page.getAnnotations(); int subtypeCount = 0; int contentsCount = 0; - for (PdfAnnotation annotation : annotations) { - if(annotation.getSubtype() != null) { + for (PDAnnotation annotation : annotations) { + if (annotation.getSubtype() != null) { subtypeCount++; // Increase subtype count } - if(annotation.getContents() != null) { + if (annotation.getContents() != null) { contentsCount++; // Increase contents count } } @@ -403,25 +439,31 @@ public class GetInfoOnPDF { annotationsObject.put("ContentsCount", contentsCount); pageInfo.set("Annotations", annotationsObject); + + // Images (simplified) // This part is non-trivial as images can be embedded in multiple ways in a PDF. // Here is a basic structure to recognize image XObjects on a page. ArrayNode imagesArray = objectMapper.createArrayNode(); - PdfResources resources = itextDoc.getPage(pageNum).getResources(); - for (PdfName name : resources.getResourceNames()) { - PdfObject obj = resources.getResource(name); - if (obj instanceof PdfStream) { - PdfStream stream = (PdfStream) obj; - if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) { - ObjectNode imageNode = objectMapper.createObjectNode(); - imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue()); - imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue()); - PdfObject colorSpace = stream.get(PdfName.ColorSpace); - if (colorSpace != null) { - imageNode.put("ColorSpace", colorSpace.toString()); - } - imagesArray.add(imageNode); + PDResources resources = page.getResources(); + + + for (COSName name : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(name); + if (xObject instanceof PDImageXObject) { + PDImageXObject image = (PDImageXObject) xObject; + + ObjectNode imageNode = objectMapper.createObjectNode(); + imageNode.put("Width", image.getWidth()); + imageNode.put("Height", image.getHeight()); + if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { + imageNode.put("Name", image.getMetadata().getFile().getFile()); } + if (image.getColorSpace() != null) { + imageNode.put("ColorSpace", image.getColorSpace().getName()); + } + + imagesArray.add(imageNode); } } pageInfo.set("Images", imagesArray); @@ -431,12 +473,13 @@ public class GetInfoOnPDF { ArrayNode linksArray = objectMapper.createArrayNode(); Set uniqueURIs = new HashSet<>(); // To store unique URIs - for (PdfAnnotation annotation : annotations) { - if (annotation instanceof PdfLinkAnnotation) { - PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation; - if(linkAnnotation != null && linkAnnotation.getAction() != null) { - String uri = linkAnnotation.getAction().toString(); - uniqueURIs.add(uri); // Add to set to ensure uniqueness + for (PDAnnotation annotation : annotations) { + if (annotation instanceof PDAnnotationLink) { + PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation; + if (linkAnnotation.getAction() instanceof PDActionURI) { + PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); + String uri = uriAction.getURI(); + uniqueURIs.add(uri); // Add to set to ensure uniqueness } } } @@ -449,96 +492,52 @@ public class GetInfoOnPDF { } pageInfo.set("Links", linksArray); - // Fonts + + // Fonts ArrayNode fontsArray = objectMapper.createArrayNode(); - PdfDictionary fontDicts = resources.getResource(PdfName.Font); - Set uniqueSubtypes = new HashSet<>(); // To store unique subtypes - - // Map to store unique fonts and their counts Map uniqueFontsMap = new HashMap<>(); - if (fontDicts != null) { - for (PdfName key : fontDicts.keySet()) { - ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font - PdfDictionary font = fontDicts.getAsDictionary(key); + for (COSName fontName : resources.getFontNames()) { + PDFont font = resources.getFont(fontName); + ObjectNode fontNode = objectMapper.createObjectNode(); - boolean isEmbedded = font.containsKey(PdfName.FontFile) || - font.containsKey(PdfName.FontFile2) || - font.containsKey(PdfName.FontFile3); - fontNode.put("IsEmbedded", isEmbedded); + fontNode.put("IsEmbedded", font.isEmbedded()); - if (font.containsKey(PdfName.Encoding)) { - String encoding = font.getAsName(PdfName.Encoding).toString(); - fontNode.put("Encoding", encoding); - } + // PDFBox provides Font's BaseFont (i.e., the font name) directly + fontNode.put("Name", font.getName()); - if (font.getAsString(PdfName.BaseFont) != null) { - fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString()); - } + fontNode.put("Subtype", font.getType()); - String subtype = null; - if (font.containsKey(PdfName.Subtype)) { - subtype = font.getAsName(PdfName.Subtype).toString(); - uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness - } - fontNode.put("Subtype", subtype); + PDFontDescriptor fontDescriptor = font.getFontDescriptor(); - PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor); - if (fontDescriptor != null) { - if (fontDescriptor.containsKey(PdfName.ItalicAngle)) { - fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue()); - } + if (fontDescriptor != null) { + fontNode.put("ItalicAngle", fontDescriptor.getItalicAngle()); + int flags = fontDescriptor.getFlags(); + fontNode.put("IsItalic", (flags & 1) != 0); + fontNode.put("IsBold", (flags & 64) != 0); + fontNode.put("IsFixedPitch", (flags & 2) != 0); + fontNode.put("IsSerif", (flags & 4) != 0); + fontNode.put("IsSymbolic", (flags & 8) != 0); + fontNode.put("IsScript", (flags & 16) != 0); + fontNode.put("IsNonsymbolic", (flags & 32) != 0); - if (fontDescriptor.containsKey(PdfName.Flags)) { - int flags = fontDescriptor.getAsNumber(PdfName.Flags).intValue(); - fontNode.put("IsItalic", (flags & 64) != 0); - fontNode.put("IsBold", (flags & 1 << 16) != 0); - fontNode.put("IsFixedPitch", (flags & 1) != 0); - fontNode.put("IsSerif", (flags & 2) != 0); - fontNode.put("IsSymbolic", (flags & 4) != 0); - fontNode.put("IsScript", (flags & 8) != 0); - fontNode.put("IsNonsymbolic", (flags & 16) != 0); - } + fontNode.put("FontFamily", fontDescriptor.getFontFamily()); + // Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity + fontNode.put("FontWeight", fontDescriptor.getFontWeight()); + } - if (fontDescriptor.containsKey(PdfName.FontFamily)) { - String fontFamily = fontDescriptor.getAsString(PdfName.FontFamily).toString(); - fontNode.put("FontFamily", fontFamily); - } - if (fontDescriptor.containsKey(PdfName.FontStretch)) { - String fontStretch = fontDescriptor.getAsName(PdfName.FontStretch).toString(); - fontNode.put("FontStretch", fontStretch); - } + // Create a unique key for this font node based on its attributes + String uniqueKey = fontNode.toString(); - if (fontDescriptor.containsKey(PdfName.FontBBox)) { - PdfArray bbox = fontDescriptor.getAsArray(PdfName.FontBBox); - fontNode.put("FontBoundingBox", bbox.toString()); - } - - if (fontDescriptor.containsKey(PdfName.FontWeight)) { - float fontWeight = fontDescriptor.getAsNumber(PdfName.FontWeight).floatValue(); - fontNode.put("FontWeight", fontWeight); - } - } - - if (font.containsKey(PdfName.ToUnicode)) { - fontNode.put("HasToUnicodeMap", true); - } - - if (fontNode.size() > 0) { - // Create a unique key for this font node based on its attributes - String uniqueKey = fontNode.toString(); - - // Increment count if this font exists, or initialize it if new - if (uniqueFontsMap.containsKey(uniqueKey)) { - ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey); - int count = existingFontNode.get("Count").asInt() + 1; - existingFontNode.put("Count", count); - } else { - fontNode.put("Count", 1); - uniqueFontsMap.put(uniqueKey, fontNode); - } - } + // Increment count if this font exists, or initialize it if new + if (uniqueFontsMap.containsKey(uniqueKey)) { + ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey); + int count = existingFontNode.get("Count").asInt() + 1; + existingFontNode.put("Count", count); + } else { + fontNode.put("Count", 1); + uniqueFontsMap.put(uniqueKey, fontNode); } } @@ -552,41 +551,49 @@ public class GetInfoOnPDF { - // Access resources dictionary - PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject(); - - // Color Spaces & ICC Profiles + + + + + + + + // Access resources dictionary ArrayNode colorSpacesArray = objectMapper.createArrayNode(); - PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace); - if (colorSpaces != null) { - for (PdfName name : colorSpaces.keySet()) { - PdfObject colorSpaceObject = colorSpaces.get(name); - if (colorSpaceObject instanceof PdfArray) { - PdfArray colorSpaceArray = (PdfArray) colorSpaceObject; - if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) { - ObjectNode iccProfileNode = objectMapper.createObjectNode(); - PdfStream iccStream = (PdfStream) colorSpaceArray.get(1); - byte[] iccData = iccStream.getBytes(); - // TODO: Further decode and analyze the ICC data if needed - iccProfileNode.put("ICC Profile Length", iccData.length); - colorSpacesArray.add(iccProfileNode); - } - } + + Iterable colorSpaceNames = resources.getColorSpaceNames(); + for (COSName name : colorSpaceNames) { + PDColorSpace colorSpace = resources.getColorSpace(name); + if (colorSpace instanceof PDICCBased) { + PDICCBased iccBased = (PDICCBased) colorSpace; + PDStream iccData = iccBased.getPDStream(); + byte[] iccBytes = iccData.toByteArray(); + + // TODO: Further decode and analyze the ICC data if needed + ObjectNode iccProfileNode = objectMapper.createObjectNode(); + iccProfileNode.put("ICC Profile Length", iccBytes.length); + colorSpacesArray.add(iccProfileNode); } } pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); + // Other XObjects Map xObjectCountMap = new HashMap<>(); // To store the count for each type - PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject); - if (xObjects != null) { - for (PdfName name : xObjects.keySet()) { - PdfStream xObjectStream = xObjects.getAsStream(name); - String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString(); - - // Increment the count for this type in the map - xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); + for (COSName name : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(name); + String xObjectType; + + if (xObject instanceof PDImageXObject) { + xObjectType = "Image"; + } else if (xObject instanceof PDFormXObject) { + xObjectType = "Form"; + } else { + xObjectType = "Other"; } + + // Increment the count for this type in the map + xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); } // Add the count map to pageInfo (or wherever you want to store it) @@ -598,14 +605,17 @@ public class GetInfoOnPDF { + ArrayNode multimediaArray = objectMapper.createArrayNode(); - for (PdfAnnotation annotation : annotations) { - if (PdfName.RichMedia.equals(annotation.getSubtype())) { + + for (PDAnnotation annotation : annotations) { + if ("RichMedia".equals(annotation.getSubtype())) { ObjectNode multimediaNode = objectMapper.createObjectNode(); - // Extract details from the dictionary as needed + // Extract details from the annotation as needed multimediaArray.add(multimediaNode); } } + pageInfo.set("Multimedia", multimediaArray); @@ -636,17 +646,21 @@ public class GetInfoOnPDF { return null; } - private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) { + private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { if (outline == null) return; + ObjectNode outlineNode = objectMapper.createObjectNode(); outlineNode.put("Title", outline.getTitle()); // You can add other properties if needed arrayNode.add(outlineNode); - - for (PdfOutline child : outline.getAllChildren()) { + + PDOutlineItem child = outline.getFirstChild(); + while (child != null) { addOutlinesToArray(child, arrayNode); + child = child.getNextSibling(); } } + public String getPageOrientation(double width, double height) { if (width > height) { return "Landscape"; @@ -678,45 +692,33 @@ public class GetInfoOnPDF { return Math.abs(pageAspectRatio - aspectRatio) <= 0.05; } - public boolean checkForStandard(PdfDocument document, String standardKeyword) { - // Check Output Intents - boolean foundInOutputIntents = checkOutputIntent(document, standardKeyword); - if (foundInOutputIntents) return true; - - // Check XMP Metadata (rudimentary) - try { - byte[] metadataBytes = document.getXmpMetadata(); - if (metadataBytes != null) { - XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes); - String xmpString = xmpMeta.dumpObject(); - if (xmpString.contains(standardKeyword)) { - return true; - } - } - } catch (XMPException e) { - e.printStackTrace(); - } - - return false; - } - public boolean checkOutputIntent(PdfDocument document, String standard) { - PdfArray outputIntents = document.getCatalog().getPdfObject().getAsArray(PdfName.OutputIntents); - if (outputIntents != null && !outputIntents.isEmpty()) { - for (int i = 0; i < outputIntents.size(); i++) { - PdfDictionary outputIntentDict = outputIntents.getAsDictionary(i); - if (outputIntentDict != null) { - PdfString s = outputIntentDict.getAsString(PdfName.S); - if (s != null && s.toString().contains(standard)) { - return true; - } - } +public static boolean checkForStandard(PDDocument document, String standardKeyword) { + // Check XMP Metadata + try { + PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); + if (pdMetadata != null) { + COSInputStream metaStream = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(metaStream); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, baos, true); + String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); + + if (xmpString.contains(standardKeyword)) { + return true; } } - return false; + } catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions. + e.printStackTrace(); } + return false; +} + + public ArrayNode exploreStructureTree(List nodes) { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { @@ -771,7 +773,7 @@ public class GetInfoOnPDF { } } - private String getPageModeDescription(PdfName pageMode) { + private String getPageModeDescription(String pageMode) { return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown"; } } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index c8cef5a3..45c37efe 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -104,7 +104,6 @@ public class ApplicationProperties { } public static class Security { private Boolean enableLogin; - private InitialLogin initialLogin; private Boolean csrfDisabled; public Boolean getEnableLogin() { @@ -115,14 +114,6 @@ public class ApplicationProperties { this.enableLogin = enableLogin; } - public InitialLogin getInitialLogin() { - return initialLogin != null ? initialLogin : new InitialLogin(); - } - - public void setInitialLogin(InitialLogin initialLogin) { - this.initialLogin = initialLogin; - } - public Boolean getCsrfDisabled() { return csrfDisabled; } @@ -134,40 +125,9 @@ public class ApplicationProperties { @Override public String toString() { - return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" + return "Security [enableLogin=" + enableLogin + ", csrfDisabled=" + csrfDisabled + "]"; } - - - public static class InitialLogin { - - private String username; - private String password; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - @Override - public String toString() { - return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - - - } } public static class System { diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 28a1e73e..34ee9c35 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -65,7 +65,8 @@ public class GeneralUtils { } else if (sizeStr.endsWith("B")) { return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); } else { - // Input string does not have a valid format, handle this case + // Assume MB if no unit is specified + return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); } } catch (NumberFormatException e) { // The numeric part of the input string cannot be parsed, handle this case diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index cecad0dd..4c227b1c 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -4,15 +4,11 @@ security: enableLogin: false # set to 'true' to enable login - initialLogin: - username: 'username' # Specify the initial username for first boot (e.g. 'admin') - password: 'password' # Specify the initial password for first boot (e.g. 'password123') csrfDisabled: true system: defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) googlevisibility: false # 'true' to allow Google visibility, 'false' to disallow - rootURIPath: / # Set the application's root URI (e.g. /pdf-app) customStaticFilePath: '/customFiles/static/' # Directory path for custom static files #ui: