-
-
Notifications
You must be signed in to change notification settings - Fork 191
/
pdImage.cls
1129 lines (842 loc) · 46.4 KB
/
pdImage.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "pdImage"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'***************************************************************************
'PhotoDemon Image class
'Copyright ©2006-2014 by Tanner Helland
'Created: sometime 2006
'Last updated: 08/April/14
'Last update: change read/write internal data functions to support direct String access (instead of files only). This is necessary
' for reading/writing layered images in PDI format.
'
'The pdImage class is used to store information on each image loaded by the user. One copy of this
' class exists for each loaded image. These copies are stored in the pdImages() array, which is declared
' in the MDIWindow module.
'
'The purpose of this class is to store a bunch of variables related to a given image - things like size, zoom,
' name, file path, format containing form, Undo/Redo tracking - as well as an array of pdLayer objects, which
' comprise the actual image data.
'
'All source code in this file is licensed under a modified BSD license. This means you may use the code in your own
' projects IF you provide attribution. For more information, please visit http://photodemon.org/about/license/
'
'***************************************************************************
Option Explicit
'Each active image in the program exists within one of these pdImage objects. This class stores all relevant information
' about the image, including references to its layers, metadata, Undo/Redo, and much more.
'This ID number matches the .Tag value of the containing form.
Public imageID As Long
'Is the form associated with this image still loaded?
Public IsActive As Boolean
'Was the image loaded successfully?
Public loadedSuccessfully As Boolean
'Image dimensions
Public Width As Long
Public Height As Long
'Image resolution (in DPI). In general, we don't deal with differing x/y resolutions - in that case,
' use the imageDPI value, which will return the average resolution of the two.
Private xResolution As Double
Private yResolution As Double
Private imageDPI As Double
'Current index of the zoom combo box
Public currentZoomValue As Long
'Size of the file when loaded
Public originalFileSize As Long
'Original name of the image
Public originalFileName As String
'Original name of the image AND its extension
Public originalFileNameAndExtension As String
'Original path of the image (full path, including filename); blank if image does not exist on disk
Public locationOnDisk As String
'Original image format. This is derived from the equivalent FreeImage constant - see the enum for FREE_IMAGE_FORMAT for details
Public originalFileFormat As Long
'Current file format. For example, if the user loads a .BMP file and uses "Save As" to save it as a .JPG, this variable will
' represent "JPG" - while OriginalFileFormat will still represent "BMP". (OriginalFileFormat is only set once, at load-time.)
Public currentFileFormat As Long
'Original color depth (a BPP value, most commonly 24 or 32, may be 8)
Public originalColorDepth As Long
'saveParameters let us save the image using settings the user has already specified (so we don't have to pester
' them every time they save)
Public saveParameters As String
'For JPEGs specifically, has a quality value been set? If it hasn't, the user needs to be prompted at least once.
Public hasSeenJPEGPrompt As Boolean
'For JPEG-2000 files, has a compression ratio been set? If it hasn't, the user needs to be prompted at least once.
Public hasSeenJP2Prompt As Boolean
'For WEBP files, has a compression ratio been set? If it hasn't, the user needs to be prompted at least once.
Public hasSeenWebPPrompt As Boolean
'For JPEG XR files, has a compression ratio been set? If it hasn't, the user needs to be prompted at least once.
Public hasSeenJXRPrompt As Boolean
'For PNG files, if the original PNG file has a background color, it will be stored here.
Public pngBackgroundColor As Long
'Has this image been saved? Access this variable via the getSaveState and setSaveState functions.
Private hasBeenSaved As Boolean
'A copy of the form's current icon
Public curFormIcon32 As Long, curFormIcon16 As Long
'If this image is simply being used for internal PhotoDemon processing, hide it from the user
Public forInternalUseOnly As Boolean
'Image's back buffer (also a DIB; this is used to hold the final composite before everything is rendered on-screen
Public backBuffer As pdDIB
'If this image is 32bpp then it requires a specialized compositing layer for rendering the transparency
Public alphaFixDIB As pdDIB
'Image's selection data (stored and handled by a mainSelection object)
Public mainSelection As pdSelection
Public selectionActive As Boolean
'Metadata handler/storage class. This class is automatically filled with an image file's metadata at load-time.
' NOTE: EXIFTOOL IS REQUIRED FOR METADATA HANDLING.
Public imgMetadata As pdMetadata
'All Undo/Redo actions are handled through this publicly available Undo/Redo handler class
Public undoManager As pdUndo
'Various viewport actions are now handled through a separate pdViewport class
Public imgViewport As pdViewport
'Image layers! All layers are stored in this array.
Private imgLayers() As pdLayer
'Current layer. Layers in PD are zero-based, with the 0 layer being created by default when a pdImage is created.
' To ensure proper behavior for things like caching viewports, this value is not publicly accessible - you must access
' it via the relevant get/set functions.
Private curLayer As Long
'Total number of layers EVER created for this image. This is used to assign canonical layer IDs to individual layers.
' It has no relation to layer position or order, and will be constant for the life of both the layer itself, and this
' pdImage object (including persistence when saving to/from file).
Private numOfLayersEverCreated As Long
'Number of ACTIVE layers in the current image. This is equal to or less than numOfLayers, above. Note that it is not
' zero-based; e.g. right after an image file is loaded, this will be set to 1.
Private numOfLayers As Long
'Some API functions are used to simplify the process of things like merging layers
Private Declare Function UnionRect Lib "user32" (ByRef lpDestRect As RECT, ByRef lpSrc1Rect As RECT, ByRef lpSrc2Rect As RECT) As Long
'Create a new, blank layer in this image. Note that layer type is not provided - that is handled separately, via the
' layer object itself. All this function does is create a new pdLayer object and rearrange the layer stack to fit.
'Optionally, a layer position can be passed. The position is assumed to be the position of the
' new layer; e.g. if zero is passed, the layer will be placed at the bottom of the stack. If no position is requested,
' the layer will be created above the currently active layer.
'
'The returned Long-type value is the CANONICAL ID of the new layer, NOT ITS INDEX. (Returning its index would be pointless,
' as the calling function likely knows that in advance.) Note that layers can be accessed by either ID or index; after
' calling this function, make sure to use the CANONICAL ID function with the returned value.
Public Function createBlankLayer(Optional ByVal layerPosition As Long = -1) As Long
'Start by seeing if the image has zero layers. If it does, we can ignore layerPosition entirely.
If numOfLayersEverCreated = 0 Then
'Make sure the current layer pointer is correct
curLayer = 0
'The layer has images. Extra work is involved
Else
'If no layer has been specified, insert the new layer above the requested layer
If layerPosition = -1 Then curLayer = curLayer + 1 Else curLayer = layerPosition + 1
'Resize the layers array by one, and shift all existing layers upward.
ReDim Preserve imgLayers(0 To numOfLayers + 1) As pdLayer
Dim i As Long
For i = UBound(imgLayers) To curLayer Step -1
Set imgLayers(i) = imgLayers(i - 1)
Next i
End If
'Initialize the new layer. (This should already have been done at creation time, but it doesn't hurt to make sure.)
Set imgLayers(curLayer) = New pdLayer
'Assign the layer a canonical ID value.
imgLayers(curLayer).assignLayerID numOfLayersEverCreated
'Increment our internal layer counts
numOfLayersEverCreated = numOfLayersEverCreated + 1
numOfLayers = numOfLayers + 1
'Return the new layer's ID value (which is the number of layers ever created for this image)
createBlankLayer = numOfLayersEverCreated - 1
End Function
'Some functions (like saving the image) need to know whether to present the user with 24 or 32bpp options. Because the composited
' image's color depth depends on a variety of factors, the calling function can simply rely on this function to determine the
' appropriate color depth for us.
Public Function getCompositeImageColorDepth() As Long
'Single-layer images are much easier to deal with!
If numOfLayers = 0 Then
getCompositeImageColorDepth = imgLayers(curLayer).layerDIB.getDIBColorDepth
Else
'TODO! Use magic to determine the ideal color depth of the composited image.
End If
End Function
'Merge two layers together. Note this can be used to merge any two arbitrary layers, with the bottom layer holding the result
' of the merge. It is up to the caller to deal with any subsequent layer deletions, etc - this sub just performs the merge.
'
'The optional parameter, "bottomLayerIsFullSize", should be set to TRUE if the bottom layer is the size of the image. This saves
' us some processing time, because we don't have to check for rect intersection.
Public Sub mergeTwoLayers(ByRef topLayer As pdLayer, ByRef bottomLayer As pdLayer, Optional ByVal bottomLayerIsFullSize As Boolean = True)
Dim xOffset As Double, yOffset As Double
'If the bottom layer is the size of the image itself, we can use the existing layer offsets in our calculation. (Nice!)
If bottomLayerIsFullSize Then
xOffset = topLayer.getLayerOffsetX
yOffset = topLayer.getLayerOffsetY
'The top and bottom layer sizes are totally independent. This makes our life somewhat unpleasant.
Else
'First, we need to find the union rect of the two layers. This is the smallest rectangle that holds both layers.
Dim topRect As RECT, bottomRect As RECT, finalRect As RECT
fillRectForLayer bottomLayer, bottomRect
fillRectForLayer topLayer, topRect
UnionRect finalRect, topRect, bottomRect
'finalRect now contains the coordinates of the resulting rect. Create a blank DIB at those dimensions.
Dim tmpDIB As pdDIB
Set tmpDIB = New pdDIB
tmpDIB.createBlank finalRect.Right - finalRect.Left, finalRect.Bottom - finalRect.Top, 32, 0
'We now need to do a couple of things. Let's start by copying the bottom DIB into this new temporary DIB.
xOffset = bottomRect.Left - finalRect.Left
yOffset = bottomRect.Top - finalRect.Top
BitBlt tmpDIB.getDIBDC, xOffset, yOffset, bottomLayer.layerDIB.getDIBWidth, bottomLayer.layerDIB.getDIBHeight, bottomLayer.layerDIB.getDIBDC, 0, 0, vbSrcCopy
'We now need to calculate a new layer offset for this temporary DIB, which will eventually be copied into the bottom layer.
' Without this, the main composite won't know where to stick the layer!
bottomLayer.setLayerOffsetX finalRect.Left
bottomLayer.setLayerOffsetY finalRect.Top
'Copy the temporary DIB into the bottom layer
bottomLayer.layerDIB.createFromExistingDIB tmpDIB
Set tmpDIB = Nothing
'Calculate new offsets for the top layer, then carry on with business as usual!
xOffset = topRect.Left - finalRect.Left
yOffset = topRect.Top - finalRect.Top
End If
'Now that the two layers are ready to be condensed into one, the merge process is actually very simple.
'For convenience reasons, separate layers into 24bpp and 32bpp categories, and handle each specially
'24bpp case
If topLayer.layerDIB.getDIBColorDepth = 24 Then
'Possible future project - provide specialized, performance-friendly handling for 24bpp layers??
With topLayer
.layerDIB.alphaBlendToDC bottomLayer.layerDIB.getDIBDC, 255, xOffset, yOffset
End With
'32bpp case
Else
'Always use the AlphaBlend API for 32bpp layers
With topLayer
.layerDIB.alphaBlendToDC bottomLayer.layerDIB.getDIBDC, .getLayerOpacity * 2.55, xOffset, yOffset
End With
End If
'The two layers have been merged successfully! Any further actions (like deleting the top layer) must be handled
' by the caller.
End Sub
'Returns all layers of the image as a single, composited image (in pdDIB format, of course). Because of the way VB handles
' object references, we ask the calling function to supply the DIB they want filled. Optionally, they can also request a
' particular premultiplication status of the composited DIB's alpha values. (This is helpful for save functions, which
' require non-premultiplied alpha, vs viewport functions, which require premultiplied alpha).
Public Sub getCompositedImage(ByRef dstDIB As pdDIB, Optional ByVal premultiplicationStatus As Boolean = True)
'TODO: perform special checks for 24bpp single-layer images. For these, we can simply copy the current layer out,
' saving a lot of time!
'Start by resizing the DIB to be the size of the current image, and placing it inside a temporary pdLayer container.
Dim tmpLayer As pdLayer
Set tmpLayer = New pdLayer
dstDIB.createBlank Me.Width, Me.Height, 32
Set tmpLayer.layerDIB = dstDIB
'Always start by rendering the bottom layer onto the (presently empty) destination DIB. The bottom layer's blend mode
' does not matter, so this is a straightforward operation.
If imgLayers(0).getLayerVisibility Then
With imgLayers(0)
.layerDIB.alphaBlendToDC dstDIB.getDIBDC, .getLayerOpacity * 2.55, .getLayerOffsetX, .getLayerOffsetY
End With
End If
'If the image has additional layers, proceed to merge the rest of them, starting from the bottom and working our way up.
' Note that if a layer is invisible, we simply skip it - this is the most performance-friendly way to handle them.
If numOfLayers > 1 Then
Dim i As Long
For i = 1 To numOfLayers - 1
If imgLayers(i).getLayerVisibility Then mergeTwoLayers imgLayers(i), tmpLayer, True
Next i
End If
'Release the temp layer's hold on the destination DIB
Set tmpLayer.layerDIB = Nothing
'If the user requested non-premultiplied alpha, calculate it now.
'TODO: how we handle premultiplication will depend on how I implement compositing, above, so this needs to be revisited
' after actual compositing has been written.
If premultiplicationStatus Then
Else
If dstDIB.getDIBColorDepth = 32 Then dstDIB.fixPremultipliedAlpha False
End If
End Sub
'Get the currently active layer index
Public Function getActiveLayerIndex() As Long
getActiveLayerIndex = curLayer
End Function
'Get the currently active layer canonical ID
Public Function getActiveLayerID() As Long
getActiveLayerID = imgLayers(curLayer).getLayerID
End Function
'Get the number of layers in this image. (Note: this function might return zero, so handle that condition correctly
' in calling functions!)
Public Function getNumOfLayers() As Long
getNumOfLayers = numOfLayers
End Function
'Retrieve a reference to the currently active layer's DIB. This is effectively a shortcut function when we
' need quick access to the current layer's DIB.
Public Function getActiveDIB() As pdDIB
'If numOfLayers > 0 Then
Set getActiveDIB = imgLayers(curLayer).layerDIB
'Else
' Set getActiveDIB = Nothing
'End If
End Function
'Retrieve the currently active layer
Public Function getActiveLayer() As pdLayer
If numOfLayers > 0 Then
Set getActiveLayer = imgLayers(curLayer)
Else
Set getActiveLayer = Nothing
End If
End Function
'Set the currently active layer by its cardinal ID value
Public Sub setActiveLayerByID(ByVal newActiveLayerID As Long)
Dim i As Long
For i = 0 To UBound(imgLayers)
If imgLayers(i).getLayerID = newActiveLayerID Then
curLayer = i
Exit For
End If
Next i
'If we made it all the way here, the requested layer was not found. Do nothing (e.g. do not change the active layer).
End Sub
Public Sub setActiveLayerByIndex(ByVal newActiveLayerIndex As Long)
'Validate the incoming layer index
If newActiveLayerIndex < 0 Then newActiveLayerIndex = 0
If newActiveLayerIndex > numOfLayers - 1 Then newActiveLayerIndex = numOfLayers - 1
curLayer = newActiveLayerIndex
End Sub
'Retrieve a layer at an arbitrary position. Remember that layers in PD are zero-based, so the base layer is layer 0, not 1.
' Also, this function does not check bounds, so make sure the passed index is valid!
Public Function getLayerByIndex(ByVal layerIndex As Long) As pdLayer
'As a failsafe, validate the incoming layer index
If layerIndex < 0 Then layerIndex = 0
If layerIndex > numOfLayers - 1 Then layerIndex = numOfLayers - 1
'Return the requested layer
Set getLayerByIndex = imgLayers(layerIndex)
End Function
'Retrieve a layer using its canonical ID. Remember that layers in PD are zero-based, so the base layer is layer 0, not 1.
' Also, this function does not check bounds, so make sure the passed index is valid!
Public Function getLayerByID(ByVal requestedID As Long) As pdLayer
Dim i As Long
For i = 0 To UBound(imgLayers)
If imgLayers(i).getLayerID = requestedID Then
Set getLayerByID = imgLayers(i)
Exit Function
End If
Next i
'If we made it all the way here, the requested layer was not found. Return Nothing.
Set getLayerByID = Nothing
End Function
'Simple function for moving a given layer up/down in the layer stack.
Public Sub moveLayerByIndex(ByVal srcLayerIndex As Long, ByVal moveLayerUp As Boolean)
'Before doing anything else, make sure the requested move is a valid one
If moveLayerUp Then
If srcLayerIndex = numOfLayers - 1 Then Exit Sub
Else
If srcLayerIndex = 0 Then Exit Sub
End If
'Process the actual move
If moveLayerUp Then
'Move the selected layer UP.
swapTwoLayers srcLayerIndex, srcLayerIndex + 1
Else
'Move the selected layer DOWN.
swapTwoLayers srcLayerIndex, srcLayerIndex - 1
End If
End Sub
'Used by various layer movement functions. Given two layer indices, swap them.
Public Sub swapTwoLayers(ByVal srcLayerID_1 As Long, ByVal srcLayerID_2 As Long)
'Create a temporary reference to the first layer
Dim tmpLayerRef As pdLayer
Set tmpLayerRef = imgLayers(srcLayerID_1)
'Overwrite the first layer's reference with the second one
Set imgLayers(srcLayerID_1) = imgLayers(srcLayerID_2)
'Overwrite the second layer's reference with our temporary copy of the first layer
Set imgLayers(srcLayerID_2) = tmpLayerRef
'Release our temporary reference
Set tmpLayerRef = Nothing
End Sub
'Delete a given layer. The pdLayers stack will automatically be resized to match.
Public Sub deleteLayerByIndex(ByVal srcLayerIndex As Long)
'Validate the layer index
If srcLayerIndex < 0 Or srcLayerIndex >= numOfLayers Then Exit Sub
'If this is the last remaining layer, exit
If numOfLayers = 1 Then Exit Sub
'Shift all layer references above this one downward
Dim i As Long
For i = srcLayerIndex To numOfLayers - 2
Set imgLayers(i) = imgLayers(i + 1)
Next i
'Shrink the layers array
numOfLayers = numOfLayers - 1
ReDim Preserve imgLayers(0 To numOfLayers - 1) As pdLayer
End Sub
'Write all relevant image information to an external file (in XML format). Note that this will not be a perfect copy of the
' current image's state, because some pdImage data is only relevant at run-time. Also, some data structures (like metadata) are
' very difficult to handle properly; I am working on a solution to this, but have not finished it yet.
'
'In the event that you want to do something else with the XML data, I have now added an optional parameter.
' Mark "filePathIsActuallyString" as TRUE to treat the filename string as a destination buffer, and this function will
' write the XML data there (instead of treating it as a filename).
Public Function writeExternalData(ByRef dstFilename As String, Optional ByVal filePathIsActuallyString As Boolean = False) As Boolean
On Error GoTo failedDataWrite
'Prepare an XML engine, which will handle the actual writing of the file
Dim xmlEngine As pdXML
Set xmlEngine = New pdXML
'Add a basic header and explanatory comment
xmlEngine.prepareNewXML "pdImage"
xmlEngine.writeBlankLine
xmlEngine.writeComment "This file contains a summary of crucial data for a given pdImage entry."
xmlEngine.writeBlankLine
'Start by writing out the imageID. This value will not be re-used in subsequent session, but it's helpful for matching up
' this image's data with any saved Undo/Redo entries (which are created by imageID for privacy reasons)
xmlEngine.writeTag "ID", imageID
'Start writing any tags that may be useful for reconstructing an autosave version of this image. We limit the information to
' properties independent of a given session (e.g. imageID); session-dependent properties will be recreated anew by PD.
xmlEngine.writeTag "Width", Width
xmlEngine.writeTag "Height", Height
xmlEngine.writeTag "xResolution", xResolution
xmlEngine.writeTag "yResolution", yResolution
xmlEngine.writeTag "DPI", imageDPI
xmlEngine.writeTag "OriginalFileSize", originalFileSize
xmlEngine.writeTag "OriginalFileName", originalFileName
xmlEngine.writeTag "OriginalFileNameAndExtension", originalFileNameAndExtension
xmlEngine.writeTag "LocationOnDisk", locationOnDisk
xmlEngine.writeTag "OriginalFileFormat", originalFileFormat
xmlEngine.writeTag "CurrentFileFormat", currentFileFormat
xmlEngine.writeTag "OriginalColorDepth", originalColorDepth
xmlEngine.writeTag "pngBackgroundColor", pngBackgroundColor
xmlEngine.writeTag "numOfLayersEverCreated", numOfLayersEverCreated
xmlEngine.writeTag "numOfLayers", numOfLayers
xmlEngine.writeTag "curLayer", curLayer
xmlEngine.writeBlankLine
'That should be enough information to reconstruct the original image state. We now have two options, depending on the value
' of the "filePathIsActuallyString" parameter.
'The string we were supplied is a blank buffer, not a filename. Write the XML data to it. Note that by default, XML indentation
' is suppressed; this is for performance reasons.
If filePathIsActuallyString Then
dstFilename = xmlEngine.returnCurrentXMLString(True)
'The string we were supplied is a filename. Write the XML data to it.
Else
xmlEngine.writeXMLToFile dstFilename
End If
writeExternalData = True
Exit Function
failedDataWrite:
Debug.Print "WARNING: could not write XML data to file. Are you sure this drive has enough free space?"
writeExternalData = False
End Function
'Sister function to writeExternalDataToFile, above. If you add a variable to one function, make sure to include it in the other.
' If the function fails, it will return FALSE.
'
'In the event that you have already loaded the XML data yourself (or have compiled it manually), I have added an optional parameter.
' Mark "filePathIsActuallyString" as TRUE to treat the filename string as actual XML data instead.
'
'Finally, if this function is being called as part of an Undo/Redo action, we can safely ignore certain entries in the file, in favor
' of the values we already have stored for this image.
Public Function readExternalData(ByRef srcFilename As String, Optional ByVal filePathIsActuallyString As Boolean = False, Optional ByVal sourceIsUndoFile As Boolean = False) As Boolean
On Error GoTo failedDataRead
Dim xmlLoadSuccessful As Boolean
xmlLoadSuccessful = False
'Prepare an XML engine, which will handle the actual reading and parsing of the file
Dim xmlEngine As pdXML
Set xmlEngine = New pdXML
'We now have two choices, depending on the value of filePathIsActuallyString. Load the supposed XML contents
' (either from file or directly) into the XML engine; note that both steps validate the incoming data automatically.
If filePathIsActuallyString Then
xmlLoadSuccessful = xmlEngine.loadXMLFromString(srcFilename)
Else
xmlLoadSuccessful = xmlEngine.loadXMLFile(srcFilename)
End If
'Make sure the supplied XML actually contains pdImage data.
If xmlLoadSuccessful And xmlEngine.isPDDataType("pdImage") Then
'The XML file checked out. Start retrieving relevant values.
Me.Width = xmlEngine.getUniqueTag_Long("Width")
Me.Height = xmlEngine.getUniqueTag_Long("Height")
xResolution = xmlEngine.getUniqueTag_Long("xResolution")
yResolution = xmlEngine.getUniqueTag_Long("yResolution")
imageDPI = xmlEngine.getUniqueTag_Long("DPI")
'All settings past this point are only relevant when loading a file anew, not when it's being used as part of
' the Undo/Redo stack.
If Not sourceIsUndoFile Then
'These values are all stored in the PDI file, but they're not really relevant. I will look at not storing
' them in the first place, but for now, just ignore them.
'TODO: make a firm decision on what data does not belong in PDI files!
'Me.originalFileSize = xmlEngine.getUniqueTag_Long("originalFileSize")
'Me.originalFileName = xmlEngine.getUniqueTag_String("originalFileName")
'Me.originalFileNameAndExtension = xmlEngine.getUniqueTag_String("originalFileNameAndExtension")
'Me.locationOnDisk = xmlEngine.getUniqueTag_String("locationOnDisk")
'Me.originalFileFormat = xmlEngine.getUniqueTag_Long("originalFileFormat")
'Me.currentFileFormat = xmlEngine.getUniqueTag_Long("currentFileFormat")
Me.originalColorDepth = xmlEngine.getUniqueTag_Long("originalColorDepth")
Me.pngBackgroundColor = xmlEngine.getUniqueTag_Long("pngBackgroundColor")
End If
'Finally, retrieve layer values from the file
numOfLayersEverCreated = xmlEngine.getUniqueTag_Long("numOfLayersEverCreated")
numOfLayers = xmlEngine.getUniqueTag_Long("numOfLayers")
curLayer = xmlEngine.getUniqueTag_Long("curLayer")
'Now that all data has been read, we can initialize our layers array. (The calling function will presumably proceed to
' fill each layer with usable data, because we can't do that here!)
ReDim imgLayers(0 To numOfLayers - 1) As pdLayer
Dim i As Long
For i = 0 To numOfLayers - 1
Set imgLayers(i) = New pdLayer
Next i
Else
GoTo failedDataRead
End If
'The pdImage struct has been created successfully!
readExternalData = True
Exit Function
failedDataRead:
Debug.Print "WARNING: could not validate XML data. pdImage internals were not updated."
readExternalData = False
End Function
'Get/Set image resolution (in DPI). Note that the vertical resolution is optional; if the two values
' differ, PD will average them when image DPI is requested.
Public Function getDPI() As Double
If imageDPI = 0 Then
getDPI = 96
Else
getDPI = imageDPI
End If
End Function
Public Sub setDPI(ByVal xRes As Double, ByVal yRes As Double, Optional ByVal sourceIsFreeImage As Boolean = False)
'Many image types do not store resolution information; default to 96 in this case
If xRes = 0 Then xRes = 96
If yRes = 0 Then yRes = 96
'FreeImage is pretty damn stupid when it comes to DPI. If no DPI information is found, it will return "72" by default.
' If the source of this call is FreeImage, we want to replace 72 with a more modern default of 96. Obviously this creates
' a problem when images have an actual resolution of 72 DPI, as there is no way to tell if FreeImage set that by default,
' or if it is the actual DPI value of the file. If this occurs, we can use ExifTool to retrieve an actual value as
' stored in the file.
If sourceIsFreeImage Then
If xRes = 72 Then xRes = 96
If yRes = 72 Then yRes = 96
End If
xResolution = xRes
yResolution = yRes
'It is extremely rare for x/y resolution to differ, but just in case, calculate an average resolution as well
imageDPI = (xRes + yRes) \ 2
End Sub
'If the image has been saved to file in its current state, this will return TRUE. Use this value to determine
' whether to enable a Save button, for example.
Public Function getSaveState() As Boolean
getSaveState = hasBeenSaved
End Function
'External functions can use this function to request a thumbnail version of the contained image. Previously, thumbnail
' generation was handled externally, but to prepare for the addition of layers, I will be rewriting all thumbnail-related
' functions to use this function. (This is necessary so that this pdImage object can composite a multi-layer image
' before passing along a thumbnail version.)
'
'FreeImage is required for thumbnail resizing.
Public Function requestThumbnail(ByRef dstThumbnailDIB As pdDIB, Optional ByVal thumbnailSize As Long = 64) As Boolean
'Thumbnails have some interesting requirements. We always want them to be square, with the image set in the middle
' of the thumbnail (with aspect ratio preserved) and any empty edges made transparent.
'Start by determining an aspect ratio for the current image.
Dim aspectRatio As Double
aspectRatio = CSng(Width) / CSng(Height)
'We also need to determine the thumbnail's actual width and height, and any x and y offset necessary to preserve the
' aspect ratio and center the image on the thumbnail.
Dim tIcoWidth As Double, tIcoHeight As Double, tX As Double, tY As Double
'If the form is wider than it is tall...
If aspectRatio > 1 Then
'Determine proper sizes and (x, y) positioning so the icon will be centered
tIcoWidth = thumbnailSize
tIcoHeight = thumbnailSize * (1 / aspectRatio)
tX = 0
tY = (thumbnailSize - tIcoHeight) / 2
Else
'Same thing, but with the math adjusted for images taller than they are wide
tIcoHeight = thumbnailSize
tIcoWidth = thumbnailSize * aspectRatio
tY = 0
tX = (thumbnailSize - tIcoWidth) / 2
End If
'Retrieve a composite version of the image
Dim compositeImage As pdDIB
Set compositeImage = New pdDIB
Me.getCompositedImage compositeImage, True
'There are two possible ways to create a thumbnail image. If FreeImage is available, we prefer to use it, as it
' provides superior results, but if it is not available, GDI+ will suffice.
If g_ImageFormats.FreeImageEnabled Then
'Convert our current DIB to a FreeImage-type DIB
Dim fi_DIB As Long
fi_DIB = FreeImage_CreateFromDC(compositeImage.getDIBDC)
'Release our copy of the composite image
Set compositeImage = Nothing
'Use that handle to request an image resize
If fi_DIB <> 0 Then
'Rescale the image
Dim returnDIB As Long
returnDIB = FreeImage_RescaleByPixel(fi_DIB, CLng(tIcoWidth), CLng(tIcoHeight), True, FILTER_BILINEAR)
'Make sure the image is 32bpp (returns a clone of the image if it's already 32bpp, so no harm done)
Dim newDIB32 As Long
newDIB32 = FreeImage_ConvertTo32Bits(returnDIB)
'Unload the original DIB
If newDIB32 <> returnDIB Then FreeImage_UnloadEx returnDIB
'If the image isn't square-shaped, we need to enlarge the DIB accordingly. FreeImage provides a function for that.
'Start by preparing a transparent quad, which we'll assign to the background of the enlarged area
Dim newColor As RGBQUAD
With newColor
.rgbBlue = 255
.rgbGreen = 255
.rgbRed = 255
.rgbReserved = 0
End With
'Enlarge the canvas as necessary
Dim finalDIB As Long
finalDIB = FreeImage_EnlargeCanvas(newDIB32, tX, tY, tX, tY, newColor, FI_COLOR_IS_RGBA_COLOR)
'Unload the original DIB
If finalDIB <> newDIB32 Then FreeImage_UnloadEx newDIB32
'At this point, finalDIB contains the 32bpp alpha icon exactly how we want it. Copy it into the destination DIB.
dstThumbnailDIB.createBlank thumbnailSize, thumbnailSize, 32
SetDIBitsToDevice dstThumbnailDIB.getDIBDC, 0, 0, thumbnailSize, thumbnailSize, 0, 0, 0, thumbnailSize, ByVal FreeImage_GetBits(finalDIB), ByVal FreeImage_GetInfo(finalDIB), 0&
'With the transfer complete, release the FreeImage DIB and unload the library
If returnDIB <> 0 Then FreeImage_UnloadEx returnDIB
requestThumbnail = True
Else
requestThumbnail = False
End If
Else
dstThumbnailDIB.createBlank thumbnailSize, thumbnailSize, 32, 0
requestThumbnail = GDIPlusResizeDIB(dstThumbnailDIB, tX, tY, tIcoWidth, tIcoHeight, compositeImage, 0, 0, compositeImage.getDIBWidth, compositeImage.getDIBHeight, InterpolationModeHighQualityBilinear)
End If
End Function
'When this DIB is no longer being used, we can deactivate it to save on resources.
Public Sub deactivateImage()
'Erase the main DIB
'If Not (mainDIB Is Nothing) Then
' mainDIB.eraseDIB
' Set mainDIB = Nothing
'End If
'Erase the back buffer
If Not (backBuffer Is Nothing) Then
backBuffer.eraseDIB
Set backBuffer = Nothing
End If
'If the alpha adjustment DIB exists, erase it too
If Not (alphaFixDIB Is Nothing) Then
alphaFixDIB.eraseDIB
Set alphaFixDIB = Nothing
End If
'If a selection exists, wipe it
If Not (mainSelection Is Nothing) Then Set mainSelection = Nothing
'Deactivate the Undo/Redo handler
If Not (undoManager Is Nothing) Then
undoManager.clearUndos
Set undoManager.parentPDImage = Nothing
Set undoManager = Nothing
End If
'Release the viewport renderer
If Not (imgViewport Is Nothing) Then Set imgViewport = Nothing
'It doesn't make a big difference, but we can also empty out this image's String-type variables to save a bit of space.
originalFileName = ""
originalFileNameAndExtension = ""
locationOnDisk = ""
'Delete the file's Autosave entry, if any
If FileExist(g_UserPreferences.getAutosavePath & imageID & ".xml") Then Kill g_UserPreferences.getAutosavePath & imageID & ".xml"
'Mark this image as inactive
IsActive = False
End Sub
'Outside actions (such as saving) can affect the HasBeenSaved variable. However, because we need to do additional
' processing based on the state of this variable, we provide this interface.
Public Sub setSaveState(ByVal newSaveState As Boolean)
If newSaveState Then
hasBeenSaved = True
'Remember the undo value at this juncture; if the user performs additional actions, but "Undos" to this point,
' we want to disable the save button for them
undoManager.notifyImageSaved
Else
hasBeenSaved = False
End If
'Any time the save state is modified, write a new AutoSave entry out to file.
If IsActive And (Not forInternalUseOnly) Then writeExternalData g_UserPreferences.getAutosavePath & imageID & ".xml"
End Sub
'If the images's size has somehow changed, this can be called to update it.
'
'The first optional parameter, assumed to be TRUE, makes the image the same size as its base layer. This is helpful when
' an image file is first loaded, as the pdImage container should be made to be the same size.
'
'If this is another type of resize action, the caller must supply the new width/height values desired.
'
'Note that this function performs NO VALIDATION on the passed width/height values. Make sure they are correct in advance!
Public Function updateSize(Optional ByVal useBaseLayer As Boolean = True, Optional ByVal newWidth As Long = 0, Optional ByVal newHeight As Long = 0)
If useBaseLayer Then
Me.Width = imgLayers(0).layerDIB.getDIBWidth
Me.Height = imgLayers(0).layerDIB.getDIBHeight
Else
Me.Width = newWidth
Me.Height = newHeight
End If
End Function
'INITIALIZE class
Private Sub Class_Initialize()
'Initially, mark the image as *not* having been saved
IsActive = False
loadedSuccessfully = False
hasBeenSaved = False
forInternalUseOnly = False
hasSeenJPEGPrompt = False
pngBackgroundColor = -1
'Initialize the back buffer
Set backBuffer = New pdDIB
'Initialize the alpha composite layer
Set alphaFixDIB = New pdDIB
'Initialize the main selection
Set mainSelection = New pdSelection
selectionActive = False
mainSelection.setSelectionShape sRectangle
Set mainSelection.containingPDImage = Me
'Initialize the metadata object (which may not get used, but this prevents errors if other functions try to access metadata)
Set imgMetadata = New pdMetadata
'Initialize the Undo/Redo handler
Set undoManager = New pdUndo
Set undoManager.parentPDImage = Me
'Initialize the viewport manager
Set imgViewport = New pdViewport
'Create at least one blank layer, so that functions referencing a layer object don't break
ReDim imgLayers(0) As pdLayer
Set imgLayers(0) = New pdLayer
'The current layer marker always starts at zero (which is considered the background layer). To check for the case of
' no active layers, do not use curLayer, use
curLayer = 0
'By default, list this image as having zero layers
numOfLayers = 0
numOfLayersEverCreated = 0
End Sub
'Pass a given layer's DIB into a new DIB, but pad it against null pixels so that it is the same size as the image.
' This may cause cropping if the layer currently lies off the image - this is a known "issue", though I haven't quite made up my
' mind as to whether it's an issue that needs resolving or not. I will revisit in the future, once I have a better idea of
' the workflow implications.
' TODO: determine if off-image layer cropping is an acceptable solution in all scenarios.
Public Sub retrieveNullPaddedLayer(ByRef dstDIB As pdDIB, ByVal srcLayerIndex As Long)
'Create a blank destination DIB at the size of the image
dstDIB.createBlank Width, Height, 32, 0
'Copy the source layer into the blank DIB
BitBlt dstDIB.getDIBDC, imgLayers(srcLayerIndex).getLayerOffsetX, imgLayers(srcLayerIndex).getLayerOffsetY, imgLayers(srcLayerIndex).layerDIB.getDIBWidth, imgLayers(srcLayerIndex).layerDIB.getDIBHeight, imgLayers(srcLayerIndex).layerDIB.getDIBDC, 0, 0, vbSrcCopy
End Sub
'Return a DIB that contains the currently selected area, fully processed according to the selection mask
Public Function retrieveProcessedSelection(ByRef dstDIB As pdDIB, Optional ByVal preMultipliedAlphaState As Boolean = False, Optional ByVal useMergedImage As Boolean = False) As Boolean
'If this image does not contain an active selection, exit now.
If (Not IsActive) Or (Not selectionActive) Then
retrieveProcessedSelection = False
Exit Function
End If
'Before doing anything else, make a temporary copy of the source data. The source data varies depending on the useMergedImage
' parameter - if it is TRUE, we use a composite version of the image. If it is FALSE, we use a copy of the current layer,
' but null-padded to be the same size as the image.
Dim srcDIB As pdDIB
Set srcDIB = New pdDIB
If useMergedImage Then
getCompositedImage srcDIB, False
Else
retrieveNullPaddedLayer srcDIB, curLayer
End If
'Selections can be one of several types. Right now, we don't give special handling to simple rectangular selections - all selections
' are fully processed according to the contents of the mask. Also, all selections are currently created as 32bpp DIBs.
'Start by initializing the destination DIB to the size of the active selection
Set dstDIB = New pdDIB
dstDIB.createBlank mainSelection.boundWidth, mainSelection.boundHeight, 32
'We now need pointers to three separate sets of image data: destination DIB, source DIB, and selection mask.
Dim srcImageData() As Byte
Dim srcSA As SAFEARRAY2D
prepSafeArray srcSA, srcDIB
CopyMemory ByVal VarPtrArray(srcImageData()), VarPtr(srcSA), 4
Dim selData() As Byte
Dim selSA As SAFEARRAY2D
prepSafeArray selSA, mainSelection.selMask
CopyMemory ByVal VarPtrArray(selData()), VarPtr(selSA), 4
Dim dstImageData() As Byte
Dim dstSA As SAFEARRAY2D
prepSafeArray dstSA, dstDIB
CopyMemory ByVal VarPtrArray(dstImageData()), VarPtr(dstSA), 4
Dim leftOffset As Long, topOffset As Long
leftOffset = mainSelection.boundLeft