Teknisk artikel

PDF/VCR-1-mallplatshållare i Delphi med PDFlibPas

PDFlibPas implementerar PDF/VCR-1 som en tagged structure-profil: ersättningsdefinitionen bor under StructTreeRoot som en direkt barn-gren, där en mallrot bär /O /GTS_Template för att deklarera fältuppsättningen och varje platshållare bär /O /GTS_Replacement för att binda ett datafält. Ingenting i det hela är en dictionary på sidnivå

Den enda meningen är den del de flesta implementationer får fel, och att få den fel kostar mer än en misslyckad validering. Ett variable-data-jobb vars ersättningsplan missläses kraschar inte. Det trycker, i volym, med fel fält i fel ruta, och den första som märker det är kunden som håller femtiotusen utskick

Var ersättningsdefinitionen faktiskt bor

PDF/VCR-1 lägger sin ersättningsmodell i den logiska strukturhierarkin som beskrivs i ISO 32000-1 §14.7, inte i en privat sid-dictionary. Mallroten är ett strukturelement som hänger direkt i StructTreeRoot, utmärkt av en attribute owner på GTS_Template, och den deklarerar hela fältvokabulären på ett ställe genom GTS_Fields plus en valfri GTS_Pages som namnger fältet som styr sidvalet

Platshållare är strukturelement nästlade inuti den roten, med attribute owner GTS_Replacement. Var och en bär GTS_Generator (som PDFlibPas kräver ska vara PassThrough), ett GTS_Data-värde som namnger exakt ett deklarerat fält och en valfri GTS_BBox. Bindningen till sidan är inte heller en sidannotation: elementet pekar på sin sida genom /Pg och på marked-content-sekvensen genom /K. Att välja struktur framför en sid-dictionary bär sedan en konsekvens som är lätt att uttala och lätt att underskatta: strukturträdet är en allmän graf som nås från katalogen, så allt som konsumerar profilen måste vara en trädvandrare med alla risker det innebär, snarare än en dictionary-läsare. Har du inte arbetat med strukturträdet förut är mekaniken utlagd i tagged PDF-struktur och tillgänglighet

Var PDFlibPas lägger PDF/VCR-1-ersättningsmodellen: mallroten hänger direkt i StructTreeRoot med en attribute owner på GTS_Template som deklarerar GTS_Fields och den valfria GTS_Pages, medan varje platshållare nästlad inuti bär GTS_Replacement, en GTS_Generator på PassThrough, ett GTS_Data-fält, en valfri GTS_BBox och pekar på sin sida genom /Pg och sitt innehåll genom /K
Ersättningsdefinitionen bor i den logiska strukturhierarkin snarare än i en sid-dictionary, vilket gör varje konsument till en trädvandrare och förvandlar elementet till en grafnod en angripare kan forma

Varför släpper kontroll av bara den första toppnivågrenen igenom dåliga filer?

Därför att StructTreeRoot kan ha många direkta barn, och en validator som stannar efter att ha lokaliserat mallroten ser aldrig resten av dem. Det är det specifika misslyckandet PDFlibPas är byggt för att undvika, och det producerar exakt den typ av falskt godkännande som är värre än ingen kontroll alls: en fil som rapporterar konform och innehåller en andra mallrot, eller en platshållare som sitter utanför mallgrenen där ingen konsument kommer att hedra den

Så ValidatePDFVCRStructure i PDFlibPas kör två pass över samma träd. Det första passet räknar upp varje direkt barn av StructTreeRoot och räknar de som bär GTS_Template; allt annat än exakt ett vägras innan en enda platshållare läses. Det andra passet går ner i alla toppnivågrenar, inte bara mallgrenen, och avvisar två saker samtidigt: ett nästlat element som bär GTS_Template och inte är roten, och varje GTS_Replacement-element som hittas utanför mallgrenen

Vandringen begränsas till 64 nivåer djup och 1 000 000 noder, och den registrerar objekt- och generationnumret för varje element den besöker så att en cykel eller en upprepad referens rapporteras i stället för att följas. De talen är inte prestandajustering. Ett strukturträd är en objektsgraf en angripare kontrollerar, och en validator som rekurserar utan tak är en stack overflow med en filändelse

Tvåpassvalidering av ValidatePDFVCRStructure i PDFlibPas: pass ett räknar de direkta barnen av StructTreeRoot som bär GTS_Template och vägrar allt annat än exakt ett, pass två går ner i varje toppnivågren för att avvisa en nästlad mallrot och varje GTS_Replacement utanför mallgrenen, begränsat till 64 nivåer och en miljon noder
En validator som stannar efter att ha lokaliserat den första mallroten ser aldrig en andra, så vandringen täcker alla toppnivågrenar och registrerar objektnummer så att en cykel rapporteras i stället för att följas

Att skapa en mall, och de begränsningar API:et vägrar böja sig för

PDFlibPas exponerar mallskapande genom ett bracketing-API som upprätthåller strukturreglerna vid skrivtillfället i stället för att upptäcka dem vid spartillfället. SetPDFVCRMode växlar dokumentet in i profilen, vilket internt kräver PDF/X-4-basläget i ISO 15930-7 och slår på mark-info-flaggan, för en PDF/VCR-1-fil utan marked content har ingenting för en platshållare att peka på. Därifrån tar BeginPDFVCRTemplate emot fältlistan och sidvalsfältet, och den är strikt på sätt värda att känna till innan du designar ditt dataschema. Fältnamn går genom PDFVCRValidFieldName, som bara accepterar ASCII-bokstäver, siffror, understreck, bindestreck och punkt, för det är de tecken som serialiseras till ett PDF-namn utan escapetvetydighet. PDFVCRNormalizeFieldList håller listan skiftlägeskänslig och avvisar dubbletter direkt, och sidvalsfältet måste självt finnas med i den deklarerade listan

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;

    if Lib.SetPDFVCRMode(1) <> 1 then
      raise Exception.Create('PDF/VCR mode was refused');

    Lib.NewPage;
    if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
                               'AccountNo') <> 1 then
      raise Exception.Create('Field list rejected');

    // En platshållare binder exakt ett deklarerat fält.
    Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
    Lib.DrawText(72, 694, 'SAMPLE NAME');
    Lib.EndPDFVCRPlaceholder;

    Lib.EndPDFVCRTemplate;

    Diag := Lib.GetPDFVCRDiagnostics;
    if Diag <> '' then
    begin
      Writeln('PDF/VCR blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('campaign-template.pdf');
  finally
    Lib.Free;
  end;
end;

Notera vad bracketingen köper dig. BeginPDFVCRPlaceholder vägrar att öppna om inte en mall är aktiv och taggstacken är exakt en nivå djup, vilket är hur PDFlibPas garanterar att platshållare är direkta barn av mallroten och aldrig nästlar inuti varandra. Koordinater valideras också, med avvisning av NaN, oändlighet och inverterade rektanglar, så en dålig bounding box misslyckas vid anropet snarare än i en preflight-rapport en vecka senare

Varför behöver en platshållare sin egen MCID-serialiseringsväg?

Därför att PDF/VCR-1 vill att /K ska vara ett naket heltal, och det är inte hur en allmän tagged-PDF-skrivare serialiserar länken mellan ett strukturelement och dess innehåll. Enligt ISO 32000-1 §14.6 refererar ett strukturelement normalt marked content antingen genom ett direkt MCID-heltal, när elementet redan identifierar sin sida genom /Pg, eller genom en marked-content-referensdictionary som namnger sidan explicit. Ett bibliotek som sänder ut tagged utdata för många ändamål tenderar att standardisera på dictionary-formen eftersom den komponerar bättre över sidor

PDFlibPas tog det smala alternativet här snarare än det allmänna. SetCurrentStructElemDirectMCIDKid flaggar det aktuella strukturelementet, och bara element som bär den flaggan serialiserar /K som ett direkt MCID med sidan buren på /Pg; allt annat lämnar den befintliga MCR-dictionary-vägen orörd. Det är en medveten avvägning och den är av det slag som är värd att uttala högt: att lägga till en profil ska inte ändra byten i utdata som varje annat tagged dokument redan producerar, så profilen får en egen väg i stället för en global switch. Varje befintlig tagged fil behåller sin tidigare serialisering, och regressionsytan förblir storleken av den nya funktionen

En vandring, två konsumenter

PDFlibPas härleder ersättningsmanifestet och konformitetsutlåtandet ur samma tvåpass-vandring, vilket är varför manifestet kan litas på som jobbplan. Vandringen fyller en TPDFVCRManifestData-post medan den validerar: Fields från mallroten, PageSelectionField när ett sådant deklareras, och en Placeholders-array av TPDFVCRPlaceholderInfo-poster som bär FieldName, PageNumber, MCID, HasBBox och BBox. Konformitetsingångspunkten är CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), och den kör sina kontroller i beroendeordning snarare än alla på en gång: den fullständiga PDF/X-kontrollen först, sedan GTS_PDFXVersion mot PDF/X-4, sedan GTS_PDFVCRVersion läst från namnrymden http://www.npes.org/pdfvcr/ns/id/, och först därefter strukturvandringen. Att passera Options som 1 får den att stanna vid första problemet, vilket är vad du vill ha i en batch-grind; att passera 0 samlar in allt, vilket är vad du vill ha i en rapport

En vandring, två konsumenter i PDFlibPas: samma begränsade vandring fyller TPDFVCRManifestData med Fields, PageSelectionField och en Placeholders-array av TPDFVCRPlaceholderInfo-poster, medan CheckCompliancePDFVCR kör den fullständiga PDF/X-kontrollen, GTS_PDFXVersion, GTS_PDFVCRVersion från npes.org-namnrymden och först därefter strukturvandringen, och returnerar 100 bara när filen kvalificerar
Manifestet är ett kontrakt, så BuildPDFVCRManifest returnerar JSON bara när problemlistan kommer tillbaka tom och rapporterar fel 608 annars, vilket hindrar en icke-konform fil från att utge sig för att vara en pålitlig jobbplan
// Verifiera en mall du fått snarare än producerat
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('supplier-template.pdf',
                               fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/VCR: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

Manifestet är ett kontrakt, så det finns bara för konforma filer

BuildPDFVCRManifest returnerar JSON bara när problemlistan kommer tillbaka tom, och returnerar en tom sträng annars. Det är inte defensiv kodning för dess egen skull. Ett manifest konsumeras av en kompositionsmotor som ska driva riktig utdata från det, och att lämna tillbaka en partiell fältlista extraherad ur en fil med två mallrötter inbjuder motorn att behandla en icke-konform struktur som en pålitlig ersättningsplan. Genom dokument-API:et sveper GetPDFVCRManifest(InputFileName, Password) samma anrop och sätter den senaste felkoden till PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) när filen inte kvalificerar, så en misslyckad extrahering och en tom mall går att skilja åt. JSON:en rapporterar fields, pageSelectionField som en sträng eller null, och en placeholders-array vars poster ger field, page, mcid och antingen en fyrsiffrig bbox eller null

var
  Lib: TPDFlib;
  Manifest: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
    if Manifest = '' then
      // 608 betyder att filen inte är PDF/VCR-1-konform,
      // 401 betyder att filen inte kunde öppnas
      Writeln('No manifest, error ', Lib.LastErrorCode)
    else
      Writeln(Manifest);
  finally
    Lib.Free;
  end;
end;

En detalj att ta med in i din konsument: värdet page följer bibliotekets nollbaserade sidkonvention, inte den ettbaserade numrering en människa ser i en viewer. Att blanda ihop dem är en tyst off-by-one som placerar innehåll på fel ark, och den fångas inte av någon validator eftersom båda talen är strukturellt giltiga

Var gränserna går

PDF/VCR-1-stödet i PDFlibPas täcker mallskapande, spargrinden, fristående preflight och manifestextrahering. Det utför inte ersättningen. Att substituera data in i platshållare och producera de personaliserade instanserna är kompositionssteget, och det bor utanför biblioteket medvetet, för manifestet är precis det gränssnitt som låter en kompositionsmotor skrivas oberoende. Spargrinden vägrar också snarare än reparerar. Till skillnad från profiler där en felaktig annoteringsflagga kan normaliseras tyst, är varje PDF/VCR-1-begränsning som PDFVCRReadyForSave upprätthåller en där gissande skulle ändra vad dokumentet betyder: att uppfinna en mallrot, eller att välja vilket fält en obunden platshållare tillhör, producerar en fil som validerar och ljuger. Samma reparera-versus-vägra-gräns diskuteras för en annan profil i PDF/E-1-authorläget och begränsad preflight, och arbetsflödet för att köra flera konformitetskontroller över ett dokument täcks i konformitets- och signeringsarbetsbänken

Mallskapande, den begränsade strukturvandringen, spargrinden och manifestextraheringen levereras alla i PDFlibPas Delphi PDF-biblioteket, vilket betyder att en mall kan produceras under profilen och sedan verifieras genom en separat kodväg som aldrig såg skrivartillståndet, och den separationen är det enda som gör ett konformitetspåstående värt något