Skip to content

Commit 8a7bd81

Browse files
committed
PSD images: fix potential crash on some vector masks
This fix means PhotoDemon once again "works" (i.e. "doesn't crash") on the full psd-tools test image suite. (Obviously some images still do not reproduce 100% identical to Photoshop due to a lack of corresponding features in PhotoDemon.)
1 parent d27f71b commit 8a7bd81

File tree

2 files changed

+137
-117
lines changed

2 files changed

+137
-117
lines changed

Classes/pdPSDLayer.cls

Lines changed: 136 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ Attribute VB_Exposed = False
1515
'PhotoDemon PSD (PhotoShop Image) Layer Container and Parser
1616
'Copyright 2019-2023 by Tanner Helland
1717
'Created: 15/January/19
18-
'Last updated: 09/April/22
19-
'Last update: fix off-by-one problem in masks that extend to the right/bottom corner of a layer
18+
'Last updated: 27/June/23
19+
'Last update: add additional failsafe checks for vector mask intersection with attached layer
20+
' (vector masks can overlap layers in unpredictable ways, with clean intersections not always guaranteed)
2021
'
2122
'This class contains layer-specific data pulled from a PSD file. It is populated by a parent
2223
' pdPSD instance. It has no purpose outside of a PSD parsing context; for layer handling inside
@@ -1731,50 +1732,54 @@ Private Function ConstructLayerMasks(ByRef warningStack As pdStringStack, ByVal
17311732
If ((m_RealLayerMaskData.lmFlags And &H2&) <> 0) Then maskDisabled = True
17321733
End If
17331734

1734-
If maskDisabled Then GoTo SkipChannel
1735-
1736-
'We now enter a branch that is a temporary fix due to PhotoDemon's lack of full mask support.
1737-
'
1738-
'In the future, we will want to extract layer masks to their own special mask structure.
1739-
' As a preliminary test, PhotoDemon *will* do this for layer groups right now (but *not* for
1740-
' normal layers). The mask will then be stored with the layer group and exported back out
1741-
' to PSD as desired.
1742-
'
1743-
'But because PD's viewport renderer (and many other parts of the program) do not have full
1744-
' support for masks yet, "normal" layers instead have their mask(s) merged into the layer's
1745-
' alpha channel. This guarantees "correct" appearances in PD, but note that this layer will
1746-
' be permanently modified by this change.
1747-
1748-
'If this is a *layer group*, we have not created a layer DIB yet (because no layer image data exists).
1749-
' As such, this is our chance to finally create one at the size specified by the mask. Note that we
1750-
' only do this if the caller specified "load layer group masks as pixels" (which is very helpful for
1751-
' analyzing mask contents, but not at all helpful for correct image display).
1752-
If (specialLayerGroupMaskCase And loadGroupMasksAsPixels) Then
1753-
If (Not m_LayerDIB Is Nothing) Then InternalError FUNC_NAME, "layer image exists for group?"
1754-
Set m_LayerDIB = New pdDIB
1755-
m_LayerDIB.CreateBlank maskWidth, maskHeight, 32, vbWhite, 255
1756-
scanStart = m_LayerDIB.GetDIBPointer()
1757-
scanWidth = m_LayerDIB.GetDIBStride()
1758-
End If
1759-
1760-
'Populate mask rect, and note that it's always clipped to this layer's size.
1761-
' (Group masks are a little weird this way - TODO!)
1762-
With m_MaskRect
1763-
.Left = maskLeft
1764-
.Top = maskTop
1765-
.Width = maskWidth
1766-
.Height = maskHeight
1767-
End With
1768-
1769-
'There are a lot of different ways to go about applying the mask to the image. AFAIK, no guarantees
1770-
' are made about things like rect intersections between the mask and layer, although common sense
1771-
' suggests the layer is typically inclusive of the mask... but this is a PSD, so expect the worst.
1772-
1773-
'To simplify handling (especially for the case where a mask obscures a large portion of the layer
1774-
' via the "default color" mask byte), let's prep a full buffer at the size of the layer, fill it with
1775-
' the default value, then simply overwrite the relevant rect with the mask's actual data.
1776-
Dim tmpBuffer() As Byte
1735+
End With
17771736

1737+
If maskDisabled Then GoTo SkipChannel
1738+
1739+
'We now enter a branch that is a temporary fix due to PhotoDemon's lack of full mask support.
1740+
'
1741+
'In the future, we will want to extract layer masks to their own special mask structure.
1742+
' As a preliminary test, PhotoDemon *will* do this for layer groups right now (but *not* for
1743+
' normal layers). The mask will then be stored with the layer group and exported back out
1744+
' to PSD as desired.
1745+
'
1746+
'But because PD's viewport renderer (and many other parts of the program) do not have full
1747+
' support for masks yet, "normal" layers instead have their mask(s) merged into the layer's
1748+
' alpha channel. This guarantees "correct" appearances in PD, but note that this layer will
1749+
' be permanently modified by this change.
1750+
1751+
'If this is a *layer group*, we have not created a layer DIB yet (because no layer image data exists).
1752+
' As such, this is our chance to finally create one at the size specified by the mask. Note that we
1753+
' only do this if the caller specified "load layer group masks as pixels" (which is very helpful for
1754+
' analyzing mask contents, but not at all helpful for correct image display).
1755+
If (specialLayerGroupMaskCase And loadGroupMasksAsPixels) Then
1756+
If (Not m_LayerDIB Is Nothing) Then InternalError FUNC_NAME, "layer image exists for group?"
1757+
Set m_LayerDIB = New pdDIB
1758+
m_LayerDIB.CreateBlank maskWidth, maskHeight, 32, vbWhite, 255
1759+
scanStart = m_LayerDIB.GetDIBPointer()
1760+
scanWidth = m_LayerDIB.GetDIBStride()
1761+
End If
1762+
1763+
'Populate mask rect, and note that it's always clipped to this layer's size.
1764+
' (Group masks are a little weird this way - TODO!)
1765+
With m_MaskRect
1766+
.Left = maskLeft
1767+
.Top = maskTop
1768+
.Width = maskWidth
1769+
.Height = maskHeight
1770+
End With
1771+
1772+
'There are a lot of different ways to go about applying the mask to the image. AFAIK, no guarantees
1773+
' are made about things like rect intersections between the mask and layer, although common sense
1774+
' suggests the layer is typically inclusive of the mask... but this is a PSD, so expect the worst.
1775+
1776+
'To simplify handling (especially for the case where a mask obscures a large portion of the layer
1777+
' via the "default color" mask byte), let's prep a full buffer at the size of the layer, fill it with
1778+
' the default value, then simply overwrite the relevant rect with the mask's actual data.
1779+
Dim tmpBuffer() As Byte
1780+
1781+
With m_Channels(chIndex)
1782+
17781783
'Normal layers can use the underlying DIB dimensions for their reference; layer groups may not have
17791784
' such a DIB, so we need to prep them differently
17801785
If (specialLayerGroupMaskCase And (Not loadGroupMasksAsPixels)) Then
@@ -1806,37 +1811,52 @@ Private Function ConstructLayerMasks(ByRef warningStack As pdStringStack, ByVal
18061811
srcMaskRect.Right = srcMaskRect.Right - m_Rect.Left
18071812
srcMaskRect.Top = srcMaskRect.Top - m_Rect.Top
18081813
srcMaskRect.Bottom = srcMaskRect.Bottom - m_Rect.Top
1814+
1815+
End With
1816+
1817+
'On a normal layer, ensure the layer and mask overlap (and also, *where* they overlap).
1818+
' (On a layer group mask, this step is technically irrelevant, because the layer DIB will have
1819+
' been created according to the mask's dimensions.)
1820+
Dim intersectOK As Boolean
1821+
If specialLayerGroupMaskCase Then
1822+
1823+
intersectOK = True
1824+
With rIntersect
1825+
.Left = 0
1826+
.Top = 0
1827+
.Right = maskWidth - 1
1828+
.Bottom = maskHeight - 1
1829+
End With
1830+
1831+
Else
1832+
1833+
intersectOK = PDMath.IntersectRectL(rIntersect, PopulateRectL(0, 0, m_LayerDIB.GetDIBWidth - 1, m_LayerDIB.GetDIBHeight - 1), srcMaskRect)
1834+
1835+
'Validate the final rect, just in case. (In the next step we need to use it to index into an array,
1836+
' and overwrites there are catastrophic.)
1837+
'
1838+
'(This change was added in June 2023 to fix crashes on the test-case image "vector-mask2.psd" from the psd-tools repo.)
1839+
If ((rIntersect.Right - rIntersect.Left) >= maskWidth) Then rIntersect.Right = rIntersect.Left + (maskWidth - 1)
1840+
If ((rIntersect.Bottom - rIntersect.Top) >= maskHeight) Then rIntersect.Bottom = rIntersect.Top + (maskHeight - 1)
18091841

1810-
'On a normal layer, ensure the layer and mask overlap (and also, *where* they overlap).
1811-
' (On a layer group mask, this step is technically irrelevant, because the layer DIB will have
1812-
' been created according to the mask's dimensions.)
1813-
Dim intersectOK As Boolean
1842+
End If
1843+
1844+
If intersectOK Then
1845+
1846+
'We now need to merge the mask data with the "default color" array. Handling varies by
1847+
' parent layer color depth, but at the end of this we'll have an 8-bit copy the correctly
1848+
' represents the mask (regardless of invert status).
1849+
Dim mOffsetLeft As Long, mOffsetTop As Long
18141850
If specialLayerGroupMaskCase Then
1815-
intersectOK = True
1816-
With rIntersect
1817-
.Left = 0
1818-
.Top = 0
1819-
.Right = maskWidth
1820-
.Bottom = maskHeight
1821-
End With
1851+
mOffsetLeft = 0
1852+
mOffsetTop = 0
18221853
Else
1823-
intersectOK = PDMath.IntersectRectL(rIntersect, PopulateRectL(0, 0, m_LayerDIB.GetDIBWidth - 1, m_LayerDIB.GetDIBHeight - 1), srcMaskRect)
1854+
mOffsetLeft = srcMaskRect.Left
1855+
mOffsetTop = srcMaskRect.Top
18241856
End If
18251857

1826-
If intersectOK Then
1858+
With m_Channels(chIndex)
18271859

1828-
'We now need to merge the mask data with the "default color" array. Handling varies by
1829-
' parent layer color depth, but at the end of this we'll have an 8-bit copy the correctly
1830-
' represents the mask (regardless of invert status).
1831-
Dim mOffsetLeft As Long, mOffsetTop As Long
1832-
If specialLayerGroupMaskCase Then
1833-
mOffsetLeft = 0
1834-
mOffsetTop = 0
1835-
Else
1836-
mOffsetLeft = srcMaskRect.Left
1837-
mOffsetTop = srcMaskRect.Top
1838-
End If
1839-
18401860
'8-bit channel
18411861
If (bytesPerChannel = 1) Then
18421862

@@ -1875,55 +1895,55 @@ Private Function ConstructLayerMasks(ByRef warningStack As pdStringStack, ByVal
18751895

18761896
End If
18771897

1878-
'Rects do not overlap; only the "default color" of the mask will be used
1879-
Else
1880-
If PSD_DEBUG_VERBOSE Then PDDebug.LogAction "Mask found, but does not overlap the layer!"
1881-
End If
1898+
End With
1899+
1900+
'Rects do not overlap; only the "default color" of the mask will be used
1901+
Else
1902+
If PSD_DEBUG_VERBOSE Then PDDebug.LogAction "Mask found, but does not overlap the layer!"
1903+
End If
1904+
1905+
'Immediately free the source channel data to relieve memory pressure
1906+
Erase m_Channels(chIndex).ciDataDecoded
1907+
1908+
'On layer masks, we now want to populate the dedicated layer mask array; on image layers,
1909+
' we'll instead apply the mask directly to the target layer.
1910+
If (specialLayerGroupMaskCase And (Not loadGroupMasksAsPixels)) Then
18821911

1883-
'Immediately free the source channel data to relieve memory pressure
1884-
Erase .ciDataDecoded
1912+
'Just copy the temporary array to a dedicated, module-level mask array
1913+
m_sizeOfLayerMask = maskWidth * maskHeight
1914+
ReDim m_LayerMaskBytes(0 To m_sizeOfLayerMask - 1) As Byte
1915+
VBHacks.CopyMemoryStrict VarPtr(m_LayerMaskBytes(0)), VarPtr(tmpBuffer(0, 0)), m_sizeOfLayerMask
18851916

1886-
'On layer masks, we now want to populate the dedicated layer mask array; on image layers,
1887-
' we'll instead apply the mask directly to the target layer.
1888-
If (specialLayerGroupMaskCase And (Not loadGroupMasksAsPixels)) Then
1889-
1890-
'Just copy the temporary array to a dedicated, module-level mask array
1891-
m_sizeOfLayerMask = maskWidth * maskHeight
1892-
ReDim m_LayerMaskBytes(0 To m_sizeOfLayerMask - 1) As Byte
1893-
VBHacks.CopyMemoryStrict VarPtr(m_LayerMaskBytes(0)), VarPtr(tmpBuffer(0, 0)), m_sizeOfLayerMask
1894-
1895-
m_MaskWasMergedIntoDIB = False
1896-
1897-
'On image layers, actually applying the mask is very simple. We simply want to multiply any
1898-
' existing alpha values by the values found in the mask.
1899-
Else
1900-
1901-
Dim layerWidth As Long, layerHeight As Long
1902-
layerWidth = m_LayerDIB.GetDIBWidth
1903-
layerHeight = m_LayerDIB.GetDIBHeight
1904-
1905-
m_LayerDIB.WrapArrayAroundScanline tmpBytes, tmpSA, 0
1906-
xLoopEnd = layerWidth - 1
1907-
1908-
Const ONE_DIV_255 As Single = 1! / 255!
1909-
1910-
For y = 0 To layerHeight - 1
1911-
tmpSA.pvData = scanStart + (scanWidth * y)
1912-
ySrcOffset = y * layerWidth
1913-
For x = 0 To xLoopEnd
1914-
tmpSingle = CSng(tmpBuffer(x, y)) * ONE_DIV_255
1915-
tmpBytes(x * 4 + 3) = Int(CSng(tmpBytes(x * 4 + 3)) * tmpSingle)
1916-
Next x
1917-
Next y
1918-
1919-
m_LayerDIB.UnwrapArrayFromDIB tmpBytes
1920-
1921-
m_MaskWasMergedIntoDIB = True
1922-
1923-
End If
1917+
m_MaskWasMergedIntoDIB = False
19241918

1925-
End With
1926-
1919+
'On image layers, actually applying the mask is very simple. We simply want to multiply any
1920+
' existing alpha values by the values found in the mask.
1921+
Else
1922+
1923+
Dim layerWidth As Long, layerHeight As Long
1924+
layerWidth = m_LayerDIB.GetDIBWidth
1925+
layerHeight = m_LayerDIB.GetDIBHeight
1926+
1927+
m_LayerDIB.WrapArrayAroundScanline tmpBytes, tmpSA, 0
1928+
xLoopEnd = layerWidth - 1
1929+
1930+
Const ONE_DIV_255 As Single = 1! / 255!
1931+
1932+
For y = 0 To layerHeight - 1
1933+
tmpSA.pvData = scanStart + (scanWidth * y)
1934+
ySrcOffset = y * layerWidth
1935+
For x = 0 To xLoopEnd
1936+
tmpSingle = CSng(tmpBuffer(x, y)) * ONE_DIV_255
1937+
tmpBytes(x * 4 + 3) = Int(CSng(tmpBytes(x * 4 + 3)) * tmpSingle)
1938+
Next x
1939+
Next y
1940+
1941+
m_LayerDIB.UnwrapArrayFromDIB tmpBytes
1942+
1943+
m_MaskWasMergedIntoDIB = True
1944+
1945+
End If
1946+
19271947
SkipChannel:
19281948
Next chIndex
19291949

PhotoDemon.vbp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ Description="PhotoDemon Photo Editor"
518518
CompatibleMode="0"
519519
MajorVer=9
520520
MinorVer=1
521-
RevisionVer=154
521+
RevisionVer=157
522522
AutoIncrementVer=1
523523
ServerSupportFiles=0
524524
VersionComments="Copyright 2000-2023 Tanner Helland - photodemon.org"

0 commit comments

Comments
 (0)