Fundamental error in PDFOutline created syntax

PDFKit's PDFOutline creates fundamentally flawed PDF syntax when it creates outlines ("Bookmarks") in a PDF document.

A PDF document is created with three outlines: "Page 1", "Page 2" and "Page 3". Each is a child of the former, so "Page 3" is a child of "Page 2", and "Page 2" is a child of "Page 1".

PDFKit produces flawed PDF syntax, with duplicate entries for each bookmark, missing sibling links, and a convoluted Parent > Child relationship.

3 0 obj
<< /Type /Pages /Parent 45 0 R /Count 8 /Kids [ 2 0 R 6 0 R 9 0 R 12 0 R 15 0 R
18 0 R 21 0 R 24 0 R ] >>
endobj
28 0 obj
<< /Type /Pages /Parent 45 0 R /Count 3 /Kids [ 27 0 R 31 0 R 34 0 R ] >>
endobj
45 0 obj
<< /Type /Pages /MediaBox [0 0 612 792] /Count 11 /Kids [ 3 0 R 28 0 R ] >>
endobj
46 0 obj
<< /Type /Catalog /Outlines 1 0 R /Pages 45 0 R >>
endobj
1 0 obj
<< /First 47 0 R /Last 48 0 R >>
endobj
48 0 obj
<< /Last 49 0 R /Count -1 /First 50 0 R /Title (Page 1) /Dest [ 2 0 R /XYZ
0 841.88 null ] /Parent 51 0 R >>
endobj
51 0 obj
<< >>
endobj
50 0 obj
<< /Last 52 0 R /Count -1 /First 53 0 R /Title (Page 2) /Dest [ 6 0 R /XYZ
0 841.88 null ] /Parent 51 0 R >>
endobj
53 0 obj
<< /Dest [ 9 0 R /XYZ 0 841.88 null ] /Count 0 /Title (Page 3) /Parent 51 0 R
>>
endobj
52 0 obj
<< /Dest [ 9 0 R /XYZ 0 841.88 null ] /Count 0 /Title (Page 3) /Parent 51 0 R
>>
endobj
49 0 obj
<< /Last 52 0 R /Count -1 /First 53 0 R /Title (Page 2) /Dest [ 6 0 R /XYZ
0 841.88 null ] /Parent 51 0 R >>
endobj
47 0 obj
<< /Last 49 0 R /Count -1 /First 50 0 R /Title (Page 1) /Dest [ 2 0 R /XYZ
0 841.88 null ] /Parent 51 0 R >>
endobj

(The Parent of all the outline objects is given as object 51, which is blank. The First/Last objects don't link correctly to the expected hierarchy, etc, etc.)

Acrobat preflight will flag this as a syntax error, though the bookmarks do largely function in most viewers.

Preview doesn't show the top-level outline, though that might relate to FB4013726, filed in 2018.

I have shown the created syntax to ChatGPT, which also confirmed that the syntax does not conform to the expected PDF spec:

This isn’t simply a matter of PDFKit choosing an unnecessarily elaborate but valid representation. There are genuine structural problems:

  1. The top-level Page 1 object’s /Parent does not point to the /Outlines dictionary.
  2. The child objects’ /Parent does not point to their actual containing outline.
  3. There are duplicate outline objects for Page 1, Page 2 and Page 3.
  4. The required sibling Prev/Next chains are absent.
  5. The /First//Last relationships don’t correspond cleanly to the claimed hierarchy.

The errors are entirely reproducible in any PDFOutlines created by PDFKit.

I have filed this as FB25328043.

... and in 2021 as FB8960602.

... and in 2019 as FB5313583.

Answered by DTS Engineer in 913721022

Thanks for the detailed report, including the comparison with another app's output. I reproduced this in macOS 27.0.1. I built a new PDFDocument with three nested PDFOutline items and saved it with write(to:). Checking the saved file with the Core Graphics PDF parser showed the same problems:

  • Every item's /Parent is an empty dictionary, not the outline dictionary or the item's actual parent.
  • Each parent has one child, but its /First and /Last point to different objects.
  • In a flat outline of three items, following /First and /Next reaches all three, but following /Last and /Prev reaches only two.

PDFKit and Core Graphics both read the saved outline back as the intended tree. That fits your observation that the bookmarks still work in most viewers.

I also tried CGPDFContextSetOutline as a workaround, using the outline dictionary format described in CGPDFDocument.h. Its outline has correct /Next and /Prev links and no duplicate items. However, none of its items has a /Parent, and the outline dictionary's /Count is negative. ISO 32000-1, published in 2008, is the PDF 1.7 standard. In its section 12.3.3, Table 153 requires /Parent in every outline item, and Table 152 says the outline dictionary's /Count "cannot be negative." In my tests, PDFKit and Core Graphics both marked their files as PDF 1.3. The PDF Reference for version 1.3, published in 2000, has the same /Parent requirement in section 7.2.2, Table 7.4. So neither public API I tried writes a conforming outline.

FB25328043 is 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.

Here's the syntax for the same bookmarks from a PDF app that doesn't use PDFKit:

99 0 obj
<</Type/Outlines/First 100 0 R /Last 102 0 R /Count 3>>
endobj
100 0 obj
<</Title(Page 1)/F 0/C[ 0 0 0]/A 96 0 R /Count 2/Parent 99 0 R /First 101 0 R /Last 101 0 R >>
endobj
101 0 obj
<</Title(Page 2)/F 0/C[ 0 0 0]/A 97 0 R /Count 1/Parent 100 0 R /First 102 0 R /Last 102 0 R >>
endobj
102 0 obj
<</Title(Page 3)/F 0/C[ 0 0 0]/A 98 0 R /Parent 101 0 R >>
endobj

Each cild clearly states its Parent, and each Parent has a clear First/Last statement of its children.

Only one object for each bookmark.

The numbers are well ordered.

Thanks for the detailed report, including the comparison with another app's output. I reproduced this in macOS 27.0.1. I built a new PDFDocument with three nested PDFOutline items and saved it with write(to:). Checking the saved file with the Core Graphics PDF parser showed the same problems:

  • Every item's /Parent is an empty dictionary, not the outline dictionary or the item's actual parent.
  • Each parent has one child, but its /First and /Last point to different objects.
  • In a flat outline of three items, following /First and /Next reaches all three, but following /Last and /Prev reaches only two.

PDFKit and Core Graphics both read the saved outline back as the intended tree. That fits your observation that the bookmarks still work in most viewers.

I also tried CGPDFContextSetOutline as a workaround, using the outline dictionary format described in CGPDFDocument.h. Its outline has correct /Next and /Prev links and no duplicate items. However, none of its items has a /Parent, and the outline dictionary's /Count is negative. ISO 32000-1, published in 2008, is the PDF 1.7 standard. In its section 12.3.3, Table 153 requires /Parent in every outline item, and Table 152 says the outline dictionary's /Count "cannot be negative." In my tests, PDFKit and Core Graphics both marked their files as PDF 1.3. The PDF Reference for version 1.3, published in 2000, has the same /Parent requirement in section 7.2.2, Table 7.4. So neither public API I tried writes a conforming outline.

FB25328043 is 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.