O PDFlibPas (PDF Library for Delphi) confere todo objeto contra uma tabela de regras de versão de PDF antes de escrever um arquivo, e até pouco tempo atrás esse preflight de versão de PDF confundia dicionários comuns de medição CAD com geoespaciais. Um desenho CAD de página única carregava bem, e então o SaveToFile retornava 0 com LastErrorCode 602 e exigia 1.7 ExtensionLevel 3. As regras corrigidas tratam dicionários /Measure retilineares (/Subtype /RL) como PDF 1.6 corrente e reservam o gate de extensão para marcadores geoespaciais de verdade
O arquivo entrou pela admissão de corpus: uma página, um grupo de optional content, dois viewports de medição retilineares, o tipo de output que um pacote CAD de arquitetura escreve para que um viewer consiga ler distâncias numa planta baixa. Nada nele era exótico, e é exatamente por isso que a recusa importava. Um preflight que bloqueia um arquivo válido é pior que um lento, porque quem chama recebe um diagnóstico de aparência autoritativa apontando para uma feature 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 percepção de que a regra não conseguia distinguir dois tipos de dicionário no nível em que olhava
Como funciona o preflight de versão no save do PDFlibPas?
O gate de save, o PrepareAndCheckSaveVersion, compara todo objeto indireto contra o PDFFeatureRules e falha na primeira regra que tanto casa quanto pede mais do que o target permite. O target é a versão do documento (ou a versão fixada pelo LockSaveVersion), mais o extension level da Adobe declarado sob /Extensions /ADBE. Cada record TPDFFeatureRule carrega 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 sempre fixa o MinVersion em 17 e acrescenta um extension level por cima, então 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 feature ficam guardados para quem chama, e as chaves 311, 312 e 313 do GetInformation os expõem
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: feature que a disparou,
// 313: versão em que o target de save está travado ('' quando destravado)
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
Por que um desenho CAD comum falhava com erro 602?
A tabela de regras continha AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil), que disparava em qualquer dicionário que simplesmente tivesse uma chave /Measure, e todo viewport de medição tem uma. O array /VP da página guarda dicionários de viewport, cada viewport aponta para o dicionário de measure dele por meio de /Measure, e o match por presença de chave parava aí, sem olhar o que o dicionário de measure realmente era. A varredura de features no load podia então subir o número de versão do documento para 1.7, mas ela nunca escreve uma declaração /Extensions em nome de um arquivo de entrada, então o gate de save via PDF 1.7 no extension level 0 e reportava 1.7 ExtensionLevel 3. Essa recusa a inventar uma declaração de extensão é deliberada: a biblioteca não promove silenciosamente um arquivo de entrada para tapar o sol com a peneira de uma regra errada
A spec é inequívoca quanto ao caso retilinear. Dicionários Measure chegaram no PDF 1.6, e a ISO 32000-1 §12.9 dá ao /Subtype um default de RL, um sistema de coordenadas retilinear descrito pelo próprio conjunto de entradas dele: razão de escala, formatos numéricos X e Y, distância e área. A medição geoespacial é a adição posterior da Adobe Extension Level 3 por cima do PDF 1.7, identificada por /Subtype /GEO e carregando arrays de pontos geográficos, dicionários de sistema de coordenadas e unidades de exibição, as estruturas percorridas em ler viewports GeoPDF e arrays GPTS e LPTS no Delphi. Os dois dicionários pendem da mesma chave /Measure, então qualquer regra que parar na chave não pode estar certa para os dois. A informação distintiva fica um nível abaixo, no próprio dicionário de measure
O que o conjunto de regras corrigido ainda reforça?
A correção apaga a regra incondicional de chave e deixa os gates que descrevem requisitos de versão reais. Uma página com /VP ou /UserUnit ainda precisa de PDF 1.6 via CB_PagePDF16Entries, uma chave /PtData ainda precisa do extension level 3, e o CB_GeospatialDictionary decide se um dicionário de measure é geoespacial pelo conteúdo dele em vez de pela chave que o trouxe
// Removido: todo dicionário com 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 compartilhadas Delphi e FPC pregam essa fronteira dos dois lados. Um viewport cujo dicionário de measure omite /Subtype e um que escreve /RL explicitamente passam ambos em PDF 1.6, a mesma página continua rejeitada em PDF 1.5, e a detecção de features não reporta mais extensão para ela. Acrescentar um array /GPTS reverte o veredito para 1.7 ExtensionLevel 3, que passa assim que o extension level é declarado, e um dicionário só de /Subtype /GEO é recusado sem ele. O callback é conservador por design: um dicionário retilinear que também carregue uma chave solta /GCS ou /PDU é tratado como geoespacial, já que essas chaves não têm significado no modelo RL
O LockSaveVersion é onde essa mudança fica visível para quem chama. O TPDFlib.LockSaveVersion aceita '1.0' até '1.7', retorna 0 para qualquer outra coisa, fixa a versão do documento e impede que chamadas do lado do writer a subam silenciosamente, mas o gate de save continua rodando contra o valor travado. Com as regras corrigidas, um arquivo CAD travado em 1.6 salva limpo. Um GeoPDF genuíno travado em 1.6 ainda leva 602, que é a resposta correta, e as chamadas de autoria geoespacial como SetMeasureDictCoordinateSystem declaram o extension level 3 por conta própria quando você constrói esse conteúdo pela 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 measure GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;
Por que a varredura das regras de versão era mais lenta do que precisava?
A varredura copiava cada TPDFFeatureRule para um record local antes de testá-lo, e como o record carrega dois campos AnsiString, cada cópia ajustava dois reference counts e liberava os valores anteriores. O preflight visita todo nó de toda árvore de objetos, escalares incluídos, então esse custo multiplicava contagem de objetos por contagem de regras, e regras que nem se aplicavam à versão target eram copiadas primeiro e puladas depois. Como o PDFFeatureRules é preenchido uma vez na inicialização da unit e tratado como read-only, a v3.539.17 passa as entradas da tabela direto ao MatchSingleRule e ao RuleExceedsTarget, cujos parâmetros const Rule tomam uma referência sem tocar nas strings
// Antes: uma cópia de record gerenciado 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 lugar
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 target PDF 1.4 dez vezes por rodada; compilado com FPC Win64 em -O2, a mediana de cinco rodadas caiu de 0,711 s para 0,203 s, e rodar as duas builds em ordem inversa deu 0,459 s contra 0,150 s. Isso é um ganho de aproximadamente 3x só no caminho de match de regras. Um save real também paga detecção de features diferida, decodificação de objetos e serialização, então a razão não se transfere para o tempo total de save. A ordem das regras, os callbacks, os limiares de versão e o diagnóstico de primeira falha estão inalterados, e nenhuma regra foi cacheada entre saves nem pulada para chegar lá
O que checar quando um PDF carregado falha no preflight de versão?
Leia as chaves 311 e 312 antes de mexer na versão. Se a feature nomeia um dicionário geoespacial e o arquivo só desenha medições retilineares, era este falso positivo, e uma build atual salva o arquivo sem mudanças. Se a feature é genuína, ou declare a extensão ou trave numa versão que honestamente contém o conteúdo; subir a versão só para silenciar o gate esconde a pergunta de se os consumidores a jusante conseguem ler o que você distribui. O mesmo princípio de checagens limitadas e embasadas em evidência guia o preflight em modo de autoria do PDF/E-1 para documentos de engenharia, em que desenhos CAD encontram um padrão de conformidade em vez de um número de versão
Checagens de conformidade de versão, dicionários de medição e geoespaciais, e o travamento da versão de save fazem todos parte da PDF Library for Delphi, o toolkit PDFlibPas para desenvolvedores Delphi, C++Builder e Lazarus