Since iOS/iPadOS 27.0 (also macOS 27.0), saving a PDF with PDFDocument.dataRepresentation() (or write(to:)) writes every stream whose raw length is 0 as an empty indirect object:
6 0 obj
endobj
That object is null, not a stream. When it is referenced from a page's /Contents array, Adobe Acrobat/Reader renders the whole page blank. Browser PDF viewers tolerate it, so the corruption is easy to miss. qpdf --check reports empty object treated as null and ignoring non-stream in an array of streams.
No edits are needed to trigger it. Zero-length content streams are legal and fairly common; for example, PDFs generated by TallComponents PDFKit.NET contain a << /Filter /FlateDecode /Length 0 >> content stream on every page. In our case users sign such documents in an iPad app (custom PDFAnnotation) and we upload the result of dataRepresentation(); on iPadOS 27 every one of them opens blank in Acrobat.
Minimal repro (builds a 1-page PDF whose /Contents is [text stream, zero-length stream], then re-serializes it):
import PDFKit
func minimalPDF() -> Data {
let objects = [
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents [4 0 R 6 0 R] >>",
"<< /Length 36 >>\nstream\nBT /F1 24 Tf 72 700 Td (Hello) Tj ET\nendstream",
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
"<< /Length 0 >>\nstream\n\nendstream", // legal zero-length stream
]
var pdf = "%PDF-1.7\n", offsets: [Int] = []
for (i, body) in objects.enumerated() {
offsets.append(pdf.utf8.count)
pdf += "\(i + 1) 0 obj\n\(body)\nendobj\n"
}
let xref = pdf.utf8.count
pdf += "xref\n0 \(objects.count + 1)\n0000000000 65535 f \n"
offsets.forEach { pdf += String(format: "%010d 00000 n \n", $0) }
pdf += "trailer\n<< /Size \(objects.count + 1) /Root 1 0 R >>\nstartxref\n\(xref)\n%%EOF\n"
return Data(pdf.utf8)
}
let saved = PDFDocument(data: minimalPDF())!.dataRepresentation()!
print(String(decoding: saved, as: UTF8.self).contains("obj\nendobj") ? "BROKEN: null object written" : "OK")
- iOS 18.1: OK (PDFKit re-renders the page through Quartz).
- iOS 27.0 (24A5370g) / macOS 27.0 (26A428): BROKEN. With
CG_PDF_VERBOSE=1CoreGraphics logswriteStream: Failed to get data from stream.
What triggers it: only the raw (encoded) length being 0. It happens with or without /Filter /FlateDecode, for a single /Contents stream, and for a zero-length Form XObject. A FlateDecode stream holding an 8-byte compressed empty payload (decoded length 0) is written correctly, so it is not about the decoded data.
Where it seems to come from: iOS 27 introduced a structure-preserving serializer in CoreGraphics (CG::PDFDocumentSerializer). Its writeStream emits N 0 obj, asks for the stream's raw bytes, and when that returns NULL it logs the error above, skips the dictionary/stream/endstream, but still emits endobj. A legal empty stream returns NULL there, so it is treated as a read failure.
Workaround we ship: after dataRepresentation(), find the empty objects (N G obj immediately followed by endobj) that are referenced from a page's /Contents, and append an incremental update that redefines them as << /Length 0 >>\nstream\n\nendstream, with a new xref section and a trailer whose /Prev points at the original startxref. The original bytes are untouched, so signatures/annotations written by PDFKit stay as they are, and files from iOS 18 (which contain no such objects) are returned unchanged.
Filed as FB25282060. If you hit this too, please file a duplicate referencing that number.
Possibly related iOS 27 save-path regressions: https://developer.apple.com/forums/thread/851708 (16-bpc annotation images truncated).
Thanks for such a precise report, and for posting the workaround. Your reproducer made this quick to confirm.
I reproduced it with your sample in macOS 27.0.1 and on an iPad with iPadOS 27.0. After dataRepresentation(), the zero-length stream in the page's /Contents array comes back as an empty object, with or without /FlateDecode. A one-byte stream in the same position saves intact. Core Graphics also rendered the saved page blank, so the problem isn't limited to Acrobat.
If flattening the signature into the page is acceptable for your uploads, there's a workaround that uses only public API. Pass burnInAnnotationsOption to dataRepresentation(options:). On both platforms, that save wrote the page as a single intact content stream. It drew both the text and a free-text annotation into that stream. Flattened annotations are no longer editable, so your incremental update remains the option that keeps annotations editable.
I checked FB25282060, and it's open and under investigation. Please continue to test your software in release and pre-release versions of system software made available to you through your developer.apple.com account. When you do, add details about that into your bug report to keep it up to date. Please see developer.apple.com/download.
Other developers who see the same blank pages after saving a PDF with zero-length streams can file their own bug report and mention FB25282060 in it. Bug Reporting: How and Why? has tips on creating your report.