Teknisk artikel

Läsa och skriva PDF-markerat innehåll i Delphi

Markerat innehåll är den mekanism ISO 32000-1 §14.6 definierar för att tagga sidinnehåll, och både taggad PDF och PDF/UA är byggda på den. PDFium Component exponerar den direkt: PageObjectMarks läser varje BDC-tagg och dess egenskapslista från ett sidobjekt, AddPageObjectMark skriver en, RemovePageObjectMark raderar en, och PageObjectMarkedContentID rapporterar det MCID som länkar innehåll till strukturträdet

Fram till att strukturträdet kan fogas tillbaka till innehållet det beskriver är tillgänglighetsverktyg gissningar. Strukturträdet säger "detta är en rubrik"; MCID:t säger vilka märken på vilken sida den rubriken faktiskt är. Båda halvorna måste vara läsbara innan en applikation kan kontrollera, reparera eller rapportera om taggning

Vad är ett märke, i byte?

En BDC-operator med ett taggnamn och en valfri egenskapslista, stängd av EMC. I innehållsströmmen ser det ut som /P <</MCID 3>> BDC ... EMC: taggen /P namnger rollen, ordboken bär egenskaper, och allting mellan operatorerna är det markerade innehållet. Ett sidobjekt inuti det spannet bär märket, vilket är vad PDFium lämnar tillbaka och vad PDFium Component förvandlar till en post

TPdfContentMark håller en referens, taggens Name, och en array av TPdfContentMarkParam. Varje parameter har en Key, en Kind och ett meningsfullt värdefält valt av det slaget: pmpInt, pmpFloat, pmpString eller pmpBlob. Slaget kommer från PDFiums egen typrapport i stället för från vilken getter som råkade lyckas, vilket är skillnaden mellan att läsa en egenskapslista och att gissa på en

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Varför pmpUnknown betyder två olika saker

pmpUnknown returneras när PDFium rapporterar FPDF_OBJECT_UNKNOWN, och PDFium returnerar det även för en nyckel som inte existerar. De två fallen kan inte åtskiljas på detta lager, och att låtsas annorlunda skulle vara värre än att säga det

Den praktiska konsekvensen för din kod: behandla pmpUnknown som "inget användbart värde här" i stället för som en typ du ändå kanske kan avkoda. Om en egenskap betyder något för ditt arbetsflöde, verifiera att den finns med ett slag du känner igen, och härled inte frånvaro från ett okänt — ett märke vars egenskapslista du inte kan läsa är ett märke du bör rapportera om, inte ett du tyst bör acceptera

En märkpost är en ögonblicksbild inte en referens du äger

Handle-fältet tillhör biblioteket. Det blir inaktuellt i det ögonblick märket tas bort, sidobjektet förstörs eller sidan urladdas, så posten är en skrivskyddad ögonblicksbild med kort liv. Cacha den över ett sidbyte och du håller en pekare in i minne motorn har återtagit

Detta är samma disciplin som gäller för sidobjektsreferenser generellt i PDFium, och det fångar folk på samma plats: en listkontroll befolkad med märkposter, en användare som navigerar till en annan sida, och en krasch som ser orelaterad ut till navigeringen. Kopiera ut de värden du behöver — namnet, nycklarna, numren — och släpp referensen. Noterna om sidobjektsreferenser som blir inaktuella efter en transform täcker den allmänna regeln och hur den biter annorstädes

Att lägga till ett märke och det sparsteg som är lätt att missa

AddPageObjectMark tar sidobjektindexet, ett taggnamn och en komplett parametermängd. Parametrar skrivs som en mängd i stället för att patchas en nyckel i taget, vilket är varför TPdfContentMarkParam inte har några Has*-sentinelvärden — fallet "uppdatera ett fält av en befintlig post" dessa skulle vakta uppstår inte

Den del som är värd att uttala explicit: att lägga till ett märke bygger om sidinnehållsströmmen så att taggen överlever ett sparande. Detta var tvungen att vara explicit eftersom SaveAs inte regenererar innehåll på egen hand — en ändring som enbart levde i objektmodellen skulle förkastas, och den sparade filen skulle se exakt ut som den du startade med. Om du någonsin har lagt till någonting till en PDFium-sida och funnit det saknas i utdata, är detta oftast varför

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Vad detta gör och inte gör ett dokument till

Märken ensamma gör inte en taggad PDF. Ett överensstämmande taggat dokument behöver ett strukturträd vars element refererar dessa MCID, en /MarkInfo-post som deklarerar dokumentet markerat, och rollnamn som betyder vad standarden säger att de betyder. Att skriva ett /P-märke med ett MCID inget strukturelement pekar på ger dig innehåll som påstår sig vara taggat och ett strukturträd som aldrig nämner det

Där markerat innehåll genuint tjänar sin plats på denna nivå är inspektion och reparation: granska vilka sidobjekt som är taggade, hitta artefakter som borde ha markerats som sådana, eller matcha MCID mot ett strukturträd för att hitta de föräldralösa. För strukturträdshalvan av det arbetet, se genomgången av PDF/UA-strukturträdvalidering, och för den läsupplevelse taggarna i slutändan är till för, noterna om att bygga en tillgänglig PDF-läsare i Delphi

PDFium Component ger Delphi-, C++Builder- och Lazarus-applikationer ett hög-nivå VCL-API över PDFium-motorn, med markerat innehåll, strukturträd och tillgänglighetsvalidering nåbara från vanlig Pascal-kod — se PDFium Component-produktsidan för hela API-ytan