mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-06 03:19:39 +02:00
Remove read only from forms (#3423)
# Description of Changes Create new tool to remove read-only properties of form fields. - Added new html file to provide a page for the tool (misc/unlock-pdf-forms.html), as well as new endpoint (/unlock-pdf-forms) under config/EndpointConfiguration.java - Added the tool to the list of "view & edit" tools under the home page in home-legacy.html and navElements.html - Mapped the frontend in controller/web/OtherWebController.java - Created a new controller (controller/api/misc/UnlockPDFFormsController.java) to handle AcroForm /Ff flags, /Lock tags and XFA Forms, removing the read-only properties of all form fields of a PDF document. - Added language entries to all the language files, to correctly display the tool's title, header,description, etc. Closes #2965 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)    ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9635e573d8
commit
715445a8dd
@@ -164,6 +164,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Other", "sign");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "repair");
|
||||
addEndpointToGroup("Other", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Other", REMOVE_BLANKS);
|
||||
addEndpointToGroup("Other", "remove-annotations");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.pdfbox.cos.*;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class UnlockPDFFormsController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
public UnlockPDFFormsController(CustomPDFDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
||||
@Operation(
|
||||
summary = "Remove read-only property from form fields",
|
||||
description =
|
||||
"Removing read-only property from form fields making them fillable"
|
||||
+ "Input:PDF, Output:PDF. Type:SISO")
|
||||
public ResponseEntity<byte[]> unlockPDFForms(@ModelAttribute PDFFile file) {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||
|
||||
if (acroForm != null) {
|
||||
acroForm.setNeedAppearances(true);
|
||||
|
||||
for (PDField field : acroForm.getFieldTree()) {
|
||||
COSDictionary dict = field.getCOSObject();
|
||||
if (dict.containsKey(COSName.getPDFName("Lock"))) {
|
||||
dict.removeItem(COSName.getPDFName("Lock"));
|
||||
}
|
||||
int currentFlags = field.getFieldFlags();
|
||||
if ((currentFlags & 1) == 1) {
|
||||
int newFlags = currentFlags & ~1;
|
||||
field.setFieldFlags(newFlags);
|
||||
}
|
||||
}
|
||||
|
||||
COSBase xfaBase = acroForm.getCOSObject().getDictionaryObject(COSName.XFA);
|
||||
if (xfaBase != null) {
|
||||
try {
|
||||
if (xfaBase instanceof COSStream xfaStream) {
|
||||
InputStream is = xfaStream.createInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
is.transferTo(baos);
|
||||
String xml = baos.toString(StandardCharsets.UTF_8);
|
||||
|
||||
xml = xml.replaceAll("access\\s*=\\s*\"readOnly\"", "access=\"open\"");
|
||||
|
||||
PDStream newStream =
|
||||
new PDStream(
|
||||
document,
|
||||
new ByteArrayInputStream(
|
||||
xml.getBytes(StandardCharsets.UTF_8)));
|
||||
acroForm.getCOSObject().setItem(COSName.XFA, newStream.getCOSObject());
|
||||
} else if (xfaBase instanceof COSArray xfaArray) {
|
||||
for (int i = 0; i < xfaArray.size(); i += 2) {
|
||||
COSBase namePart = xfaArray.getObject(i);
|
||||
COSBase streamPart = xfaArray.getObject(i + 1);
|
||||
if (namePart instanceof COSString
|
||||
&& streamPart instanceof COSStream stream) {
|
||||
InputStream is = stream.createInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
is.transferTo(baos);
|
||||
String xml = baos.toString(StandardCharsets.UTF_8);
|
||||
|
||||
xml =
|
||||
xml.replaceAll(
|
||||
"access\\s*=\\s*\"readOnly\"",
|
||||
"access=\"open\"");
|
||||
|
||||
PDStream newStream =
|
||||
new PDStream(
|
||||
document,
|
||||
new ByteArrayInputStream(
|
||||
xml.getBytes(StandardCharsets.UTF_8)));
|
||||
xfaArray.set(i + 1, newStream.getCOSObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
String mergedFileName =
|
||||
file.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_unlocked_forms.pdf";
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document, Filenames.toSimpleFileName(mergedFileName));
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,13 @@ public class OtherWebController {
|
||||
return "misc/change-metadata";
|
||||
}
|
||||
|
||||
@GetMapping("/unlock-pdf-forms")
|
||||
@Hidden
|
||||
public String unlockPDFForms(Model model) {
|
||||
model.addAttribute("currentPage", "unlock-pdf-forms");
|
||||
return "misc/unlock-pdf-forms";
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@Hidden
|
||||
public String compareForm(Model model) {
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UploadLimitService {
|
||||
|
||||
@Autowired
|
||||
private ApplicationProperties applicationProperties;
|
||||
@Autowired private ApplicationProperties applicationProperties;
|
||||
|
||||
public long getUploadLimit() {
|
||||
String maxUploadSize =
|
||||
applicationProperties.getSystem().getFileUploadLimit() != null
|
||||
? applicationProperties.getSystem().getFileUploadLimit()
|
||||
: "";
|
||||
applicationProperties.getSystem().getFileUploadLimit() != null
|
||||
? applicationProperties.getSystem().getFileUploadLimit()
|
||||
: "";
|
||||
|
||||
if (maxUploadSize.isEmpty()) {
|
||||
return 0;
|
||||
} else if (!Pattern.compile("^[1-9][0-9]{0,2}[KMGkmg][Bb]$").matcher(maxUploadSize).matches()) {
|
||||
} else if (!Pattern.compile("^[1-9][0-9]{0,2}[KMGkmg][Bb]$")
|
||||
.matcher(maxUploadSize)
|
||||
.matches()) {
|
||||
log.error(
|
||||
"Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}",
|
||||
maxUploadSize);
|
||||
"Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}",
|
||||
maxUploadSize);
|
||||
return 0;
|
||||
} else {
|
||||
String unit = maxUploadSize.replaceAll("[1-9][0-9]{0,2}", "").toUpperCase();
|
||||
@@ -41,7 +43,7 @@ public class UploadLimitService {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: why do this server side not client?
|
||||
// TODO: why do this server side not client?
|
||||
public String getReadableUploadLimit() {
|
||||
return humanReadableByteCount(getUploadLimit());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user