formatting

This commit is contained in:
Anthony Stirling
2023-12-30 19:11:27 +00:00
parent 7b43fca6fc
commit 5f771b7851
155 changed files with 5539 additions and 4767 deletions

View File

@@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@@ -61,198 +62,228 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs")
public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static {
Security.addProvider(new BouncyCastleProvider());
}
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<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
@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<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
PrivateKey privateKey = null;
X509Certificate cert = null;
PrivateKey privateKey = null;
X509Certificate cert = null;
if (certType != null) {
logger.info("Cert type provided: {}", certType);
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();
if (!ks.isKeyEntry(alias)) {
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
}
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", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
if (certType != null) {
logger.info("Cert type provided: {}", certType);
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();
if (!ks.isKeyEntry(alias)) {
throw new IllegalArgumentException(
"The provided PKCS12 file does not contain a private key.");
}
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", BouncyCastleProvider.PROVIDER_NAME);
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",
BouncyCastleProvider.PROVIDER_NAME);
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_SHA1);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
// Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// Load certificate
CertificateFactory certFactory =
CertificateFactory.getInstance(
"X.509", BouncyCastleProvider.PROVIDER_NAME);
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_SHA1);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
// If you want to show the signature
// Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// ATTEMPT 2
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
// If you want to show the signature
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
// ATTEMPT 2
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
// Create a new signature field and widget
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
acroForm = new PDAcroForm(document);
document.getDocumentCatalog().setAcroForm(acroForm);
}
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
widget.setRectangle(rect);
page.getAnnotations().add(widget);
// Create a new signature field and widget
// Set the appearance for the signature field
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
appearanceStream.setResources(new PDResources());
appearanceStream.setBBox(rect);
appearanceDict.setNormalAppearance(appearanceStream);
widget.setAppearance(appearanceDict);
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
PDRectangle rect =
new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
widget.setRectangle(rect);
page.getAnnotations().add(widget);
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.newLineAtOffset(110, 130);
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown"));
contentStream.newLineAtOffset(0, -15);
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()));
contentStream.newLineAtOffset(0, -15);
if (reason != null && !reason.isEmpty()) {
contentStream.showText("Reason: " + reason);
contentStream.newLineAtOffset(0, -15);
}
if (location != null && !location.isEmpty()) {
contentStream.showText("Location: " + location);
contentStream.newLineAtOffset(0, -15);
}
contentStream.endText();
}
// Set the appearance for the signature field
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
appearanceStream.setResources(new PDResources());
appearanceStream.setBBox(rect);
appearanceDict.setNormalAppearance(appearanceStream);
widget.setAppearance(appearanceDict);
// Add the widget annotation to the page
page.getAnnotations().add(widget);
try (PDPageContentStream contentStream =
new PDPageContentStream(document, appearanceStream)) {
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.newLineAtOffset(110, 130);
contentStream.showText(
"Digitally signed by: " + (name != null ? name : "Unknown"));
contentStream.newLineAtOffset(0, -15);
contentStream.showText(
"Date: "
+ new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z")
.format(new Date()));
contentStream.newLineAtOffset(0, -15);
if (reason != null && !reason.isEmpty()) {
contentStream.showText("Reason: " + reason);
contentStream.newLineAtOffset(0, -15);
}
if (location != null && !location.isEmpty()) {
contentStream.showText("Location: " + location);
contentStream.newLineAtOffset(0, -15);
}
contentStream.endText();
}
// Add the signature field to the acroform
acroForm.getFields().add(signatureField);
// Add the widget annotation to the page
page.getAnnotations().add(widget);
// Handle multiple signatures by ensuring a unique field name
String baseFieldName = "Signature";
String signatureFieldName = baseFieldName;
int suffix = 1;
while (acroForm.getField(signatureFieldName) != null) {
suffix++;
signatureFieldName = baseFieldName + suffix;
}
signatureField.setPartialName(signatureFieldName);
}
document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning = document
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
// Add the signature field to the acroform
acroForm.getFields().add(signatureField);
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
// Handle multiple signatures by ensuring a unique field name
String baseFieldName = "Signature";
String signatureFieldName = baseFieldName;
int suffix = 1;
while (acroForm.getField(signatureFieldName) != null) {
suffix++;
signatureFieldName = baseFieldName + suffix;
}
signatureField.setPartialName(signatureFieldName);
}
// Using BouncyCastle to sign
CMSTypedData cmsData = new CMSProcessableByteArray(content);
document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning =
document.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
.build(signer, cert));
// Using BouncyCastle to sign
CMSTypedData cmsData = new CMSProcessableByteArray(content);
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer =
new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(privateKey);
byte[] cmsSignature = signedData.getEncoded();
logger.info("About to sign content using BouncyCastle");
externalSigning.setSignature(cmsSignature);
logger.info("Signature set successfully");
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder()
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build())
.build(signer, cert));
// After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
byte[] cmsSignature = signedData.getEncoded();
logger.info("About to sign content using BouncyCastle");
externalSigning.setSignature(cmsSignature);
logger.info("Signature set successfully");
return null;
}
// After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(
signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent();
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
private boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
return null;
}
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");
}
}

View File

@@ -72,23 +72,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class GetInfoOnPDF {
static ObjectMapper objectMapper = new ObjectMapper();
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
static ObjectMapper objectMapper = new ObjectMapper();
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request)
throws IOException {
MultipartFile inputFile = request.getFileInput();
try (
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
) {
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput();
try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode jsonOutput = objectMapper.createObjectNode();
@@ -100,8 +99,7 @@ public class GetInfoOnPDF {
ObjectNode compliancy = objectMapper.createObjectNode();
ObjectNode encryption = objectMapper.createObjectNode();
ObjectNode other = objectMapper.createObjectNode();
metadata.put("Title", info.getTitle());
metadata.put("Author", info.getAuthor());
metadata.put("Subject", info.getSubject());
@@ -111,14 +109,11 @@ public class GetInfoOnPDF {
metadata.put("CreationDate", formatDate(info.getCreationDate()));
metadata.put("ModificationDate", formatDate(info.getModificationDate()));
jsonOutput.set("Metadata", metadata);
// Total file size of the PDF
long fileSizeInBytes = inputFile.getSize();
basicInfo.put("FileSizeInBytes", fileSizeInBytes);
// Number of words, paragraphs, and images in the entire document
String fullText = new PDFTextStripper().getText(pdfBoxDoc);
String[] words = fullText.split("\\s+");
@@ -129,8 +124,7 @@ public class GetInfoOnPDF {
// Number of characters in the entire document (including spaces and special characters)
int charCount = fullText.length();
basicInfo.put("CharacterCount", charCount);
// Initialize the flags and types
boolean hasCompression = false;
String compressionType = "None";
@@ -147,26 +141,21 @@ public class GetInfoOnPDF {
}
}
basicInfo.put("Compression", hasCompression);
if(hasCompression)
basicInfo.put("CompressionType", compressionType);
if (hasCompression) basicInfo.put("CompressionType", compressionType);
String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
basicInfo.put("Language", language);
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
String pageMode = catalog.getPageMode().name();
// Document Information using PDFBox
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
docInfoNode.put("Trapped", info.getTrapped());
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));;
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));
;
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
ObjectNode formFieldsNode = objectMapper.createObjectNode();
@@ -177,41 +166,37 @@ public class GetInfoOnPDF {
}
jsonOutput.set("FormFields", formFieldsNode);
//embeed files TODO size
if(catalog.getNames() != null) {
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
if (efTree != null) {
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
if (efMap != null) {
for (Map.Entry<String, PDComplexFileSpecification> 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);
// embeed files TODO size
if (catalog.getNames() != null) {
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
if (efTree != null) {
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
if (efMap != null) {
for (Map.Entry<String, PDComplexFileSpecification> 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
// attachments TODO size
ArrayNode attachmentsArray = objectMapper.createArrayNode();
for (PDPage page : pdfBoxDoc.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
PDAnnotationFileAttachment fileAttachmentAnnotation =
(PDAnnotationFileAttachment) annotation;
ObjectNode attachmentNode = objectMapper.createObjectNode();
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
@@ -223,7 +208,7 @@ public class GetInfoOnPDF {
}
other.set("Attachments", attachmentsArray);
//Javascript
// Javascript
PDDocumentNameDictionary namesDict = catalog.getNames();
ArrayNode javascriptArray = objectMapper.createArrayNode();
@@ -254,9 +239,9 @@ public class GetInfoOnPDF {
}
other.set("JavaScript", javascriptArray);
//TODO size
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
// TODO size
PDOptionalContentProperties ocProperties =
pdfBoxDoc.getDocumentCatalog().getOCProperties();
ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) {
@@ -268,34 +253,38 @@ public class GetInfoOnPDF {
}
other.set("Layers", layersArray);
//TODO Security
// TODO Security
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
PDStructureTreeRoot structureTreeRoot =
pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
ArrayNode structureTreeArray;
try {
if(structureTreeRoot != null) {
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
other.set("StructureTree", structureTreeArray);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
if (structureTreeRoot != null) {
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
other.set("StructureTree", structureTreeArray);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
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.
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);
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
@@ -304,10 +293,6 @@ public class GetInfoOnPDF {
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
ArrayNode bookmarksArray = objectMapper.createArrayNode();
@@ -318,33 +303,29 @@ public class GetInfoOnPDF {
}
other.set("Bookmarks/Outline/TOC", bookmarksArray);
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);
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()) {
encryption.put("IsEncrypted", true);
encryption.put("IsEncrypted", true);
// Retrieve encryption details using getEncryption()
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
@@ -353,31 +334,30 @@ public class GetInfoOnPDF {
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
if (ap != null) {
ObjectNode permissionsNode = objectMapper.createObjectNode();
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint());
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
encryption.set("Permissions", permissionsNode); // set the node under "Permissions"
}
encryption.set(
"Permissions", permissionsNode); // set the node under "Permissions"
}
// Add other encryption-related properties as needed
} else {
encryption.put("IsEncrypted", false);
encryption.put("IsEncrypted", false);
}
ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode();
// Retrieve the page
// Retrieve the page
PDPage page = pdfBoxDoc.getPage(pageNum);
// Page-level Information
@@ -387,20 +367,20 @@ public class GetInfoOnPDF {
float height = mediaBox.getHeight();
ObjectNode sizeInfo = objectMapper.createObjectNode();
getDimensionInfo(sizeInfo, width, height);
sizeInfo.put("Standard Page", getPageSize(width, height));
pageInfo.set("Size", sizeInfo);
pageInfo.put("Rotation", page.getRotation());
pageInfo.put("Page Orientation", getPageOrientation(width, height));
// Boxes
pageInfo.put("MediaBox", mediaBox.toString());
// Assuming the following boxes are defined for your document; if not, you may get null values.
// 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());
@@ -416,13 +396,13 @@ public class GetInfoOnPDF {
// Content Extraction
PDFTextStripper textStripper = new PDFTextStripper();
textStripper.setStartPage(pageNum + 1);
textStripper.setEndPage(pageNum +1);
textStripper.setEndPage(pageNum + 1);
String pageText = textStripper.getText(pdfBoxDoc);
pageInfo.put("Text Characters Count", pageText.length()); //
// Annotations
List<PDAnnotation> annotations = page.getAnnotations();
int subtypeCount = 0;
@@ -430,10 +410,10 @@ public class GetInfoOnPDF {
for (PDAnnotation annotation : annotations) {
if (annotation.getSubtype() != null) {
subtypeCount++; // Increase subtype count
subtypeCount++; // Increase subtype count
}
if (annotation.getContents() != null) {
contentsCount++; // Increase contents count
contentsCount++; // Increase contents count
}
}
@@ -442,26 +422,25 @@ public class GetInfoOnPDF {
annotationsObject.put("SubtypeCount", subtypeCount);
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();
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.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());
@@ -472,10 +451,9 @@ public class GetInfoOnPDF {
}
pageInfo.set("Images", imagesArray);
// Links
ArrayNode linksArray = objectMapper.createArrayNode();
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
for (PDAnnotation annotation : annotations) {
if (annotation instanceof PDAnnotationLink) {
@@ -483,7 +461,7 @@ public class GetInfoOnPDF {
if (linkAnnotation.getAction() instanceof PDActionURI) {
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
String uri = uriAction.getURI();
uniqueURIs.add(uri); // Add to set to ensure uniqueness
uniqueURIs.add(uri); // Add to set to ensure uniqueness
}
}
}
@@ -495,8 +473,7 @@ public class GetInfoOnPDF {
linksArray.add(linkNode);
}
pageInfo.set("Links", linksArray);
// Fonts
ArrayNode fontsArray = objectMapper.createArrayNode();
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
@@ -526,13 +503,13 @@ public class GetInfoOnPDF {
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity
// Font stretch and BBox are not directly available in PDFBox's API, so
// these are omitted for simplicity
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
}
// Create a unique key for this font node based on its attributes
String uniqueKey = fontNode.toString();
String uniqueKey = fontNode.toString();
// Increment count if this font exists, or initialize it if new
if (uniqueFontsMap.containsKey(uniqueKey)) {
@@ -551,17 +528,7 @@ public class GetInfoOnPDF {
}
pageInfo.set("Fonts", fontsArray);
// Access resources dictionary
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
@@ -572,7 +539,7 @@ public class GetInfoOnPDF {
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);
@@ -580,14 +547,14 @@ public class GetInfoOnPDF {
}
}
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
// Other XObjects
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
Map<String, Integer> xObjectCountMap =
new HashMap<>(); // To store the count for each type
for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name);
String xObjectType;
if (xObject instanceof PDImageXObject) {
xObjectType = "Image";
} else if (xObject instanceof PDFormXObject) {
@@ -597,7 +564,8 @@ public class GetInfoOnPDF {
}
// Increment the count for this type in the map
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
xObjectCountMap.put(
xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
}
// Add the count map to pageInfo (or wherever you want to store it)
@@ -606,14 +574,11 @@ public class GetInfoOnPDF {
xObjectCountNode.put(entry.getKey(), entry.getValue());
}
pageInfo.set("XObjectCounts", xObjectCountNode);
ArrayNode multimediaArray = objectMapper.createArrayNode();
for (PDAnnotation annotation : annotations) {
if ("RichMedia".equals(annotation.getSubtype())) {
if ("RichMedia".equals(annotation.getSubtype())) {
ObjectNode multimediaNode = objectMapper.createObjectNode();
// Extract details from the annotation as needed
multimediaArray.add(multimediaNode);
@@ -622,32 +587,29 @@ public class GetInfoOnPDF {
pageInfo.set("Multimedia", multimediaArray);
pageInfoParent.set("Page " + (pageNum+1), pageInfo);
pageInfoParent.set("Page " + (pageNum + 1), pageInfo);
}
jsonOutput.set("BasicInfo", basicInfo);
jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy);
jsonOutput.set("Encryption", encryption);
jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent);
// Save JSON to file
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON);
String jsonString =
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
return WebResponseUtils.bytesToWebResponse(
jsonString.getBytes(StandardCharsets.UTF_8),
"response.json",
MediaType.APPLICATION_JSON);
} catch (Exception e) {
e.printStackTrace();
}
return null;
return null;
}
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
@@ -665,7 +627,7 @@ public class GetInfoOnPDF {
}
}
public String getPageOrientation(double width, double height) {
public String getPageOrientation(double width, double height) {
if (width > height) {
return "Landscape";
} else if (height > width) {
@@ -674,6 +636,7 @@ public class GetInfoOnPDF {
return "Square";
}
}
public String getPageSize(float width, float height) {
// Define standard page sizes
Map<String, PDRectangle> standardSizes = new HashMap<>();
@@ -696,21 +659,22 @@ public class GetInfoOnPDF {
return "Custom";
}
private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) {
private boolean isCloseToSize(
float width, float height, float standardWidth, float standardHeight) {
float tolerance = 1.0f; // You can adjust the tolerance as needed
return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance;
return Math.abs(width - standardWidth) <= tolerance
&& Math.abs(height - standardHeight) <= tolerance;
}
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
float ppi = 72; // Points Per Inch
float widthInInches = width / ppi;
float heightInInches = height / ppi;
float widthInCm = widthInInches * 2.54f;
float heightInCm = heightInInches * 2.54f;
dimensionInfo.put("Width (px)", String.format("%.2f", width));
dimensionInfo.put("Height (px)", String.format("%.2f", height));
dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches));
@@ -720,33 +684,33 @@ public class GetInfoOnPDF {
return dimensionInfo;
}
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);
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;
if (xmpString.contains(standardKeyword)) {
return true;
}
}
} catch (
Exception
e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions.
e.printStackTrace();
}
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
e.printStackTrace();
}
return false;
}
return false;
}
public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode();
if (nodes != null) {
@@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return elementsArray;
}
public String getContent(PDStructureElement structureElement) {
StringBuilder contentBuilder = new StringBuilder();
@@ -790,8 +753,7 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return contentBuilder.toString();
}
private String formatDate(Calendar calendar) {
if (calendar != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

View File

@@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
@@ -26,29 +28,31 @@ public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
@Operation(
summary = "Remove password from a PDF file",
description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException {
summary = "Remove password from a PDF file",
description =
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput();
String password = request.getPassword();
PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
}
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
@Operation(
summary = "Add password to a PDF file",
description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF"
)
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException {
summary = "Add password to a PDF file",
description =
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput();
String ownerPassword = request.getOwnerPassword();
String password = request.getPassword();
@@ -74,16 +78,19 @@ public class PasswordController {
ap.setCanPrintFaithful(!canPrintFaithful);
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
if(!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength);
if (!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength);
}
spp.setPermissions(ap);
document.protect(spp);
if("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
if ("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
}
}

View File

@@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
@@ -37,11 +39,13 @@ public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation(summary = "Redacts listOfText in a PDF document",
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception {
@Operation(
summary = "Redacts listOfText in a PDF document",
description =
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
String listOfTextString = request.getListOfText();
boolean useRegex = request.isUseRegex();
@@ -49,15 +53,15 @@ public class RedactController {
String colorString = request.getRedactColor();
float customPadding = request.getCustomPadding();
boolean convertPDFToImage = request.isConvertPDFToImage();
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
byte[] bytes = file.getBytes();
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
Color redactColor;
try {
if (!colorString.startsWith("#")) {
if (!colorString.startsWith("#")) {
colorString = "#" + colorString;
}
redactColor = Color.decode(colorString);
@@ -66,18 +70,14 @@ public class RedactController {
redactColor = Color.BLACK;
}
for (String text : listOfText) {
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding,redactColor);
redactFoundText(document, foundTexts, customPadding, redactColor);
}
if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
@@ -97,27 +97,33 @@ public class RedactController {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent,
return WebResponseUtils.bytesToWebResponse(
pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
}
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException {
private void redactFoundText(
PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor)
throws IOException {
var allPages = document.getDocumentCatalog().getPages();
for (PDFText block : blocks) {
var page = allPages.get(block.getPageIndex());
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true);
PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(redactColor);
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
PDRectangle pageBox = page.getBBox();
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding);
contentStream.addRect(
block.getX1(),
pageBox.getHeight() - block.getY1() - padding,
block.getX2() - block.getX1(),
block.getY2() - block.getY1() + 2 * padding);
contentStream.fill();
contentStream.close();
}
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.security;
import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary;
@@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@@ -36,59 +38,68 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs")
public class SanitizeController {
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(summary = "Sanitize a PDF file",
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException {
MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
boolean removeMetadata = request.isRemoveMetadata();
boolean removeLinks = request.isRemoveLinks();
boolean removeFonts = request.isRemoveFonts();
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(
summary = "Sanitize a PDF file",
description =
"This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
throws IOException {
MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
boolean removeMetadata = request.isRemoveMetadata();
boolean removeLinks = request.isRemoveLinks();
boolean removeFonts = request.isRemoveFonts();
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if (removeJavaScript) {
sanitizeJavaScript(document);
}
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if (removeJavaScript) {
sanitizeJavaScript(document);
}
if (removeEmbeddedFiles) {
sanitizeEmbeddedFiles(document);
}
if (removeEmbeddedFiles) {
sanitizeEmbeddedFiles(document);
}
if (removeMetadata) {
sanitizeMetadata(document);
}
if (removeMetadata) {
sanitizeMetadata(document);
}
if (removeLinks) {
sanitizeLinks(document);
}
if (removeLinks) {
sanitizeLinks(document);
}
if (removeFonts) {
sanitizeFonts(document);
}
if (removeFonts) {
sanitizeFonts(document);
}
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
}
}
private void sanitizeJavaScript(PDDocument document) throws IOException {
// Get the root dictionary (catalog) of the PDF
PDDocumentCatalog catalog = document.getDocumentCatalog();
return WebResponseUtils.pdfDocToWebResponse(
document,
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_sanitized.pdf");
}
}
// Get the Names dictionary
COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
private void sanitizeJavaScript(PDDocument document) throws IOException {
// Get the root dictionary (catalog) of the PDF
PDDocumentCatalog catalog = document.getDocumentCatalog();
if (namesDict != null) {
// Get the JavaScript dictionary
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
// Get the Names dictionary
COSDictionary namesDict =
(COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
if (javaScriptDict != null) {
// Remove the JavaScript dictionary
namesDict.removeItem(COSName.getPDFName("JavaScript"));
}
}
for (PDPage page : document.getPages()) {
if (namesDict != null) {
// Get the JavaScript dictionary
COSDictionary javaScriptDict =
(COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
if (javaScriptDict != null) {
// Remove the JavaScript dictionary
namesDict.removeItem(COSName.getPDFName("JavaScript"));
}
}
for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationWidget) {
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
@@ -96,33 +107,30 @@ public class SanitizeController {
if (action instanceof PDActionJavaScript) {
widget.setAction(null);
}
}
}
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm != null) {
for (PDField field : acroForm.getFields()) {
PDFormFieldAdditionalActions actions = field.getActions();
if(actions != null) {
if (actions.getC() instanceof PDActionJavaScript) {
actions.setC(null);
}
if (actions.getF() instanceof PDActionJavaScript) {
actions.setF(null);
}
if (actions.getK() instanceof PDActionJavaScript) {
actions.setK(null);
}
if (actions.getV() instanceof PDActionJavaScript) {
actions.setV(null);
}
}
}
}
}
}
}
}
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
if (acroForm != null) {
for (PDField field : acroForm.getFields()) {
PDFormFieldAdditionalActions actions = field.getActions();
if (actions != null) {
if (actions.getC() instanceof PDActionJavaScript) {
actions.setC(null);
}
if (actions.getF() instanceof PDActionJavaScript) {
actions.setF(null);
}
if (actions.getK() instanceof PDActionJavaScript) {
actions.setK(null);
}
if (actions.getV() instanceof PDActionJavaScript) {
actions.setV(null);
}
}
}
}
}
}
private void sanitizeEmbeddedFiles(PDDocument document) {
PDPageTree allPages = document.getPages();
@@ -134,7 +142,6 @@ public class SanitizeController {
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
}
}
private void sanitizeMetadata(PDDocument document) {
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
@@ -143,8 +150,6 @@ public class SanitizeController {
}
}
private void sanitizeLinks(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) {
@@ -163,5 +168,4 @@ public class SanitizeController {
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
}
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.WebResponseUtils;
@@ -38,154 +39,198 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs")
public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
String watermarkType = request.getWatermarkType();
String watermarkText = request.getWatermarkText();
MultipartFile watermarkImage = request.getWatermarkImage();
String alphabet = request.getAlphabet();
float fontSize = request.getFontSize();
float rotation = request.getRotation();
float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer();
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(
summary = "Add watermark to a PDF file",
description =
"This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request)
throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
String watermarkType = request.getWatermarkType();
String watermarkText = request.getWatermarkText();
MultipartFile watermarkImage = request.getWatermarkImage();
String alphabet = request.getAlphabet();
float fontSize = request.getFontSize();
float rotation = request.getRotation();
float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer();
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Create a page in the document
for (PDPage page : document.getPages()) {
// Create a page in the document
for (PDPage page : document.getPages()) {
// Get the page's content stream
PDPageContentStream contentStream = new PDPageContentStream(document, page,
PDPageContentStream.AppendMode.APPEND, true);
// Get the page's content stream
PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
// Set transparency
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState);
// Set transparency
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState);
if (watermarkType.equalsIgnoreCase("text")) {
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
fontSize, alphabet);
} else if (watermarkType.equalsIgnoreCase("image")) {
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
fontSize);
}
if (watermarkType.equalsIgnoreCase("text")) {
addTextWatermark(
contentStream,
watermarkText,
document,
page,
rotation,
widthSpacer,
heightSpacer,
fontSize,
alphabet);
} else if (watermarkType.equalsIgnoreCase("image")) {
addImageWatermark(
contentStream,
watermarkImage,
document,
page,
rotation,
widthSpacer,
heightSpacer,
fontSize);
}
// Close the content stream
contentStream.close();
}
// Close the content stream
contentStream.close();
}
return WebResponseUtils.pdfDocToWebResponse(document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
}
return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
}
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document,
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException {
String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD;
switch (alphabet) {
case "arabic":
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
break;
case "japanese":
resourceDir = "static/fonts/Meiryo.ttf";
break;
case "korean":
resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
private void addTextWatermark(
PDPageContentStream contentStream,
String watermarkText,
PDDocument document,
PDPage page,
float rotation,
int widthSpacer,
int heightSpacer,
float fontSize,
String alphabet)
throws IOException {
String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD;
switch (alphabet) {
case "arabic":
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
break;
case "japanese":
resourceDir = "static/fonts/Meiryo.ttf";
break;
case "korean":
resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
if(!resourceDir.equals("")) {
if (!resourceDir.equals("")) {
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os);
}
font = PDType0Font.load(document, tempFile);
tempFile.deleteOnExit();
}
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
// Set size and location of text watermark
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
float watermarkHeight = heightSpacer + fontSize;
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
// Add the text watermark
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
contentStream.beginText();
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation),
j * watermarkWidth, i * watermarkHeight));
contentStream.showText(watermarkText);
contentStream.endText();
}
}
}
// Set size and location of text watermark
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
float watermarkHeight = heightSpacer + fontSize;
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation,
int widthSpacer, int heightSpacer, float fontSize) throws IOException {
// Add the text watermark
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
contentStream.beginText();
contentStream.setTextMatrix(
Matrix.getRotateInstance(
(float) Math.toRadians(rotation),
j * watermarkWidth,
i * watermarkHeight));
contentStream.showText(watermarkText);
contentStream.endText();
}
}
}
// Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
private void addImageWatermark(
PDPageContentStream contentStream,
MultipartFile watermarkImage,
PDDocument document,
PDPage page,
float rotation,
int widthSpacer,
int heightSpacer,
float fontSize)
throws IOException {
// Compute width based on original aspect ratio
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
// Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
// Desired physical height (in PDF points)
float desiredPhysicalHeight = fontSize ;
// Compute width based on original aspect ratio
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
// Desired physical width based on the aspect ratio
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
// Desired physical height (in PDF points)
float desiredPhysicalHeight = fontSize;
// Convert the BufferedImage to PDImageXObject
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
// Desired physical width based on the aspect ratio
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
// Calculate the number of rows and columns for watermarks
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
// Convert the BufferedImage to PDImageXObject
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
float x = j * (desiredPhysicalWidth + widthSpacer);
float y = i * (desiredPhysicalHeight + heightSpacer);
// Calculate the number of rows and columns for watermarks
float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight();
int watermarkRows =
(int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols =
(int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
// Save the graphics state
contentStream.saveGraphicsState();
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
float x = j * (desiredPhysicalWidth + widthSpacer);
float y = i * (desiredPhysicalHeight + heightSpacer);
// Create rotation matrix and rotate
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
}
// Save the graphics state
contentStream.saveGraphicsState();
// Create rotation matrix and rotate
contentStream.transform(
Matrix.getTranslateInstance(
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
contentStream.transform(
Matrix.getTranslateInstance(
-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
}
}