Fix no-ToUnicode redaction fixture and add font-matrix and cucumber e2e tests

This commit is contained in:
Anthony Stirling
2026-07-03 08:37:32 +01:00
parent 7eaa54d633
commit e91d535c36
3 changed files with 121 additions and 2 deletions
@@ -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 {
+37
View File
@@ -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()