Tehnički članak

Mapiranje PDF teksta na bajtove toka sadržaja u Delphiju

Jedan pogrešan znak u broju računa, a jedini alat za uređivanje koji imate prepisuje cijeli tekstualni niz. PDF Library for Delphi zatvara tu prazninu: GetTextBlockCharContentLocation svaki izdvojeni položaj UTF-16 vraća do naredbe toka sadržaja, operanda i raspona kodiranih bajtova koji su ga proizveli, a ReplaceTextBlockCharSourceBytes prepisuje samo taj raspon. Izdvajanje teksta obično odbacuje sve što bi vam za ovo trebalo. Dobijete Unicode, širine i geometriju, a podrijetlo nestane, pa je znak na položaju 7 bloka 3 samo znak. Koji ga je tok proizveo, koja naredba, koji operand, koji bajt unutar tog operanda: sve nestane. Svaka strategija uređivanja pojedinačne točke izgrađena na tome mora pogađati, obično pretražujući dekodirani sadržaj za podniz i nadajući se da se pojavljuje točno jednom. Na stvarnoj stranici ne pojavljuje se

Zašto prepisivanje cijelog tekstualnog niza pokvari stranicu?

Jer niz nije samo tekst. Operatori za prikaz teksta u ISO 32000-1 §9.4.3 uključuju TJ, čiji je operand polje koje izmjenjuje nizove i brojčane prilagodbe, a upravo ti brojevi čine tipografiju. Redak složen kao [(AB) -120 (CD)] TJ nosi kern od 120 tisućinki em-a između dvaju nizova. Emitirajte novi Tj sa spojenim tekstom i kern nestaje, redak se za dlaku prelama, a na obrascu vrijednost isklizne iz svojeg okvira. Isti prigovor vrijedi za font: bajtovi operanda kodovi su u kodiranju koje je odabrao Tf, a ne Unicode, dok kod složenog fonta mogu biti dvobajtni CID-ovi koji nemaju odnos sa znakom koji ste pročitali iz ekstraktora. Ponovno izradite niz i morate pogoditi kodiranje fonta, njegovu mapu /ToUnicode i pokrivenost glifova. Uređivanje točke zaobilazi sve to tako što nikad ne napušta domenu bajtova

Što vraća GetTextBlockCharContentLocation?

Metoda jedan znak razrješava u zapis s devet polja, a svako je polje adresa, a ne vrijednost. ContentLayer je indeks s početkom od 1 u polju stranice /Contents, ili 0 kada znak dolazi iz ugniježđenog sadržaja. StreamObjectNumber i StreamGeneration određuju tok koji ga sadrži. InstructionIndex je položaj s početkom od 0 u dekodiranom programu sadržaja, OperandIndex tekstualni operand, a ArrayElementIndex element unutar polja TJ ili -1 za izravni string operand. SourceByteOffset i SourceByteLength zatim imenuju raspon bajtova unutar tog dekodiranog niza

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 vlastitog 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 ugniježđenom Form XObjectu
        // ArrayElementIndex = -1 znači običan Tj operand, a ne polje TJ
      End;
    Finally
      Lib.ReleaseTextBlocks(ListID);
    End;
  Finally
    Lib.Free;
  End;
End;

Pretraga u trenutku upita ne košta ništa. Dok renderer dekodira svaki sloj sadržaja, registrira logičke raspone kroz koje prolazi, pa je upit položaja binarno pretraživanje uređenog popisa intervala, a ne linearni prolaz kroz svaki raspon sadržaja za svaki znak. Kada pitate, ništa se ponovno ne raščlanjuje; mapa je izgrađena tijekom prolaza izdvajanja koji ste već platili. Ako već nabrajate pogotke pomoću pretraživanja PDF teksta koje vraća koordinate pogodaka, dodavanje lokacije sadržaja po pogotku gotovo je besplatno

Uređivanje bajtova, a ne Unicodea

ReplaceTextBlockCharSourceBytes prima AnsiString sirovih zamjenskih bajtova u aktivnom kodiranju PDF fonta. To je cijeli dizajn i namjeran je. Ništa se ne transkodira, ništa se ponovno ne kodira i ništa ne nagađa o fontu. Biblioteka umeće vaše bajtove preko imenovanog raspona ciljnog niza i ponovno emitira sloj sadržaja koji ga sadrži. Susjedni nizovi u istom polju TJ i brojčani kernovi između njih ostaju bajt po bajt jednaki. Uzmite prethodni raspored: pronalazak B u [(AB) -120 (CD)] TJ daje ArrayElementIndex 0, SourceByteOffset 1 i SourceByteLength 1. Zamijenite ga s Z i emitirani sadržaj sadrži (AZ), a iza njega i dalje slijede -120 i (CD), oba netaknuta. Regresijski paket upravo to potvrđuje jer je tvrdnja da smo sačuvali kerning jedna od onih koje potajno prestanu vrijediti

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;

// Nastavak primjera
If EditableHere(Flags) Then
Begin
  If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
  Begin
    // Svaka lokacija na starom popisu sada je zastarjela. Izdvoji ponovno.
    Lib.ReleaseTextBlocks(ListID);
    ListID:= Lib.ExtractPageTextBlocks(3);
  End
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
    // Sloj se promijenio ispod nas nakon izdvajanja
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
    // Zastavica koju nismo provjerili ili zastavica dodana u novijoj verziji
End;

Vrijedi usvojiti dva operativna detalja. Poziv se privremeno prebacuje na stranicu iz koje je popis teksta izdvojen i vraća prethodno odabranu stranicu i pri uspjehu i pri neuspjehu, pa vam tiho ne pomiče pokazivač. Nakon uspjeha briše snimke elemenata stranice, čime poništava sve ručke koje ste držali iz ranijeg prolaza nabrajanja

Koji se znakovi ne mogu uređivati?

Šest kategorija, a biblioteka svaku imenuje u bitmaski Flags umjesto nejasnog neuspjeha. To je važnije od sretne staze jer su slučajevi bez mapiranja na stvarnim dokumentima česti i svaki ima drugačiji razlog

  • PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: više izdvojenih položaja UTF-16 proširuje se iz jednog izvornog glifa. Unos /ToUnicode koji jedan kod preslikava u fi daje vam dva znaka koji dijele jedan raspon bajtova, pa ih tretirajte kao jedan izvorni glif i raspon uredite jednom
  • PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: znak je sintetiziran tijekom raspoređivanja. Izvedeni razmaci između riječi uobičajen su slučaj i uopće nemaju izvorne bajtove, pa se SourceByteOffset vraća kao -1, a SourceByteLength kao 0
  • PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: tekst koji ste pročitali došao je iz zamjene /ActualText. Ne postoji jedinstveno obrnuto mapiranje iz zamijenjenog niza na izvorne bajtove, pa je lokacija samo dijagnostička
  • PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glif je unutar Form XObjecta. Bajtovima se može adresirati, ali Form može crtati više stranica, pa bi njegovo uređivanje kroz API više razine bilo uređivanje koje niste zatražili
  • PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand je bio heksadecimalni string s oznakom redoslijeda bajtova UTF-16BE, koji postojeći put izdvajanja dekodira prije mapiranja fonta. Pomaci u dekodiranom rezultatu više ne adresiraju izvorne bajtove, pa se valjana zastavica poništava
  • PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand i njegov operator prikaza teksta žive u dvama različitim tokovima

Ovaj posljednji slučaj zaslužuje vlastitu rečenicu jer inženjeri redovito pretpostave da se ne može dogoditi. ISO 32000-1 §7.8.2 kaže da su tokovi u polju stranice /Contents konkatenirani, a granica između njih mora pasti samo na leksičku granicu. Zato su BT /F1 16 Tf 220 340 Td (CrossLayer) u jednom toku i Tj ET u sljedećem potpuno legalna stranica. Mapiranje čuva dijagnostički položaj, ali ga označava samo za čitanje, jer indeks naredbe pripada drugom sloju od bajtova operanda, a korištenje jednoga za adresiranje drugoga oštetilo bi datoteku

Kako biblioteka zna da je mapa još valjana?

Otiscima, provjerenima neposredno prije pisanja. Svaki popis izdvajanja bilježi izvornu stranicu te za svaki sloj sadržaja duljinu sloja i dva neovisna klizna hasha: FNV-1a hash i XOR hash u stilu DJB2. Prije nego što ReplaceTextBlockCharSourceBytes išta raščlani, ponovno čita ciljni sloj i uspoređuje sve tri vrijednosti. Svaka promjena bajta bilo gdje u tom sloju vraća PDFLIB_ERROR_TEXT_LOCATION_STALE i pisanje se ne događa. To je namjerno konzervativno: provjera je po sloju, a ne po naredbi, pa nepovezana izmjena drugdje u istom toku sadržaja također poništava vašu lokaciju. To je ispravna razmjena: pomak u toku koji se pomaknuo i za jedan bajt nije skoro pogođen, nego je tiha korupcija. Ista disciplina upravlja ostatkom površine za uređivanje, uključujući praćenje stanja toka sadržaja za CTM i clipping. Nakon svake uspješne zamjene odbacite popis i izdvojite ponovno

Mapiranje samo za čitanje kroz Direct Access

DAGetTextBlockCharContentLocation daje identičan zapis za stranicu otvorenu kroz put Direct Access, uz identičan rječnik zastavica. Po konstrukciji je samo dijagnostički: ReplaceTextBlockCharSourceBytes radi nad odabranim dokumentom koji se može uređivati, a Direct Access je put za čitanje. Podaci lokacije preživljavaju u popisu blokova teksta nakon zatvaranja ručke datoteke, što ih čini upotrebljivima za izvanmrežnu reviziju

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 odgovaranje na pitanja, a ne za mijenjanje stvari. Koje stranice nose tekst koji nikad niste mogli urediti na mjestu? Koliki dio ovog korpusa stiže s preinakama /ActualText? Čiji izlaz razdvaja operatore preko slojeva sadržaja? To su jeftini upiti kada svaki znak ima adresu i vrijedi ih pokrenuti prije nego što se obvežete na cjevovod ispravaka

Gdje uređivanje pojedinačne točke staje

Uređivanje pojedinačne točke skalpel je, a ne tekstualni mehanizam. Mijenja bajtove na mjestu, pa zamjenski tekst širi ili uži od izvornog neće ponovno prelomiti tekst, neće ga prelomiti u novi red i neće ažurirati kernove oko njega. Zamjena jedne znamenke drugom u polju s monospaced fontom dobar je slučaj. Ponovno pisanje odlomka nije. I nipošto nije sigurnosni alat: prepisivanje bajtova glifova ostavlja izvorne bajtove dohvatljivima iz povijesti revizija datoteke, pa sve što zahtijeva povjerljivost pripada u pravu redakciju koja uklanja sadržaj umjesto da ga prekriva. Zauzvrat dobivate poštenje. Svaki znak ili ima bajtnu adresu na koju možete djelovati ili imenovanu zastavicu koja govori zašto je nema, a provjera otiska pretvara zastarjelu mapu u tvrdu pogrešku umjesto u oštećenu stranicu. Mapiranje znaka na bajt sadržaja i zamjena izvornih bajtova na mjestu isporučuju se kao dio površine za izdvajanje teksta i uređivanje sadržaja u PDF Library for Delphi, izvornoj Object Pascal PDF biblioteci za Delphi, C++Builder i Lazarus