<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jason Farrar</title>
    <description>The latest articles on DEV Community by Jason Farrar (@boscorat_50).</description>
    <link>https://dev.to/boscorat_50</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4055313%2Fccb0aed1-ffd1-49a6-8a0c-90ad8443c408.jpg</url>
      <title>DEV Community: Jason Farrar</title>
      <link>https://dev.to/boscorat_50</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boscorat_50"/>
    <language>en</language>
    <item>
      <title>How to anonymise sensitive data in PDF documents with Python</title>
      <dc:creator>Jason Farrar</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:21:53 +0000</pubDate>
      <link>https://dev.to/boscorat_50/how-to-anonymise-sensitive-data-in-pdf-documents-with-python-c7d</link>
      <guid>https://dev.to/boscorat_50/how-to-anonymise-sensitive-data-in-pdf-documents-with-python-c7d</guid>
      <description>&lt;p&gt;Have you ever needed to share a bank statement with someone? Maybe you've needed a user of your project to share one with you for troubleshooting.&lt;/p&gt;

&lt;p&gt;I hit this exact problem. I've been building an extensible tool to convert UK bank statements into a relational database, but I only have access to statements from my own bank. How can I get access to other bank statements without asking people to send me sensitive, personally identifiable information?&lt;/p&gt;

&lt;p&gt;I needed anonymised bank statements that retained their layout and font encoding so the statements could be processed in exactly the same way as the original.  I also wanted to scramble everything by default, but enable the protection of dates and transaction types.  Finally, it would be helpful if scrambling multiple statements from the same account could be configured to always scramble to the same fake value!&lt;/p&gt;

&lt;p&gt;So I built a Python tool that does it properly — scrambles the sensitive data while keeping the PDF looking exactly the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem with PDF redaction
&lt;/h3&gt;

&lt;p&gt;Traditional redaction tools typically do one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Black boxes&lt;/strong&gt; — overlay opaque rectangles. The text is still underneath (visible as soon as my parser processes it). Not actually secure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text replacement&lt;/strong&gt; — swap the text, but break the font encoding, layout, or both. The PDF not only looks wrong, but even the slightest change in the makeup of the statement could invalidate my config files and render them useless.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is ideal. What I needed was to actually replace the bytes in the PDF's content stream — the raw instructions that tell the PDF viewer what to draw — while preserving the font encoding and positioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why pikepdf?
&lt;/h3&gt;

&lt;p&gt;I looked at three Python PDF libraries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Why it didn't work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyPDF2&lt;/td&gt;
&lt;td&gt;High-level&lt;/td&gt;
&lt;td&gt;Merging, splitting, extracting — no content stream access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pdfplumber&lt;/td&gt;
&lt;td&gt;High-level&lt;/td&gt;
&lt;td&gt;I already use this for text extraction, but it doesn't modify content streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pikepdf&lt;/td&gt;
&lt;td&gt;Low-level&lt;/td&gt;
&lt;td&gt;Direct access to content stream operators and font encodings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;pikepdf gives you access to the raw PDF operators — &lt;code&gt;Tj&lt;/code&gt; (show text), &lt;code&gt;TJ&lt;/code&gt; (show text with positioning), &lt;code&gt;Tm&lt;/code&gt; (set text matrix). This is where the actual text lives, and where I needed to make changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; the PDF's content stream into operators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan&lt;/strong&gt; for sensitive patterns (sort codes, account numbers, IBANs, card numbers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace&lt;/strong&gt; the matching bytes with scrambled alternatives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; the modified content stream back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the tricky part: PDF font encodings. Different banks use different encoding strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latin-1&lt;/strong&gt; (single-byte, straightforward)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ToUnicode CMap&lt;/strong&gt; (maps character codes to Unicode — common in modern PDFs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity-H&lt;/strong&gt; (CID fonts, multi-byte — used by some banking software)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool handles all three, so the scrambled text renders correctly regardless of the bank's PDF generator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;One function, one import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bank_statement_anonymiser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anonymise_pdf&lt;/span&gt;

&lt;span class="nf"&gt;anonymise_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statement.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anonymised.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The output PDF looks identical to the input, but with sensitive data scrambled.&lt;/p&gt;

&lt;h3&gt;
  
  
  What gets anonymised
&lt;/h3&gt;

&lt;p&gt;The tool targets these patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sort codes&lt;/strong&gt; (UK-specific: XX-XX-XX format)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account numbers&lt;/strong&gt; (8-digit UK accounts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IBANs&lt;/strong&gt; (international bank account numbers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card numbers&lt;/strong&gt; (Visa, Mastercard patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merchant names&lt;/strong&gt; (configurable via config files)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What doesn't get anonymised
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Values&lt;/strong&gt; (as my dependent projects validate the value of transaction lines against the total statement movement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates&lt;/strong&gt; (various formats to cover transaction and statement dates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Types&lt;/strong&gt; (specified in the never_anonymise_system.toml containing known transaction types for supported banks)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;You can further customise what gets anonymised by passing your own TOML config files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# always_anonymise.toml — force-replace these patterns&lt;/span&gt;
&lt;span class="py"&gt;"12-12-12"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"99-99-99"&lt;/span&gt;
&lt;span class="py"&gt;"Jason Farrar"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"John Doe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# never_anonymise.toml — protect these phrases&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"Balance Brought Forward"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Account Summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system uses an exclusion-based approach: everything is scrambled by default, then you specify what to protect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;This is UK-specific. The regex patterns for sort codes, account numbers, and IBAN formats are included for UK banks. Currently tested successfully on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HSBC&lt;/li&gt;
&lt;li&gt;Natwest&lt;/li&gt;
&lt;li&gt;TSB&lt;/li&gt;
&lt;li&gt;Halifax&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may also work for other banks, so please give it a go and let me know!&lt;br&gt;
Other banks may require adding new regex patterns, and will almost certainly use different transaction types.  There may also be other font encodings I haven't come across yet.&lt;br&gt;
All contributions are very much appreciated!&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;uk-bank-statement-anonymiser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Further instructions on installation and usage are available in the &lt;a href="https://github.com/boscorat/uk-bank-statement-anonymiser/blob/master/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open source
&lt;/h3&gt;

&lt;p&gt;MIT licensed. All processing is local — no network requests, no accounts, no data collection.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/boscorat/uk-bank-statement-anonymiser" rel="noopener noreferrer"&gt;github.com/boscorat/uk-bank-statement-anonymiser&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you encountered similar PDF anonymisation challenges? I'd be interested to hear about other banks' PDF formats — the more patterns I can add, the more useful this becomes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Any questions or comments are very welcome. I'd love to hear if you think there's a better way to approach this problem, or changes I can make to the structure to widen the useful scope of this project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
