Uma planta baixa exportada por um pacote CAD de arquitetura tipicamente tem um array /VP em sua primeira página com duas viewports, e cada viewport carrega uma entrada /Measure para que um visualizador leia metros direto do desenho. Uma folha de mapa GeoPDF tem o mesmo array /VP e a mesma chave /Measure. A planta baixa é um PDF 1.6 comum. A folha de mapa precisa de PDF 1.7 mais Adobe Extension Level 3. Uma verificação de versão que para no nome da chave vai exigir a extensão para ambos, e a planta baixa é recusada no save com uma mensagem sobre conteúdo geoespacial que ela nunca teve
Uma chave /Measure, dois dicionários diferentes
O nome da chave diz que uma viewport é mensurável; só o dicionário por trás dela diz como. Dicionários Measure chegaram no PDF 1.6, e a ISO 32000-1 §12.9 define o tipo retilíneo: uma string de razão de escala em /R, arrays de formato numérico em /X e /Y, e arrays adicionais para distância (/D) e área (/A). A medição geoespacial veio depois, como parte do Adobe Extension Level 3 sobre o PDF 1.7. Ela é marcada por /Subtype /GEO e carrega um sistema de coordenadas geográfico em /GCS, um sistema de coordenadas de exibição opcional em /DCS, arrays de pontos pareados em /GPTS e /LPTS, e unidades de exibição preferenciais em /PDU — as estruturas percorridas em lendo viewports GeoPDF e arrays GPTS e LPTS no Delphi
O detalhe que pega as pessoas é o padrão. A §12.9 dá a /Subtype um valor padrão de RL, e muitos exportadores CAD aproveitam isso e deixam a entrada de fora. Um verificador que lê um subtype ausente como "desconhecido, assuma o pior" transforma cada um desses desenhos em um documento geoespacial. Um verificador que casa na chave /Measure nem chega à questão do subtype, porque o dicionário de viewport que segura a chave está um nível acima do dicionário measure que responde a ela
Como o preflight de versão do save da PDF Library for Delphi chega a um veredito
A PDF Library for Delphi (PDFlibPas) roda um preflight de versão dentro do SaveToFile antes de qualquer saída ser gravada. Ele visita cada objeto indireto, descendo em dicionários e arrays aninhados, e testa cada nó contra uma tabela de regras de funcionalidades. Toda regra nomeia uma versão mínima de PDF e, para funcionalidades de extensão, um extension level da Adobe; uma regra de extensão só pode ser satisfeita por PDF 1.7 junto com uma entrada /Extensions /ADBE correspondente no catálogo. O alvo contra o qual ele verifica é a versão do documento, ou a versão que você travou com LockSaveVersion, mais o extension level que o catálogo declara. A primeira regra que ao mesmo tempo casa e excede esse alvo encerra o save: SaveToFile devolve 0 e LastErrorCode vira 602, o valor de PDFLIB_ERROR_VERSION_COMPLIANCE
Um código de erro sozinho não diz o que consertar. Três chaves do GetInformation carregam o resto: 311 é a versão que a funcionalidade infratora precisa (por exemplo 1.7 ExtensionLevel 3), 312 é o nome da funcionalidade, e 313 é a versão na qual o alvo de save está travado, vazia quando nada está travado
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 disparou,
// 313: versão na qual o alvo 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;
Para conteúdo de medição a tabela guarda três regras e deliberadamente nenhuma regra na chave /Measure pura. Uma página com /VP ou /UserUnit precisa de PDF 1.6. Um dicionário /PtData precisa do extension level 3. Um dicionário measure precisa do extension level 3 quando diz /Subtype /GEO ou contém qualquer um de /GCS, /DCS, /GPTS, /LPTS ou /PDU. Então uma viewport cujo dicionário measure omite /Subtype e uma que escreve /RL explicitamente passam em 1.6, enquanto a mesma página em 1.5 é recusada por causa do /VP. Adicione um array /GPTS e a resposta vira 1.7 ExtensionLevel 3
O teste de conteúdo é conservador de propósito. Um dicionário retilíneo que também carrega um /GCS ou /PDU extraviado é tratado como geoespacial. Preferimos recusar esse arquivo a deixá-lo passar: essas chaves não significam nada no modelo retilíneo, então quem as escreveu ou era uma ferramenta geoespacial ou algo que danificou o arquivo, e um visualizador consciente de GEO pode muito bem tentar interpretá-las. O loader é igualmente cuidadoso na outra direção. Sua varredura de funcionalidades pode elevar o número de versão do documento para 1.7, mas nunca grava uma declaração /Extensions em nome de um arquivo de entrada — declarar uma extensão é uma afirmação sobre conteúdo, e só o seu código ou as chamadas de autoria deveriam fazê-la
Checando as viewports de um desenho antes de salvar
Quando o 312 nomeia uma funcionalidade geoespacial e você tem certeza de que o arquivo é um desenho simples, olhe os dicionários measure você mesmo. As chamadas públicas de viewport bastam. O GetMeasureDictCoordinateSystem lê o /Subtype e devolve 1 para RL, 2 para GEO e 0 quando a entrada está ausente — o que, pela §12.9, também significa retilíneo. As contagens de arrays de pontos dizem se há dados de georreferenciamento presentes
// Reporta todo dicionário measure do documento carregado.
// Todas as chamadas aqui só leem; nenhuma adiciona chaves ao arquivo.
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = omitido (RL), 1 = RL, 2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
Duas chamadas estão fora desse loop de propósito: GetMeasureDictGCSDict e GetMeasureDictDCSDict. Essas duas são acessores get-or-create construídos para autoria. Em um dicionário measure sem entrada /GCS, o GetMeasureDictGCSDict adiciona uma vazia e declara o extension level 3 para o documento. Chame-a como sonda em uma planta baixa retilínea e o dicionário agora genuinamente contém /GCS; um save seguinte travado em 1.6 é recusado, corretamente, por conteúdo que o seu diagnóstico acabou de escrever. Se você precisa saber se existe um sistema de coordenadas, o subtype e as contagens de arrays de pontos respondem isso sem efeitos colaterais
Travando a saída em PDF 1.6
Uma versão travada mantém a saída honesta quando seus consumidores têm um teto. O TPDFlib.LockSaveVersion aceita as strings '1.0' a '1.7' e devolve 0 para qualquer outra coisa. Ele fixa a versão do documento e impede que chamadas do escritor a elevem por conta própria, mas o preflight do save ainda roda contra o valor travado. Um desenho CAD travado em 1.6 salva limpo. Um GeoPDF de verdade travado em 1.6 leva 602, e essa é a resposta que você quer, porque a alternativa é um arquivo cujo cabeçalho promete menos do que o conteúdo exige
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// Conteúdo real acima de 1.6, por exemplo um dicionário measure GEO
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
Pdf.UnlockSaveVersion;
end;
Leia o 311 e o 312 antes de tocar na versão. Se a funcionalidade é conteúdo geoespacial genuíno, você tem duas escolhas honestas: deixar o documento destravado em 1.7 com a extensão declarada, ou derrubar os dados geoespaciais da cópia que você envia a um consumidor só-1.6. Elevar o travamento só para fazer o erro sumir é a opção a evitar. Ele esconde a pergunta real — o leitor downstream consegue lidar com o que você está entregando? Quando você constrói conteúdo geoespacial pela API, os setters SetMeasureDict* como SetMeasureDictCoordinateSystem e SetMeasureDictGPTSCount declaram o extension level 3 eles mesmos, então arquivos autorados chegam ao preflight já rotulados. O mesmo hábito de verificações delimitadas e apoiadas em evidência aparece em o preflight do modo autor PDF/E-1 para documentos de engenharia, onde desenhos CAD encontram um padrão de conformidade em vez de um número de versão
Por que a varredura de regras lê a tabela no lugar
O custo do preflight é aproximadamente nós de objeto vezes regras, e a contagem de nós inclui todo número e nome dentro de todo array. Isso faz a sobrecarga por regra importar mais do que parece. Cada regra na tabela é um registro com dois campos AnsiString, a string de casamento e o nome da funcionalidade. Copiar esse registro para uma variável local faz o Delphi ou o FPC incrementar duas contagens de referência e liberar o que quer que a local tivesse antes. Feito uma vez por regra por nó, isso significa pagar a contabilidade de strings mesmo para regras que não se aplicam à versão do alvo e são descartadas na linha seguinte
A tabela de regras é preenchida uma vez na inicialização da unit e nunca modificada depois, então a varredura entrega entradas da tabela ao seu código de casamento por meio de parâmetros de registro const e as lê onde vivem. Um parâmetro de registro const é passado por referência, então testar uma regra não toca em nenhum campo string e as contagens de referência ficam onde estavam. O efeito se concentra no caminho de casamento de regras. Um save de verdade também decodifica objetos, roda detecção de funcionalidades diferida e serializa a saída, e essas etapas não são tocadas pela forma como as regras são passadas. A ordem das regras, os limiares e o diagnóstico da primeira falha no 311 e no 312 não dependem disso, e nenhum resultado de regra é cacheado entre saves, então um documento que você edita e salva duas vezes é verificado por inteiro nas duas vezes
O mesmo padrão vale checar no seu próprio código Delphi onde quer que um loop percorra uma tabela de registros que contêm strings, interfaces ou arrays dinâmicos:
type
TMyRule = record
Name: string; // campo gerenciado: cópias custam trabalho de contagem de referência
Threshold: Integer;
end;
// Código do leitor, mostrado como padrão
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const: sem cópia, sem tráfego de refcount
end;
// No loop quente, passe a entrada da tabela direto em vez de
// atribuí-la a um registro local primeiro
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
Viewports de medição, dicionários geoespaciais, travamento de versão e a verificação de compliance na hora do save todos vêm na PDF Library for Delphi, disponível para projetos Delphi, C++Builder e Lazarus