Fix no-ToUnicode redaction fixture and add font-matrix and cucumber e2e tests
This commit is contained in:
+43
-2
@@ -24,6 +24,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
|
||||
import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
@@ -142,6 +143,16 @@ class RedactionPdfVarietyTest {
|
||||
assertThat(pdfText(out)).contains("alpha").contains("omega");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("symbolic TrueType (no encoding, no ToUnicode): target not extractable")
|
||||
void symbolicTrueType() throws IOException {
|
||||
// PDFBox extraction is glyph-blind here, so removal is guaranteed by the native pass +
|
||||
// rasterise fallback rather than surgical decode; this pins that the pipeline handles a
|
||||
// symbolic font without error and leaves no extractable target.
|
||||
byte[] out = autoRedact(symbolicTtfPdf("alpha SECRET omega"), "SECRET");
|
||||
assertGone(out, "SECRET");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("subset-tagged font (ABCDEF+): target gone, neighbours survive")
|
||||
void subsetTaggedFont() throws IOException {
|
||||
@@ -372,16 +383,46 @@ class RedactionPdfVarietyTest {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] symbolicTtfPdf(String line) throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDTrueTypeFont font;
|
||||
try (InputStream ttf = PDDocument.class.getResourceAsStream(LIBERATION)) {
|
||||
font = PDTrueTypeFont.load(doc, ttf, WinAnsiEncoding.INSTANCE);
|
||||
}
|
||||
addTextPage(doc, font, line, 0, null);
|
||||
// Strip /Encoding and flag the descriptor Symbolic so extraction must use the cmap.
|
||||
font.getCOSObject().removeItem(org.apache.pdfbox.cos.COSName.ENCODING);
|
||||
PDFontDescriptor fd = font.getFontDescriptor();
|
||||
fd.setFlags((fd.getFlags() | 4) & ~32);
|
||||
return save(doc);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] type0NoToUnicodePdf(String line) throws IOException {
|
||||
byte[] pdf;
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDFont font;
|
||||
try (InputStream ttf = PDDocument.class.getResourceAsStream(LIBERATION)) {
|
||||
font = PDType0Font.load(doc, ttf, true);
|
||||
// Full embed, not subset: the subsetter regenerates /ToUnicode at save, undoing the strip.
|
||||
font = PDType0Font.load(doc, ttf, false);
|
||||
}
|
||||
addTextPage(doc, font, line, 0, null);
|
||||
font.getCOSObject().removeItem(org.apache.pdfbox.cos.COSName.TO_UNICODE);
|
||||
return save(doc);
|
||||
pdf = save(doc);
|
||||
}
|
||||
// Guard: the strip must survive save so this fixture genuinely exercises the no-mapping path.
|
||||
try (PDDocument reopened = Loader.loadPDF(pdf)) {
|
||||
PDResources res = reopened.getPage(0).getResources();
|
||||
for (org.apache.pdfbox.cos.COSName n : res.getFontNames()) {
|
||||
assertThat(
|
||||
res.getFont(n)
|
||||
.getCOSObject()
|
||||
.containsKey(org.apache.pdfbox.cos.COSName.TO_UNICODE))
|
||||
.as("no-ToUnicode fixture must not ship /ToUnicode")
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
return pdf;
|
||||
}
|
||||
|
||||
private byte[] subsetTaggedPdf(String line) throws IOException {
|
||||
|
||||
@@ -104,3 +104,40 @@ Feature: PDF redaction physically removes text
|
||||
Then the response status code should be 200
|
||||
And the response PDF should not contain the text "EXAMPLE"
|
||||
And the response PDF should contain the text "CROP"
|
||||
|
||||
Scenario: Auto-redact removes the target from an embedded TrueType font PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf pages all contain the text "keepA SECRET88 keepB" in an embedded font
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | SECRET88 |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response content type should be "application/pdf"
|
||||
And the response PDF should not contain the text "SECRET88"
|
||||
And the response PDF should contain the text "keepA"
|
||||
And the response PDF should contain the text "keepB"
|
||||
|
||||
Scenario: Auto-redact with a regex pattern on an embedded-font PDF
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf pages all contain the text "invoice 987-65-4321 total" in an embedded font
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | \d{3}-\d{2}-\d{4} |
|
||||
| useRegex | true |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response PDF should not contain the text "987-65-4321"
|
||||
And the response PDF should contain the text "invoice"
|
||||
|
||||
Scenario: Auto-redact convert-to-image on an embedded-font PDF drops the text layer
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf pages all contain the text "keep SECRET66 hidden" in an embedded font
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| listOfText | SECRET66 |
|
||||
| convertPDFToImage | true |
|
||||
When I send the API request to the endpoint "/api/v1/security/auto-redact"
|
||||
Then the response status code should be 200
|
||||
And the response PDF should not contain the text "SECRET66"
|
||||
And the response PDF should not contain the text "keep"
|
||||
|
||||
@@ -280,6 +280,47 @@ def step_pdf_pages_contain_text(context, text):
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
# DejaVuSans is a repo asset; embedding it exercises redaction over a real embedded (Type0/CID,
|
||||
# non Standard-14) font whose text pypdf can still extract to verify removal end-to-end.
|
||||
_DEJAVU_TTF = os.path.normpath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..", "..", "..",
|
||||
"app", "core", "src", "main", "resources", "static", "fonts", "DejaVuSans.ttf",
|
||||
)
|
||||
)
|
||||
_embedded_font_registered = False
|
||||
|
||||
|
||||
def _ensure_embedded_font():
|
||||
global _embedded_font_registered
|
||||
if not _embedded_font_registered:
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
|
||||
pdfmetrics.registerFont(TTFont("DejaVuEmbedded", _DEJAVU_TTF))
|
||||
_embedded_font_registered = True
|
||||
|
||||
|
||||
@given('the pdf pages all contain the text "{text}" in an embedded font')
|
||||
def step_pdf_pages_contain_text_embedded(context, text):
|
||||
_ensure_embedded_font()
|
||||
buffer = io.BytesIO()
|
||||
c = canvas.Canvas(buffer, pagesize=letter)
|
||||
width, height = letter
|
||||
for _ in range(len(PdfReader(context.file_name).pages)):
|
||||
c.setFont("DejaVuEmbedded", 14)
|
||||
c.drawString(100, height - 100, text)
|
||||
c.showPage()
|
||||
c.save()
|
||||
|
||||
with open(context.file_name, "wb") as f:
|
||||
f.write(buffer.getvalue())
|
||||
|
||||
context.files[context.param_name].close()
|
||||
context.files[context.param_name] = open(context.file_name, "rb")
|
||||
|
||||
|
||||
@given('the pdf is encrypted with password "{password}"')
|
||||
def step_encrypt_pdf(context, password):
|
||||
writer = PdfWriter()
|
||||
|
||||
Reference in New Issue
Block a user