Teknisk artikkel

Koble PDF-tekst til innholdsbyte i Delphi

Én feil tegn i et fakturanummer, og det eneste redigeringsprimitivet du har for hånden, skriver hele tekstløpet på nytt. PDF Library for Delphi lukker dette gapet: GetTextBlockCharContentLocation kobler hver uttrukket UTF-16-posisjon tilbake til content-stream-instruksjonen, operanden og det kodede byteområdet som produserte den, og ReplaceTextBlockCharSourceBytes overskriver bare dette området. Tekstuthenting kaster normalt bort alt du trenger til dette. Du får Unicode, bredder og geometri, mens opphavet forsvinner, så tegnet på posisjon 7 i blokk 3 bare er et tegn. Hvilken strøm produserte det, hvilken instruksjon, hvilken operand, hvilken byte inne i operanden: borte. Enhver punktredigeringsstrategi som bygges oppå det, må gjette, vanligvis ved å søke etter en dekodet delstreng i innholdet og håpe at den forekommer nøyaktig én gang. På en ekte side gjør den ikke det

Hvorfor ødelegger det siden å skrive hele tekstløpet på nytt?

Fordi løpet ikke bare er tekst. Tekstvisningsoperatorene i ISO 32000-1 §9.4.3 inkluderer TJ, der operanden er en matrise som fletter strenger sammen med numeriske justeringer, og tallene er satsen. En linje som er lagt ut som [(AB) -120 (CD)] TJ, har en 120-tusendels-em-kern mellom de to strengene. Sender du ut en ny Tj med den sammenkjedede teksten, er kerningen borte, linjen flyter litt ut og på et skjema flytter verdien seg ut av boksen. Den samme innvendingen gjelder fonten: Operand-byte-ene er koder i kodingen som Tf valgte, ikke Unicode, og i en sammensatt font kan de være to-byte-CID-er uten forhold til tegnet du leste ut. Regenererer du løpet, må du ha rett om fontens koding, /ToUnicode-kartet og glyph-dekningen. Punktredigering omgår alt dette ved aldri å forlate byte-domenet

Hva returnerer GetTextBlockCharContentLocation?

Metoden løser ett tegn til en record med ni felt, og hvert felt er en adresse, ikke en verdi. ContentLayer er den 1-baserte indeksen i sidens /Contents-matrise, eller 0 når tegnet kom fra nestet innhold. StreamObjectNumber og StreamGeneration identifiserer strømmen som inneholder det. InstructionIndex er den 0-baserte posisjonen i det dekodede innholdsprogrammet, OperandIndex er tekststreng-operanden og ArrayElementIndex er elementet inne i en TJ-matrise eller -1 for en direkte strengoperand. SourceByteOffset og SourceByteLength navngir deretter byteområdet inne i den dekodede strengen

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 og CharPos kommer fra din egen gjennomgang av GetTextBlockText
      If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
        ContentLayer, StreamObjectNumber, StreamGeneration,
        InstructionIndex, OperandIndex, ArrayElementIndex,
        SourceByteOffset, SourceByteLength, Flags)= 1 Then
      Begin
        // ContentLayer = 0 betyr at glyphen ligger i et nestet Form XObject
        // ArrayElementIndex = -1 betyr en vanlig Tj-operand, ikke en TJ-matrise
      End;
    Finally
      Lib.ReleaseTextBlocks(ListID);
    End;
  Finally
    Lib.Free;
  End;
End;

Oppslaget koster ingenting ved spørringstidspunktet. Mens rendereren dekoder hvert innholdslag, registrerer den de logiske spennene den går gjennom, så en posisjonsspørring er et binærsøk over en ordnet intervalliste, ikke en lineær skanning av alle innholdsspenn for hvert tegn. Ingenting parses på nytt når du spør; kartet ble bygget under uthentingspasset du allerede betalte for. Hvis du allerede teller treff med PDF-tekstsøk som returnerer treffkoordinater, er det nesten gratis å legge til en innholdsposisjon per treff

Rediger byte, ikke Unicode

ReplaceTextBlockCharSourceBytes tar en AnsiString med rå erstatningsbyte i den aktive PDF-fontkodingen. Det er hele designet, og det er med hensikt. Ingenting transkoder, ingenting omkoder og ingenting gjetter hvilken font som brukes. Biblioteket skjøter byte-ene dine over det navngitte området i målstrengen og skriver det inneholdende innholdslaget på nytt. Nabo-strenger i den samme TJ-matrisen og de numeriske kerningene mellom dem forblir byte-identiske. Ta layouten over: Å finne B i [(AB) -120 (CD)] TJ gir ArrayElementIndex 0, SourceByteOffset 1 og SourceByteLength 1. Bytt det ut med Z, så inneholder det utsendte innholdet (AZ), fortsatt fulgt av -120 og (CD), begge urørt. Regresjonstesten hevder nøyaktig dette, fordi "vi bevarte kerningen" er den typen påstand som stille slutter å være sann

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
    // Alle posisjonene i den gamle listen er nå foreldede. Hent ut på nytt.
    Lib.ReleaseTextBlocks(ListID);
    ListID:= Lib.ExtractPageTextBlocks(3);
  End
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
    // Laget ble endret under oss etter uthentingen
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
    // Et flagg vi ikke kontrollerte, eller et flagg som kom i en senere versjon
End;

To praktiske detaljer er verdt å ta inn over seg. Kallet bytter midlertidig til siden tekstlisten ble hentet fra og gjenoppretter den tidligere valgte siden både ved suksess og ved feil, så det flytter ikke markøren din i stillhet. Ved suksess tømmer det også øyeblikksbildene av sideelementene, noe som ugyldiggjør alle håndtak du holdt fra en tidligere opptellingsrunde

Hvilke tegn kan ikke redigeres?

Seks kategorier, og biblioteket navngir hver av dem i Flags-bitmasken i stedet for å feile vagt. Dette betyr mer enn happy path, fordi tilfellene uten mapping er vanlige i ekte dokumenter, og hvert av dem har sin egen grunn

  • PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: Flere uttrukne UTF-16-posisjoner utvides fra én kild glyph. En /ToUnicode-oppføring som mapper én kode til fi, gir deg to tegn som deler ett byteområde, så behandle dem som én kild glyph og rediger området én gang
  • PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: Tegnet ble syntetisert under layout. Implisitte ordmellomrom er det vanlige tilfellet, og de har ingen kildebyte i det hele tatt, så SourceByteOffset kommer tilbake som -1 og SourceByteLength som 0
  • PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: Teksten du leste, kom fra en /ActualText-erstatning. Det finnes ingen entydig omvendt mapping fra den erstattede strengen til kildebyte, så posisjonen er bare diagnostisk
  • PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: Glyphen ligger inne i et Form XObject. Byte-ene kan adresseres, men Form-en kan tegnes av flere sider, så redigering gjennom high-level API-et ville vært en redigering du ikke ba om
  • PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: Operanden var en hex-streng med et UTF-16BE-byteordremarkør, som den eksisterende uthentingsstien dekoder før fontmapping. Offset-er i det dekodede resultatet adresserer ikke lenger originalbyte-ene, så valid-flagget fjernes
  • PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: Strengoperanden og tekstvisningsoperatoren ligger i to forskjellige strømmer

Det siste punktet fortjener en egen setning, fordi ingeniører rutinemessig antar at det ikke kan skje. ISO 32000-1 §7.8.2 sier at strømmene i en sides /Contents-matrise settes sammen, og at skillet mellom dem bare må falle ved en leksikalsk grense. Derfor er BT /F1 16 Tf 220 340 Td (CrossLayer) i én strøm og Tj ET i den neste en helt lovlig side. Mappingen beholder den diagnostiske posisjonen, men merker den som skrivebeskyttet, fordi instruksjonsindeksen til operatoren tilhører et annet lag enn byte-ene til operanden, og bruk av den ene til å adressere den andre ville ødelagt filen

Hvordan vet biblioteket at kartet fortsatt er gyldig?

Fingeravtrykk, kontrollert umiddelbart før skriving. Hver uthentingsliste registrerer kildesiden og, for hvert innholdslag, laglengden og to uavhengige rullerende hasher: en FNV-1a-hash og en DJB2-lignende xor-hash. Før ReplaceTextBlockCharSourceBytes parser noe som helst, leser den mållaget på nytt og sammenligner alle tre verdiene. Enhver byteendring hvor som helst i laget returnerer PDFLIB_ERROR_TEXT_LOCATION_STALE, og skrivingen skjer ikke. Dette er med hensikt konservativt: Kontrollen er per lag, ikke per instruksjon, så en urelatert endring et annet sted i den samme content stream-en også ugyldiggjør posisjonen din. Det er riktig avveiing: Et offset inn i en strøm som har flyttet seg med bare én byte, er ikke nesten riktig, men stille korrupsjon. Den samme disiplinen styrer resten av redigeringsflaten, inkludert tilstandssporeren for CTM og clipping i content stream. Etter en vellykket erstatning skal listen forkastes og hentes ut på nytt

Skrivebeskyttet mapping gjennom Direct Access

DAGetTextBlockCharContentLocation gir deg den identiske record-en for en side som er åpnet gjennom Direct Access-stien, med det identiske flaggordforrådet. Den er bare diagnostisk, av konstruksjon: ReplaceTextBlockCharSourceBytes arbeider på det valgte redigerbare dokumentet, mens Direct Access er en lesesti. Posisjonsdataene overlever i tekstblokklisten etter at filhåndtaket er lukket, noe som gjør dem brukbare til offline-kontroll

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);
    // Posisjoner kan fortsatt leses etter DACloseFile
  Finally
    Lib.DAReleaseTextBlocks(DirectList);
  End;
Finally
  Lib.DACloseFile(FileHandle);
End;

Bruk det til å svare på spørsmål, ikke til å endre ting. Hvilke sider inneholder tekst du aldri kunne redigere på stedet? Hvor mye av korpuset kommer med /ActualText-overstyringer? Hvilken leverandørs output deler operatorer over flere innholdslag? Dette er billige spørringer når hvert tegn har en adresse, og de er verdt å kjøre før du binder deg til en korrigeringspipeline

Hvor punktredigering stopper

Punktredigering er en skalpell, ikke en tekstmotor. Den endrer byte på stedet, så erstatningstekst som er bredere eller smalere enn originalen, flyter ikke på nytt, brytes ikke på nytt og oppdaterer ikke kerningene rundt seg. Å bytte ett siffer med et annet i et felt med fast bredde passer godt. Å skrive et avsnitt på nytt gjør det ikke. Og det er bestemt ikke et sikkerhetsverktøy: Å overskrive glyph-byte lar originalbyte-ene være gjenopprettelige fra filens revisjonshistorikk, så alt med krav om konfidensialitet hører hjemme i ekte redaksjon som fjerner innholdet i stedet for å dekke det. Til gjengjeld får du ærlighet. Hvert tegn har enten en byteadresse du kan handle på eller et navngitt flagg som forteller hvorfor det ikke har det, og fingeravtrykkskontrollen gjør et foreldet kart til en hard feil i stedet for en ødelagt side. Kobling fra tegn til innholdsbyte og erstatning av kildebyte på stedet leveres som del av flaten for tekstuthenting og innholdsredigering i PDF Library for Delphi, det native Object Pascal PDF-biblioteket for Delphi, C++Builder og Lazarus