@@ -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+
19271947SkipChannel:
19281948 Next chIndex
19291949
0 commit comments