Merge branch 'removeReadOnlyFromForms' of github.com:PedroPF1233/Stirling-PDF into removeReadOnlyFromForms

This commit is contained in:
MariaLaranjeira 2025-04-25 20:44:06 +01:00
commit 36e1c5fb84
9 changed files with 193 additions and 1 deletions

View File

@ -166,6 +166,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "sign");
addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", "remove-read-only");
addEndpointToGroup("Other", REMOVE_BLANKS);
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "compare");

View File

@ -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 RemoveReadOnlyController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public RemoveReadOnlyController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/remove-read-only")
@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[]> removeReadOnly(@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("[.][^.]+$", "")
+ "_removed_readonly.pdf";
return WebResponseUtils.pdfDocToWebResponse(
document, Filenames.toSimpleFileName(mergedFileName));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
}

View File

@ -99,6 +99,13 @@ public class OtherWebController {
return "misc/change-metadata";
}
@GetMapping("/remove-read-only")
@Hidden
public String removeReadOnlyForm(Model model) {
model.addAttribute("currentPage", "remove-read-only");
return "misc/remove-read-only";
}
@GetMapping("/compare")
@Hidden
public String compareForm(Model model) {

View File

@ -364,6 +364,9 @@ home.compressPdfs.title=Compress
home.compressPdfs.desc=Compress PDFs to reduce their file size.
compressPdfs.tags=squish,small,tiny
home.removeReadOnly.title=Remove Read-Only from Form Fields
home.removeReadOnly.desc=Remove read-only property of form fields in a PDF document.
removeReadOnly.tags=remove,delete,form,field,readonly
home.changeMetadata.title=Change Metadata
home.changeMetadata.desc=Change/Remove/Add metadata from a PDF document
@ -377,7 +380,6 @@ home.ocr.title=OCR / Cleanup scans
home.ocr.desc=Cleanup scans and detects text from images within a PDF and re-adds it as text.
ocr.tags=recognition,text,image,scan,read,identify,detection,editable
home.extractImages.title=Extract Images
home.extractImages.desc=Extracts all images from a PDF and saves them to zip
extractImages.tags=picture,photo,save,archive,zip,capture,grab
@ -1193,6 +1195,10 @@ changeMetadata.selectText.4=Other Metadata:
changeMetadata.selectText.5=Add Custom Metadata Entry
changeMetadata.submit=Change
#removeReadOnly
removeReadOnly.title=Remove Read-Only from Form Fields
removeReadOnly.header=Remove Read-Only from Form Fields
removeReadOnly.submit=Remove
#pdfToPDFA
pdfToPDFA.title=PDF To PDF/A

View File

@ -364,6 +364,9 @@ home.compressPdfs.title=Compress
home.compressPdfs.desc=Compress PDFs to reduce their file size.
compressPdfs.tags=squish,small,tiny
home.removeReadOnly.title=Remove Read-Only from Form Fields
home.removeReadOnly.desc=Remove read-only property of form fields in a PDF document.
removeReadOnly.tags=remove,delete,form,field,readonly
home.changeMetadata.title=Change Metadata
home.changeMetadata.desc=Change/Remove/Add metadata from a PDF document
@ -1193,6 +1196,10 @@ changeMetadata.selectText.4=Other Metadata:
changeMetadata.selectText.5=Add Custom Metadata Entry
changeMetadata.submit=Change
#removeReadOnly
removeReadOnly.title=Remove Read-Only from Form Fields
removeReadOnly.header=Remove Read-Only from Form Fields
removeReadOnly.submit=Remove
#pdfToPDFA
pdfToPDFA.title=PDF To PDF/A

View File

@ -364,6 +364,9 @@ home.compressPdfs.title=Comprimir
home.compressPdfs.desc=Comprimir PDFs para reduzir o seu tamanho.
compressPdfs.tags=comprimir,pequeno,minúsculo
home.removeReadOnly.title=Remover Apenas Leitura de Formulários
home.removeReadOnly.desc=Remover propriedades de apenas leitura dos formulários de um PDF
removeReadOnly.tags=remover,apagar,formulário,campo,apenas leitura
home.changeMetadata.title=Alterar Metadados
home.changeMetadata.desc=Alterar/Remover/Adicionar metadados de um documento PDF
@ -1193,6 +1196,10 @@ changeMetadata.selectText.4=Outros Metadados:
changeMetadata.selectText.5=Adicionar Entrada de Metadados Personalizada
changeMetadata.submit=Alterar
#removeReadOnly
removeReadOnly.title=Remover Apenas Leitura de Formulários
removeReadOnly.header=Remover Apenas Leitura de Formulários
removeReadOnly.submit=Remover
#pdfToPDFA
pdfToPDFA.title=PDF Para PDF/A

View File

@ -231,6 +231,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('replace-and-invert-color-pdf', 'format_color_fill', 'home.replaceColorPdf.title', 'home.replaceColorPdf.desc', 'replaceColorPdf.tags', 'other')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('remove-read-only', 'preview_off', 'home.removeReadOnly.title', 'home.removeReadOnly.desc', 'removeReadOnly.tags', 'other')}">
</div>
</div>
</div>
<div id="groupAdvanced" class="feature-group">

View File

@ -287,6 +287,9 @@
<div
th:replace="~{fragments/card :: card(id='replace-and-invert-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
</div>
<div
th:replace="~{fragments/card :: card(id='remove-read-only', cardTitle=#{home.removeReadOnly.title}, cardText=#{home.removeReadOnly.desc}, cardLink='remove-read-only', toolIcon='preview_off', tags=#{removeReadOnly.tags}, toolGroup='other')}">
</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{removeReadOnly.title}, header=#{removeReadOnly.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon other">preview_off</span>
<span class="tool-header-text" th:text="#{removeReadOnly.header}"></span>
</div>
<form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/remove-read-only'}" id="pdfForm" class="mb-3">
<div class="custom-file">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeReadOnly.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>