Jeden nesprávny znak v čísle faktúry a jediný dostupný primitív úpravy prepíše celý textový beh. PDF Library for Delphi túto medzeru vypĺňa: GetTextBlockCharContentLocation mapuje každú pozíciu extrahovaného UTF-16 späť na inštrukciu content streamu, operand a rozsah zakódovaných bajtov, ktoré ju vytvorili, a ReplaceTextBlockCharSourceBytes prepíše iba tento rozsah. Extrakcia textu za bežných okolností zahodí všetko, čo by ste na to potrebovali. Dostanete Unicode, šírky a geometriu a pôvod znaku zmizne, takže znak na pozícii 7 bloku 3 je jednoducho znak. Ktorý stream ho vytvoril, ktorá inštrukcia, ktorý operand, ktorý bajt v operande: preč. Každá stratégia bodovej úpravy postavená nad tým musí hádať, zvyčajne hľadaním podreťazca v dekódovanom obsahu a nádejou, že sa vyskytuje presne raz. Na skutočnej stránke sa to nestáva
Prečo prepísanie celého textového behu rozbije stránku?
Pretože beh nie je iba text. Operátory zobrazujúce text v ISO 32000-1 §9.4.3 zahŕňajú TJ, ktorého operand je pole prekladajúce stringy s číselnými úpravami a tieto čísla sú sadzba. Riadok rozložený ako [(AB) -120 (CD)] TJ nesie medzi dvoma stringami kerning 120-tisícin em. Vytvorte nový Tj so zreťazeným textom a kerning zmizne, riadok sa o vlas preformátuje a vo formulári sa hodnota posunie mimo svojho poľa. Rovnaká námietka platí pre font: bajty operandu sú kódy v kódovaní, ktoré vybral Tf, nie Unicode, a pri composite fonte môžu byť dvojbajtové CID bez vzťahu k znaku, ktorý ste dostali z extractora. Pri regenerovaní behu musíte správne poznať kódovanie fontu, jeho mapu /ToUnicode aj pokrytie glyfmi. Bodová úprava to všetko obíde tým, že nikdy neopustí bajtovú doménu
Čo vracia GetTextBlockCharContentLocation?
Metóda preloží jeden znak na deväťprvkový record a každé pole je adresa, nie hodnota. ContentLayer je index od 1 do poľa stránky /Contents alebo 0, keď znak pochádza z vnoreného obsahu. StreamObjectNumber a StreamGeneration identifikujú obsahujúci stream. InstructionIndex je pozícia od 0 v dekódovanom content programe, OperandIndex je operand textového stringu a ArrayElementIndex je prvok vo vnútri poľa TJ alebo -1 pre priamy string operand. SourceByteOffset a SourceByteLength potom pomenujú rozsah bajtov vo vnútri tohto dekódovaného stringu
Var
Lib: TPDFlib;
ListID, Block, CharPos: Integer;
ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
Lib:= TPDFlib.Create;
Try
Lib.LoadFromFile('invoice.pdf', '');
Lib.SelectPage(1);
ListID:= Lib.ExtractPageTextBlocks(3);
Try
// Block a CharPos pochádzajú z vašej vlastnej prehliadky GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 znamená, že glyf žije vo vnorenom Form XObject
// ArrayElementIndex = -1 znamená obyčajný operand Tj, nie pole TJ
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Vyhľadanie nestojí v čase dotazu nič. Kým renderer dekóduje každú content layer, zaregistruje logické rozsahy, ktorými prechádza, takže dotaz na pozíciu je binárne vyhľadanie v usporiadanom zozname intervalov, nie lineárny prechod každým obsahovým rozsahom pre každý znak. Pri dotaze sa nič neparsuje znova; mapa vznikla počas extrakčného prechodu, ktorý ste už zaplatili. Ak už enumerujete hity pomocou vyhľadávania textu PDF vracajúceho súradnice hitov, pridať ku každému hitu content location je takmer zadarmo
Úprava bajtov, nie Unicode
ReplaceTextBlockCharSourceBytes prijíma AnsiString so surovými náhradnými bajtmi v aktívnom kódovaní PDF fontu. To je celý návrh a je zámerný. Nič sa netranskóduje, nič sa nekóduje znova, nič sa neháda o fonte. Knižnica vloží vaše bajty cez pomenovaný rozsah cieľového stringu a znovu emituje obsahujúcu content layer. Susedné stringy v rovnakom poli TJ aj číselné kerningy medzi nimi zostanú bajtovo identické. Vezmite predchádzajúce rozloženie: nájdenie B v [(AB) -120 (CD)] TJ dá ArrayElementIndex 0, SourceByteOffset 1 a SourceByteLength 1. Nahraďte ho Z a emitovaný obsah obsahuje (AZ), po ktorom stále nasledujú -120 a (CD), obe bez zásahu. Regresná suite presne toto overuje, pretože tvrdenie „kerning sme zachovali“ je presne ten typ tvrdenia, ktoré potichu prestane platiť
Function EditableHere(Flags: Integer): Boolean;
Begin
Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;
// ...
If EditableHere(Flags) Then
Begin
If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
Begin
// Každá location v starom zozname je teraz zastaraná. Extrahujte znova.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Vrstva sa od extrakcie zmenila pod našimi rukami
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Príznak, ktorý sme zabudli skontrolovať, alebo príznak pridaný neskoršou verziou
End;
Dva prevádzkové detaily sa oplatí vryť si do pamäti. Volanie dočasne prepne na stránku, z ktorej bol textový zoznam extrahovaný, a pri úspechu aj pri zlyhaní obnoví predtým vybranú stránku, takže ticho neposunie váš kurzor. Pri úspechu zároveň vymaže snapshoty elementov stránky, čím zneplatní všetky handle, ktoré ste držali z predchádzajúceho enumeračného prechodu
Ktoré znaky nemožno upraviť?
Šesť kategórií a knižnica každú pomenúva v bitovej maske Flags namiesto vágneho zlyhania. Je to dôležitejšie než happy path, pretože v reálnych dokumentoch sú nemapovateľné prípady bežné a každý má iný dôvod
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: niekoľko pozícií extrahovaného UTF-16 sa rozvinie z jedného zdrojového glyfu. Záznam/ToUnicode, ktorý mapuje jeden kód nafi, vám dá dva znaky zdieľajúce jeden rozsah bajtov, takže s nimi zaobchádzajte ako s jedným zdrojovým glyfom a rozsah upravte razPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: znak bol syntetizovaný počas layoutu. Typickým prípadom sú odvodené medzery medzi slovami a tie nemajú vôbec žiadne zdrojové bajty, takžeSourceByteOffsetsa vráti ako -1 aSourceByteLengthako 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: text, ktorý čítate, pochádza z náhrady/ActualText. Neexistuje jednoznačné spätné mapovanie nahradeného stringu na zdrojové bajty, takže location je iba diagnostickáPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyf je vo Form XObject. Bajty sú adresovateľné, ale Form môže kresliť viacero stránok, takže jeho úprava cez high-level API by bola úpravou, o ktorú ste nežiadaliPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand bol hex string s byte-order markom UTF-16BE, ktorý existujúca extrakčná cesta dekóduje pred mapovaním fontu. Offsety v dekódovanom výsledku už neadresujú pôvodné bajty, preto sa platný príznak zrušíPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand a jeho text-showing operator žijú v dvoch rôznych streamoch
Posledný prípad si zaslúži vlastnú vetu, pretože inžinieri pravidelne predpokladajú, že nemôže nastať. ISO 32000-1 §7.8.2 hovorí, že streamy v poli stránky /Contents sa zreťazia a hranica medzi nimi musí spadnúť iba na lexikálnu hranicu. Preto BT /F1 16 Tf 220 340 Td (CrossLayer) v jednom streame a Tj ET v ďalšom predstavujú úplne legálnu stránku. Mapovanie ponechá diagnostickú pozíciu, ale označí ju iba na čítanie, pretože index inštrukcie operátora patrí inej vrstve než bajty operandu a použitie jedného na adresovanie druhého by poškodilo súbor
Ako knižnica vie, že mapovanie je stále platné?
Pomocou fingerprintov kontrolovaných bezprostredne pred zápisom. Každý extrakčný zoznam zaznamená zdrojovú stránku a pre každú content layer dĺžku vrstvy aj dva nezávislé rolling hashe: hash FNV-1a a XOR hash v štýle DJB2. Skôr než ReplaceTextBlockCharSourceBytes čokoľvek parsuje, znovu načíta cieľovú vrstvu a porovná všetky tri hodnoty. Akákoľvek zmena bajtu kdekoľvek v tejto vrstve vráti PDFLIB_ERROR_TEXT_LOCATION_STALE a zápis sa neuskutoční. Je to zámerne konzervatívne: kontrola je na úrovni vrstvy, nie inštrukcie, takže aj nesúvisiaca úprava inde v tom istom content streame zneplatní vašu location. To je správny trade-off: offset do streamu, ktorý sa posunul čo i len o jeden bajt, nie je takmer správny, ale tichá korupcia. Rovnaká disciplína riadi zvyšok editačnej plochy vrátane state trackera content streamu pre CTM a clipping. Po každej úspešnej náhrade zoznam zahodťe a extrahujte znova
Mapovanie iba na čítanie cez Direct Access
DAGetTextBlockCharContentLocation vám dá identický record pre stránku otvorenú cez Direct Access path s identickou slovnou zásobou flagov. Je diagnostický už svojou konštrukciou: ReplaceTextBlockCharSourceBytes pracuje s vybraným editovateľným dokumentom a Direct Access je read path. Lokalizačné údaje prežijú v zozname textových blokov aj po zatvorení file handle, vďaka čomu sa dajú použiť na offline audit
FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
Try
Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags);
// Location zostávajú čitateľné po DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Použite to na odpovede na otázky, nie na zmenu vecí. Ktoré stránky obsahujú text, ktorý by ste nikdy nemohli upraviť na mieste? Koľko z tohto korpusu prichádza s override /ActualText? Ktorý vendor rozdeľuje operátory medzi content layers? Keď má každý znak adresu, tieto otázky sú lacné a oplatí sa ich položiť skôr, než sa zaviažete ku korekčnému pipeline
Kde sa bodová úprava končí
Bodová úprava je skalpel, nie textový engine. Mení bajty na mieste, takže náhradný text širší alebo užší než originál sa nepreformátuje, nezalomí a neaktualizuje kerny okolo seba. Nahradenie jednej číslice inou v monospaced poli je dobrý kandidát. Prepis odseku nie. A rozhodne to nie je bezpečnostný nástroj: prepísanie bajtov glyfov ponechá pôvodné bajty obnoviteľné z histórie revízií súboru, takže všetko s požiadavkou dôvernosti patrí do skutočnej redakcie, ktorá obsah odstraňuje namiesto jeho prekrytia. Za tieto obmedzenia dostanete poctivosť. Každý znak má buď bajtovú adresu, na ktorej môžete konať, alebo pomenovaný flag vysvetľujúci, prečo ju nemá, a kontrola fingerprintu zmení zastaranú mapu na tvrdú chybu namiesto poškodenej stránky. Mapovanie znaku na content bytes a náhrada zdrojových bajtov na mieste sú súčasťou plochy extrakcie textu a úprav obsahu v PDF Library for Delphi, natívnej Object Pascal PDF knižnici pre Delphi, C++Builder a Lazarus