Artigo Técnico

Leitura de PDF mapeado no Delphi: janela deslizante

O PDFlibPas consegue abrir um PDF local por meio de uma visão de memória mapeada, limitada e somente para leitura: LoadFromMappedFile e DAOpenMappedFile mantêm exatamente uma janela deslizante sobre o arquivo, remapeiam sob demanda e fornecem cada fatia de objeto por leituras em offset absoluto. A biblioteca PDF para Delphi nunca mantém a fonte inteira na memória, então o uso do espaço de endereçamento permanece estável à medida que o arquivo cresce. O design existe para uma carga específica: PDFs de gigabytes em que o parser terminou de carregar e ainda continua voltando ao disco, objeto por objeto e fragmento de stream por fragmento de stream

Por que leituras esparsas continuam caras depois que o PDF foi carregado?

Carregar um PDF não termina de lê-lo, e em um arquivo de vários gigabytes é nessa diferença que o tempo vai embora. Uma tabela de referências cruzadas ou um stream de referências cruzadas (ISO 32000-1 §7.5.4 e §7.5.8) registra apenas onde cada objeto indireto começa. Os bytes chegam depois, quando uma página é renderizada, um programa de fonte é decodificado ou um stream de arquivo incorporado (ISO 32000-1 §7.11.4) é extraído. Um arquivo de 2 GB com dezenas de milhares de objetos vira dezenas de milhares de leituras pequenas e fora de ordem, e nenhuma delas é conhecida no momento do carregamento

O caminho que essas leituras usavam era um Seek compartilhado seguido de Read em um stream posicional, e ele falha em duas direções ao mesmo tempo. Cada fragmento paga por uma leitura de arquivo mesmo quando a página já está residente no cache do sistema operacional, e o cursor é estado mutável compartilhado, portanto um arquivo local e a fonte de byte range por trás do carregamento progressivo de PDF por ranges com prefetch não podiam executar o mesmo código do parser sem disputar a posição. O PDFlibPas corrige os dois problemas ao transformar a leitura por offset absoluto de otimização em contrato

O que TPDFReadAtStream garante?

TPDFReadAtStream garante uma leitura em offset absoluto que não depende do cursor lógico do stream nem o altera. É um descendente abstrato de TStream com exatamente um método virtual, e as duas fontes independentes de cursor da biblioteca derivam dele: TReadOnlyMappedFileStream para arquivos locais e TByteRangeStream para fontes remotas servidas por range. O leitor de fatias de objetos pergunta uma vez se a fonte é um TPDFReadAtStream e recua para a sequência antiga de seek e read quando não é, então um file stream comum ou memory stream continua funcionando sem alterações

type
  // Streams somente leitura cujas leituras absolutas evitam um Seek mais Read compartilhado
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Acesso somente leitura, em janela, a um único arquivo local
  TReadOnlyMappedFileStream = class(TPDFReadAtStream)
  private
    FMemoryMapped: Boolean;
  public
    constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
    function GetStats: TPDFMappedFileStats;
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; override;
    property MemoryMapped: Boolean read FMemoryMapped;
  end;

A distinção importa mais do que a assinatura sugere. ReadAt usa o offset recebido e deixa Position exatamente onde estava, o que permite que níveis aninhados do parser emitam leituras sem um ritual de salvar e restaurar a cada chamada. TReadOnlyMappedFileStream ainda implementa Read, Seek e Size como qualquer outro TStream, Seek limita a posição lógica ao arquivo, e Write sempre retorna 0 porque a fonte é aberta somente para leitura

Abrindo um PDF por uma visão mapeada no Delphi

Dois entry points explícitos abrem uma fonte mapeada, e nenhum deles altera o comportamento dos entry points que você já usa. LoadFromMappedFile carrega e seleciona um documento; DAOpenMappedFile devolve um handle de Direct Access sobre o mesmo arquivo, que é o modo desejado ao mesclar e dividir PDFs de gigabytes por Direct Access. LoadFromFile e DAOpenFile mantêm intactos seus semânticas de compartilhamento de arquivo, erros e compatibilidade, portanto nada muda para chamadores que não optarem pelo recurso. Os dois entry points mapeados recebem um WindowSize solicitado em bytes e uma máscara de bits Options, e ambos aceitam 0 para qualquer um deles

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0 seleciona o padrao de 64 MiB; o mapeamento e obrigatorio aqui
    if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
      PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
      raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
        [Pdf.LastErrorCode]);

    // A extracao adiada agora percorre janelas mapeadas em vez de fazer seeks
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

O que PDF_MAPPED_FILE_REQUIRE_MAPPING realmente impõe?

PDF_MAPPED_FILE_REQUIRE_MAPPING transforma um fallback silencioso em uma falha imediata e diagnosticável no momento da abertura. Com Options em 0, os dois entry points aceitam um fallback para file stream somente leitura: se a plataforma não tiver código de mapeamento ou se a chamada de mapeamento falhar, o documento ainda abre e cada leitura passa por um file stream normal. Com a flag definida, o PDFlibPas aceita a entrada somente quando a primeira visão foi estabelecida e reporta a recusa por LastErrorCode 401, em vez de carregar um documento que executaria silenciosamente exatamente como o caminho antigo

No Windows, o stream mapeado abre um segundo handle somente leitura com FILE_SHARE_READ, FILE_SHARE_WRITE e FILE_SHARE_DELETE, além de FILE_FLAG_RANDOM_ACCESS, cria um mapeamento PAGE_READONLY sobre ele e mapeia a primeira janela dentro do construtor. Mapear de forma eager é justamente o ponto: uma falha de "mapeamento obrigatório" aparece em LoadFromMappedFile, não na primeira leitura lazy de objeto no meio de um job de renderização. Mas é importante saber onde a garantia termina. O código de mapeamento só é compilado para alvos Windows, e um arquivo de zero bytes nunca tenta ser mapeado, então PDF_MAPPED_FILE_REQUIRE_MAPPING é uma solicitação que pode falhar legitimamente, não uma promessa portável. Um WindowSize negativo, ou qualquer bit em Options que não seja o valor documentado, é rejeitado diretamente com o mesmo erro 401

Uma janela, remapeada na granularidade de alocação

Somente uma visão é mantida por vez, e é isso que faz o uso do espaço de endereçamento ser independente do tamanho do arquivo. Um WindowSize igual a 0 seleciona 64 MiB; um valor abaixo da granularidade de alocação do sistema é elevado até ela; um valor acima de 1 GiB é limitado; e o resultado é arredondado para um número inteiro de unidades de granularidade, 65536 bytes no Windows, a menos que GetSystemInfo informe outro dwAllocationGranularity. Quando uma leitura cai fora da visão atual, o PDFlibPas a desmapeia, alinha o offset solicitado para baixo até um limite de granularidade e mapeia uma janela nova nesse ponto. A janela final é limitada ao tamanho físico do arquivo, portanto a visão nunca ultrapassa o fim do arquivo

Uma única leitura pode atravessar qualquer quantidade de janelas: o loop copia o que a visão atual consegue fornecer, remapeia e continua, e uma solicitação que passa do fim retorna uma contagem curta em vez de falhar. O que o PDFlibPas deliberadamente não faz é entregar um ponteiro para dentro da visão, porque a próxima leitura que cruzar uma janela o invalida e nenhum chamador conseguiria se proteger razoavelmente contra isso. Os bytes mapeados são copiados diretamente para buffers de destino pertencentes ao parser, removendo o buffer extra de entrada do arquivo e a alternância de posição, mas a biblioteca não promete armazenamento final zero-copy no parser. O windowing de leitura também combina com o lado de escrita, pois o deslocamento de referências em nível de byte durante uma mesclagem rápida de PDF transmite bytes de objetos para fora enquanto a fonte mapeada os transmite para dentro. A troca no tamanho da janela é óbvia: uma janela menor ocupa menos espaço de endereçamento e remapeia com mais frequência, o que geralmente é a escolha certa dentro de um processo de 32 bits

O que o lock protege e o que GetMappedFileInfo informa

Uma única seção crítica cobre a visão mapeada, o cursor do arquivo de fallback, a posição lógica e as estatísticas, e a divisão entre os dois métodos de leitura sai diretamente daí. ReadAt obtém o lock e chama o leitor interno sem lock; Read obtém o mesmo lock, chama o leitor interno na posição lógica atual e então a avança. Reutilizar a função interna em vez do ReadAt público evita lock recursivo, e manter o lock durante todo o loop de cópia é o que mantém correto um remapeamento de janela única em chamadas concorrentes. Há um detalhe do Free Pascal que vale conhecer antes de portar: a unit Windows do FPC declara seu próprio record chamado TCriticalSection, então o campo e sua construção precisam ser escritos como SyncObjs.TCriticalSection. O Delphi compila a forma não qualificada sem problemas; o FPC a resolve para um record sem Create, Enter ou Leave

var
  Pdf: TPDFlib;
  Handle, PageRef: Integer;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
      16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
    if Handle = 0 then
      Exit;
    try
      PageRef := Pdf.DAFindPage(Handle, 1);
      Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));

      // {"memoryMapped":true,"fileSize":...,"remapCount":...}
      if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
        Writeln(Info);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;
  • memoryMapped é false sempre que o fallback portável para file stream está ativo, e é o único campo que prova que um mapeamento nunca foi estabelecido
  • windowSize é a janela efetiva alinhada, não o valor solicitado, e mappedBytes é menor que ela na janela final
  • mappedOffset é o início alinhado à alocação da visão mantida, ou -1 quando nenhuma visão está ativa no momento
  • readCalls conta solicitações de leitura bem-sucedidas dentro do intervalo, bytesRead conta bytes copiados para os chamadores, e remapCount inclui a visão inicial

As regressões direcionadas cobrem leituras absolutas entre janelas, preservação do cursor lógico, leituras curtas no final, offsets inválidos, escritas rejeitadas, remapeamento entre janelas separadas, extração adiada de um anexo incompressível de 220 KB e estatísticas invalidadas depois de DACloseFile; as suítes headless Win32 e Win64 descobriram 1467 testes cada e passaram em todos, sem resultados ignorados, falhos, com erro ou vazamento. Se você trabalha com PDFs de gigabytes em Delphi ou C++Builder e o profiler continua apontando para leituras de arquivo, em vez do parsing, vale dedicar uma tarde aos entry points de arquivo mapeado, e GetMappedFileInfo dirá se você realmente obteve um mapeamento. A referência completa da API e um build de avaliação estão na página da biblioteca PDF Delphi do PDFlibPas