Technisch artikel

PDF content streams veilig herschrijven in Delphi

HotPDF doorloopt elke operator in de content stream van een pagina en geeft elk ervan door aan uw code via VisitLoadedPageContentOperators. Voor elke operator kiest u een actie: behouden, verwijderen, vervangen door uw eigen bytes, de doorloop stoppen, of de hele bewerking afwijzen. Wanneer elke operator wordt behouden, is de herschreven stream byte-voor-byte identiek aan het origineel, wat de eigenschap is die het mechanisme veilig maakt om over documenten te draaien die u niet zelf hebt gemaakt

PDF-content-stream-operators zoals BT, Tf en TJ die naar de visitor-eventdata stromen met fase, operatornaam, aantal operanden, bytespan en nestingstatus, en vervolgens één van vijf acties teruggeven: behouden, vervangen, verwijderen, stoppen of afwijzen
Elke bezochte operator levert precies één beslissing op, en een doorloop die alles behoudt schrijft onaangeraakte bereiken terug voor byte-identieke output

Content-stream-bewerking is waar PDF-tools meestal fout gaan. Een stream is een reeks operators over een stackmachine, geen documentboom, en de naïeve aanpak om een reguliere expressie over de gedecodeerde bytes te laten lopen, breekt zodra een string-literal iets bevat dat op een operator lijkt, of een inline afbeelding willekeurige binaire data bevat. Een visitor die de stream al heeft getokeniseerd, heeft dat probleem niet

Wat kunt u eigenlijk doen met toegang op operatorniveau?

Drie taakfamilies. Auditing: tellen hoe vaak een pagina een kleur buiten een bekende set instelt, of tekst vinden die met een niet-ingesloten lettertype is getekend. Verwijdering: een watermerk verwijderen dat als gemarkeerde inhoud is getekend, of een specifieke afbeeldingsaanroep strippen. En normalisatie: één operatorvorm vervangen door een equivalente die een downstream-consument beter afhandelt

Elk van die deelt één vereiste: u moet de overige 99 procent van de stream ongemoeid kunnen laten. De syntaxisboom van HotPDF slaat één kopie van de bron plus compacte spans op, en bewerkingen zijn gesorteerde, niet-overlappende overlays. Het schrijven van het resultaat kopieert onaangeraakte bereiken rechtstreeks en geeft alleen vervangende bytes uit waar u iets hebt veranderd, zodat een no-op-rondgang geen drift kan introduceren

Het visitorcontract

Implementeer de interface of ken het event toe, roep dan de visit-methode aan met ApplyChanges ingesteld op false voor een audit of true voor een herschrijving. De eventdata draagt de fase, de operatornaam, het aantal operanden, de bytespan, de huidige nestingstatus en een geleende view om operanden te lezen:

uses
  HPDFDoc, HPDFContentStream;

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

  // Tel tekst-tonende operators die binnen gemarkeerde inhoud zijn getekend
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Lees een operand alleen wanneer u hem nodig hebt
  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);   // alleen audit
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Waarom is de view geleend in plaats van muteerbaar?

De callback ontvangt nooit een bewerkbare syntaxisboom. Ze ontvangt een view die op aanvraag de bytes van de huidige bewerking of een specifieke operand kan ophalen, en dat is alles. Elke verandering loopt in plaats daarvan via de actie- en vervangingsvelden

Die omweg is geen formaliteit. Directe boommutatie zou een visitor in staat stellen het vervangingsbudget en de controle op overlappende bewerkingen te omzeilen, en overlappende bewerkingen in een byte-span-overlaymodel produceren output die stilzwijgend fout is in plaats van luid kapot. Veranderingen via het actieveld leiden betekent dat elke bewerking op dezelfde manier wordt gevalideerd, ongeacht wat de visitor bedoelde

Op aanvraag lezen houdt het gewone geval ook goedkoop. Een audit die alleen operatornamen telt, materialiseert operandbytes helemaal nooit, wat er toe doet bij een content stream met honderdduizenden bewerkingen

Het statusveld beschrijft altijd de invoer

Elk event draagt de diepte van de grafische status, de diepte van het tekstobject, de diepte van gemarkeerde inhoud en de diepte van de compatibiliteitssectie zoals die stonden in de oorspronkelijke invoer onmiddellijk vóór het uitvoeren van die operator. Dat geldt zelfs als uw visitor eerdere operators heeft verwijderd of vervangen

Status verankeren aan de oorspronkelijke invoer in plaats van aan de bewerkte output is een bewuste keuze met een praktisch voordeel: dezelfde audit twee keer over hetzelfde document uitvoeren levert identieke resultaten op, en een audit die naast bewerkingen draait, verschuift zijn eigen referentiekader niet. Als u de diepte in de output moet weten, weet u wat u hebt verwijderd, dus kunt u die berekenen; het omgekeerde, stabiele invoerposities reconstrueren uit een bewegende output, is niet mogelijk

Verwijderen en vervangen, met de regels die u beschermen

Een vervanging bestaat uit rauwe content-stream-bytes, en u bent verantwoordelijk voor hun syntaxis. De waarborgen eromheen zijn het budget op totale vervangende bytes, de niet-overlappingsvereiste, en de optionele outputvalidatie die het resultaat opnieuw parseert voordat het wordt geaccepteerd:

Herschrijfwaarborgen in Delphi: een visitor die Action- en Replacement-intentie declareert, geleid via het vervangingsbudget, niet-overlappende bytespan en outputvalidatiewaarborgen naar de gevalideerde opgeslagen pagina
Elke verandering loopt via het actieveld naar de waarborgen voor budget, overlap en herparsen, zodat een mislukte validatie afbreekt in plaats van kapotte operators te schrijven
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Laat de operators vallen die het watermerkformulier tekenen
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Normaliseer een afwijkende kleur naar het huisgrijs
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Wijs de hele bewerking af als we iets tegenkomen dat we niet begrijpen
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // opnieuw parseren voordat het wordt geaccepteerd
  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;

De actieset bevat om een reden twee uitgangen. coaStop beëindigt de doorloop en behoudt wat tot dan toe is beslist, wat juist is wanneer u hebt gevonden wat u zocht. coaReject geeft de hele bewerking op en laat de pagina ongemoeid, wat juist is wanneer u een constructie tegenkomt die uw herschrijfregels niet dekken. Inline afbeeldingen zijn het klassieke voorbeeld: hun binaire payload is niet iets waar een kleurnormaliserende visitor omheen zou moeten bewerken

De statistieken lezen, en waar de doorloop stopt

Het statusveld onderscheidt voltooiing van stoppen, afwijzing, ongeldige invoer, een overschreden limiet, ongeldige output en een visitorfout. Dat zijn verschillende problemen: ongeldige output betekent dat uw vervangende bytes niet opnieuw parseerden, terwijl ongeldige invoer betekent dat de bronstream al misvormd was en er geen bewerking is geprobeerd

De tellers maken een audit zelfcontrolerend. VisitedOperatorCount tegenover KeptOperatorCount, DeletedOperatorCount en ReplacedOperatorCount moeten optellen, en de maximale dieptecijfers vertellen u of de pagina diep genoeg genest is om een nadere blik waard te zijn. SourceBytes tegenover OutputBytes geeft het grootte-effect van uw bewerking in één getal

Reconciliatie van auditstatistieken waarbij het aantal behouden, verwijderde en vervangen operators optelt tot het bezochte aantal, naast zeven statuseindes van voltooid tot visitorfout, met de opmerking dat statusdiepten de oorspronkelijke invoer beschrijven zodat audits identiek herhalen
Tellers die moeten optellen plus een status die elk mogelijk einde benoemt, maken een audit zelfcontrolerend op documenten die u niet zelf hebt gemaakt

Voor de gewone taken bestaan er routes op hoger niveau die geen visitor vereisen om te schrijven. Inhoud permanent verwijderen, met de resource-opruiming die echte redactie nodig heeft, wordt behandeld in redactie en het samenvoegen van pagina's op geladen documenten. Zichtbare tekst vervangen wordt behandeld in zoeken en vervangen in een bestaande PDF. Grijp naar de operatorvisitor wanneer uw regel structureel is in plaats van tekstueel, of wanneer u een audit nodig hebt die geen bestaand rapport beantwoordt

Content-stream-visiting, extractie en herschrijving draaien op hetzelfde geladen documentmodel in Delphi en C++Builder; de volledige functielijst staat op de HotPDF Delphi PDF component-pagina