PDFlibPas (PDF Library for Delphi) tjekker hvert objekt mod en tabel af PDF-version-regler, før den skriver en fil, og indtil for nylig forvekslede det PDF-version-preflight almindelige CAD-målings-dictionaries med geospatiale. En CAD-tegning på én side loadede fint, og så returnerede SaveToFile 0 med LastErrorCode 602 og krævede 1.7 ExtensionLevel 3. De korrigerede regler behandler rectilinear /Measure-dictionaries (/Subtype /RL) som almindelig PDF 1.6 og reserverer extension-gaten til rigtige geospatiale markører
Filen kom ind gennem corpus-admission: én side, én optional-content-gruppe, to rectilinear målings-viewports — den slags output, en arkitektur-CAD-pakke skriver, så en viewer kan aflæse afstande fra en etageplan. Intet ved den var eksotisk, og det er præcis derfor, afvisningen betød noget. Et preflight, der blokerer en gyldig fil, er værre end et langsomt, for calleren får en autoritetsudstrålende diagnose, der peger på en feature, dokumentet ikke indeholder. Fixet bestod af to dele: spec-læsningen bag én regel, og erkendelsen af, at reglen ikke kunne skelne de to dictionary-typer på det niveau, den kiggede
Hvordan fungerer PDFlibPas' save-time version preflight?
Save-gaten, PrepareAndCheckSaveVersion, sammenligner hvert indirekte objekt mod PDFFeatureRules og fejler ved den første regel, der både matcher og kræver mere, end target tillader. Target er dokumentversionen (eller den version, LockSaveVersion har fastlåst), plus det Adobe-extension-level, der er deklareret under /Extensions /ADBE. Hver TPDFFeatureRule-record bærer en MinVersion, et MinExtensionLevel, en MatchKind som fmkDictKey eller fmkDictSubtype, en Match-streng, et menneskelæsbart Feature-navn og en valgfri callback. AddRule registrerer en almindelig versionsregel; AddExtensionRule fastlåser altid MinVersion på 17 og lægger et extension level ovenpå, så en extension-regel kun nogensinde kan opfyldes af PDF 1.7 plus den rigtige /Extensions-entry. Når gaten udløses, gemmes den krævede version og feature-navnet til calleren, og GetInformation-nøglerne 311, 312 og 313 eksponerer dem
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311: krævet version, 312: featuren, der udløste den,
// 313: version, som save-target er låst på ('' når ulåst)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Hvorfor fejlede en almindelig CAD-tegning med fejl 602?
Regeltabellen indeholdt AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), som udløstes af enhver dictionary, der blot havde en /Measure-nøgle, og hver målings-viewport har én. Sidens /VP-array indeholder viewport-dictionaries, hver viewport peger på sit measure dictionary gennem /Measure, og nøgle-tilstedeværelses-matchet stoppede der uden at kigge på, hvad measure dictionaryen faktisk var. Feature-scanningen ved load kunne derefter hæve dokumentversionsnummeret til 1.7, men den skriver aldrig en /Extensions-deklaration på vegne af en inputfil, så save-gaten så PDF 1.7 på extension level 0 og rapporterede 1.7 ExtensionLevel 3. Den nægtelse af at finde på en extension-deklaration er bevidst: biblioteket promoverer ikke i stilhed en inputfil for at overmale en regel, der er forkert
Specifikationen er entydig om det rectilinear-tilfælde. Measure-dictionaries kom med PDF 1.6, og ISO 32000-1 §12.9 giver /Subtype en default på RL, et rectilinear koordinatsystem beskrevet af sit eget sæt entries: skalaforhold, X- og Y-talformater, distance og areal. Geospatiel måling er den senere tilføjelse fra Adobe Extension Level 3 oven på PDF 1.7, identificeret ved /Subtype /GEO og med geografiske point-arrays, koordinatsystem-dictionaries og display-enheder — de strukturer, der gennemgås i at læse GeoPDF-viewports og GPTS- og LPTS-arrays i Delphi. Begge dictionaries hænger på samme /Measure-nøgle, så enhver regel, der stopper ved nøglen, kan ikke være rigtig for begge. Den skelnen information ligger ét niveau nede, i measure dictionaryen selv
Hvad håndhæver det korrigerede regelsæt stadig?
Fixet sletter den betingelsesløse nøgleregel og efterlader gaterne, der beskriver rigtige versionskrav. En side med /VP eller /UserUnit kræver stadig PDF 1.6 gennem CB_PagePDF16Entries, en /PtData-nøgle kræver stadig extension level 3, og CB_GeospatialDictionary afgør, om et measure dictionary er geospatiel ud fra dets indhold snarere end nøglen, der førte til det
// Fjernet: hver dictionary med en /Measure-nøgle blev talt som geospatial
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);
AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);
function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
Dict: TPDFDictionary;
begin
Result := False;
if not (Obj is TPDFDictionary) then
Exit;
Dict := TPDFDictionary(Obj);
Result := (Dict.StringValue('Subtype') = 'GEO') or
(Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
(Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
(Dict.FindIndexByKeyName('PDU') >= 0);
end;
De delte Delphi- og FPC-regressioner fastlægger den grænse fra begge sider. En viewport, hvis measure dictionary udelader /Subtype, og en, der staver /RL ud, består begge ved PDF 1.6, samme side afvises stadig ved PDF 1.5, og feature-detektion rapporterer ikke længere en extension for den. Tilføjes der en /GPTS-array, vender dommen tilbage til 1.7 ExtensionLevel 3, som består, så snart extension level er deklareret, og en bar /Subtype /GEO-dictionary afvises uden den. Callbacken er konservativ af design: et rectilinear dictionary, der også bærer en løs /GCS- eller /PDU-nøgle, behandles som geospatial, for de nøgler har ingen betydning i RL-modellen
LockSaveVersion er stedet, hvor denne ændring bliver synlig for calleren. TPDFlib.LockSaveVersion accepterer '1.0' til '1.7', returnerer 0 for alt andet, fastlåser dokumentversionen og stopper writer-side-kald i at hæve den i stilhed, men save-gaten kører stadig mod den låste værdi. Med de korrigerede regler gemmer en CAD-fil låst på 1.6 rent. En ægte GeoPDF låst på 1.6 får stadig 602, hvilket er det korrekte svar, og de geospatiale authoring-kald som SetMeasureDictCoordinateSystem deklarerer selv extension level 3, når du bygger det indhold gennem API'en
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Rigtigt indhold over 1.6, for eksempel et GEO measure dictionary
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Hvorfor var versionsregel-scannen langsommere, end den behøvede?
Scannen kopierede hver TPDFFeatureRule ind i en lokal record, før den testede den, og fordi recorden holder to AnsiString-felter, justerede hver kopi to reference counts og frigav de forrige værdier. Preflightet besøger hver node i hvert objekttræ, skalarer inkluderet, så den omkostning ganged objektantal med regelantal, og regler, der slet ikke gjaldt target-versionen, blev kopieret først og sprunget over bagefter. Da PDFFeatureRules fyldes én gang ved unit-initialisering og behandles som read-only, giver v3.539.17 tabel-entries videre direkte til MatchSingleRule og RuleExceedsTarget, hvis const Rule-parametre tager en reference uden at røre strengene
// Før: en managed record-kopi pr. regel, pr. besøgt objekt
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Efter: const-parametre læser den immutable tabel-entry in place
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
FeatureName := PDFFeatureRules[X].Feature;
Result := False;
Exit;
end;
Den målte effekt er snæver og bør citeres sådan. Benchmarket tjekker et array af 20.000 numeriske objekter mod et PDF 1.4-target ti gange pr. runde; bygget med FPC Win64 ved -O2 faldt medianen af fem runder fra 0,711 s til 0,203 s, og at køre de to builds i omvendt rækkefølge gav 0,459 s mod 0,150 s. Det er omtrent en 3x gevinst på rule-matching-stien alene. En ægte gemning betaler også for udsat feature-detektion, objekt-dekodning og serialisering, så forholdet overføres ikke til den samlede save-tid. Regelrækkefølge, callbacks, versionsgrænser og første-fejl-diagnosen er uændret, og ingen regel blev cachet på tværs af gemninger eller sprunget over for at komme dertil
Hvad skal du tjekke, når en loadet PDF fejler version-preflightet?
Læs nøglerne 311 og 312, før du rører versionen. Navngiver featuren en geospatiel dictionary, og tegner filen kun rectilinear målinger, var det denne false positive, og en aktuel build gemmer filen uændret. Er featuren ægte, skal du enten deklarere extensionen eller låse til en version, der ærligt indeholder indholdet; at hæve versionen blot for at tavsgøre gaten skjuler spørgsmålet om, hvorvidt downstream-forbrugere kan læse, hvad du sender ud. Samme princip om afgrænsede, evidensbaserede tjek driver PDF/E-1 author-mode-preflightet til tekniske dokumenter, hvor CAD-tegninger møder en conformance-standard frem for et versionsnummer
Version compliance-tjek, measure- og geospatiale dictionaries og save-version-låsning er alle en del af PDF Library for Delphi, PDFlibPas-værktøjskassen til Delphi-, C++Builder- og Lazarus-udviklere