Markierter Inhalt ist der Mechanismus, den ISO 32000-1 §14.6 zum Taggen von Seiteninhalt definiert, und sowohl getaggtes PDF als auch PDF/UA bauen auf ihm auf. PDFium Component legt ihn direkt frei: PageObjectMarks liest jedes BDC-Tag und seine Eigenschaftsliste von einem Seitenobjekt, AddPageObjectMark schreibt eines, RemovePageObjectMark löscht eines, und PageObjectMarkedContentID meldet die MCID, die Inhalt mit dem Strukturbaum verknüpft
Bis der Strukturbaum mit dem Inhalt, den er beschreibt, zusammengeführt werden kann, ist Barrierefreiheits-Werkzeug Raterei. Der Strukturbaum sagt „das ist eine Überschrift"; die MCID sagt, welche Markierungen auf welcher Seite diese Überschrift tatsächlich sind. Beide Hälften müssen lesbar sein, bevor eine Anwendung Tagging prüfen, reparieren oder darüber berichten kann
Was ist eine Markierung, in Bytes?
Ein BDC-Operator mit einem Tag-Namen und einer optionalen Eigenschaftsliste, geschlossen durch EMC. Im Content-Stream sieht das aus wie /P <</MCID 3>> BDC ... EMC: Das Tag /P benennt die Rolle, das Dictionary trägt Eigenschaften, und alles zwischen den Operatoren ist der markierte Inhalt. Ein Seitenobjekt innerhalb dieser Spanne trägt die Markierung, was PDFium zurückgibt und was PDFium Component in einen Datensatz verwandelt
TPdfContentMark hält ein Handle, den Tag-Name und ein Array von TPdfContentMarkParam. Jeder Parameter hat einen Key, eine Kind und ein bedeutungstragendes Wertfeld, das durch diese Kind ausgewählt wird: pmpInt, pmpFloat, pmpString oder pmpBlob. Die Kind kommt aus PDFiums eigener Typmeldung statt von dem Getter, der zufällig erfolgreich war, und das ist der Unterschied zwischen dem Lesen einer Eigenschaftsliste und dem Raten an einer
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;
Warum pmpUnknown zwei verschiedene Dinge bedeutet
pmpUnknown wird zurückgegeben, wenn PDFium FPDF_OBJECT_UNKNOWN meldet, und PDFium liefert das auch für einen Schlüssel, der nicht existiert. Die beiden Fälle lassen sich auf dieser Schicht nicht unterscheiden, und so zu tun, als wäre dem so, wäre schlimmer, als es auszusprechen
Die praktische Folge für den Code: pmpUnknown als „hier kein verwendbarer Wert" behandeln statt als einen Typ, den man vielleicht trotzdem dekodieren könnte. Wenn eine Eigenschaft für den Workflow zählt, sollte man verifizieren, dass sie mit einer Kind vorhanden ist, die man erkennt, und Abwesenheit nicht aus einem Unknown ableiten — eine Markierung, deren Eigenschaftsliste man nicht lesen kann, ist eine, über die man berichten sollte, nicht eine, die man stillschweigend akzeptiert
Ein Mark-Datensatz ist eine Momentaufnahme, kein selbst geowntes Handle
Das Feld Handle gehört der Bibliothek. Es veraltet im Moment, in dem die Markierung entfernt, das Seitenobjekt zerstört oder die Seite entladen wird, sodass der Datensatz eine nur-lesbare Momentaufnahme mit kurzem Leben ist. Wer ihn über einen Seitenwechsel hinweg cachet, hält einen Zeiger in Speicher, den die Engine bereits zurückgefordert hat
Das ist dieselbe Disziplin, die für Seitenobjekt-Handles in PDFium generell gilt, und sie erwischt Leute an derselben Stelle: ein Listen-Control, das mit Mark-Datensätzen gefüllt ist, ein Nutzer, der zu einer anderen Seite navigiert, und ein Absturz, der unrelated zur Navigation wirkt. Die Werte herauskopieren, die man braucht — den Namen, die Schlüssel, die Zahlen — und das Handle loslassen. Die Hinweise zu Seitenobjekt-Handles, die nach einer Transformation veralten behandeln die allgemeine Regel und wie sie woanders zubeißt
Eine Markierung hinzufügen, und der Speicher-Schritt, der leicht übersehen wird
AddPageObjectMark nimmt den Seitenobjekt-Index, einen Tag-Namen und einen vollständigen Parameter-Satz. Parameter werden als Menge geschrieben statt Schlüssel für Schlüssel gepatcht, weshalb TPdfContentMarkParam keine Has*-Sentinels hat — der Fall „ein Feld eines bestehenden Datensatzes aktualisieren", den diese bewachen würden, tritt nicht auf
Der Teil, den man offen aussprechen sollte: Eine Markierung hinzuzufügen baut den Seiten-Content-Stream neu auf, sodass das Tag einen Speichervorgang überlebt. Das musste explizit sein, weil SaveAs Inhalt nicht von selbst neu erzeugt — eine Änderung, die nur im Objekt-Modell lebte, würde verworfen, und die gespeicherte Datei sähe genauso aus wie die, mit der man begonnen hat. Wer jemals etwas zu einer PDFium-Seite hinzugefügt und in der Ausgabe vermisst hat, hat meist hier den Grund
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;
Was das einem Dokument antut und was nicht
Markierungen allein machen noch kein getaggtes PDF. Ein konformes getaggtes Dokument braucht einen Strukturbaum, dessen Elemente diese MCIDs referenzieren, einen /MarkInfo-Eintrag, der das Dokument als markiert deklariert, und Rollennamen, die das bedeuten, was der Standard sagt. Eine /P-Markierung mit einer MCID zu schreiben, auf die kein Strukturelement zeigt, liefert Inhalt, der behauptet getaggt zu sein, und einen Strukturbaum, der ihn nie erwähnt
Wo markierter Inhalt auf dieser Stufe seinen Wert wirklich verdient, ist Inspektion und Reparatur: prüfen, welche Seitenobjekte getaggt sind, Artefakte finden, die als solche hätten markiert werden sollen, oder MCIDs gegen einen Strukturbaum abgleichen, um die Waisen zu finden. Für die Strukturbaum-Hälfte dieser Arbeit siehe die Erläuterung zur PDF/UA-Strukturbaum-Validierung, und für die Lese-Erfahrung, für die die Tags letztlich sind, die Hinweise zum Bau eines barrierefreien PDF-Readers in Delphi
PDFium Component gibt Delphi-, C++Builder- und Lazarus-Anwendungen eine High-Level-VCL-API über die PDFium-Engine, mit markiertem Inhalt, Strukturbäumen und Barrierefreiheits-Validierung erreichbar aus gewöhnlichem Pascal-Code — siehe die PDFium-Component-Produktseite für die vollständige API-Oberfläche