Skip to content

Commit

Permalink
PDF security features (#26)
Browse files Browse the repository at this point in the history
- Support for adding and removing passwords
- Support for watermarks
- Dedicated page remover
- Support for PDF permissions
- Removed endpoint /home and replaced with /
- Code cleanups
- Fixed page titles
  • Loading branch information
Frooodle authored Feb 3, 2023
1 parent 1937a83 commit 5275866
Show file tree
Hide file tree
Showing 34 changed files with 1,245 additions and 596 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ I will support and fix/add things to this if there is a demand [Discord](https:/
- Add images to PDFs at specified locations.
- Rotating PDFs in 90 degree increments.
- Compressing PDFs to decrease their filesize.
- Add and remove passwords
- Set PDF Permissions
- Add watermark(s)
- Dark mode support.

## Technologies used
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'stirling.software'
version = '0.2.3'
version = '0.3.0'
sourceCompatibility = '17'

repositories {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.spire.pdf.graphics.PdfBitmap;

import stirling.software.SPDF.utils.PdfUtils;

//import com.spire.pdf.*;
@Controller
public class CompressController {
Expand All @@ -40,48 +41,34 @@ public String compressPdfForm(Model model) {
@PostMapping("/compress-pdf")
public ResponseEntity<byte[]> compressPDF(@RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("imageCompressionLevel") String imageCompressionLevel) throws IOException {


//Load a sample PDF document
PdfDocument document = new PdfDocument();
document.loadFromBytes(pdfFile.getBytes());

//Compress PDF
document.getFileInfo().setIncrementalUpdate(false);
document.setCompressionLevel(PdfCompressionLevel.Best);

//compress PDF Images
for (int i = 0; i < document.getPages().getCount(); i++) {

PdfPageBase page = document.getPages().get(i);
PdfImageInfo[] images = page.getImagesInfo();
if (images != null && images.length > 0)
for (int j = 0; j < images.length; j++) {
PdfImageInfo image = images[j];
PdfBitmap bp = new PdfBitmap(image.getImage());
//bp.setPngDirectToJpeg(true);
bp.setQuality(Integer.valueOf(imageCompressionLevel));

page.replaceImage(j, bp);

}
}

// Save the rearranged PDF to a ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
document.saveToStream(outputStream);

// Close the original document
document.close();

// Prepare the response headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "compressed.pdf");
headers.setContentLength(outputStream.size());

// Return the response with the PDF data and headers
return new ResponseEntity<>(outputStream.toByteArray(), headers, HttpStatus.OK);

// Load a sample PDF document
PdfDocument document = new PdfDocument();
document.loadFromBytes(pdfFile.getBytes());

// Compress PDF
document.getFileInfo().setIncrementalUpdate(false);
document.setCompressionLevel(PdfCompressionLevel.Best);

// compress PDF Images
for (int i = 0; i < document.getPages().getCount(); i++) {

PdfPageBase page = document.getPages().get(i);
PdfImageInfo[] images = page.getImagesInfo();
if (images != null && images.length > 0)
for (int j = 0; j < images.length; j++) {
PdfImageInfo image = images[j];
PdfBitmap bp = new PdfBitmap(image.getImage());
// bp.setPngDirectToJpeg(true);
bp.setQuality(Integer.valueOf(imageCompressionLevel));

page.replaceImage(j, bp);

}
}

return PdfUtils.pdfDocToWebResponse(document, pdfFile.getName() + "_compressed.pdf");

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package stirling.software.SPDF.controller;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class MergeController {

private static final Logger logger = LoggerFactory.getLogger(MergeController.class);

@GetMapping("/merge-pdfs")
public String hello(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}

@PostMapping("/merge-pdfs")
public ResponseEntity<InputStreamResource> mergePdfs(@RequestParam("fileInput") MultipartFile[] files)
throws IOException {
// Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>();

// Loop through the files array and read each file into a PDDocument
for (MultipartFile file : files) {
documents.add(PDDocument.load(file.getInputStream()));
}

PDDocument mergedDoc = mergeDocuments(documents);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
mergedDoc.save(byteArrayOutputStream);
mergedDoc.close();

// Create an InputStreamResource from the merged PDF
InputStreamResource resource = new InputStreamResource(
new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));

// Return the merged PDF as a response
return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body(resource);
}

private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document
PDDocument mergedDoc = new PDDocument();

// Iterate over the list of documents and add their pages to the merged document
for (PDDocument doc : documents) {
// Get all pages from the current document
PDPageTree pages = doc.getPages();
// Iterate over the pages and add them to the merged document
for (PDPage page : pages) {
mergedDoc.addPage(page);
}
}

// Return the merged document
return mergedDoc;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,8 @@ public ResponseEntity<byte[]> overlayImage(@RequestParam("fileInput") MultipartF
byte[] pdfBytes = pdfFile.getBytes();
byte[] imageBytes = imageFile.getBytes();
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "overlayed.pdf");
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return new ResponseEntity<>(result, headers, HttpStatus.OK);

return PdfUtils.bytesToWebResponse(result, pdfFile.getName() + "_overlayed.pdf");
} catch (IOException e) {
logger.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
Expand Down
53 changes: 4 additions & 49 deletions src/main/java/stirling/software/SPDF/controller/PdfController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,63 +26,18 @@ public class PdfController {

private static final Logger logger = LoggerFactory.getLogger(PdfController.class);

@GetMapping("/")
@GetMapping("/home")
public String root(Model model) {
return "redirect:/home";
return "redirect:/";
}

@GetMapping("/merge-pdfs")
public String hello(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}

@GetMapping("/home")
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
return "home";
}

@PostMapping("/merge-pdfs")
public ResponseEntity<InputStreamResource> mergePdfs(@RequestParam("fileInput") MultipartFile[] files)
throws IOException {
// Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>();

// Loop through the files array and read each file into a PDDocument
for (MultipartFile file : files) {
documents.add(PDDocument.load(file.getInputStream()));
}

PDDocument mergedDoc = mergeDocuments(documents);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
mergedDoc.save(byteArrayOutputStream);
mergedDoc.close();

// Create an InputStreamResource from the merged PDF
InputStreamResource resource = new InputStreamResource(
new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));

// Return the merged PDF as a response
return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body(resource);
}

private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document
PDDocument mergedDoc = new PDDocument();

// Iterate over the list of documents and add their pages to the merged document
for (PDDocument doc : documents) {
// Get all pages from the current document
PDPageTree pages = doc.getPages();
// Iterate over the pages and add them to the merged document
for (PDPage page : pages) {
mergedDoc.addPage(page);
}
}

// Return the merged document
return mergedDoc;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import stirling.software.SPDF.utils.PdfUtils;

@Controller
public class RearrangePagesPDFController {

Expand All @@ -31,6 +33,60 @@ public String pageOrganizer(Model model) {
return "pdf-organizer";
}

@GetMapping("/delete-pages")
public String pageDeleter(Model model) {
model.addAttribute("currentPage", "delete-pages");
return "delete-pages";
}

@PostMapping("/delete-pages")
public ResponseEntity<byte[]> deletePages(@RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("pagesToDelete") String pagesToDelete) throws IOException {

PDDocument document = PDDocument.load(pdfFile.getBytes());

// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pagesToDelete.split(",");

List<Integer> pagesToRemove = pageOrderToString(pageOrderArr, document.getNumberOfPages());

for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
int pageIndex = pagesToRemove.get(i);
document.removePage(pageIndex);
}

return PdfUtils.pdfDocToWebResponse(document, pdfFile.getName() + "_removed_pages.pdf");

}

private List<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) {
List<Integer> newPageOrder = new ArrayList<Integer>();
// loop through the page order array
for (String element : pageOrderArr) {
// check if the element contains a range of pages
if (element.contains("-")) {
// split the range into start and end page
String[] range = element.split("-");
int start = Integer.parseInt(range[0]);
int end = Integer.parseInt(range[1]);
// check if the end page is greater than total pages
if (end > totalPages) {
end = totalPages;
}
// loop through the range of pages
for (int j = start; j <= end; j++) {
// print the current index
newPageOrder.add(j - 1);
}
} else {
// if the element is a single page
newPageOrder.add(Integer.parseInt(element) - 1);
}
}

return newPageOrder;
}

@PostMapping("/rearrange-pages")
public ResponseEntity<byte[]> rearrangePages(@RequestParam("fileInput") MultipartFile pdfFile,
@RequestParam("pageOrder") String pageOrder) {
Expand All @@ -40,33 +96,11 @@ public ResponseEntity<byte[]> rearrangePages(@RequestParam("fileInput") Multipar

// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder.split(",");
List<Integer> newPageOrder = new ArrayList<Integer>();
//int[] newPageOrder = new int[pageOrderArr.length];
// int[] newPageOrder = new int[pageOrderArr.length];
int totalPages = document.getNumberOfPages();

// loop through the page order array
for (String element : pageOrderArr) {
// check if the element contains a range of pages
if (element.contains("-")) {
// split the range into start and end page
String[] range = element.split("-");
int start = Integer.parseInt(range[0]);
int end = Integer.parseInt(range[1]);
// check if the end page is greater than total pages
if (end > totalPages) {
end = totalPages;
}
// loop through the range of pages
for (int j = start; j <= end; j++) {
// print the current index
newPageOrder.add( j - 1);
}
} else {
// if the element is a single page
newPageOrder.add( Integer.parseInt(element) - 1);
}
}

List<Integer> newPageOrder = pageOrderToString(pageOrderArr, totalPages);

// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
Expand All @@ -83,21 +117,7 @@ public ResponseEntity<byte[]> rearrangePages(@RequestParam("fileInput") Multipar
document.addPage(page);
}

// Save the rearranged PDF to a ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
document.save(outputStream);

// Close the original document
document.close();

// Prepare the response headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "rearranged.pdf");
headers.setContentLength(outputStream.size());

// Return the response with the PDF data and headers
return new ResponseEntity<>(outputStream.toByteArray(), headers, HttpStatus.OK);
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getName() + "_rearranged.pdf");
} catch (IOException e) {

logger.error("Failed rearranging documents", e);
Expand Down
Loading

0 comments on commit 5275866

Please sign in to comment.