Jedan pogrešan znak u broju fakture, a jedina dostupna primitiva za izmenu prepisuje ceo tekstualni niz. PDF Library for Delphi zatvara tu prazninu: GetTextBlockCharContentLocation mapira svaku izdvojenu UTF-16 poziciju nazad na instrukciju content stream-a, operand i kodirani opseg bajtova koji su je proizveli, a ReplaceTextBlockCharSourceBytes prepisuje samo taj opseg. Ekstrakcija teksta obično odbacuje sve što bi vam za ovo bilo potrebno. Dobijete Unicode, širine i geometriju, a poreklo ispari, pa je znak na poziciji 7 bloka 3 samo znak. Koji stream ga je proizveo, koja instrukcija, koji operand, koji bajt unutar tog operanda: sve nestane. Svaka strategija tačkaste izmene izgrađena na tome mora da nagađa, obično pretragom dekodiranog sadržaja za podniskom i nadom da se pojavljuje tačno jednom. Na stvarnoj stranici to se ne dešava
Zašto prepisivanje celog tekstualnog niza uništava stranicu?
Zato što niz nije samo tekst. Operatori za prikaz teksta u ISO 32000-1 §9.4.3 uključuju TJ, čiji je operand niz u kojem se smenjuju stringovi i numerička podešavanja, a upravo ti brojevi predstavljaju tipografiju. Red postavljen kao [(AB) -120 (CD)] TJ nosi kerning od 120 hiljaditih em-a između dva stringa. Emitujte novi Tj sa spojenim tekstom i kerning nestaje, red se malo prelama, a na formi vrednost isklizne iz svog okvira. Isti prigovor važi i za font: bajtovi operanda su kodovi u kodiranju koje je izabrao Tf, a ne Unicode, dok kod kompozitnog fonta mogu biti dvobajtni CID-ovi koji nemaju nikakvu vezu sa znakom koji ste dobili iz ekstraktora. Ako ponovo generišete niz, morate tačno pogoditi kodiranje fonta, njegovu /ToUnicode mapu i pokrivenost glifova. Tačkasta izmena zaobilazi sve to tako što nikada ne napušta domen bajtova
Šta vraća GetTextBlockCharContentLocation?
Metoda razrešava jedan znak u zapis od devet polja, a svako polje predstavlja adresu, a ne vrednost. ContentLayer je indeks počevši od 1 u nizu stranice /Contents, ili 0 kada je znak došao iz ugnježdenog sadržaja. StreamObjectNumber i StreamGeneration identifikuju stream koji ga sadrži. InstructionIndex je pozicija počevši od 0 u dekodiranom content programu, OperandIndex je operand tekstualnog stringa, a ArrayElementIndex je element unutar TJ niza ili -1 za direktni string operand. SourceByteOffset i SourceByteLength zatim imenuju opseg bajtova unutar tog dekodiranog stringa
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 i CharPos dolaze iz sopstvenog prolaza kroz GetTextBlockText
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0 znači da glif živi u ugnježdenom Form XObject-u
// ArrayElementIndex = -1 znači običan Tj operand, a ne TJ niz
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Pretraga ne košta ništa u trenutku upita. Dok renderer dekodira svaki content layer, on registruje logičke spanove kroz koje prolazi, pa je upit za poziciju binarna pretraga uređenog spiska intervala, a ne linearni prolaz kroz svaki content span za svaki znak. Pri upitu se ništa ponovo ne parsira; mapa je izgrađena tokom ekstrakcionog prolaza koji ste već platili. Ako već nabrajate pogotke pomoću PDF pretrage teksta koja vraća koordinate pogodaka, dodavanje content lokacije po pogotku gotovo je besplatno
Izmena bajtova, a ne Unicode-a
ReplaceTextBlockCharSourceBytes prima AnsiString sirovih zamenskih bajtova u aktivnom kodiranju PDF fonta. To je čitav dizajn i potpuno je nameran. Ništa se ne transkodira, ništa se ponovo ne kodira, ništa ne nagađa o fontu. Biblioteka umeće vaše bajtove preko imenovanog opsega ciljnog stringa i ponovo emituje content layer koji ga sadrži. Susedni stringovi u istom TJ nizu i numerički kerning između njih ostaju bajtovski identični. Uzmite prethodni raspored: pronalaženje znaka B u [(AB) -120 (CD)] TJ daje ArrayElementIndex 0, SourceByteOffset 1 i SourceByteLength 1. Zamenite ga znakom Z i emitovani sadržaj sadrži (AZ), i dalje praćen sa -120 i (CD), oba netaknuta. Regression suite upravo to potvrđuje, jer je „sačuvali smo kerning“ vrsta tvrdnje koja neprimetno prestane da bude tačna
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
// Svaka lokacija iz stare liste sada je zastarela. Ponovo izdvoji tekst.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Layer se promenio od trenutka ekstrakcije
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Zastavica koju nismo proverili ili zastavica dodata u novijoj verziji
End;
Dva operativna detalja vredi usvojiti. Poziv privremeno prelazi na stranicu iz koje je lista teksta izdvojena i vraća prethodno izabranu stranicu i pri uspehu i pri neuspehu, pa ne pomera vaš kursor neprimetno. Pri uspehu takođe briše snimke elemenata stranice, čime se poništavaju svi handle-ovi koje ste držali iz ranijeg prolaza nabrajanja
Koji znakovi ne mogu da se menjaju?
Postoji šest kategorija, a biblioteka svaku imenuje u bitmaski Flags umesto da neodređeno prijavi neuspeh. To je važnije od srećne putanje, jer su slučajevi bez mapiranja na stvarnim dokumentima česti i svaki ima drugi razlog
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: više izdvojenih UTF-16 pozicija proširuje se iz jednog izvornog glifa./ToUnicodeunos koji jedan kod mapira nafidaje vam dva znaka koji dele isti opseg bajtova, pa ih tretirajte kao jedan izvorni glif i menjajte opseg jednomPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: znak je sintetizovan tokom raspoređivanja. Izvedeni razmaci između reči uobičajen su slučaj i uopšte nemaju izvorne bajtove, pa seSourceByteOffsetvraća kao -1, aSourceByteLengthkao 0PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: tekst koji ste pročitali došao je iz/ActualTextzamene. Ne postoji jedinstveno obrnuto mapiranje zamenjenog stringa na izvorne bajtove, pa je lokacija samo dijagnostičkaPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glif se nalazi unutar Form XObject-a. Bajtovima se može adresirati, ali Form može da crta više stranica, pa bi izmena kroz high-level API bila izmena koju niste tražiliPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand je bio heksadecimalni string sa UTF-16BE oznakom redosleda bajtova, koji postojeća putanja ekstrakcije dekodira pre mapiranja fonta. Pomaci u dekodiranom rezultatu više ne adresiraju izvorne bajtove, pa se validna zastavica poništavaPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand i njegov operator za prikaz teksta žive u dva različita stream-a
Poslednji slučaj zaslužuje sopstvenu rečenicu, jer inženjeri rutinski pretpostave da ne može da se desi. ISO 32000-1 §7.8.2 kaže da se stream-ovi u nizu stranice /Contents konkateniraju, a granica između njih mora da padne samo na leksičku granicu. Zato je BT /F1 16 Tf 220 340 Td (CrossLayer) u jednom stream-u, pa Tj ET u sledećem, potpuno legalna stranica. Mapiranje zadržava dijagnostičku poziciju, ali je označava kao read-only, jer indeks instrukcije operatora pripada drugom layer-u nego bajtovi operanda, a korišćenje jednog za adresiranje drugog oštetilo bi fajl
Kako biblioteka zna da je mapa i dalje važeća?
Po otiscima, proverеним neposredno pre upisa. Svaka ekstrakciona lista beleži izvornu stranicu i za svaki content layer dužinu layer-a i dva nezavisna klizna hash-a: FNV-1a hash i DJB2-style XOR hash. Pre nego što ReplaceTextBlockCharSourceBytes bilo šta parsira, ponovo čita ciljni layer i poredi sve tri vrednosti. Svaka promena bajta bilo gde u tom layer-u vraća PDFLIB_ERROR_TEXT_LOCATION_STALE i upis se ne izvršava. To je namerno konzervativno: provera je po layer-u, a ne po instrukciji, pa i nepovezana izmena drugde u istom content stream-u poništava vašu lokaciju. To je ispravan kompromis: pomak u stream-u koji se promenio makar za jedan bajt nije skoro pogođen, već tiha korupcija. Ista disciplina upravlja ostatkom površine za izmenu, uključujući tracker stanja content stream-a za CTM i clipping. Posle svake uspešne zamene odbacite listu i ponovo izvršite ekstrakciju
Read-only mapiranje kroz Direct Access
DAGetTextBlockCharContentLocation daje isti zapis za stranicu otvorenu kroz Direct Access putanju, uz isti rečnik zastavica. Po konstrukciji je samo dijagnostički: ReplaceTextBlockCharSourceBytes radi nad izabranim dokumentom koji se može menjati, a Direct Access je putanja za čitanje. Podaci o lokaciji preživljavaju u listi blokova teksta nakon zatvaranja handle-a, pa mogu da se koriste za 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);
// Lokacije ostaju čitljive nakon DACloseFile
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Koristite ga za odgovore, a ne za promene. Koje stranice nose tekst koji nikada ne biste mogli da menjate u mestu? Koliki deo ovog korpusa stiže sa /ActualText zamenama? Čiji izlaz deli operatore između content layer-a? To su jeftini upiti čim svaki znak dobije adresu i vredi ih pokrenuti pre nego što se obavežete na pipeline za korekciju
Gde se tačkasta izmena zaustavlja
Tačkasta izmena je skalpel, a ne tekstualni engine. Ona menja bajtove u mestu, pa se zamenski tekst koji je širi ili uži od originala neće ponovo prelomiti, neće se ponovo umotati i neće ažurirati kerning oko sebe. Zamena jedne cifre drugom u monospaced polju dobro joj odgovara. Prepisivanje pasusa ne odgovara. I izrazito nije bezbednosni alat: prepisivanje bajtova glifa ostavlja izvorne bajtove dostupnim u istoriji revizija fajla, pa sve što zahteva poverljivost pripada pravoj redakciji koja uklanja sadržaj umesto da ga prekriva. Za cenu tih ograničenja dobijate iskrenost. Svaki znak ili ima bajtovnu adresu nad kojom možete da delujete ili imenovanu zastavicu koja kaže zašto nema, a provera otiska zastarelu mapu pretvara u tvrdu grešku umesto u oštećenu stranicu. Mapiranje znaka na content bajt i zamena izvornih bajtova u mestu isporučuju se kao deo površine za ekstrakciju teksta i izmenu sadržaja u PDF Library for Delphi, izvornoj Object Pascal PDF biblioteci za Delphi, C++Builder i Lazarus