Teknisk artikel

Skriv om PDF-innehållsströmmar säkert i Delphi

HotPDF går igenom varje operator i en sidas innehållsström och lämnar över var och en till din kod via VisitLoadedPageContentOperators. För varje operator väljer du en åtgärd: behåll den, ta bort den, ersätt den med dina egna byte, avbryt genomgången, eller avvisa hela redigeringen. När varje operator behålls är den omskrivna strömmen byte-för-byte identisk med originalet, vilket är egenskapen som gör mekanismen säker att köra över dokument du inte skapat

Redigering av innehållsströmmar är där PDF-verktyg vanligtvis går fel. En ström är en sekvens av operatorer över en stackmaskin, inte ett dokumentträd, och den naiva metoden att köra ett reguljärt uttryck över de avkodade byten går sönder så fort en stränglitteral innehåller något som liknar en operator, eller en inline-bild innehåller godtycklig binärdata. En besökare som redan har tokeniserat strömmen har inte det problemet

PDF-innehållsströmsoperatorer som BT, Tf och TJ flödar in i besökarens händelsedata med fas, operatornamn, operandantal, byteintervall och nästlingstillstånd, och returnerar sedan en av fem åtgärder: behåll, ersätt, ta bort, stoppa eller avvisa
Varje besökt operator ger exakt ett beslut, och en genomgång som behåller allt skriver tillbaka orörda intervall för byte-identisk utdata

Vad kan du egentligen göra med åtkomst på operatornivå?

Tre familjer av uppgifter. Granskning: räkna hur många gånger en sida ställer in en färg utanför en känd uppsättning, eller hitta text ritad med ett typsnitt som inte är inbäddat. Borttagning: ta bort en vattenstämpel som ritades som markerat innehåll, eller ta bort ett specifikt bildanrop. Och normalisering: ersätt en operatorform med en likvärdig som en nedströms konsument hanterar bättre

Var och en av dessa delar ett krav: du måste kunna lämna de andra 99 procenten av strömmen orörda. HotPDF:s syntaxträd lagrar en kopia av källan plus kompakta intervall, och redigeringar är sorterade icke-överlappande överlägg. Att skriva resultatet kopierar orörda intervall direkt och avger endast ersättningsbyte där du ändrat något, så en no-op-rundtur kan inte introducera avvikelser

Besökarkontraktet

Implementera gränssnittet eller tilldela händelsen, anropa sedan besöksmetoden med ApplyChanges satt till false för en granskning eller true för en omskrivning. Händelsedatan bär fasen, operatornamnet, dess operandantal, dess byteintervall, det aktuella nästlingstillståndet och en lånad vy för att läsa operander:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Räkna textvisande operatorer ritade inuti markerat innehåll
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Läs en operand bara när du behöver den
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // enbart granskning
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Varför är vyn lånad istället för muterbar?

Callbacken tar aldrig emot ett redigerbart syntaxträd. Den tar emot en vy som kan hämta den aktuella operationens byte eller en specifik operand på begäran, och det är allt. Varje ändring går genom fälten action och replacement istället

Den indirektionen är ingen formalitet. Direkt trädmutation skulle låta en besökare kringgå ersättningsbudgeten och kontrollen för överlappande redigeringar, och överlappande redigeringar i en byteintervallöverlägg-modell ger utdata som är tyst fel snarare än högljutt trasig. Att kanalisera ändringar genom action-fältet betyder att varje redigering valideras på samma sätt oavsett vad besökaren avsåg

Att läsa på begäran håller också det vanliga fallet billigt. En granskning som bara räknar operatornamn materialiserar aldrig operandbyte alls, vilket spelar roll på en innehållsström med hundratusentals operationer

State-fältet beskriver alltid indata

Varje händelse bär grafiktillståndsdjupet, textobjektsdjupet, det markerade innehållsdjupet och kompatibilitetssektionsdjupet som de stod i den ursprungliga indatan omedelbart innan den operatorn kördes. Det gäller även om din besökare tagit bort eller ersatt tidigare operatorer

Att förankra tillståndet till den ursprungliga indatan snarare än till den redigerade utdatan är ett medvetet val med en praktisk vinst: att köra samma granskning två gånger över samma dokument ger identiska resultat, och en granskning som körs jämsides med redigeringar förskjuter inte sin egen referensram. Om du behöver veta djupet i utdatan vet du vad du tagit bort, så du kan beräkna det; det omvända, att rekonstruera stabila indatapositioner från en rörlig utdata, är inte möjligt

Att ta bort och ersätta, med reglerna som skyddar dig

En ersättning är rå innehållsströmsbyte, och du ansvarar för deras syntax. Skyddsmekanismerna kring dem är budgeten för totala ersättningsbyte, kravet på icke-överlappning, och den valfria utdatavalideringen som tolkar om resultatet innan det accepteras:

Skyddsmekanismer för omskrivning i Delphi: en besökare som deklarerar avsikt via Action och Replacement dirigeras genom ersättningsbudgeten, kontrollen för icke-överlappande byteintervall och utdatavalideringsskydden till den validerade sparade sidan
Varje ändring kanaliseras genom action-fältet till budget-, överlapp- och omtolkningsskydden, så en misslyckad validering avbryter istället för att skriva trasiga operatorer
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Ta bort operatorerna som ritar vattenstämpelformuläret
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normalisera en avvikande färg till husets grå
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Avvisa hela redigeringen om vi stöter på något vi inte förstår
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // tolka om innan det accepteras
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

Åtgärdsuppsättningen innehåller två utgångar av en anledning. coaStop avslutar genomgången och behåller vad som beslutats hittills, vilket är rätt när du hittat det du letade efter. coaReject överger hela besöket och lämnar sidan orörd, vilket är rätt när du stöter på en konstruktion dina omskrivningsregler inte täcker. Inline-bilder är det klassiska exemplet: deras binära last är inte något en färgnormaliserande besökare bör redigera runt

Att läsa statistiken, och var genomgången stannar

Statusfältet skiljer mellan slutförande, avbrytning, avvisning, ogiltig indata, en överskriden gräns, ogiltig utdata och ett besökarfel. Det är olika problem: ogiltig utdata betyder att dina ersättningsbyte inte gick att tolka om, medan ogiltig indata betyder att källströmmen redan var felformad och ingen redigering försöktes

Räknarna gör en granskning självkontrollerande. VisitedOperatorCount mot KeptOperatorCount, DeletedOperatorCount och ReplacedOperatorCount måste summeras korrekt, och de maximala djupvärdena talar om huruvida sidan nästlar tillräckligt djupt för att vara värd en närmare titt. SourceBytes mot OutputBytes ger storlekseffekten av din redigering i en enda siffra

Avstämning av granskningsstatistik där antalet behållna, borttagna och ersatta operatorer summerar till det besökta antalet, bredvid sju statusutgångar från slutfört till besökarfel, ovanpå en notering om att tillståndsdjupen beskriver den ursprungliga indatan så att granskningar upprepas identiskt
Räknare som måste summeras korrekt plus en status som namnger varje möjlig utgång gör en granskning självkontrollerande på dokument du inte skapat

För de vanliga uppgifterna finns det routes på högre nivå som inte kräver att skriva en besökare alls. Att ta bort innehåll permanent, med den resursuppstädning riktig redigering behöver, täcks i redigering och sidsammanfogning på inlästa dokument. Att ersätta synlig text täcks i sök och ersätt text i en befintlig PDF. Ta till operatorbesökaren när din regel är strukturell snarare än textmässig, eller när du behöver en granskning ingen befintlig rapport besvarar

Genomgång, extraktion och omskrivning av innehållsströmmar körs mot samma inlästa dokumentmodell i Delphi och C++Builder; den fullständiga funktionslistan finns på HotPDF Delphi PDF-komponentsidan