PDFium Component produces Fast Web View (linearized) PDFs from Delphi through TPdf.SaveAsLinearized. The method saves the current document with PDFium, then rewrites the result according to ISO 32000-1 Annex F: first-page objects move to the front, two cross-reference sections are written, and page offset and shared object hint tables tell a viewer which byte ranges to fetch for any page
The reason anyone cares is the network. A 40 MB catalog served from object storage or a document portal should show page one after the first few kilobytes, and jumping to page 380 should cost a couple of HTTP Range requests, not a full download. Viewers can only do that when the file is linearized. Most PDFs leaving an editing pipeline are not, because the natural way to write a PDF puts the cross-reference table at the end and scatters page objects wherever the writer happened to emit them
Why can PDFium read linearized files but not write them?
PDFium has a complete reader for linearization and no writer for it. The FPDFAvail_* availability API detects the linearization dictionary, decodes hint tables, and asks the host for exactly the byte ranges it needs, which is what the progressive download and cancellation workflow in PDFium Component is built on. The public save ABI, on the other hand, only offers a plain copy save and a save with a target PDF version. Nothing in it reorders objects or emits hint streams
PDFium Component closes that gap without re-implementing what PDFium already does well. TPdf.SaveAsLinearized first calls the existing SaveAs, so PDFium still owns the semantic save: edited pages, form field state, and version handling all go through the code path that has been exercised for years. The component then runs an Annex F restructuring pass over that stable intermediate PDF and writes the linearized result to your target. Splitting the work this way means linearization never has to understand an annotation or a form, only objects, references, and byte offsets
Calling TPdf.SaveAsLinearized from Delphi
TPdf.SaveAsLinearized has two overloads, one for a TStream and one for a file name, and both take the same optional Option: TSaveOption and PdfVersion: TPdfVersion parameters as SaveAs. Both return True on success. The file overload writes to a temporary file in the target directory and replaces the destination atomically, so a failed save never leaves a half-written PDF where a good one used to be
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.Active := True;
// ... edit pages, fill forms, stamp, whatever the pipeline does ...
if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
raise Exception.Create('Linearized save failed');
finally
Pdf.Free;
end;
end;
The parameter rules are strict on purpose. saIncremental raises an error, because an incremental update appends a new revision to the end of the file and would immediately destroy the front-loaded layout. A target version below PDF 1.2 is also rejected; pvUnknown keeps the source version. Encrypted input needs saRemoveSecurity, since the writer renumbers objects and PDF object-level encryption keys are derived from the object number. And because linearization is a full rewrite, any existing digital signature is invalidated. If you have to deliver signed and linearized, linearize first and sign afterward
How the writer groups objects for Fast Web View
Linearization is not just moving page one to the top of the file. The writer parses the current revision, whether its cross-reference data is a classic table, a hybrid file, or a cross-reference stream, and expands every member of an active object stream into a standalone object. It then builds a reference graph and sorts objects into five groups: document-level objects needed to open the file, first-page objects, objects private to one later page, objects shared by several later pages, and everything else
The trap in that graph walk is /Parent. Every page dictionary points back up the page tree, and a naive traversal follows that pointer to the parent node, then down through /Kids into every sibling page and its content streams. One step later the first-page group contains the whole document, and the file is technically linearized but useless for range-first access. PDFium Component ignores the page /Parent link and the page tree nodes outside the page being collected, which is what keeps the first-page section small. On a 51-page regression sample with a deep page tree, the first-page section is 5289 bytes, 0.48% of the 1105354-byte output
Object streams and cross-reference streams in the input are flattened into ordinary generation 0 indirect objects, every reference is rewritten to the new numbers, and the output uses classic cross-reference sections. That costs some file size compared with compressed object streams, but it simplifies hint table coverage and is the layout every linearization-aware viewer handles. If compactness matters more than Fast Web View for a given output, PDFium Component has a separate object stream writer for that job
What goes into the linearization dictionary and the two xref sections?
A linearized file written by PDFium Component carries two classic cross-reference sections and a linearization dictionary in the very first object. The first xref section follows the linearization dictionary and covers the document-level and first-page objects, so a viewer can render page one from the front of the file alone. The main xref section sits at the end and covers everything else
The linearization dictionary itself holds the numbers a viewer needs before it reads anything else: /L the file length, /H the hint stream offset and length, /O the first page object number, /E the end of the first-page section, /N the page count, and /T the offset of the first entry in the main xref table. All of those depend on the final layout, and the dictionary sits before the layout. Writing it with natural-width numbers creates a loop: a length that grows from 999999 to 1000000 adds a byte, shifts every following offset, which can change another number's width, and so on
PDFium Component breaks the loop by reserving a fixed 320 bytes for the linearization dictionary. Every object, the hint stream, both xref sections, and the trailers are written first; once all offsets are final the dictionary is filled in place and padded to the reserved width. One subtle detail is /T: it must point at the object 0 entry of the main xref table, not at the xref keyword that precedes it. Strict validators flag the keyword offset even though forgiving viewers open the file anyway
Why do hint table offsets ignore the hint stream?
Hint table offsets in a linearized PDF are interpreted as if the hint stream did not exist in the file. Annex F defines it that way so the hint stream can sit between the first-page section and the rest without its own size feeding back into the numbers it contains. The practical consequence for a writer is easy to get wrong: any real offset that lies after the hint stream has to be reduced before it goes into the page offset or shared object hint table
The question is reduced by how much. The tempting answer is the length of the stream data, because that is the part that feels like the hint content. The correct answer is the length of the entire hint indirect object, including N 0 obj, the stream dictionary, the stream and endstream keywords, and endobj. Subtract only the data length and every later offset is off by a few dozen bytes. Page one still renders because it lives before the hint stream, so the bug survives a quick visual check and only appears when a viewer jumps to a later page and requests a range that starts in the middle of the wrong object
Both hint tables are bit-packed with the most significant bit first, again per Annex F. The component verifies the result the way a real client would: a sparse range store supplies only the first 1 KiB of the 51-page sample, and PDFium's native hint handling requests the rest. The last page opens without the full file ever being present
Verifying a linearized PDF in Delphi
The cheapest check is to ask PDFium itself. TPdf.InspectAvailability runs the availability API against a stream without loading the document, and its Linearization field reports plnLinearized, plnNotLinearized, or plnUnknown
function IsFastWebView(const FileName: string): Boolean;
var
Stream: TFileStream;
Info: TPdfAvailability;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Info := TPdf.InspectAvailability(Stream);
Result := Info.Linearization = plnLinearized;
finally
Stream.Free;
end;
end;
That confirms PDFium recognizes the layout; it does not prove the hints are correct. For a real end-to-end test, serve the file through a range-limited transport and open a late page with TPdfProgressiveDocument, the same way the guide to streaming large PDFs on demand with PDFium VCL does. If the page renders after fetching only a small fraction of the file, the hint tables are doing their job. Adding a second independent linearization checker to the release pipeline is also worth it, because viewers differ in how forgiving they are about a misplaced /T or a slightly wrong offset
Two limits to keep in mind. The object materializer still rejects arrays of 2 GiB or more, so extremely large documents need a different strategy for now. And linearization only pays off when the server honors HTTP Range requests; behind a proxy that strips Accept-Ranges, a linearized file downloads exactly like any other
FAQ
Does TPdf.SaveAsLinearized keep digital signatures valid?
No. TPdf.SaveAsLinearized renumbers and rewrites every object, so existing signatures are invalidated. Linearize first, then apply the signature
Can PDFium Component linearize an encrypted PDF?
Only when you pass saRemoveSecurity. Object-level encryption keys depend on object numbers, which the writer changes, so the output is written without encryption
Which PDF versions does linearized output support?
TPdf.SaveAsLinearized accepts pvUnknown to keep the source version or any target from PDF 1.2 upward, and rejects saIncremental because an appended revision would break the layout
Fast Web View output, progressive loading, and the availability API are all part of the PDFium Component for Delphi and C++Builder, which wraps PDFium for Delphi, C++Builder, and Lazarus projects