Skip to content

Commit 651e132

Browse files
committed
refactor so singleton like objects used for pdf obj and their references in the pdf files (vba objects with a non-zero id) when loading existing pdf documents. Allows simple renumbering of objects (all references automatically updated).
1 parent 052b774 commit 651e132

18 files changed

+1560
-1408
lines changed

dist/CombinePDF.xlsm

13.7 KB
Binary file not shown.

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

Lines changed: 70 additions & 70 deletions
Large diffs are not rendered by default.

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

Lines changed: 1159 additions & 71 deletions
Large diffs are not rendered by default.

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Public Property Get Length() As Long
2323
' Length is stored in meta
2424
' it should be same as size of data
2525
If Meta.Exists("/Length") Then
26-
Length = CLng(Meta.Item("/Length").Value)
26+
Length = CLng(Meta.Item("/Length").value)
2727
End If
2828

2929
On Error Resume Next
@@ -39,7 +39,7 @@ End Property
3939

4040
' this will add/update /Length property in meta and resize data()
4141
Public Property Let Length(ByVal Count As Long)
42-
Meta("/Length").Value = Count ' add or replace
42+
Meta("/Length").value = Count ' add or replace
4343

4444
On Error Resume Next
4545
Dim dataLen As Long
@@ -54,7 +54,7 @@ End Property
5454

5555
' returns stream data [as stored, ie. compressed] as a Byte array
5656
Public Property Get data() As Byte()
57-
data = stream_data.Value
57+
data = stream_data.value
5858
End Property
5959

6060
' returns stream data in uncompressed form as Byte array
@@ -66,13 +66,13 @@ Public Property Get udata() As Byte()
6666
If Meta.Exists("/Filter") Then
6767
' TODO support all /Filter types
6868
Dim filter As String
69-
filter = Meta.Item("/Filter").Value
69+
filter = Meta.Item("/Filter").value
7070
Select Case LCase(filter)
7171
Case "/flatedecode"
7272
Dim startIndex As Long: startIndex = 2 ' skip past zlib wrapper to raw Deflate data
7373
Dim outSize As Long ' as count of bytes -- should be 0 on entry so will be size on output
7474
Dim estBufSize As Long ' if known, uncompressed size
75-
If Meta.Exists("/DL") Then estBufSize = CLng(Meta.Item("/DL").Value) ' only a hint
75+
If Meta.Exists("/DL") Then estBufSize = CLng(Meta.Item("/DL").value) ' only a hint
7676
Erase m_udata
7777

7878
' Note: libdeflate_inflate arguments are byVal, we expect them be byRef
@@ -90,15 +90,15 @@ Public Property Get udata() As Byte()
9090
Dim pdfV As pdfValue
9191
Set pdfV = Meta.Item("/DecodeParms")
9292
Dim decodeParms As Dictionary
93-
Set decodeParms = pdfV.Value
93+
Set decodeParms = pdfV.value
9494
If decodeParms.Exists("/Predictor") Then
9595
Set pdfV = decodeParms.Item("/Predictor")
96-
predictor = pdfV.Value
96+
predictor = pdfV.value
9797
End If
9898
' should only be supplied if predictor > 1, but we can load value regardless, only used if predictor > 1
9999
If decodeParms.Exists("/Columns") Then
100100
Set pdfV = decodeParms.Item("/Columns")
101-
columns = pdfV.Value
101+
columns = pdfV.value
102102
End If
103103
Set pdfV = Nothing
104104
End If
@@ -184,7 +184,7 @@ End Property
184184

185185
' returns meta data as a VBA Dictionary object
186186
Public Property Get Meta() As Dictionary
187-
Set Meta = stream_meta.Value
187+
Set Meta = stream_meta.value
188188
End Property
189189

190190
Public Sub Init(ByRef Meta As pdfValue, ByRef data As pdfValue)

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

Lines changed: 180 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,85 @@ END
55
Attribute VB_Name = "pdfValue"
66
Attribute VB_GlobalNameSpace = False
77
Attribute VB_Creatable = False
8-
Attribute VB_PredeclaredId = False
8+
Attribute VB_PredeclaredId = True
99
Attribute VB_Exposed = True
1010
' stores a PDF value type
1111
Option Explicit
1212

13-
Public id As Long
14-
Public generation As Long
13+
Private m_id As Long
14+
Private m_generation As Long
1515
Public valueType As PDF_ValueType
16-
Public Value As Variant
16+
Public value As Variant
1717

18+
' to help maintain id across indirect references, PDF_Object provides a PDF_Reference object to be used by others
19+
Private m_referenceObj As pdfValue
1820

21+
22+
' keep our referencObj in sync
23+
Public Property Let id(ByVal newId As Long)
24+
m_id = newId
25+
If valueType = PDF_ValueType.PDF_Object Then
26+
If Not referenceObj Is Nothing Then
27+
referenceObj.value = m_id
28+
End If
29+
End If
30+
End Property
31+
Public Property Get id() As Long
32+
id = m_id
33+
End Property
34+
35+
' keep our referencObj in sync
36+
Public Property Let generation(ByVal newGeneration As Long)
37+
m_generation = newGeneration
38+
If valueType = PDF_ValueType.PDF_Object Then
39+
If Not m_referenceObj Is Nothing Then
40+
m_referenceObj.generation = m_generation
41+
End If
42+
End If
43+
End Property
44+
Public Property Get generation() As Long
45+
generation = m_generation
46+
End Property
47+
48+
49+
' returns an indirect reference object to self
50+
Friend Property Set referenceObj(ByRef refObj As pdfValue)
51+
Set m_referenceObj = refObj
52+
End Property
53+
Public Property Get referenceObj() As pdfValue
54+
' create if not already exists
55+
If m_referenceObj Is Nothing Then
56+
Set m_referenceObj = pdfValue.NewValue(Me, "/Reference")
57+
End If
58+
59+
' always return same object so all references updated if id changes
60+
Set referenceObj = m_referenceObj
61+
End Property
62+
63+
64+
#If False Then
1965
' is this merely an object containing a pdf dictionary? i.e. is Value a pdfValue of PDF_Dictionary
2066
Function isDictionaryObject() As Boolean
21-
If (valueType = PDF_ValueType.PDF_Object) And (Not Value Is Nothing) Then
22-
Dim pdfDict As pdfValue: Set pdfDict = Value
67+
If (valueType = PDF_ValueType.PDF_Object) And (Not value Is Nothing) Then
68+
Dim pdfDict As pdfValue: Set pdfDict = value
2369
isDictionaryObject = (pdfDict.valueType = PDF_ValueType.PDF_Dictionary)
2470
End If
2571
End Function
72+
#End If
2673

2774
' returns vba Dictionary instance contained in pdf dictionary
28-
' Warning: returns Nothing if object lacks dictionary to return
75+
' Warning: returns Nothing if object lacks dictionary to return, hasKey()
2976
Function asDictionary() As Dictionary
3077
On Error GoTo errHandler
3178
If ((valueType = PDF_ValueType.PDF_Object) Or (valueType = PDF_ValueType.PDF_Dictionary) Or (valueType = PDF_ValueType.PDF_Trailer)) _
32-
And (Not IsEmpty(Value)) Then
79+
And (Not IsEmpty(value)) Then
3380
Dim pdfDict As pdfValue
3481
If (valueType = PDF_ValueType.PDF_Dictionary) Then
3582
Set pdfDict = Me
3683
Else
37-
Set pdfDict = Value
84+
Set pdfDict = value
3885
End If
39-
Set asDictionary = pdfDict.Value
86+
Set asDictionary = pdfDict.value
4087
End If
4188
Exit Function
4289
errHandler:
@@ -45,6 +92,20 @@ errHandler:
4592
Resume
4693
End Function
4794

95+
' returns True if Dictionary object contains key
96+
Function hasKey(ByRef key As Variant) As Boolean
97+
On Error GoTo errHandler
98+
Dim dict As Dictionary
99+
Set dict = asDictionary()
100+
If Not dict Is Nothing Then hasKey = dict.Exists(key)
101+
'Else return False
102+
Exit Function
103+
errHandler:
104+
Debug.Print Err.Description & " (" & Err.Number & ")"
105+
Stop
106+
Resume
107+
End Function
108+
48109

49110
' Helper function to convert pdf string to byte array (0..N-1)
50111
' escaping values as needed
@@ -111,41 +172,41 @@ End Function
111172

112173

113174
' convert pdf Value (Me) into a Byte() array as stored in pdf file
114-
Function serialize(Optional ByVal baseId As Long = 0) As Byte()
175+
Public Function serialize(Optional ByVal baseId As Long = 0) As Byte()
115176
On Error GoTo errHandler
116-
Dim Value As pdfValue: Set Value = Me
177+
Dim value As pdfValue: Set value = Me
117178
Dim objStr As String: objStr = vbNullString
118179
Dim objBytes() As Byte
119180
Dim IsBytes As Boolean ' for most we convert at end, but stream we leave as Byte()
120181
Dim v As Variant
121182
Dim pv As pdfValue
122183
Dim firstPass As Boolean
123184

124-
Select Case Value.valueType
185+
Select Case value.valueType
125186
Case PDF_ValueType.PDF_Null
126187
objStr = "null"
127188
Case PDF_ValueType.PDF_Name
128-
objStr = Value.Value
189+
objStr = value.value
129190
Case PDF_ValueType.PDF_Boolean
130-
If Value.Value Then
191+
If value.value Then
131192
objStr = "true"
132193
Else
133194
objStr = "false"
134195
End If
135196
Case PDF_ValueType.PDF_Integer
136-
objStr = Format(CLng(Value.Value), "0")
197+
objStr = Format(CLng(value.value), "0")
137198
Case PDF_ValueType.PDF_Real
138-
objStr = CDbl(Value.Value) ' dont' format as we want all current digits stored, it won't add extra 0s anyway
199+
objStr = CDbl(value.value) ' dont' format as we want all current digits stored, it won't add extra 0s anyway
139200
' ensure has .0 if whole number
140201
If InStr(1, objStr, ".", vbBinaryCompare) < 1 Then objStr = objStr & ".0"
141202
Case PDF_ValueType.PDF_String
142203
' convert to bytes and escape values as needed
143204
IsBytes = True
144-
objBytes = pdfStringToBytes(Value.Value)
205+
objBytes = pdfStringToBytes(value.value)
145206
Case PDF_ValueType.PDF_Array
146207
objStr = "[ "
147208
firstPass = True
148-
For Each v In Value.Value
209+
For Each v In value.value
149210
Set pv = v
150211
If Not firstPass Then objStr = objStr & " "
151212
firstPass = False
@@ -155,44 +216,50 @@ Function serialize(Optional ByVal baseId As Long = 0) As Byte()
155216
Case PDF_ValueType.PDF_Dictionary
156217
objStr = "<<" & vbLf
157218
Dim dict As Dictionary
158-
Set dict = Value.Value
219+
Set dict = value.value
159220
firstPass = True
160221
For Each v In dict.Keys
161-
Set pv = dict.Item(v)
162222
If Not firstPass Then objStr = objStr & vbLf
163223
firstPass = False
164-
objStr = objStr & CStr(v) & " "
224+
Dim key As String
225+
If TypeName(v) = "String" Then
226+
key = CStr(v)
227+
Else ' assume pdfValue with valueType=PDF_Name
228+
key = CStr(v.value)
229+
End If
230+
Set pv = dict.Item(key)
231+
objStr = objStr & key & " "
165232
objStr = objStr & BytesToString(pv.serialize(baseId))
166233
Next v
167234
If Right(objStr, 1) <> vbLf Then objStr = objStr & vbLf
168235
objStr = objStr & ">>" & vbLf
169236
Case PDF_ValueType.PDF_Stream ' actual stream object with dictionary and data
170237
Dim stream As pdfStream
171-
Set stream = Value.Value
238+
Set stream = value.value
172239
IsBytes = True
173240
objBytes = stream.stream_meta.serialize(baseId)
174241
CopyBytes stream.stream_data.serialize(baseId), objBytes, 0, UBound(objBytes) + 1
175242
Case PDF_ValueType.PDF_StreamData ' represents only stream ... endstream portion
176243
IsBytes = True
177244
objBytes = StringToBytes("stream" & vbLf)
178-
CopyBytes Value.Value, objBytes, 0, UBound(objBytes) + 1
245+
CopyBytes value.value, objBytes, 0, UBound(objBytes) + 1
179246
CopyBytes StringToBytes(vbLf & "endstream" & vbLf), objBytes, 0, UBound(objBytes) + 1
180247

181248
' to simplify processing, not one of 9 basic types either
182249
Case PDF_ValueType.PDF_Object ' id generation obj << dictionary >> endobj
183250
IsBytes = True
184-
objBytes = StringToBytes(baseId + Value.id & " " & Value.generation & " obj" & vbLf)
185-
Dim pdfObj As pdfValue: Set pdfObj = Value.Value
251+
objBytes = StringToBytes(baseId + value.id & " " & value.generation & " obj" & vbLf)
252+
Dim pdfObj As pdfValue: Set pdfObj = value.value
186253
CopyBytes pdfObj.serialize(baseId), objBytes, 0, UBound(objBytes) + 1
187254
If objBytes(UBound(objBytes)) <> 10 Then CopyBytes StringToBytes(vbLf), objBytes, 0, UBound(objBytes) + 1
188255
CopyBytes StringToBytes("endobj" & vbLf), objBytes, 0, UBound(objBytes) + 1
189256
Case PDF_ValueType.PDF_Reference ' indirect object
190257
' Note: if indirect reference to /Parent and that obj is not in current set, will have wrong id, offset correct id by -baseId in Reference object prior to saving
191-
objStr = baseId + Value.Value & " " & Value.generation & " R"
258+
objStr = baseId + value.value & " " & value.generation & " R"
192259
Case PDF_ValueType.PDF_Comment
193-
objStr = Value.Value & vbLf
260+
objStr = value.value & vbLf
194261
Case PDF_ValueType.PDF_Trailer
195-
Dim pdfTrailer As pdfValue: Set pdfTrailer = Value.Value
262+
Dim pdfTrailer As pdfValue: Set pdfTrailer = value.value
196263
objStr = "trailer" & vbLf
197264
objStr = objStr & BytesToString(pdfTrailer.serialize(baseId))
198265
Case Else
@@ -212,3 +279,87 @@ errHandler:
212279
Resume
213280
End Function
214281

282+
283+
284+
' construction methods, these return new instances of pdfValue
285+
' can be used by predeclared object, e.g. pdfValue.NewValueObj("/MyName", "/Name")
286+
287+
' returns a name as a pdfValue /Name obj
288+
Function NewNameValue(ByVal name As String) As pdfValue
289+
Set NewNameValue = NewValue(name, "/Name")
290+
End Function
291+
292+
293+
' returns value as a pdfValue obj
294+
' Note: if value is String then valueType can be used if want a PDF_Name or PDF_Trailer object instead of PDF_String
295+
' a Dictionary returns a PDF_Dictionary and a Collection returns as PDF_Array
296+
Function NewValue(ByRef value As Variant, Optional ByRef valueType As String = vbNullString, Optional ByVal id As Long = 0) As pdfValue
297+
Dim obj As pdfValue
298+
Set obj = New pdfValue
299+
300+
' initialize indirect reference object instance to be shared
301+
If id <> 0 Then
302+
obj.id = id
303+
Set obj.referenceObj = NewValue(obj, "/Reference")
304+
End If
305+
306+
' set obj's value and type information
307+
Select Case VarType(value)
308+
Case vbNull
309+
' always return same instance for Null
310+
Static NullValue As pdfValue
311+
If NullValue Is Nothing Then
312+
obj.valueType = PDF_ValueType.PDF_Null
313+
Set NullValue = obj
314+
Else
315+
Set obj = NullValue
316+
End If
317+
Case vbLong, vbInteger
318+
obj.valueType = PDF_ValueType.PDF_Integer
319+
obj.value = CLng(value)
320+
Case vbSingle, vbDouble
321+
obj.valueType = PDF_ValueType.PDF_Real
322+
obj.value = CDbl(value)
323+
Case vbBoolean
324+
obj.valueType = PDF_ValueType.PDF_Boolean
325+
obj.value = CBool(value)
326+
Case vbString
327+
Select Case valueType
328+
Case "/Name"
329+
obj.valueType = PDF_ValueType.PDF_Name
330+
' ensure /Name object begin with a /
331+
If Left(value, 1) <> "/" Then value = "/" & value
332+
obj.value = value
333+
Case Else
334+
obj.valueType = PDF_ValueType.PDF_String
335+
obj.value = CStr(value)
336+
End Select
337+
Case vbObject
338+
Select Case TypeName(value)
339+
Case "Dictionary"
340+
obj.valueType = PDF_ValueType.PDF_Dictionary
341+
Set obj.value = value
342+
Case "Collection"
343+
obj.valueType = PDF_ValueType.PDF_Array
344+
Set obj.value = value
345+
Case "pdfValue"
346+
Select Case valueType
347+
Case "/Trailer"
348+
obj.valueType = PDF_ValueType.PDF_Trailer
349+
Set obj.value = value
350+
Case "/Reference"
351+
obj.valueType = PDF_ValueType.PDF_Reference
352+
obj.value = value.id
353+
obj.generation = value.generation
354+
Case Else ' default to "/Object"
355+
obj.valueType = PDF_ValueType.PDF_Object
356+
Set obj.value = value
357+
End Select
358+
Case Else
359+
Stop ' ???
360+
End Select
361+
End Select
362+
363+
Set NewValue = obj
364+
End Function
365+

0 commit comments

Comments
 (0)