@@ -3,9 +3,10 @@ Attribute VB_Name = "DIBs"
33'DIB Support Functions
44'Copyright 2012-2021 by Tanner Helland
55'Created: 27/March/15 (though many individual functions are much older!)
6- 'Last updated: 10/October/21
7- 'Last update: move ancient CreateDibFromStdPicture function out of pdDIB and into here (it's currently
8- ' only used by
6+ 'Last updated: 15/October/21
7+ 'Last update: new functions for comparing two DIBs and outputting a new DIB with the lowest-entropy bits
8+ ' from either input stream (we use this during animation export to determine ideal strategies
9+ ' when pixel-blanking back-to-back frames)
910'
1011'This module contains support functions for the pdDIB class. In old versions of PD,
1112' these functions were provided by pdDIB, but there's no sense cluttering up that class
@@ -894,7 +895,12 @@ End Function
894895'
895896'Note that - by design - neither DIB is modified by this function. Only the transparency table
896897' is modified.
897- Public Function ApplyAlpha_DuplicatePixels (ByRef topDIB As pdDIB , ByRef bottomDIB As pdDIB , ByRef dstTransparencyTable() As Byte , Optional ByVal topOffsetX As Long = 0 , Optional ByVal topOffsetY As Long = 0 ) As Boolean
898+ '
899+ 'If activated, the optional "autoDenoise" parameter will change the algorithm to *not* blank out
900+ ' pixels unless at least two of them are touching (under the assumption that introducing 1-px
901+ ' noise will hurt most compression schemes). Note that the denoiser does not work across
902+ ' scanline boundaries, at present, but could be modified to do so.
903+ Public Function ApplyAlpha_DuplicatePixels (ByRef topDIB As pdDIB , ByRef bottomDIB As pdDIB , ByRef dstTransparencyTable() As Byte , Optional ByVal topOffsetX As Long = 0 , Optional ByVal topOffsetY As Long = 0 , Optional ByVal autoDenoise As Boolean = False ) As Boolean
898904
899905 If (topDIB Is Nothing ) Then Exit Function
900906 If (bottomDIB Is Nothing ) Then Exit Function
@@ -909,17 +915,55 @@ Public Function ApplyAlpha_DuplicatePixels(ByRef topDIB As pdDIB, ByRef bottomDI
909915 finalX = (topDIB.GetDIBWidth - 1 )
910916 finalY = (topDIB.GetDIBHeight - 1 )
911917
918+ 'Failsafe check for single-pixel images
919+ If (finalX < 1 ) Then
920+ ApplyAlpha_DuplicatePixels = True
921+ Exit Function
922+ End If
923+
912924 Dim srcDataTop() As Long , tmpSATop As SafeArray1D
913925 Dim srcDataBottom() As Long , tmpSABottom As SafeArray1D
926+ Dim origAlpha As Byte
914927
915928 'Loop through the image, checking alphas as we go
916929 For y = 0 To finalY
917930 topDIB.WrapLongArrayAroundScanline srcDataTop, tmpSATop, y
918931 bottomDIB.WrapLongArrayAroundScanline srcDataBottom, tmpSABottom, y + topOffsetY
919932 For x = 0 To finalX
920933
921- 'Make matching pixels transparent
922- If srcDataTop(x) = srcDataBottom(x + topOffsetX) Then dstTransparencyTable(x, y) = 0
934+ 'We use two strategies here, based on whether autoDenoise is active
935+ If autoDenoise Then
936+
937+ origAlpha = dstTransparencyTable(x, y)
938+
939+ 'First, see if this pixel will be blanked at all
940+ If srcDataTop(x) = srcDataBottom(x + topOffsetX) Then
941+
942+ If (x > 0 ) Then
943+
944+ 'Check left pixel regardless; if it matches, we can blank the pixel immediately
945+ If srcDataTop(x - 1 ) = srcDataBottom(x + topOffsetX - 1 ) Then
946+ dstTransparencyTable(x, y) = 0
947+
948+ 'Left pixel doesn't match; try right pixel
949+ Else
950+ If (x < finalX) Then
951+ If srcDataTop(x + 1 ) = srcDataBottom(x + topOffsetX + 1 ) Then dstTransparencyTable(x, y) = 0
952+ End If
953+ End If
954+
955+ 'x = 0, check right pixel before blanking
956+ Else
957+ If srcDataTop(x + 1 ) = srcDataBottom(x + topOffsetX + 1 ) Then dstTransparencyTable(x, y) = 0
958+ End If
959+
960+ '/do nothing if pixels don't match
961+ End If
962+
963+ 'When autoDenoise is disabled, just make matching pixels transparent
964+ Else
965+ If srcDataTop(x) = srcDataBottom(x + topOffsetX) Then dstTransparencyTable(x, y) = 0
966+ End If
923967
924968 Next x
925969 Next y
@@ -1551,6 +1595,181 @@ Public Function ColorizeDIB(ByRef srcDIB As pdDIB, ByVal newColor As Long) As Bo
15511595
15521596End Function
15531597
1598+ 'This function is used specifically for optimizing animation frames. PD can perform an optimization
1599+ ' called "pixel blanking", which involves making pixels transparent if they are identical to the
1600+ ' previous frame's pixels. It is difficult to predict the cost vs benefit of this optimization
1601+ ' because sometimes pixel blanking creates a lot of noise, which actually compresses poorly, while
1602+ ' other times it can provide massive gains. To try and maximize our benefits from pixel blanking,
1603+ ' PD's animated GIF exporter will produce two copies of an exported frame: a non-pixel-blanked one,
1604+ ' and a maximally-pixel-blanked one. This function will then loop through each scanline and pick
1605+ ' the one with minimal entropy (using a shorthand estimator by the PNG working group). Only that
1606+ ' line gets copied into the destination DIB. The result is generally a mixed-blanking frame that
1607+ ' compresses better than either of the source DIBs.
1608+ Public Function MakeMinimalEntropyScanlines (ByRef srcData1() As Byte , ByRef srcData2() As Byte , ByVal dataWidth As Long , ByVal dataHeight As Long , ByRef dstData() As Byte ) As Boolean
1609+
1610+ 'Ensure the destination array exists and is the correct size
1611+ ReDim dstData(0 To dataWidth - 1 , 0 To dataHeight - 1 ) As Byte
1612+
1613+ 'We now split into two possible sub-tests, which vary their behavior based on the size of the
1614+ ' incoming dataset(s). (If the set is too small, DEFLATE is a poor predictor of entropy
1615+ ' because there's not enough data to build a meaningful compression table; in these instances,
1616+ ' we drop back to a simpler RLE scheme.)
1617+ If (dataWidth * dataHeight < 128 ) Then
1618+ MakeMinimalEntropyScanlines = MakeMinimalEntropy_Small(srcData1, srcData2, dataWidth, dataHeight, dstData)
1619+ Else
1620+ MakeMinimalEntropyScanlines = MakeMinimalEntropy_Big(srcData1, srcData2, dataWidth, dataHeight, dstData)
1621+ End If
1622+
1623+ End Function
1624+
1625+ Private Function MakeMinimalEntropy_Small (ByRef srcData1() As Byte , ByRef srcData2() As Byte , ByVal dataWidth As Long , ByVal dataHeight As Long , ByRef dstData() As Byte ) As Boolean
1626+
1627+ 'For small data sets, we use a simple RLE-based entropy detector. Whichever source dataset
1628+ ' currently maintains the longest run of identical bytes gets sent to the destination.
1629+ Dim ent1 As Long , ent2 As Long
1630+ ent1 = 0
1631+ ent2 = 0
1632+
1633+ Dim cmpPrevious1 As Long , cmpPrevious2 As Long
1634+ Dim pX As Long , pY As Long
1635+
1636+ 'Iterate through the image, tracking consecutive matching pixels as we go
1637+ Dim x As Long , y As Long
1638+ For y = 0 To dataHeight - 1
1639+
1640+ For x = 0 To dataWidth - 1
1641+
1642+ 'Determine a previous pixel value for both data sets, accounting for scanline wrapping
1643+ ' (compression generally treats the data as a 1D dataset)
1644+ If (x = 0 ) Then
1645+ If (y > 0 ) Then
1646+ pX = dataWidth - 1
1647+ pY = y - 1
1648+ Else
1649+ pX = 0
1650+ pY = 0
1651+ End If
1652+ Else
1653+ pX = x - 1
1654+ pY = y
1655+
1656+ End If
1657+
1658+ cmpPrevious1 = srcData1(pX, pY)
1659+ cmpPrevious2 = srcData2(pX, pY)
1660+
1661+ 'If this is *not* the first pixel, store a pixel from whichever data set
1662+ ' has the longest run of identical pixels.
1663+ If (x > 0 ) Or (y > 0 ) Then
1664+ If (srcData1(x, y) = cmpPrevious1) Then ent1 = ent1 + 1 Else ent1 = 0
1665+ If (srcData2(x, y) = cmpPrevious2) Then ent2 = ent2 + 1 Else ent2 = 0
1666+
1667+ 'Whichever pixel value is higher determines what we store for the *previous* pixel.
1668+ ' (If both are 0, it doesn't matter what gets stored; the previous pixel doesn't
1669+ ' match either of these ones, so there's no obvious winner.)
1670+ If (ent1 >= ent2) Then
1671+ dstData(x, y) = srcData1(x, y)
1672+ If (x = 1 ) And (y = 0 ) Then dstData(0 , 0 ) = srcData1(0 , 0 )
1673+ Else
1674+ dstData(x, y) = srcData2(x, y)
1675+ If (x = 1 ) And (y = 0 ) Then dstData(0 , 0 ) = srcData2(0 , 0 )
1676+ End If
1677+
1678+ End If
1679+
1680+ Next x
1681+
1682+ Next y
1683+
1684+ 'Handle the final pixel manually
1685+ pX = dataWidth - 1
1686+ pY = dataHeight - 1
1687+ If (ent1 >= ent2) Then
1688+ dstData(pX, pY) = srcData1(pX, pY)
1689+ Else
1690+ dstData(pX, pY) = srcData2(pX, pY)
1691+ End If
1692+
1693+ MakeMinimalEntropy_Small = True
1694+
1695+ End Function
1696+
1697+ Private Function MakeMinimalEntropy_Big (ByRef srcData1() As Byte , ByRef srcData2() As Byte , ByVal dataWidth As Long , ByVal dataHeight As Long , ByRef dstData() As Byte ) As Boolean
1698+
1699+ 'On larger data sets, an easy test for entropy is a compression engine (any works).
1700+ ' Just attempt to compress the source data streams and assume whichever compresses better
1701+ ' will produce a similar result in the destination stream.
1702+ Dim chunkSize As Long
1703+ chunkSize = dataWidth
1704+
1705+ 'Wrap 1D arrays around source and destination targets because it makes life much simpler
1706+ Dim totalSize As Long
1707+ totalSize = dataWidth * dataHeight
1708+
1709+ Dim src1() As Byte , src2() As Byte , dst() As Byte
1710+ Dim srcSA1 As SafeArray1D , srcSA2 As SafeArray1D , dstSA As SafeArray1D
1711+ VBHacks.WrapArrayAroundPtr_Byte src1, srcSA1, VarPtr(srcData1(0 , 0 )), totalSize
1712+ VBHacks.WrapArrayAroundPtr_Byte src2, srcSA2, VarPtr(srcData2(0 , 0 )), totalSize
1713+ VBHacks.WrapArrayAroundPtr_Byte dst, dstSA, VarPtr(dstData(0 , 0 )), totalSize
1714+
1715+ Dim curOffset As Long
1716+ curOffset = 0
1717+
1718+ Dim tmpCompress() As Byte , tmpCompressSize As Long
1719+ tmpCompressSize = Compression.GetWorstCaseSize(chunkSize, cf_Lz4)
1720+ ReDim tmpCompress(0 To tmpCompressSize - 1 ) As Byte
1721+
1722+ Dim size1 As Long , size2 As Long
1723+
1724+ 'To try and prevent overly-aggressive "flipping" between streams, we apply a slight penalty
1725+ ' to whichever stream was *not* chosen last. This biases the encoder toward consistently
1726+ ' selecting the same stream (which likely provides better long-term compression benefits)
1727+ ' unless switching to a new stream shows a meaningful compression advantage.
1728+ '
1729+ 'The current value of this constant was chosen by trial-and-error. I am open to modifying
1730+ ' it further pending better data. Because the modifier is multiplied directly by the
1731+ ' compressed size of the targeted stream, make sure it is > 1 or you'll bias it the
1732+ ' wrong way!
1733+ Const AVOIDANCE_PENALTY_PERCENT As Double = 1.025
1734+ Dim idLastChosenStream As Long
1735+ idLastChosenStream = 0
1736+
1737+ 'Iterate both source arrays and copy over the best-compressing chunks from either
1738+ Do While (curOffset < totalSize)
1739+
1740+ If (curOffset + chunkSize) > totalSize Then chunkSize = totalSize - curOffset
1741+
1742+ size1 = tmpCompressSize
1743+ size2 = tmpCompressSize
1744+ Compression.CompressPtrToPtr VarPtr(tmpCompress(0 )), size1, VarPtr(src1(curOffset)), chunkSize, cf_Lz4, 1
1745+ Compression.CompressPtrToPtr VarPtr(tmpCompress(0 )), size2, VarPtr(src2(curOffset)), chunkSize, cf_Lz4, 1
1746+
1747+ 'Apply a slight penalty to whichever stream was *not* used previously
1748+ If (idLastChosenStream = 1 ) Then size2 = Int(size2 * AVOIDANCE_PENALTY_PERCENT)
1749+ If (idLastChosenStream = 2 ) Then size1 = Int(size1 * AVOIDANCE_PENALTY_PERCENT)
1750+
1751+ 'Favor the first input on matches
1752+ If (size1 <= size2) Then
1753+ CopyMemoryStrict VarPtr(dst(curOffset)), VarPtr(src1(curOffset)), chunkSize
1754+ idLastChosenStream = 1
1755+ Else
1756+ CopyMemoryStrict VarPtr(dst(curOffset)), VarPtr(src2(curOffset)), chunkSize
1757+ idLastChosenStream = 2
1758+ End If
1759+
1760+ If (chunkSize = dataWidth) Then curOffset = curOffset + chunkSize Else curOffset = totalSize
1761+
1762+ Loop
1763+
1764+ 'Unwrap unsafe arrray wrappers
1765+ VBHacks.UnwrapArrayFromPtr_Byte src1
1766+ VBHacks.UnwrapArrayFromPtr_Byte src2
1767+ VBHacks.UnwrapArrayFromPtr_Byte dst
1768+
1769+ MakeMinimalEntropy_Big = True
1770+
1771+ End Function
1772+
15541773'Outline a 32-bpp DIB. The outline is drawn along the first-encountered border where transparent and opaque pixels meet.
15551774' The caller must supply the outline pen they want used and optionally, an edge threshold on the range [0, 100].
15561775'Returns: TRUE if successful; FALSE otherwise
0 commit comments