Articol tehnic

Redactarea reală a PDF-urilor în Delphi: ștergerea și igienizarea

Redactarea reală a PDF-urilor în Delphi înseamnă ștergerea efectivă a octeților subiacenți, nu acoperirea lor cu desen. losLab PDF Library definește această graniță prin două API-uri: RedactRegion editează fluxul de conținut al paginii, astfel încât textul, vectorii și imaginile eliminate dispar fizic, în timp ce SanitizeDocument curăță metadatele, scripturile și acțiunile în care se pot ascunde de asemenea informații redactate. Dacă greșiți oricare dintre aceste două etape, veți livra un fișier care pare cenzurat pe ecran, dar care oferă înapoi secretele oricui copiază textul sau deschide Proprietățile Documentului

Modul de eșec este de notorietate publică. Ziarele, instanțele judecătorești și agențiile guvernamentale au publicat adesea documente PDF cu un dreptunghi negru plasat peste un nume sau un număr, pentru ca ulterior cititorul să selecteze textul de dedesubt, să îl copieze și să lipească șirul presupus redactat direct la vedere. Dreptunghiul era doar un desen. Textul era încă acolo, aflat în fluxul de conținut cu un strat mai jos de desen, complet intact și selectabil. losLab PDF Library, motorul PDF pentru Delphi și C++Builder expus prin clasa TPDFlib, tratează această distincție ca fiind esența redactării, nu o simplă adăugire ulterioară

De ce desenarea unei casete negre peste textul PDF nu este o redactare reală?

losLab PDF Library expune funcția MaskRect pentru cazurile în care acoperirea este exact ceea ce doriți: un filigran (watermark), o blocare vizuală sau o ștampilă de dovadă. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) adaugă un dreptunghi la fluxul de conținut al paginii și îl desenează folosind operatorul de construcție a căii re, urmat de operatorul de umplere f (ISO 32000-1 §8.5). Fiecare octet care se afla deja pe pagină — glifele numelui pe care l-ați acoperit, liniile vectoriale, imaginea încorporată — rămâne exact acolo unde era, cu o instrucțiune mai devreme în același flux. Un vizualizator conform specificațiilor desenează caseta neagră la sfârșit, astfel încât aceasta domină vizual. Acoperirea nu modifică nimic din ce se află dedesubt

Oricine poate recupera conținutul original în trei moduri: selectând și copiind textul direct prin caseta opacă, rulând un extractor de text pe pagină sau ștergând dreptunghiul de acoperire din fluxul de conținut. MaskRect este onest prin însuși numele său — maschează, nu elimină. Rezultatul este ca un autocolant aplicat peste un cuvânt pe care îl puteți citi în continuare dezlipind autocolantul

Cum șterge RedactRegion conținutul la nivel de instrucțiuni

RedactRegion(Page, Left, Top, Width, Height, Options) funcționează cu un strat mai jos de desen. losLab PDF Library analizează fiecare strat de conținut al paginii într-un TPDFContentProgram, parcurge lista de instrucțiuni cu un analizor de urmărire CTM (FindInstructionsInRect în modelul de conținut) și marchează fiecare operator de afișare a textului, desenare a căii și imagine Do a cărui zonă desenată se intersectează cu dreptunghiul dvs. Aceste instrucțiuni sunt eliminate cu TPDFContentProgram.Delete, iar stratul editat este rescris cu WritePageContentLayer. Octeții nu glisează în spatele unei acoperiri — ei părăsesc fluxul. Un extractor de text rulat ulterior nu va mai găsi nimic, deoarece nu mai are ce să găsească

Două detalii de implementare merită reținute. RedactRegion primește un dreptunghi stânga-sus în originea și unitățile curente și îl convertește intern în spațiul utilizatorului stânga-jos pe care îl folosește fluxul de conținut, astfel încât specificați regiunea așa cum o vedeți pe pagină. Atunci când un dreptunghi acoperă mai multe instrucțiuni, RedactRegion le șterge în ordinea descrescătoare a indicilor, deoarece eliminarea instrucțiunii 4 înaintea instrucțiunii 7 ar decala fiecare indice ulterior și ar corupe intervalul. Dacă transmiteți Options = 0, losLab PDF Library aplică de asemenea o casetă neagră solidă peste regiunea curățată ca marcaj vizibil — dar acel marcaj este pur cosmetic, aplicat după ce conținutul real a fost deja eliminat

În practică, rareori codificați rigid dreptunghiul. Căutați pe pagină șirul de caractere sensibil și îi citiți caseta de încadrare (bounding box) — aceeași căutare de pagină și enumerare de elemente descrise în localizarea textului și a elementelor de pagină în Delphi — apoi transmiteți acel dreptunghi către RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // Delete everything painted inside a 220 x 14 pt box on page 1.
      // Coordinates are top-left, in the current origin and units.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 stamps a black marker
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Unde se mai ascund informațiile redactate într-un PDF?

Redactarea care se oprește la stratul vizibil lasă urme, deoarece un PDF stochează informații în locuri în care cititorul nu se uită niciodată. losLab PDF Library grupează aceste scurgeri în canale ce merită menționate. Dicționarul de Informații al Documentului (Document Information Dictionary) și fluxul de metadate XMP transportă de regulă șiruri de caractere pentru Author (Autor), Producer (Producător) sau Keywords (Cuvinte cheie) care menționează exact persoana sau cazul pe care l-ați eliminat din textul principal. JavaScript-ul la nivel de document și catalogul OpenAction se pot executa la deschidere și pot accesa date pe care le credeați șterse. Fiecare adnotare are o acțiune opțională /A, iar o adnotare în sine — o notă adezivă, un câmp de formular învechit, un link — poate fi situată direct peste zona redactată, conținând propria copie a textului. Nimic din toate acestea nu este afectat de criptare; un fișier criptat cu un pachet XMP în format text simplu continuă să își dezvăluie titlul oricărui indexator, o vulnerabilitate ce merită o auditarea proprie a criptării și permisiunilor. O igienizare solidă trebuie să verifice fiecare dintre aceste locuri ascunse, nu doar pagina la care vă uitați

Curățarea documentului cu SanitizeDocument

losLab PDF Library răspunde acestei game de scurgeri prin SanitizeDocument(Flags), un apel de coordonare care rulează un set de eliminări independente selectate printr-o mască de biți (bitmask) și returnează numărul de eliminări aplicate. Flagurile pot fi combinate. SANITIZE_JAVASCRIPT elimină fiecare pachet JavaScript la nivel de document și orice script OpenAction. SANITIZE_ACTIONS elimină dicționarul de acțiuni /A din fiecare adnotare, prin intermediul noii funcții TPDFAnnots.RemoveAnnotAction, și curăță catalogul OpenAction. SANITIZE_METADATA golește câmpurile Author, Subject, Keywords, Creator și Producer atât în Dicționarul de Informații al Documentului (ISO 32000-1 §14.3.3), cât și în fluxul de metadate XMP (§14.3.2). SANITIZE_ANNOTATIONS șterge fiecare adnotare din întregul document, iar SANITIZE_OPEN_ACTION elimină intrarea /OpenAction a catalogului. SANITIZE_ALL reprezintă uniunea pe biți a tuturor celor cinci flaguri

Convențiile de indici din interiorul acestor eliminări sunt exact genul de detalii care blochează discret o buclă creată manual, acesta fiind principalul motiv pentru care SanitizeDocument este oferit ca un singur apel. GlobalJavaScriptPackageName este bazat pe 0, iar RemoveGlobalJavaScript micșorează lista pe măsură ce avansează, astfel încât secvența JavaScript citește întotdeauna indicele de pachet 0 și elimină până când contorul ajunge la zero. DeleteAnnotation, în schimb, este bazat pe 1. AnnotationCount raportează în raport cu pagina selectată în prezent, astfel încât secvențele de adnotări selectează fiecare pagina la rând înainte de numărare. losLab PDF Library gestionează corect aceste limite intern, astfel încât transmiteți un singur set de flaguri și citiți un singur contor înapoi, în loc să reproduceți cinci reguli de iterare diferite

var
  Applied: Integer;
begin
  // Strip metadata, scripts, and actions but keep the annotations:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITOPEN_ACTION);

  // Or clear every hidden channel, annotations included:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

Fluxul complet de redactare și igienizare

Combinând cele două metode, întreega procedură devine scurtă. losLab PDF Library elimină conținutul vizibil cu RedactRegion, pagină cu pagină, apoi curăță fiecare canal ascuns cu SanitizeDocument(SANITIZE_ALL) înainte de scrierea fișierului. Deoarece redactarea se execută aproape întotdeauna pe documente sosite din afara controlului dvs., merită să le încărcați printr-o cale securizată — aceeași atitudine defensivă descrisă în analizarea securizată a PDF-urilor din surse nesigure — astfel încât o intrare defectuoasă să eșeze curat, mai degrabă decât la jumătatea procesului de redactare

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // delete the sensitive line
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // clear metadata, JS, actions, annots
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Unde se oprește redactarea fluxului de conținut

RedactRegion operează pe fluxul de conținut, care este exact locul unde se află textul digital nativ și exact unde nu se află textul scanat. Când o pagină este o imagine scanată, cuvintele sunt pixeli în interiorul unui XObject de imagine, nu operatori de text, iar losLab PDF Library nu poate extrage un nume din mijlocul unei imagini raster așa cum șterge o glifă. Gestionarea imaginilor este în mod deliberat conservatoare: orice imagine a cărei poziție se intersectează cu dreptunghiul este eliminată complet, astfel încât o casetă de redactare pe o pagină scanată va elimina întreaga scanare, nu doar o porțiune din ea. Redactarea unei regiuni din interiorul unei imagini înseamnă rasterizarea, desenarea și re-încorporarea la nivel de pixeli, ceea ce este o operațiune diferită de ștergerea fluxului de conținut. Aceeași precauție se aplică și unui strat de text OCR de peste o pagină scanată: RedactRegion elimină operatorii de text pe care îi poate detecta, dar imaginea de dedesubt rămâne neatinsă dacă imaginea sa XObject nu se intersectează de asemenea cu caseta

Două aspecte subtile garantează corectitudinea rezultatelor. Testul de intersecție aproximează lățimea fiecărui bloc de text pe baza numărului de caractere și a dimensiunii fontului, mai degrabă decât prin măsurarea metricilor exacte ale glifelor, astfel încât un cuvânt foarte scurt scris într-un font proporțional poate scăpa dintr-un dreptunghi delimitat prea strâns — acordați casetei o mică marjă. De asemenea, ștergerea se face la nivel de instrucțiune: RedactRegion elimină operatori întregi de afișare a textului, astfel încât un dreptunghi care decupează doar o parte dintr-un bloc de text poate elimina caracterele adiacente care partajau același operator cu acesta. Niciuna dintre limite nu este un motiv de neîncredere în mecanism, ci doar un motiv pentru a defini cu atenție dreptunghiul și a verifica rezultatul

Regula de aur care încheie orice flux de redactare este verificarea, nu încrederea oarbă. Redactați regiunea, igienizați documentul, salvați într-un fișier nou, redeschideți-l și încercați să extrageți atât textul pe care l-ați eliminat, cât și metadatele pe care le-ați curățat; când ambele revin goale, redactarea a reușit. API-urile RedactRegion și SanitizeDocument prezentate aici sunt livrate împreună cu losLab PDF Library pentru Delphi și C++Builder, alături de documentația completă a modelului de conținut și a igienizării