Jeden chybný znak v čísle faktury a jediná dostupná editační primitiva přepíše celý textový run. PDF Library for Delphi tuto mezeru uzavírá: GetTextBlockCharContentLocation mapuje každou extrahovanou pozici UTF-16 zpět na instrukci content streamu, operand a zakódovaný rozsah bajtů, který ji vytvořil, a ReplaceTextBlockCharSourceBytes přepíše pouze tento rozsah. Extrakce textu obvykle zahodí vše, co byste k tomu potřebovali. Získáte Unicode, šířky a geometrii, ale provenance zmizí, takže znak na pozici 7 bloku 3 je prostě znak. Který stream ho vytvořil, která instrukce a který operand, který bajt uvnitř operandu: pryč. Každá strategie bodové úpravy nad tím musí hádat, obvykle hledáním podřetězce v dekódovaném contentu a nadějí, že se vyskytuje právě jednou. Na skutečné stránce tomu tak není
Proč přepis celého textového runu rozbije stránku
Protože run není jen text. Operátory pro vykreslení textu v ISO 32000-1 §9.4.3 zahrnují TJ, jehož operandem je pole střídající řetězce s číselnými úpravami, a právě tato čísla tvoří sazbu. Řádek zapsaný jako [(AB) -120 (CD)] TJ obsahuje kern 120 tisícin em mezi oběma řetězci. Vytvořte nové Tj se spojeným textem, kern zmizí, řádek se nepatrně přeskupí a ve formuláři hodnota uteče z rámečku. Stejná námitka platí pro font: bajty operandu jsou kódy v kódování, které zvolil Tf, nikoli Unicode, a u composite fontu mohou být dvoubajtovými CID bez vztahu ke znaku, který vám vrátil extractor. Když run vygenerujete znovu, musíte správně trefit kódování fontu, jeho mapu /ToUnicode i pokrytí glyfů. Bodová úprava to vše obchází tím, že nikdy neopouští bajtovou doménu
Co vrací GetTextBlockCharContentLocation
Metoda převede jeden znak na record s devíti poli a každé pole je adresa, nikoli hodnota. ContentLayer je index od 1 do pole stránky /Contents, nebo 0, když znak pochází z vnořeného contentu. StreamObjectNumber a StreamGeneration identifikují obsahující stream. InstructionIndex je pozice od 0 v dekódovaném content programu, OperandIndex je operand textového řetězce a ArrayElementIndex je prvek uvnitř pole TJ nebo -1 u přímého string operandu. SourceByteOffset a SourceByteLength potom pojmenují rozsah bajtů uvnitř tohoto dekódovaného řetězce
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ázejí z vlastního průchodu GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 znamená glyph ve vnořeném Form XObject
// ArrayElementIndex = -1 znamená běžný operand Tj, nikoli pole TJ
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Vyhledání při dotazu nic nestojí. Když renderer dekóduje každou content layer, registruje logické rozsahy, kterými prochází, takže dotaz na pozici je binární vyhledávání v uspořádaném seznamu intervalů místo lineárního průchodu každým content spanem pro každý znak. Při dotazu se nic znovu neparsuje, mapa vznikla v extrakčním průchodu, který už jste zaplatili. Pokud už vypisujete hity přes PDF text search vracející souřadnice hitů, přidání content location ke každému hitu je téměř zadarmo
Úprava bajtů, ne Unicode
ReplaceTextBlockCharSourceBytes přijímá AnsiString surových náhradních bajtů v aktivním kódování PDF fontu. To je celý návrh a je záměrný. Nic se nepřekóduje, nic se znovu nekóduje a nic se neodhaduje o fontu. Knihovna vloží vaše bajty přes pojmenovaný rozsah cílového řetězce a znovu vypíše obsahující content layer. Sousední řetězce ve stejném poli TJ i číselné kerny mezi nimi zůstanou bajtově identické. Vezměte předchozí rozvržení: nalezení B v [(AB) -120 (CD)] TJ vrátí ArrayElementIndex 0, SourceByteOffset 1 a SourceByteLength 1. Nahraďte ho Z a vypsaný content bude obsahovat (AZ), za ním stále -120 a (CD), obojí beze změny. Regresní sada přesně tohle ověřuje, protože tvrzení „kern jsme zachovali“ je přesně ten druh tvrzení, které potichu přestane platit
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 ve starém seznamu je nyní neplatná. Extrahuj znovu.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Vrstva se od extrakce změnila pod rukama
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Příznak, který jsme nekontrolovali, nebo příznak přidaný pozdější verzí
End;
Stojí za to zapamatovat si dva provozní detaily. Volání dočasně přepne na stránku, ze které byl textový seznam extrahován, a při úspěchu i při selhání obnoví dříve vybranou stránku, takže vám potichu neposune kurzor. Při úspěchu také vymaže snapshoty prvků stránky, čímž zneplatní všechny handly držené z dřívějšího průchodu enumerací
Které znaky nelze upravit
Šest kategorií a knihovna každou pojmenuje v bitmasce Flags místo vágního selhání. To je důležitější než šťastná cesta, protože v reálných dokumentech jsou nemapovatelné případy běžné a každý má jiný důvod
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: několik extrahovaných pozic UTF-16 se rozvine z jednoho zdrojového glyfu. Položka/ToUnicode, která mapuje jeden kód nafi, vám dá dva znaky sdílející jeden rozsah bajtů, takže s nimi zacházejte jako s jediným zdrojovým glyfem a rozsah upravte jednouPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: znak byl syntetizován při layoutu. Obvyklým případem jsou odvozené mezery mezi slovy a ty nemají žádné zdrojové bajty, takžeSourceByteOffsetvrací -1 aSourceByteLength0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: přečtený text pochází z náhrady/ActualText. Neexistuje jednoznačné zpětné mapování nahrazeného řetězce na zdrojové bajty, takže location slouží pouze k diagnosticePDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyph leží uvnitř Form XObject. Bajty jsou adresovatelné, ale Form může vykreslovat několik stránek, takže jeho úprava přes high-level API by byla změnou, o kterou jste nežádaliPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand byl hexadecimální řetězec s BOM UTF-16BE, který existující extrakční cesta dekóduje před mapováním fontu. Offsety do dekódovaného výsledku už neadresují původní bajty, proto se validní příznak vyčistíPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand a jeho textový zobrazovací operátor leží ve dvou různých streamech
Poslední případ si zaslouží vlastní větu, protože inženýři rutinně předpokládají, že nastat nemůže. ISO 32000-1 §7.8.2 říká, že streamy v poli stránky /Contents se spojují a rozdělení mezi nimi musí spadat jen na lexikální hranici. BT /F1 16 Tf 220 340 Td (CrossLayer) v jednom streamu a Tj ET v dalším je tedy naprosto legální stránka. Mapování si ponechá diagnostickou pozici, ale označí ji jako pouze pro čtení, protože index instrukce operátoru patří jiné layer než bajty operandu a použití jednoho k adresaci druhého by soubor poškodilo
Jak knihovna pozná, že mapa stále platí
Podle fingerprintů kontrolovaných bezprostředně před zápisem. Každý extrakční seznam zaznamená zdrojovou stránku a pro každou content layer její délku a dva nezávislé rolling hashe: hash FNV-1a a hash ve stylu DJB2 s XOR. Než ReplaceTextBlockCharSourceBytes cokoli parsuje, znovu načte cílovou layer a porovná všechny tři hodnoty. Jakákoli změna bajtu kdekoli v této layer vrátí PDFLIB_ERROR_TEXT_LOCATION_STALE a zápis se neprovede. Je to záměrně konzervativní: kontrola je po layer, nikoli po instrukci, takže i nesouvisející úprava jinde ve stejném content streamu zneplatní vaši location. To je správný kompromis: offset do streamu, který se posunul třeba o jediný bajt, není skoro správně, ale tichá korupce. Stejná disciplína řídí i zbytek editační plochy včetně trackeru stavu content streamu pro CTM a clipping. Po každé úspěšné náhradě seznam zahoďte a extrahujte znovu
Mapování pouze pro čtení přes Direct Access
DAGetTextBlockCharContentLocation vám dá identický record pro stránku otevřenou přes cestu Direct Access se stejnou slovní zásobou příznaků. Z konstrukce je pouze diagnostický: ReplaceTextBlockCharSourceBytes pracuje s vybraným editovatelným dokumentem a Direct Access je čtecí cesta. Data location přežijí v seznamu textových bloků i po zavření file handle, takže je lze použít pro 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);
// Locations zůstávají čitelné po DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Využijte to k odpovědím, ne ke změnám. Které stránky obsahují text, který nikdy nepůjde upravit na místě? Kolik z tohoto korpusu přichází s přepsáním /ActualText? Který dodavatel rozděluje operátory přes content layers? Jakmile má každý znak adresu, jsou to levné dotazy a vyplatí se je spustit ještě předtím, než se zavážete ke korekční pipeline
Kde bodová úprava končí
Bodová úprava je skalpel, nikoli textový engine. Mění bajty na místě, takže náhradní text širší nebo užší než původní se nepřelomí, nezabalí na další řádek a neupraví kerny kolem sebe. Nahrazení jedné číslice jinou v monospaced poli je dobré použití. Přepis odstavce ne. A rozhodně nejde o bezpečnostní nástroj: přepsání bajtů glyfu ponechá původní bajty obnovitelné z revision history souboru, takže vše s požadavkem na důvěrnost patří do skutečné redakce, která obsah odstraní místo jeho překrytí. Na oplátku získáte poctivost. Každý znak buď má bajtovou adresu, na kterou lze působit, nebo pojmenovaný příznak vysvětlující, proč ji nemá, a kontrola fingerprintu změní zastaralou mapu v tvrdou chybu místo poškozené stránky. Mapování znaku na bajty contentu a náhrada zdrojových bajtů na místě jsou součástí plochy extrakce textu a úprav contentu v PDF Library for Delphi, nativní Object Pascalové PDF knihovně pro Delphi, C++Builder a Lazarus