O PDFlibPas (PDF Library for Delphi) confere todos os objetos contra uma tabela de regras de versão PDF antes de escrever um ficheiro, e até há pouco tempo esse preflight de versão PDF confundia dicionários de medição CAD vulgares com geoespaciais. Um desenho CAD de página única carregava bem, depois o SaveToFile devolvia 0 com LastErrorCode 602 e exigia 1.7 ExtensionLevel 3. As regras corrigidas tratam dicionários /Measure retilíneos (/Subtype /RL) como PDF 1.6 simples e reservam o gate de extensão para marcadores geoespaciais a sério
O ficheiro entrou pela admissão de corpus: uma página, um grupo de optional-content, dois viewports de medição retilíneos, o tipo de output que um pacote CAD de arquitetura escreve para que um visualizador consiga ler distâncias a partir de uma planta. Nada nele era exótico, o que é precisamente a razão por que a recusa importava. Um preflight que bloqueia um ficheiro válido é pior do que um lento, porque o chamador recebe um diagnóstico de aparência autoritária a apontar para uma funcionalidade que o documento não contém. A correção teve duas partes: a leitura da spec por trás de uma regra, e a perceção de que a regra não conseguia distinguir dois tipos de dicionário ao nível a que estava a olhar
Como funciona o preflight de versão na gravação do PDFlibPas?
O gate de gravação, o PrepareAndCheckSaveVersion, compara cada objeto indireto contra PDFFeatureRules e falha na primeira regra que tanto corresponde como precisa de mais do que o alvo permite. O alvo é a versão do documento (ou a versão fixada pelo LockSaveVersion), mais o extension level Adobe declarado sob /Extensions /ADBE. Cada registo TPDFFeatureRule transporta um MinVersion, um MinExtensionLevel, um MatchKind como fmkDictKey ou fmkDictSubtype, uma string Match, um nome de Feature legível por humanos e um callback opcional. O AddRule registra uma regra de versão simples; o AddExtensionRule crava sempre o MinVersion em 17 e acrescenta um extension level por cima, por isso uma regra de extensão só pode ser satisfeita por PDF 1.7 mais a entrada /Extensions certa. Quando o gate dispara, a versão exigida e o nome da funcionalidade ficam guardados para o chamador, e as chaves 311, 312 e 313 do GetInformation expõem-nos
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: versão exigida, 312: funcionalidade que a despoletou,
// 313: versão a que o alvo de gravação está bloqueado ('' quando desbloqueado)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Porque é que um desenho CAD simples falhava com o erro 602?
A tabela de regras continha AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), que disparava sobre qualquer dicionário que meramente tivesse uma chave /Measure, e todos os viewports de medição têm um. O array /VP da página guarda dicionários de viewport, cada viewport aponta para o seu dicionário de medição através de /Measure, e a correspondência por presença de chave parava aí sem olhar ao que o dicionário de medição era de facto. A varredura de funcionalidades em tempo de carga podia então subir o número de versão do documento para 1.7, mas nunca escreve uma declaração /Extensions em nome de um ficheiro de entrada, por isso o gate de gravação via PDF 1.7 ao extension level 0 e reportava 1.7 ExtensionLevel 3. Essa recusa em inventar uma declaração de extensão é deliberada: a biblioteca não promove silenciosamente um ficheiro de entrada para tapar uma regra que está errada
A spec é inequívoca quanto ao caso retilíneo. Os dicionários Measure chegaram no PDF 1.6, e a ISO 32000-1 §12.9 dá ao /Subtype um valor por defeito de RL, um sistema de coordenadas retilíneo descrito pelo seu próprio conjunto de entradas: rácio de escala, formatos numéricos X e Y, distância e área. A medição geoespacial é a adição posterior do Adobe Extension Level 3 sobre o PDF 1.7, identificada por /Subtype /GEO e a transportar arrays de pontos geográficos, dicionários de sistemas de coordenadas e unidades de apresentação, as estruturas percorridas em ler viewports GeoPDF e arrays GPTS e LPTS no Delphi. Ambos os dicionários pendem da mesma chave /Measure, por isso qualquer regra que pare na chave não pode estar certa para os dois. A informação distintiva está um nível abaixo, no próprio dicionário de medição
O que é que o conjunto de regras corrigido continua a impor?
A correção apaga a regra de chave incondicional e deixa os gates que descrevem requisitos de versão reais. Uma página com /VP ou /UserUnit continua a precisar de PDF 1.6 através do CB_PagePDF16Entries, uma chave /PtData continua a precisar do extension level 3, e o CB_GeospatialDictionary decide se um dicionário de medição é geoespacial pelo seu conteúdo em vez de pela chave que o trouxe
// Removida: qualquer dicionário com uma chave /Measure contava como geoespacial
// 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;
As regressões partilhadas Delphi e FPC cravam essa fronteira dos dois lados. Um viewport cujo dicionário de medição omite o /Subtype e um que o escreva por extenso como /RL passam ambos em PDF 1.6, a mesma página continua a ser rejeitada em PDF 1.5, e a deteção de funcionalidades deixa de reportar uma extensão para ela. Acrescentar um array /GPTS devolve o veredito a 1.7 ExtensionLevel 3, que passa assim que o extension level é declarado, e um dicionário só com /Subtype /GEO é recusado sem ele. O callback é conservador de propósito: um dicionário retilíneo que também transporte uma chave /GCS ou /PDU perdida é tratado como geoespacial, porque essas chaves não têm significado nenhum no modelo RL
O LockSaveVersion é onde esta mudança fica visível para os chamadores. O TPDFlib.LockSaveVersion aceita '1.0' a '1.7', devolve 0 para qualquer outra coisa, crava a versão do documento, e impede que chamadas do lado do writer a subam em silêncio, mas o gate de gravação continua a correr contra o valor bloqueado. Com as regras corrigidas, um ficheiro CAD bloqueado em 1.6 grava limpo. Um GeoPDF genuíno bloqueado em 1.6 continua a levar 602, que é a resposta certa, e as chamadas de autoria geoespacial como o SetMeasureDictCoordinateSystem declaram elas próprias o extension level 3 quando se constrói esse conteúdo através da API
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
// Conteúdo real acima de 1.6, por exemplo um dicionário de medição GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Porque é que a varredura das regras de versão era mais lenta do que precisava?
A varredura copiava cada TPDFFeatureRule para um registo local antes de o testar, e como o registo guarda dois campos AnsiString, cada cópia ajustava dois reference counts e libertava os valores anteriores. O preflight visita todos os nós de todas as árvores de objetos, escalares incluídos, por isso esse custo multiplicava a contagem de objetos pela contagem de regras, e regras que nem sequer se aplicavam à versão alvo eram primeiro copiadas e depois saltadas. Como o PDFFeatureRules é preenchido uma vez na inicialização da unit e tratado como só de leitura, a v3.539.17 passa as entradas da tabela diretamente ao MatchSingleRule e ao RuleExceedsTarget, cujos parâmetros const Rule tomam uma referência sem tocar nas strings
// Antes: uma cópia gerida do registo por regra, por objeto visitado
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
Continue;
// Depois: parâmetros const leem a entrada imutável da tabela no sítio
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;
O efeito medido é estreito e deve ser citado assim. O benchmark confere um array de 20.000 objetos numéricos contra um alvo PDF 1.4 dez vezes por ronda; compilado com FPC Win64 a -O2, a mediana de cinco rondas desceu de 0.711 s para 0.203 s, e correr as duas builds pela ordem inversa deu 0.459 s contra 0.150 s. Isso é aproximadamente um ganho de 3x só no caminho de matching de regras. Uma gravação real também paga a deteção de funcionalidades adiada, a descodificação de objetos e a serialização, por isso o rácio não se transporta para o tempo total de gravação. A ordem das regras, os callbacks, os limiares de versão e o diagnóstico de primeira falha não mudam, e nenhuma regra foi posta em cache entre gravações nem saltada para lá chegar
O que deve verificar quando um PDF carregado falha o preflight de versão?
Leia as chaves 311 e 312 antes de tocar na versão. Se a funcionalidade nomeia um dicionário geoespacial e o ficheiro só desenha medições retilíneas, era este falso positivo, e uma build atual grava o ficheiro sem alterações. Se a funcionalidade é genuína, ou declara a extensão ou bloqueia numa versão que contenha honestamente o conteúdo; subir a versão só para fazer calar o gate esconde a questão de saber se os consumidores a jusante conseguem ler o que distribui. O mesmo princípio de verificações limitadas e apoiadas em evidências conduz o preflight em modo de autor PDF/E-1 para documentos de engenharia, onde desenhos CAD encontram uma norma de conformidade e não um número de versão
As verificações de conformidade de versão, os dicionários de medição e geoespaciais, e o bloqueio da versão de gravação fazem todos parte da PDF Library for Delphi, o toolkit PDFlibPas para programadores Delphi, C++Builder e Lazarus