Skip to content

Commit e7f06af

Browse files
committed
merge bookmarks (/Outlines) when combining, add top level bookmark to first page of each combined document (TODO make this optional), start of bookmark editor form
1 parent 4f206fc commit e7f06af

File tree

7 files changed

+724
-103
lines changed

7 files changed

+724
-103
lines changed

dist/CombinePDF.xlsm

40.9 KB
Binary file not shown.

src/pdfLib.xlsm/Class Modules/pdfDocument.cls

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -616,23 +616,88 @@ Public Function NewOutlines(ByRef parent As pdfValue, Optional ByRef defaults As
616616
Set NewOutlines = NewTopLevelDictionary("/Outlines", parent, defaults)
617617
End Function
618618

619-
' adds a /Outlines to document, specifies top level /Outline item(s)
619+
' add (or sets) top level /Root /Outlines of document
620+
' if anOutlineItem is Nothing then creates default toplevel /Outlines item if doesn't already exist
621+
' otherwise sets Outlines to specified object
622+
Public Sub AddOutlines(Optional ByRef anOutlineItem As pdfValue = Nothing)
623+
If anOutlineItem Is Nothing Then
624+
If Outlines.valueType = PDF_ValueType.PDF_Null Then
625+
Set Outlines = NewOutlines(Nothing)
626+
' don't forget to add to our object cache
627+
Set objectCache(Outlines.ID) = Outlines
628+
End If
629+
Else
630+
Set Outlines = anOutlineItem
631+
End If
632+
End Sub
633+
634+
' follows a linked list and returns pdfValue at end
635+
' item can be obj or obj reference, returns obj reference
636+
Private Function GetEndOfChain(ByRef item As pdfValue, ByRef linkName As String) As pdfValue
637+
Dim nullValue As pdfValue: Set nullValue = pdfValue.NewValue(Null)
638+
Dim obj As pdfValue
639+
Dim nextItem As pdfValue: Set nextItem = item
640+
If nextItem Is Nothing Then Set nextItem = nullValue
641+
' we expect item and nextItem to object references, but we need actual obj to follow linked list
642+
If nextItem.valueType = PDF_ValueType.PDF_Reference Then Set obj = getObject(nextItem.value, nextItem.generation)
643+
Set GetEndOfChain = nextItem.referenceObj
644+
Do While nextItem.valueType = PDF_ValueType.PDF_Dictionary
645+
Dim dict As Dictionary: Set dict = nextItem.asDictionary()
646+
Set nextItem = nullValue
647+
If dict.Exists(linkName) Then
648+
Set nextItem = dict.item(linkName)
649+
If nextItem.valueType = PDF_ValueType.PDF_Reference Then Set obj = getObject(nextItem.value, nextItem.generation)
650+
If nextItem.valueType = PDF_ValueType.PDF_Dictionary Then
651+
Set GetEndOfChain = nextItem.referenceObj
652+
End If
653+
End If
654+
Loop
655+
End Function
656+
657+
658+
' creates a default initialized /Outline item object
659+
' defaults should include /First /Last along with /Count, /Title /Prev /Next and optionally /Dest
660+
Public Function NewOutlineItem(ByRef parent As pdfValue, Optional ByRef defaults As Dictionary) As pdfValue
661+
If defaults Is Nothing Then Set defaults = New Dictionary
662+
With defaults
663+
'If Not .Exists("/Next") Then Set .Item("/Next") = pdfValue.NewValue(Null)
664+
'If Not .Exists("/Prev") Then Set .Item("/Prev") = pdfValue.NewValue(Null)
665+
'If Not .Exists("/Count") Then Set .item("/Count") = pdfValue.NewValue(1)
666+
End With
667+
' Note: /Type /OutlineItem doesn't exist in spec, but should be safe as no /Type specified & additional entries allowed
668+
Set NewOutlineItem = NewTopLevelDictionary("/OutlineItem", parent, defaults)
669+
End Function
670+
671+
' adds a /Outlines to document, specifies parent /Outline
672+
' if parent is Nothing or PDF_Null then will use root /Outline (.Outlines)
620673
' if Outlines is Nothing then will also set top level /Root /Outlines to newOutlines
621-
' IF OutlineItem does not have its /Next and /Prev values set already then adds to end of list
674+
'
675+
' If OutlineItem does not have its /Next and /Prev values set already then adds to end of list
622676
' we update it's /Parent and if needed /First and /Last of /Outline
623677
' OutlineItem's /Count is added to Outline's /Count
624-
Public Sub AddOutlines(Optional ByRef anOutlineItem As pdfValue = Nothing)
678+
Public Sub AddOutlineItem(ByRef parent As pdfValue, Optional ByRef anOutlineItem As pdfValue = Nothing)
625679
' ensure top level dictionary is initialized
680+
If parent Is Nothing Then Set parent = pdfValue.NewValue(Null)
626681
If Outlines.valueType = PDF_ValueType.PDF_Null Then
627682
Set Outlines = NewOutlines(Nothing)
628683
End If
684+
If parent.valueType = PDF_ValueType.PDF_Null Then Set parent = Outlines
629685

630686
' insert a new /OutlineItem into our outlines
631687
If Not anOutlineItem Is Nothing Then
688+
' ensure parent is correctly set
689+
Set anOutlineItem.asDictionary("/Parent") = parent.referenceObj
690+
' see if we need to assign and id and add to our objectcache
691+
If anOutlineItem.ID < 0 Then
692+
anOutlineItem.ID = nextObjId
693+
anOutlineItem.generation = 0
694+
objectCache.Add anOutlineItem.ID, anOutlineItem
695+
End If
696+
632697
' determine if /First and /Last exists or need to be created
633698
' determine if this item is being appended to end or has picked its place
634699
Dim first As pdfValue, last As pdfValue, prevItem As pdfValue, nextItem As pdfValue
635-
With Outlines.asDictionary()
700+
With parent.asDictionary()
636701
If .Exists("/First") Then
637702
Set first = .item("/First")
638703
Else
@@ -699,57 +764,6 @@ Public Sub AddOutlines(Optional ByRef anOutlineItem As pdfValue = Nothing)
699764
End If
700765
End Sub
701766

702-
' follows a linked list and returns pdfValue at end
703-
' item can be obj or obj reference, returns obj reference
704-
Private Function GetEndOfChain(ByRef item As pdfValue, ByRef linkName As String) As pdfValue
705-
Dim nullValue As pdfValue: Set nullValue = pdfValue.NewValue(Null)
706-
Dim obj As pdfValue
707-
Dim nextItem As pdfValue: Set nextItem = item
708-
If nextItem Is Nothing Then Set nextItem = nullValue
709-
' we expect item and nextItem to object references, but we need actual obj to follow linked list
710-
If nextItem.valueType = PDF_ValueType.PDF_Reference Then Set obj = getObject(nextItem.value, nextItem.generation)
711-
Set GetEndOfChain = nextItem.referenceObj
712-
Do While nextItem.valueType = PDF_ValueType.PDF_Dictionary
713-
Dim dict As Dictionary: Set dict = nextItem.asDictionary()
714-
Set nextItem = nullValue
715-
If dict.Exists(linkName) Then
716-
Set nextItem = dict.item(linkName)
717-
If nextItem.valueType = PDF_ValueType.PDF_Reference Then Set obj = getObject(nextItem.value, nextItem.generation)
718-
If nextItem.valueType = PDF_ValueType.PDF_Dictionary Then
719-
Set GetEndOfChain = nextItem.referenceObj
720-
End If
721-
End If
722-
Loop
723-
End Function
724-
725-
726-
' creates a default initialized /Outline item object
727-
' defaults should include /First /Last along with /Count, /Title /Prev /Next and optionally /Dest
728-
Public Function NewOutlineItem(ByRef parent As pdfValue, Optional ByRef defaults As Dictionary) As pdfValue
729-
If defaults Is Nothing Then Set defaults = New Dictionary
730-
With defaults
731-
'If Not .Exists("/Next") Then Set .Item("/Next") = pdfValue.NewValue(Null)
732-
'If Not .Exists("/Prev") Then Set .Item("/Prev") = pdfValue.NewValue(Null)
733-
'If Not .Exists("/Count") Then Set .item("/Count") = pdfValue.NewValue(1)
734-
End With
735-
' Note: /Type /OutlineItem doesn't exist in spec, but should be safe as no /Type specified & additional entries allowed
736-
Set NewOutlineItem = NewTopLevelDictionary("/OutlineItem", parent, defaults)
737-
End Function
738-
739-
' adds a /Outlines to document, specifies top level /Outline item(s)
740-
' if Outlines is Nothing then will also set top level /Root /Outlines to newOutlines
741-
Public Sub AddOutlineItem(ByRef parent As pdfValue, Optional ByRef anOutlineItem As pdfValue = Nothing)
742-
' ensure top level dictionary is initialized
743-
If Outlines.valueType = PDF_ValueType.PDF_Null Then
744-
Set Outlines = NewOutlines(Nothing)
745-
End If
746-
747-
' insert a new /OutlineItem into our outlines
748-
If Not anOutlineItem Is Nothing Then
749-
' create /First and /Last if needed, otherwise update /Last and /Count and /Next & /Prev
750-
End If
751-
End Sub
752-
753767

754768

755769
' updates id of all objects under root pdfValue beginning at baseId
@@ -1111,7 +1125,7 @@ Sub SavePdfObject(ByRef outputFileNum As Integer, ByRef obj As pdfValue, _
11111125
ByRef offset As Long, Optional ByVal baseId As Long = 0, Optional ByVal prettyPrint As Boolean = True)
11121126
On Error GoTo errHandler
11131127

1114-
If False And (obj.valueType = PDF_ValueType.PDF_Stream) Then
1128+
If (obj.valueType = PDF_ValueType.PDF_Stream) Then
11151129
' see if its a stream object stream (any other object stream written out asis)
11161130
If obj.asDictionary.Exists("/Type") Then
11171131
Dim pdfNameValue As pdfValue
@@ -1130,6 +1144,35 @@ errHandler:
11301144
Stop
11311145
End Sub
11321146

1147+
1148+
' save all nested /Outlines objects beginning with outlineParentObj
1149+
' pass in .Outlines to save full outline hierarchy, or any subtree as needed
1150+
Sub SaveOutlinePdfObjects(ByRef outputFileNum As Integer, ByRef outlineParentObj As pdfValue, _
1151+
ByRef offset As Long, Optional ByVal baseId As Long = 0, Optional ByVal prettyPrint As Boolean = True)
1152+
On Error GoTo ErrHandler
1153+
SavePdfObject outputFileNum, outlineParentObj, offset, baseId, prettyPrint
1154+
If outlineParentObj.hasKey("/First") Then
1155+
Dim outlineObj As pdfValue
1156+
Set outlineObj = outlineParentObj.asDictionary().item("/First")
1157+
' next line should always be true, only an indirect reference not object itself
1158+
If outlineObj.valueType = PDF_ValueType.PDF_Reference Then Set outlineObj = getObject(outlineObj.value, outlineObj.generation)
1159+
Do While Not outlineObj Is Nothing
1160+
' recursively save outline tree
1161+
SaveOutlinePdfObjects outputFileNum, outlineObj, offset, baseId, prettyPrint
1162+
If outlineObj.hasKey("/Next") Then
1163+
Set outlineObj = outlineObj.asDictionary().item("/Next")
1164+
Set outlineObj = getObject(outlineObj.value, outlineObj.generation)
1165+
Else
1166+
Set outlineObj = Nothing
1167+
End If
1168+
Loop
1169+
End If
1170+
Exit Sub
1171+
ErrHandler:
1172+
Debug.Print "Error: " & Err.Description & " (" & Err.Number & ")"
1173+
Stop
1174+
End Sub
1175+
11331176
' given current position in file, updates cross reference table and output serialized object
11341177
' for each object in pdfObjs dictionary.
11351178
' Note: assumes PDF Objects all have valid id values (not modified)

src/pdfLib.xlsm/Class Modules/pdfValue.cls

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ Private m_referenceObj As pdfValue
2121
' used to help encode certain information about how to represent
2222
Public Enum ValueFlags
2323
flgNone = 0
24-
flgUtf8 = 1
24+
flgUtf8BOM = 1
2525
flgBinary = 2
2626
End Enum
2727

2828
Public flags As ValueFlags
2929

3030

3131
' keep our referencObj in sync
32-
Public Property Let id(ByVal newId As Long)
32+
Public Property Let ID(ByVal newId As Long)
3333
m_id = newId
3434
If valueType = PDF_ValueType.PDF_Object Then
3535
If Not referenceObj Is Nothing Then
@@ -195,7 +195,7 @@ Public Function serialize(Optional ByVal baseId As Long = 0) As Byte()
195195
Case PDF_ValueType.PDF_Null
196196
objStr = "null"
197197
Case PDF_ValueType.PDF_Name
198-
objStr = pdfDocument.EscapeName(value.value, value.flags And flgUtf8) ' see below where Dictionary Key Name encoded
198+
objStr = pdfDocument.EscapeName(value.value, value.flags And flgUtf8BOM) ' see below where Dictionary Key Name encoded
199199
Case PDF_ValueType.PDF_Boolean
200200
If value.value Then
201201
objStr = "true"
@@ -234,8 +234,10 @@ Public Function serialize(Optional ByVal baseId As Long = 0) As Byte()
234234
If typeName(v) = "String" Then
235235
key = CStr(v)
236236
Else 'If typeName(v) = "pdfValue" Then ' assume pdfValue with valueType=PDF_Name
237-
key = pdfDocument.EscapeName(v.value, v.flags And flgUtf8)
237+
key = CStr(v.value)
238238
End If
239+
' key is always a /Name and should be considered UTF8 without BOM
240+
key = pdfDocument.EscapeName(key, addUtf8BOM:=False)
239241
Set pv = dict.item(v) ' v may be pdfValue or String version of key
240242
objStr = objStr & key & " "
241243
objStr = objStr & BytesToString(pv.serialize(baseId))
@@ -294,10 +296,10 @@ End Function
294296
' can be used by predeclared object, e.g. pdfValue.NewValueObj("/MyName", "/Name")
295297

296298
' returns a name as a pdfValue /Name obj
297-
Function NewNameValue(ByVal name As String, Optional ByVal utf8 As Boolean = False) As pdfValue
298-
If Left$(name, 1) <> "/" Then name = "/" & name
299-
Set NewNameValue = NewValue(name, "/Name")
300-
If utf8 Then NewNameValue.flags = flgUtf8
299+
Function NewNameValue(ByVal Name As String, Optional ByVal utf8BOM As Boolean = False) As pdfValue
300+
If Left$(Name, 1) <> "/" Then Name = "/" & Name
301+
Set NewNameValue = NewValue(Name, "/Name")
302+
If utf8BOM Then NewNameValue.flags = flgUtf8BOM
301303
End Function
302304

303305

0 commit comments

Comments
 (0)