Skip to content

Commit c4beae5

Browse files
committed
Copy+Paste can now retain editable text layers
PD has always supported copy+paste of text layers, but it would always rasterize the text into a normal image (raster) layer during the process. This is required to copy+paste text layers into e.g. Photoshop. For copy+pasting within PD however, we can do better! PD will now detect when you are copy+pasting layer data inside PD, and instead of copying only pixel data, it will copy the entire source layer as-is, including attributes like layer name, blend mode, opacity, rotation, etc - and if the layer is a text or typography layer, the newly pasted layer will also be a text or typography layer with identical attributes, including all text and style settings. (In fact, the way this is implemented is layer-agnostic, so it will work on any vector layer types in the future.) Note that copy+pasting while a selection is active will still produce a rasterized layer, by necessity. This new behavior only works when Copy is initiated without an active selection.
1 parent c7a7ecf commit c4beae5

File tree

5 files changed

+168
-31
lines changed

5 files changed

+168
-31
lines changed

Classes/pdClipboard.cls

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,15 @@ Public Enum PredefinedClipboardFormatConstants
112112
CF_DSPMETAFILEPICT = &H83
113113
CF_DSPENHMETAFILE = &H8E
114114

115-
'IMPORTANT NOTE! Data on the clipboard in this range is *not* owned by the clipboard, but by the application who stuck the data there.
116-
' As such, attempting to call GlobalFree() will cause major issues. Don't do it.
115+
'IMPORTANT NOTE! Data on the clipboard in this range is *not* owned by the clipboard,
116+
' but by the application who stuck the data there. As such, calling GlobalFree() on it
117+
' breaks everything. Don't do it.
117118
CF_PRIVATEFIRST = &H200
118119
CF_PRIVATELAST = &H2FF
119120

120-
'PD defines some internal formats, which is awesome for Cut/Copy/Paste within PD herself.
121+
'PD defines some internal formats, which is awesome for Cut/Copy/Paste within PD itself.
121122
CF_PD_DIB = CF_PRIVATEFIRST + 1
123+
CF_PD_LAYER = CF_PRIVATEFIRST + 2
122124

123125
'IMPORTANT NOTE! These have nothing to do with GDI objects, despite the stupid names. They are simply private-range objects
124126
' that the Clipboard itself is responsible for freeing. Per MSDN:
@@ -137,7 +139,7 @@ End Enum
137139
Private Const CF_DIB = 8, CF_PALETTE = 9, CF_PENDATA = 10, CF_RIFF = 11, CF_WAVE = 12, CF_UNICODETEXT = 13, CF_ENHMETAFILE = 14
138140
Private Const CF_HDROP = 15, CF_LOCALE = 16, CF_DIBV5 = 17, CF_MAX = 18, CF_OWNERDISPLAY = &H80, CF_DSPTEXT = &H81
139141
Private Const CF_DSPBITMAP = &H82, CF_DSPMETAFILEPICT = &H83, CF_DSPENHMETAFILE = &H8E, CF_PRIVATEFIRST = &H200, CF_PRIVATELAST = &H2FF
140-
Private Const CF_GDIOBJFIRST = &H300, CF_GDIOBJLAST = &H3FF, CF_PD_DIB = 0
142+
Private Const CF_GDIOBJFIRST = &H300, CF_GDIOBJLAST = &H3FF, CF_PD_DIB = 0, CF_PD_LAYER = 0
141143
#End If
142144

143145
'Helper function for dealing with file lists

Classes/pdClipboardMain.cls

Lines changed: 157 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@ Private Const PD_CB_ALLOW_PNG_PASTE As Boolean = True
5151
Public Enum PD_ClipboardFormats
5252
pdcf_All = 0
5353
pdcf_InternalPD = CF_PD_DIB
54+
pdcf_InternalLayer = CF_PD_LAYER
5455
pdcf_Bitmap = CF_BITMAP
5556
pdcf_Dib = CF_DIB
5657
pdcf_DibV5 = CF_DIBV5
5758
pdcf_PNG = -1
5859
End Enum
5960

6061
#If False Then
61-
Private Const pdcf_All = 0, pdcf_InternalPD = CF_PD_DIB, pdcf_Bitmap = CF_BITMAP, pdcf_Dib = CF_DIB, pdcf_DibV5 = CF_DIBV5, pdcf_PNG = -1
62+
Private Const pdcf_All = 0, pdcf_InternalPD = CF_PD_DIB, pdcf_InternalLayer = CF_PD_LAYER, pdcf_Bitmap = CF_BITMAP, pdcf_Dib = CF_DIB, pdcf_DibV5 = CF_DIBV5, pdcf_PNG = -1
6263
#End If
6364

6465
'Some format constants are not inherently defined by VB; we cache these at load-time
@@ -159,8 +160,12 @@ Private m_ClipboardInfo As PD_Clipboard_Info
159160
'Lots of clipboard actions require a temporary DIB
160161
Private m_ClipboardDIB As pdDIB
161162

162-
'When stashing clipboard data, we use a temp file.
163-
Private m_StashFile As String
163+
'Stashing layer data will generate a layer header and vector stash as well
164+
Private m_StashedLayer As String, m_StashedVector As String
165+
166+
'When stashing clipboard data, we use a temp file. Note that certain internal types of copy+paste (e.g. copy+pasting
167+
' a vector layer) may store multiple types of stash data.
168+
Private m_StashFile As String, m_StashFileLayer As String, m_StashFileVector As String
164169

165170
'Clipboard interaction object; importantly, if delayed rendering is active, this class *will* raise events that must be handled.
166171
Private WithEvents m_Clipboard As pdClipboard
@@ -327,7 +332,14 @@ Friend Sub ClipboardCopy(ByVal copyMerged As Boolean, Optional ByVal updateUI As
327332
'By default, we offer all supported formats
328333
If (cFormat = pdcf_All) Then
329334

335+
'Internal pixel data is always available
330336
m_Clipboard.SetClipboardData_DelayedRendering CF_PD_DIB
337+
338+
'If the copy is of a whole layer (not a selection or merged image), we'll also save out a full
339+
' layer header and any relevant vector data so that we can re-create the whole layer as-is.
340+
If (Not copyMerged) And (Not PDImages.GetActiveImage.IsSelectionActive) Then m_Clipboard.SetClipboardData_DelayedRendering CF_PD_LAYER
341+
342+
'Bitmap formats are for other apps
331343
m_Clipboard.SetClipboardData_DelayedRendering m_Clipboard.AddClipboardFormat("PNG")
332344
m_Clipboard.SetClipboardData_DelayedRendering CF_BITMAP
333345
m_Clipboard.SetClipboardData_DelayedRendering CF_DIBV5
@@ -363,6 +375,8 @@ Friend Sub ClipboardCopy(ByVal copyMerged As Boolean, Optional ByVal updateUI As
363375
m_Clipboard.SetClipboardData_DelayedRendering CF_DIBV5
364376
ElseIf (cFormat = pdcf_InternalPD) Then
365377
m_Clipboard.SetClipboardData_DelayedRendering CF_PD_DIB
378+
ElseIf (cFormat = pdcf_InternalLayer) Then
379+
m_Clipboard.SetClipboardData_DelayedRendering CF_PD_LAYER
366380
ElseIf (cFormat = pdcf_PNG) Then
367381
m_Clipboard.SetClipboardData_DelayedRendering m_Clipboard.AddClipboardFormat("PNG")
368382
Else
@@ -430,7 +444,7 @@ Friend Function ClipboardPaste(ByVal srcIsMeantAsLayer As Boolean, Optional ByRe
430444
PDDebug.LogAction "Clipboard reports the following formats: " & m_Clipboard.GetListOfAvailableFormatNames()
431445

432446
'If PD was used to Cut or Copy something onto the clipboard, our own private format(s) will be listed first.
433-
If m_Clipboard.DoesClipboardHaveFormatID(CF_PD_DIB) And PD_CB_ALLOW_INTERNAL_FORMAT_PASTE Then
447+
If (m_Clipboard.DoesClipboardHaveFormatID(CF_PD_DIB) Or m_Clipboard.DoesClipboardHaveFormatID(CF_PD_LAYER)) And PD_CB_ALLOW_INTERNAL_FORMAT_PASTE Then
434448
pasteWasSuccessful = ClipboardPaste_InternalData(srcIsMeantAsLayer, pasteToThisDIBInstead)
435449
End If
436450

@@ -574,9 +588,10 @@ Friend Function ClipboardPaste(ByVal srcIsMeantAsLayer As Boolean, Optional ByRe
574588

575589
End Function
576590

577-
'If the clipboard contains internal PD-format data (most commonly a bare DIB), you can call this function to initiate a "paste" command
578-
' using the internal data as a source. The parameter "srcIsMeantAsLayer" controls whether the clipboard data is loaded as a new image,
579-
' or as a new layer in an existing image.
591+
'If the clipboard contains internal PD-format data (most commonly a bare DIB, but also potentially a
592+
' layer header and/or layer vector contents), you can call this function to initiate a "paste" command
593+
' using the internal data as a source. The parameter "srcIsMeantAsLayer" controls whether the clipboard
594+
' data is loaded as a new image, or as a new layer in an existing image.
580595
'
581596
'RETURNS: TRUE if successful; FALSE otherwise.
582597
Private Function ClipboardPaste_InternalData(ByVal srcIsMeantAsLayer As Boolean, Optional ByRef pasteToThisDIBInstead As pdDIB = Nothing) As Boolean
@@ -600,8 +615,92 @@ Private Function ClipboardPaste_InternalData(ByVal srcIsMeantAsLayer As Boolean,
600615
ClipboardPaste_InternalData = Loading.QuickLoadImageToDIB(m_StashFile, pasteToThisDIBInstead, False, False)
601616
Else
602617
If srcIsMeantAsLayer Then
603-
m_LayerCounter = m_LayerCounter + 1
604-
ClipboardPaste_InternalData = Layers.LoadImageAsNewLayer(False, m_StashFile, g_Language.TranslateMessage("Clipboard Image") & " " & CStr(m_LayerCounter), False, False)
618+
619+
'If a layer header was saved out to file, try to use it as the paste source
620+
' (instead of creating a generic new layer object - this will result in a copy+paste operation
621+
' that preserves attributes like layer blend mode, position, rotation, text/vector contents, etc)
622+
Dim useStashedLayerHeader As Boolean
623+
useStashedLayerHeader = (LenB(m_StashFileLayer) <> 0)
624+
If useStashedLayerHeader Then useStashedLayerHeader = Files.FileExists(m_StashFileLayer)
625+
If useStashedLayerHeader Then useStashedLayerHeader = Files.FileLoadAsString(m_StashFileLayer, m_StashedLayer)
626+
627+
'Only attempt to use a stashed layer header if all previous validation steps were successful.
628+
If useStashedLayerHeader Then
629+
630+
PDDebug.LogAction "Pasting internal layer source..."
631+
632+
'Ask the parent pdImage to create a blank, new layer object
633+
Dim newLayerID As Long
634+
newLayerID = PDImages.GetActiveImage.CreateBlankLayer()
635+
636+
'See if we stashed raster or vector data. (This affects how we initialize the layer's contents -
637+
' from pixel buffer or manually, by reading text-based vector data and generating objects accordingly.)
638+
Dim useVectorData As Boolean
639+
useVectorData = False
640+
641+
Dim cSerialize As pdSerialize
642+
Set cSerialize = New pdSerialize
643+
cSerialize.SetParamString m_StashedLayer
644+
645+
Dim srcLayerType As PD_LayerType
646+
srcLayerType = Layers.GetLayerTypeIDFromString(cSerialize.GetString("type", Layers.GetLayerTypeStringFromID(PDL_Image), True))
647+
648+
If (srcLayerType <> PDL_Image) Then
649+
useVectorData = Files.FileExists(m_StashFileVector)
650+
If useVectorData Then useVectorData = Files.FileLoadAsString(m_StashFileVector, m_StashedVector)
651+
End If
652+
653+
'useVectorData will now be TRUE iff the copied layer was a text/vector layer,
654+
' *and* we stashed vector data successfully.
655+
656+
'Create the new layer using the appropriate layer type
657+
PDImages.GetActiveImage.GetLayerByID(newLayerID).InitializeNewLayer srcLayerType
658+
659+
'Initialize the layer using the cached layer header. (This preserves all editable layer attributes.)
660+
PDImages.GetActiveImage.GetLayerByID(newLayerID).CreateNewLayerFromXML m_StashedLayer, newLayerID, True
661+
662+
'If this is a vector layer, initialize it from text-based vector data
663+
If useVectorData Then useVectorData = PDImages.GetActiveImage.GetLayerByID(newLayerID).SetVectorDataFromXML(m_StashedVector)
664+
665+
'If the vector creation was successful, no further work is required.
666+
' (If vector creation was *not* successful - which is unexpected and shouldn't happen -
667+
' fall back to the cached raster data instead.)
668+
If useVectorData Then
669+
PDDebug.LogAction "Pasting used vector data."
670+
ClipboardPaste_InternalData = True
671+
Else
672+
673+
PDDebug.LogAction "Paste will use raster data..."
674+
675+
'Load the stashed pixel data into a standalone DIB
676+
Dim tmpDIB As pdDIB
677+
Set tmpDIB = New pdDIB
678+
ClipboardPaste_InternalData = Loading.QuickLoadImageToDIB(m_StashFile, tmpDIB, False, False)
679+
If ClipboardPaste_InternalData Then
680+
681+
'Forcibly convert the new layer to 32bpp
682+
' (failsafe only; it should already be in 32-bpp mode from the loader)
683+
If (tmpDIB.GetDIBColorDepth <> 32) Then tmpDIB.ConvertTo32bpp
684+
685+
'Replace the new layer's backing surface with the temporary DIB
686+
PDImages.GetActiveImage.GetLayerByID(newLayerID).SetLayerDIB tmpDIB
687+
688+
Else
689+
PDDebug.LogAction "WARNING! ClipboardPaste_InternalData failed to load the stash file. Paste abandoned."
690+
End If
691+
692+
End If
693+
694+
'Notify the parent image of these changes, as it needs to generate a new composite image
695+
PDImages.GetActiveImage.NotifyImageChanged UNDO_Image_VectorSafe
696+
697+
'If a layer header *wasn't* cached, we are probably copying from a non-layer source (like a selection).
698+
' Simply load the cached raster data as a new, standalone layer.
699+
Else
700+
m_LayerCounter = m_LayerCounter + 1
701+
ClipboardPaste_InternalData = Layers.LoadImageAsNewLayer(False, m_StashFile, g_Language.TranslateMessage("Clipboard Image") & " " & CStr(m_LayerCounter), False, False)
702+
End If
703+
605704
Else
606705
ClipboardPaste_InternalData = Loading.LoadFileAsNewImage(m_StashFile, sTitle, False)
607706
End If
@@ -1634,13 +1733,14 @@ DragDropTextFailed:
16341733

16351734
End Function
16361735

1637-
'When a Copy or Paste event is initiated, PD doesn't actually copy anything to the clipboard. Instead, it "stashes" a copy of the
1638-
' generic, core image data that would be translated to some clipboard format (e.g. a layer or standalone DIB or something). When some
1639-
' other program (or PD itself) requests that data via Paste, we then retrieve the "stashed" data and render it into the requested
1640-
' format. This provides as ton of benefits, including better Copy performance (as we only render data in a singular format if/when it's
1641-
' actually needed), reduced memory (we don't flood the clipboard with a given format until it's actually required), and cleaner code.
1736+
'When a Copy or Paste event is initiated, PD doesn't actually copy anything to the clipboard.
1737+
' Instead, it "stashes" a copy of the generic, core image data that would be translated to some clipboard format
1738+
' (e.g. a layer or standalone DIB or something). When some other program (or PD itself) requests that data via Paste,
1739+
' we then retrieve the "stashed" data and render it into the requested format. This provides as ton of benefits,
1740+
' including better Copy performance (as we only render data in a singular format if/when it's actually needed),
1741+
' reduced memory (we don't flood the clipboard with a given format until it's actually required), and cleaner code.
16421742
'
1643-
'FYI: stashed data uses premultiplied alpha, by design.
1743+
'FYI: stashed pixel data uses premultiplied alpha, by design.
16441744
Private Sub StashClipboardData(ByVal copyMerged As Boolean)
16451745

16461746
'The specific data we stash varies according to a few different parameters.
@@ -1671,11 +1771,13 @@ Private Sub StashClipboardData(ByVal copyMerged As Boolean)
16711771

16721772
End If
16731773

1674-
'If we already have a stash file, remove it
1774+
'Remove any previously stashed file(s). (This can happen if the user copies, but doesn't paste.)
16751775
If (LenB(m_StashFile) <> 0) Then Files.FileDeleteIfExists m_StashFile
1776+
If (LenB(m_StashFileLayer) <> 0) Then Files.FileDeleteIfExists m_StashFileLayer
1777+
If (LenB(m_StashFileVector) <> 0) Then Files.FileDeleteIfExists m_StashFileVector
16761778

1677-
'Grab a new temporary path name (TODO: use system-generated temp file names)
1678-
If (LenB(m_StashFile) = 0) Then m_StashFile = UserPrefs.GetTempPath() & "cbStash.tmpdib"
1779+
'Grab a new temporary filename for pixel data
1780+
If (LenB(m_StashFile) = 0) Then m_StashFile = OS.UniqueTempFilename(customExtension:="tmpdib")
16791781

16801782
PDDebug.LogAction "Writing clipboard stash now..."
16811783
If (Not m_ClipboardDIB Is Nothing) Then
@@ -1686,6 +1788,28 @@ Private Sub StashClipboardData(ByVal copyMerged As Boolean)
16861788
'Erase the temporary DIB
16871789
m_ClipboardDIB.EraseDIB
16881790

1791+
'Erase any stored layer and vector data
1792+
m_StashedLayer = vbNullString
1793+
m_StashedVector = vbNullString
1794+
1795+
'If the cut/copy action is an entire source layer, also cache the layer header (and vector data if relevant).
1796+
If (Not PDImages.GetActiveImage.IsSelectionActive) And (Not copyMerged) Then
1797+
'Private m_StashedLayer As String, m_StashedVector As String
1798+
1799+
If (LenB(m_StashFileLayer) = 0) Then m_StashFileLayer = OS.UniqueTempFilename(customExtension:="txt")
1800+
Files.FileSaveAsText PDImages.GetActiveImage.GetActiveLayer.GetLayerHeaderAsXML(), m_StashFileLayer
1801+
1802+
If PDImages.GetActiveImage.GetActiveLayer.IsLayerVector() Then
1803+
If (LenB(m_StashFileVector) = 0) Then m_StashFileVector = OS.UniqueTempFilename(customExtension:="txt")
1804+
Files.FileSaveAsText PDImages.GetActiveImage.GetActiveLayer.GetVectorDataAsXML(), m_StashFileVector
1805+
End If
1806+
1807+
'If we're *not* saving layer data, blank out those filenames so we know not to use them later
1808+
Else
1809+
m_StashFileLayer = vbNullString
1810+
m_StashFileVector = vbNullString
1811+
End If
1812+
16891813
PDDebug.LogAction "Clipboard stashed successfully."
16901814

16911815
Else
@@ -1694,7 +1818,10 @@ Private Sub StashClipboardData(ByVal copyMerged As Boolean)
16941818

16951819
End Sub
16961820

1697-
'Retrieve previously stashed data. If this function returns TRUE, the stashed data has been successfully loaded into m_ClipboardDIB.
1821+
'Retrieve previously stashed data. If this function returns TRUE:
1822+
' 1) stashed pixel data has been successfully loaded into m_ClipboardDIB (always guaranteed)
1823+
' 2) optionally, stashed layer header (if any) has been successfully loaded into m_stashedLayer
1824+
' 3) optionally, stashed vector data (if any) has been successfully loaded into m_stashedVector
16981825
Private Function UnstashClipboardData() As Boolean
16991826

17001827
UnstashClipboardData = False
@@ -1711,22 +1838,28 @@ Private Function UnstashClipboardData() As Boolean
17111838

17121839
End If
17131840

1841+
If Files.FileExists(m_StashFileLayer) Then Files.FileLoadAsString m_StashFileLayer, m_StashedLayer
1842+
If Files.FileExists(m_StashFileVector) Then Files.FileLoadAsString m_StashFileVector, m_StashedVector
1843+
17141844
End If
17151845

17161846
End Function
17171847

1718-
'If you know it's safe to destroy PD's clipboard cache, you can do so via this function. Note that this action is not reversible,
1719-
' so make sure it's what you want/need to do.
1848+
'If you know it's safe to destroy PD's clipboard cache, you can do so via this function.
1849+
' Note that this action is *not* reversible, so make sure it's what you want/need to do.
17201850
Private Sub DestroyStashedData()
17211851
If (LenB(m_StashFile) <> 0) Then Files.FileDeleteIfExists m_StashFile
1852+
If (LenB(m_StashFileLayer) <> 0) Then Files.FileDeleteIfExists m_StashFileLayer
1853+
If (LenB(m_StashFileVector) <> 0) Then Files.FileDeleteIfExists m_StashFileVector
17221854
End Sub
17231855

17241856
Friend Function IsPDDataOnClipboard() As Boolean
17251857
IsPDDataOnClipboard = m_Clipboard.IsOurDataOnTheClipboard()
17261858
End Function
17271859

17281860
'Want to render PD's current clipboard stash to the clipboard, WITHOUT delayed rendering? Call this function.
1729-
' (PD does this prior to shutdown, because WM_RENDERALLFORMATS does not play nicely with the order VB unloads objects / destroys windows.)
1861+
' (PD does this prior to shutdown, because WM_RENDERALLFORMATS does not play nicely with the order
1862+
' VB unloads objects / destroys windows.)
17301863
Friend Sub RenderAllClipboardFormatsManually()
17311864

17321865
PDDebug.LogAction "pdClipboardMain received notification to render all clipboard formats. Rendering now..."
@@ -2136,5 +2269,7 @@ Private Sub Class_Terminate()
21362269

21372270
'Perform a failsafe check against un-destroyed stash data
21382271
If (LenB(m_StashFile) <> 0) Then Files.FileDeleteIfExists m_StashFile
2272+
If (LenB(m_StashFileLayer) <> 0) Then Files.FileDeleteIfExists m_StashFileLayer
2273+
If (LenB(m_StashFileVector) <> 0) Then Files.FileDeleteIfExists m_StashFileVector
21392274

21402275
End Sub

0 commit comments

Comments
 (0)