Artigo Técnico

Preflight de versão de PDF no Delphi: Measure RL vs GEO

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

Uma chave /Measure, dois dicionários no PDFlibPas: medição retilínea, com /RL ou o subtype omitido, precisa só de PDF 1.6, enquanto um dicionário geoespacial precisa de 1.7 ExtensionLevel 3, então o preflight decide pelo conteúdo do dicionário measure, onde uma regra no nível da chave não os distingue
Um preflight que bloqueia um arquivo válido é pior que um lento, porque o chamador recebe um diagnóstico autoritativo sobre uma funcionalidade que o documento nunca teve, e é por isso que a verificação de distinção fica um nível abaixo

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

Preflight de versão na hora do save no PDFlibPas: o SaveToFile verifica cada objeto contra uma tabela fixa de regras de funcionalidades, cada uma carregando uma versão mínima de PDF e, para funcionalidades de extensão, um extension level Adobe que só PDF 1.7 com uma entrada /Extensions correspondente pode satisfazer, e a primeira regra que o alvo não satisfaz para o save com erro 602
As chaves 311, 312 e 313 do GetInformation transformam uma recusa em um diagnóstico, reportando a versão exigida, a funcionalidade que a disparou e o alvo travado, então o chamador conserta o arquivo ou a regra em vez de adivinhar

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

Varredura de regras no PDFlibPas: copiar um registro de regra que guarda campos string ajusta contagens de referência a cada visita, enquanto passar a entrada somente leitura da tabela como parâmetro const lê no lugar sem tocar nas strings
Ler a tabela somente leitura no lugar tira o trabalho de contagem de referência do caminho de casamento de regras, enquanto um save completo ainda paga detecção de funcionalidades, decodificação de objetos e serialização

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