#FEATURE by cg
authorClaus Gittinger <cg@exept.de>
Fri, 17 Feb 2017 11:57:52 +0100
changeset 3900 a95276d36ce8
parent 3899 fd43372bf11d
child 3901 f2c56662a45e
#FEATURE by cg class: PNGReader handle transparency chunk handle some alpha
PNGReader.st
--- a/PNGReader.st	Fri Feb 17 10:51:06 2017 +0100
+++ b/PNGReader.st	Fri Feb 17 11:57:52 2017 +0100
@@ -16,8 +16,10 @@
 ImageReader subclass:#PNGReader
 	instanceVariableNames:'colorType bitsPerChannel depth compressionMethod filterMethod
 		interlaceMode bytesPerScanline globalDataChunk thisScanline
-		prevScanline processTextChunks specialChunkHandlers'
-	classVariableNames:'Verbose'
+		prevScanline processTextChunks specialChunkHandlers
+		paletteAlphaEntries paletteIndexForMaskedPixels image'
+	classVariableNames:'Verbose ColorTypeGray ColorTypeRGB ColorTypePalette
+		ColorTypeGrayAlpha ColorTypeRGBAlpha'
 	poolDictionaries:''
 	category:'Graphics-Images-Readers'
 !
@@ -93,6 +95,14 @@
     MIMETypes defineImageType:'image/png'  suffix:'png' reader:self.
     "/ backward compatibility from times when png was not yet so common...
     MIMETypes defineImageType:'image/x-png' suffix:nil reader:self.
+
+    ColorTypeGray := 0.
+    ColorTypeRGB := 2.
+    ColorTypePalette := 3.
+    ColorTypeGrayAlpha := 4.
+    ColorTypeRGBAlpha := 6.
+
+    "Modified: / 16-02-2017 / 19:41:40 / cg"
 ! !
 
 !PNGReader class methodsFor:'queries'!
@@ -138,6 +148,21 @@
     "Modified: 21.6.1996 / 20:38:46 / cg"
 ! !
 
+!PNGReader methodsFor:'accessing'!
+
+makeImage
+    image isNil ifTrue:[
+        image := super makeImage.
+    ].
+    ^ image
+
+    "Created: / 17-02-2017 / 11:14:45 / cg"
+!
+
+pngHeader
+    ^ #[137 80 78 71 13 10 26 10]
+! !
+
 !PNGReader methodsFor:'hooks'!
 
 specialChunkHandlerAt:chunkType put:aHandlerBlock
@@ -153,7 +178,309 @@
     "Created: / 14-02-2017 / 11:43:33 / cg"
 ! !
 
-!PNGReader methodsFor:'private-chunks'!
+!PNGReader methodsFor:'reading'!
+
+fromStream:aStream
+    "read a stream containing a PNG image.
+     Leave the image description in instance variables
+     (i.e. to get the image, ask with image)."
+
+    |header|
+
+    inStream := aStream.
+    aStream binary.
+
+    "PNG-files are always msb (network-world)"
+    byteOrder := #msb.
+
+    header := ByteArray new:8.
+    aStream nextBytes:8 into:header.
+
+    header = (self pngHeader) ifFalse:[
+        Logger warning:'PNGReader: not a png file (%1)' with:aStream.
+        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: not a png file'.
+        ^ nil
+    ].
+
+    (self getIHDRChunk) ifFalse:[
+        Logger warning:'PNGReader: required IHDR chunk missing (%1)' with:aStream.
+        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: required IHDR chunk missing'.
+        ^ nil
+    ].
+
+    compressionMethod ~~ 0 ifTrue:[
+        ('PNGReader: compressionMethod %s not supported.' printfWith:compressionMethod printString) infoPrintCR.
+        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: unsupported compression method'.
+        ^ nil
+    ].
+    filterMethod > 0 ifTrue:[
+        'PNGReader: invalid filterMethod' infoPrintCR.
+        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: invalid/unsupported filterMethod'.
+        ^ nil.
+    ].
+    interlaceMode > 1 ifTrue:[
+        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: invalid/unsupported interlaceMode'.
+        'PNGReader: invalid interlaceMode' infoPrintCR.
+        ^ nil.
+    ].
+    (self setColorType:colorType) ifFalse:[
+        ^ nil
+    ].
+
+    [self getChunk] whileTrue.
+
+    bytesPerScanline := self bytesPerRow.
+    data := ByteArray new:(bytesPerScanline * height).
+
+    globalDataChunk notNil ifTrue:[
+        self processGlobalIDATChunk.
+        globalDataChunk := nil.
+    ].
+
+    photometric == #palette ifTrue:[
+        colorMap isNil ifTrue:[
+            Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: palette chunk missing'.
+            'PNGReader: palette chunk missing.' infoPrintCR.
+            "/ ^ nil
+        ].
+    ].
+    
+    paletteAlphaEntries notNil ifTrue:[
+        "/ this vector now provides alpha values.
+        "/ for now, we can only deal with 0/255 alpha values...
+        (paletteAlphaEntries conform:[:a | (a == 0) or:[a == 255]]) ifFalse:[
+            "/ must generate real alpha info (eg. rgba)
+        ] ifTrue:[
+            self generateMaskFromPaletteAlphaEntries
+        ].    
+    ].
+    
+    "
+     PNGReader fromFile:'/home/cg/libpng-0.89c/pngtest.png'
+    "
+
+    "Modified: / 17-02-2017 / 11:34:01 / cg"
+! !
+
+!PNGReader methodsFor:'reading-private'!
+
+generateMaskFromPaletteAlphaEntries
+    image isNil ifTrue:[
+        self makeImage
+    ].
+    
+    colorType == ColorTypePalette ifTrue:[
+        "/ if there is only one entry to care for...
+        (paletteAlphaEntries occurrencesOf:0) == 1 ifTrue:[
+            (paletteAlphaEntries conform:[:alpha | (alpha == 0) or:[alpha == 255]]) ifTrue:[
+                mask := self class buildMaskFromColor:(paletteAlphaEntries indexOf:0)-1 for:data width:width height:height.
+                ^ self
+            ].    
+        ].    
+
+        "/ multiple masked pixels...
+        paletteAlphaEntries keysAndValuesDo:[:pixelIndex :alpha |
+            alpha == 0 ifTrue:[
+                image createMaskForPixelValue:pixelIndex
+            ] ifFalse:[
+                alpha == 255 ifTrue:[
+                    "/ ok; already
+                ] ifFalse:[
+                    self error:'should generate an alpha image' 
+                ]    
+            ].
+        ].
+        ^ self.
+    ].
+    
+    colorType == ColorTypeGray ifTrue:[
+        |maskPixel|
+
+        maskPixel := paletteAlphaEntries unsignedInt16At:1 MSB:true.
+        image createMaskForPixelValue:maskPixel.
+        ^ self 
+    ].    
+    colorType == ColorTypeRGB ifTrue:[
+        |mr mg mb maskPixel|
+        
+        mr := paletteAlphaEntries unsignedInt16At:1 MSB:true.
+        mg := paletteAlphaEntries unsignedInt16At:3 MSB:true.
+        mb := paletteAlphaEntries unsignedInt16At:5 MSB:true.
+        depth <= 32 ifTrue:[
+            "/ only use the low bits...
+            mr > 255 ifTrue:[ self error:'only 8bit RGB supported'].
+            mg > 255 ifTrue:[ self error:'only 8bit RGB supported'].
+            mb > 255 ifTrue:[ self error:'only 8bit RGB supported'].
+            maskPixel := ((mr bitShift:16) bitOr:(mg bitShift:8)) bitOr:mb.
+        ] ifFalse:[
+            maskPixel := ((mr bitShift:32) bitOr:(mg bitShift:16)) bitOr:mb.
+        ].   
+        image createMaskForPixelValue:maskPixel.
+        ^ self 
+    ].
+
+    "Created: / 17-02-2017 / 08:07:04 / cg"
+    "Modified: / 17-02-2017 / 11:55:57 / cg"
+!
+
+getChunk
+    |len type crc|
+
+    inStream atEnd ifTrue:[^ false].
+
+    len := inStream nextInt32MSB:true.
+    type := String new:4.
+    (inStream nextBytes:4 into:type) ~~ 4 ifTrue:[^ false].
+
+    Verbose == true ifTrue:[
+        'len: ' infoPrint. len infoPrint. ' type: ' infoPrint. type infoPrintCR.
+    ].
+
+    type = 'IEND' ifTrue:[^ false].
+
+    (self processChunk:type len:len) ifFalse:[^ false].
+
+    crc := inStream nextInt32MSB:true.  "/ ignored - for now
+    ^ true
+
+    "Created: 21.6.1996 / 21:09:36 / cg"
+    "Modified: 21.6.1996 / 21:20:26 / cg"
+!
+
+getIHDRChunk
+    |len type crc|
+
+    len := inStream nextInt32MSB:true.
+
+    type := String new:4.
+    inStream nextBytes:4 into:type.
+
+    type = 'IHDR' ifFalse:[
+        Logger warning:'PNGReader: expected IHDR magic (%1)' with:inStream.
+        ^ false
+    ].
+    len == 13 ifFalse:[
+        Logger warning:'PNGReader: bad IHDR size (%1)' with:inStream.
+        ^ false
+    ].
+
+    width := inStream nextInt32MSB:true.        
+    height := inStream nextInt32MSB:true.        
+
+    (width <= 0 or:[height <= 0]) ifTrue:[
+        Logger warning:'PNGReader: invalid dimension(s) (%1)' with:inStream.
+        ^ false.
+    ].
+    self reportDimension.
+
+    bitsPerChannel := inStream nextByte.
+    depth := bitsPerChannel. "/ will be changed by setColorType.
+    colorType := inStream nextByte.
+
+    compressionMethod := inStream nextByte.
+    filterMethod := inStream nextByte.
+    interlaceMode := inStream nextByte.
+
+    Verbose == true ifTrue:[
+        'PNGReader: IHDR:' infoPrintCR.
+        'PNGReader:   width: ' infoPrint. width infoPrintCR.
+        'PNGReader:   height: ' infoPrint. height infoPrintCR.
+        'PNGReader:   bitsPerChannel: ' infoPrint. bitsPerChannel infoPrintCR.
+        'PNGReader:   colorType: ' infoPrint. colorType infoPrintCR.
+        'PNGReader:   compressionMethod: ' infoPrint. compressionMethod infoPrintCR.
+        'PNGReader:   filterMethod: ' infoPrint. filterMethod infoPrintCR.
+        'PNGReader:   interlaceMode: ' infoPrint. interlaceMode infoPrintCR.
+    ].
+
+    crc := inStream nextInt32MSB:true.
+    ^ true
+
+    "Modified: / 17-02-2017 / 11:35:20 / cg"
+!
+
+handleText:text keyword:keyword
+    "/ things like exif data etc.
+    "/ self halt.
+
+    "/Standard keywords for text chunks:
+    "/
+    "/   Title            Short (one line) title or caption for image
+    "/   Author           Name of image's creator
+    "/   Description      Description of image (possibly long)
+    "/   Copyright        Copyright notice
+    "/   Creation Time    Time of original image creation
+    "/   Software         Software used to create the image
+    "/   Disclaimer       Legal disclaimer
+    "/   Warning          Warning of nature of content
+    "/   Source           Device used to create the image
+    "/   Comment          Miscellaneous comment; conversion from
+    "/                    GIF comment
+!
+
+setColorType:colorType
+    colorType == 0 ifTrue:[
+        photometric := #blackIs0.
+        samplesPerPixel := 1.
+        bitsPerSample := Array with:bitsPerChannel.
+        depth := bitsPerChannel.
+        ^ true.
+    ].
+
+    colorType == 2 ifTrue:[
+        bitsPerChannel < 8 ifTrue:[
+            'PNGReader: invalid colorType/depth combination' infoPrintCR.
+            ^ false.
+        ].
+        photometric := #rgb.
+        samplesPerPixel := 3.
+        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel.
+        depth := bitsPerChannel * 3.
+        ^ true.
+    ].
+
+    colorType == 3 ifTrue:[
+        bitsPerChannel == 16 ifTrue:[
+            'PNGReader: invalid colorType/depth combination' infoPrintCR.
+            ^ false.
+        ].
+        photometric := #palette.
+        samplesPerPixel := 1.
+        bitsPerSample := Array with:bitsPerChannel.
+        depth := bitsPerChannel.
+        ^ true.
+    ].
+
+    colorType == 4 ifTrue:[
+        bitsPerChannel < 8 ifTrue:[
+            'PNGReader: invalid colorType/depth combination' infoPrintCR.
+            ^ false.
+        ].
+        photometric := #blackIs0.
+        samplesPerPixel := 2.
+        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel.
+        depth := bitsPerChannel * 2.
+        ^ true.
+    ].
+
+    colorType == 6 ifTrue:[
+        bitsPerChannel < 8 ifTrue:[
+            'PNGReader: invalid colorType/depth combination' infoPrintCR.
+            ^ false.
+        ].
+        photometric := #rgba.
+        samplesPerPixel := 4.
+        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel.
+        depth := bitsPerChannel * 4.
+        ^ true.
+    ].
+
+    ('PNGReader: invalid colorType: ' , colorType printString , '.') infoPrintCR.
+    ^ false
+
+    "Modified: / 16-02-2017 / 17:19:19 / cg"
+! !
+
+!PNGReader methodsFor:'reading-private chunks'!
 
 doPass: pass
     "Certain interlace passes are skipped with certain small image dimensions"
@@ -237,6 +564,8 @@
 
     type = 'iTXt' ifTrue:[^ self processITXTChunkLen:len].
     type = 'iCCP' ifTrue:[^ self processICCPChunkLen:len].
+    type = 'hIST' ifTrue:[^ self processHISTChunkLen:len].
+    type = 'spAL' ifTrue:[^ self processSPALChunkLen:len].
 
     specialChunkHandlers notNil ifTrue:[
         |handler chunkData|
@@ -257,7 +586,7 @@
     ^ true.
 
     "Created: / 21-06-1996 / 21:10:37 / cg"
-    "Modified: / 14-02-2017 / 22:37:29 / cg"
+    "Modified: / 17-02-2017 / 11:32:06 / cg"
 !
 
 processGAMAChunkLen:len
@@ -274,6 +603,16 @@
     ^ self processInterlacedGlobalDATA
 !
 
+processHISTChunkLen:len
+    "/ ignored
+    "/ inStream nextBytes:len.
+
+    inStream skip:len.
+    ^ true
+
+    "Created: / 17-02-2017 / 11:31:36 / cg"
+!
+
 processICCPChunkLen:len
     "/ ignored
     "/ inStream nextBytes:len.
@@ -336,9 +675,10 @@
         ] ifFalse:[
             compressionFlag == 1 ifTrue:[
                 "/ for now, only zlib compression is supported...
-                'zlib compression currently unsupported' infoPrintCR.
+                "/ ... no - not even that is
+                Logger warning:'PNG: zlib compressed text currently unsupported'.
             ] ifFalse:[
-                'unsupported compression method' infoPrintCR.
+                Logger warning:'PNG: unsupported text compression method'.
             ].    
         ].    
         text := text utf8Decoded.
@@ -348,7 +688,8 @@
     ].
     ^ true
 
-    "Created: 21.6.1996 / 21:15:58 / cg"
+    "Created: / 21-06-1996 / 21:15:58 / cg"
+    "Modified: / 16-02-2017 / 19:51:21 / cg"
 !
 
 processInterlacedDATA:len
@@ -549,6 +890,16 @@
     "Created: 21.6.1996 / 21:13:09 / cg"
 !
 
+processSPALChunkLen:len
+    "/ ignored
+    "/ inStream nextBytes:len.
+
+    inStream skip:len.
+    ^ true
+
+    "Created: / 17-02-2017 / 11:31:40 / cg"
+!
+
 processTEXTChunkLen:len    
     "/ textual data in iso8859 coding.
 
@@ -583,8 +934,10 @@
 !
 
 processTRNSChunkLen:len
-    inStream skip:len.
+    paletteAlphaEntries := inStream nextBytes:len.
     ^ true
+
+    "Modified: / 17-02-2017 / 09:39:25 / cg"
 !
 
 processZTXTChunkLen:len
@@ -597,7 +950,7 @@
     "Created: 21.6.1996 / 21:15:58 / cg"
 ! !
 
-!PNGReader methodsFor:'private-filtering'!
+!PNGReader methodsFor:'reading-private filtering'!
 
 filterAverage:count 
     "Use the average of the pixel to the left and the pixel above as a predictor"
@@ -801,7 +1154,7 @@
     ^ al
 ! !
 
-!PNGReader methodsFor:'private-pixel copy'!
+!PNGReader methodsFor:'reading-private pixel copy'!
 
 copyPixels:y at:startX by:incX 
     "Handle interlaced pixels of supported colorTypes"
@@ -810,17 +1163,18 @@
 
     "/ per color type copy methods
     s := #( 
-             #copyPixelsGray:at:by:         "/ 0
+             #copyPixelsGray:at:by:         "/ 0 = ColorTypeGray
              nil
-             #copyPixelsRGB:at:by:          "/ 2
-             #copyPixelsIndexed:at:by:      "/ 3
-             #copyPixelsGrayAlpha:at:by:    "/ 4 
+             #copyPixelsRGB:at:by:          "/ 2 = ColorTypeRGB
+             #copyPixelsIndexed:at:by:      "/ 3 = ColorTypePalette
+             #copyPixelsGrayAlpha:at:by:    "/ 4 = ColorTypeGrayAlpha
              nil 
-             #copyPixelsRGBA:at:by:         "/ 6
+             #copyPixelsRGBA:at:by:         "/ 6 = ColorTypeRGBA
         ) at:colorType + 1.
     self perform:s with:y with:startX with:incX
 
     "Modified: / 03-05-2011 / 12:02:45 / cg"
+    "Modified (comment): / 16-02-2017 / 19:42:47 / cg"
 !
 
 copyPixelsGray:y at:startX by:incX 
@@ -995,237 +1349,7 @@
     ].
 ! !
 
-!PNGReader methodsFor:'private-reading'!
-
-getChunk
-    |len type crc|
-
-    inStream atEnd ifTrue:[^ false].
-
-    len := inStream nextInt32MSB:true.
-    type := String new:4.
-    (inStream nextBytes:4 into:type) ~~ 4 ifTrue:[^ false].
-
-    Verbose == true ifTrue:[
-        'len: ' infoPrint. len infoPrint. ' type: ' infoPrint. type infoPrintCR.
-    ].
-
-    type = 'IEND' ifTrue:[^ false].
-
-    (self processChunk:type len:len) ifFalse:[^ false].
-
-    crc := inStream nextInt32MSB:true.  "/ ignored - for now
-    ^ true
-
-    "Created: 21.6.1996 / 21:09:36 / cg"
-    "Modified: 21.6.1996 / 21:20:26 / cg"
-!
-
-getIHDRChunk
-    |len type crc|
-
-    len := inStream nextInt32MSB:true.
-
-    type := String new:4.
-    inStream nextBytes:4 into:type.
-
-    type = 'IHDR' ifFalse:[self halt:'expected IHDR magic'. ^ false].
-    len == 13 ifFalse:[self halt:'unexpected IHDR length'. ^ false].
-
-    width := inStream nextInt32MSB:true.        
-    height := inStream nextInt32MSB:true.        
-
-    (width <= 0 or:[height <= 0]) ifTrue:[
-        'PNGReader: invalid dimension(s)' infoPrintCR.
-        ^ false.
-    ].
-    self reportDimension.
-
-    bitsPerChannel := inStream nextByte.
-    depth := bitsPerChannel. "/ will be changed by setColorType.
-    colorType := inStream nextByte.
-
-    compressionMethod := inStream nextByte.
-    filterMethod := inStream nextByte.
-    interlaceMode := inStream nextByte.
-
-    Verbose == true ifTrue:[
-        'PNGReader: IHDR:' infoPrintCR.
-        'PNGReader:   width: ' infoPrint. width infoPrintCR.
-        'PNGReader:   height: ' infoPrint. height infoPrintCR.
-        'PNGReader:   bitsPerChannel: ' infoPrint. bitsPerChannel infoPrintCR.
-        'PNGReader:   colorType: ' infoPrint. colorType infoPrintCR.
-        'PNGReader:   compressionMethod: ' infoPrint. compressionMethod infoPrintCR.
-        'PNGReader:   filterMethod: ' infoPrint. filterMethod infoPrintCR.
-        'PNGReader:   interlaceMode: ' infoPrint. interlaceMode infoPrintCR.
-    ].
-
-    crc := inStream nextInt32MSB:true.
-    ^ true
-
-    "Modified: 21.6.1996 / 21:38:35 / cg"
-!
-
-handleText:text keyword:keyword
-    "/ things like exif data etc.
-    "/ self halt.
-
-    "/Standard keywords for text chunks:
-    "/
-    "/   Title            Short (one line) title or caption for image
-    "/   Author           Name of image's creator
-    "/   Description      Description of image (possibly long)
-    "/   Copyright        Copyright notice
-    "/   Creation Time    Time of original image creation
-    "/   Software         Software used to create the image
-    "/   Disclaimer       Legal disclaimer
-    "/   Warning          Warning of nature of content
-    "/   Source           Device used to create the image
-    "/   Comment          Miscellaneous comment; conversion from
-    "/                    GIF comment
-!
-
-setColorType:colorType
-    colorType == 0 ifTrue:[
-        photometric := #blackIs0.
-        samplesPerPixel := 1.
-        bitsPerSample := Array with:bitsPerChannel.
-        depth := bitsPerChannel.
-        ^ true.
-    ].
-
-    colorType == 2 ifTrue:[
-        bitsPerChannel < 8 ifTrue:[
-            'PNGReader: invalid colorType/depth combination' infoPrintCR.
-            ^ false.
-        ].
-        photometric := #rgb.
-        samplesPerPixel := 3.
-        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel.
-        depth := bitsPerChannel * 3.
-        ^ true.
-    ].
-
-    colorType == 3 ifTrue:[
-        bitsPerChannel == 16 ifTrue:[
-            'PNGReader: invalid colorType/depth combination' infoPrintCR.
-            ^ false.
-        ].
-        photometric := #palette.
-        samplesPerPixel := 1.
-        bitsPerSample := Array with:bitsPerChannel.
-        depth := bitsPerChannel.
-        ^ true.
-    ].
-
-    colorType == 4 ifTrue:[
-        bitsPerChannel < 8 ifTrue:[
-            'PNGReader: invalid colorType/depth combination' infoPrintCR.
-            ^ false.
-        ].
-        photometric := #blackIs0.
-        samplesPerPixel := 2.
-        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel.
-        depth := bitsPerChannel * 2.
-        ^ true.
-    ].
-
-    colorType == 6 ifTrue:[
-        bitsPerChannel < 8 ifTrue:[
-            'PNGReader: invalid colorType/depth combination' infoPrintCR.
-            ^ false.
-        ].
-        photometric := #rgba.
-        samplesPerPixel := 4.
-        bitsPerSample := Array with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel with:bitsPerChannel.
-        depth := bitsPerChannel * 4.
-        ^ true.
-    ].
-
-    ('PNGReader: invalid colorType: ' , colorType printString , '.') infoPrintCR.
-    ^ false
-
-    "Modified: / 16-02-2017 / 17:19:19 / cg"
-! !
-
-!PNGReader methodsFor:'reading'!
-
-fromStream:aStream
-    "read a stream containing a PNG image.
-     Leave image description in instance variables."
-
-    |header|
-
-    inStream := aStream.
-    aStream binary.
-
-    "PNG-files are always msb (network-world)"
-    byteOrder := #msb.
-
-    header := ByteArray new:8.
-    aStream nextBytes:8 into:header.
-
-    header ~= (self pngHeader) ifTrue:[
-        'PNGReader: not a png file.' infoPrintCR.
-        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: not a png file'.
-        ^ nil
-    ].
-
-    (self getIHDRChunk) ifFalse:[
-        'PNGReader: required IHDR chunk missing.' infoPrintCR.
-        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: required IHDR chunk missing'.
-        ^ nil
-    ].
-
-    compressionMethod ~~ 0 ifTrue:[
-        ('PNGReader: compressionMethod %s not supported.' printfWith:compressionMethod printString) infoPrintCR.
-        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: unsupported compression method'.
-        ^ nil
-    ].
-    filterMethod > 0 ifTrue:[
-        'PNGReader: invalid filterMethod' infoPrintCR.
-        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: invalid/unsupported filterMethod'.
-        ^ nil.
-    ].
-    interlaceMode > 1 ifTrue:[
-        Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: invalid/unsupported interlaceMode'.
-        'PNGReader: invalid interlaceMode' infoPrintCR.
-        ^ nil.
-    ].
-    (self setColorType:colorType) ifFalse:[
-        ^ nil
-    ].
-
-    [self getChunk] whileTrue.
-
-    bytesPerScanline := self bytesPerRow.
-    data := ByteArray new:(bytesPerScanline * height).
-
-    globalDataChunk notNil ifTrue:[
-        self processGlobalIDATChunk.
-        globalDataChunk := nil.
-    ].
-
-    photometric == #palette ifTrue:[
-        colorMap isNil ifTrue:[
-            Image badImageFormatQuerySignal raiseRequestErrorString:'PNGReader: palette chunk missing'.
-            'PNGReader: palette chunk missing.' infoPrintCR.
-            "/ ^ nil
-        ].
-    ].
-
-    "
-     PNGReader fromFile:'/home/cg/libpng-0.89c/pngtest.png'
-    "
-
-    "Modified: / 03-05-2011 / 12:17:34 / cg"
-! !
-
-!PNGReader methodsFor:'writing to file'!
-
-pngHeader
-    ^ #[137 80 78 71 13 10 26 10]
-!
+!PNGReader methodsFor:'writing'!
 
 save:image onStream:aStream
     "save image in PNG-file-format onto aStream"
@@ -1243,15 +1367,71 @@
     data := image bits.
     mask := image mask.
     depth := self bitsPerPixel.
+
+    outStream nextPutAll:(self pngHeader).
     
-    outStream nextPutAll:(self pngHeader).
+    image mask notNil ifTrue:[
+        (photometric ~~ #rgba) ifTrue: [
+            image maskedPixelsAre0 ifTrue:[
+                paletteIndexForMaskedPixels := 0
+            ] ifFalse:[
+                self determinePaletteIndexForMaskedPixels
+            ]
+        ].
+    ].
+
     self writeIHDRChunk.
     photometric == #palette ifTrue: [self writePaletteChunk].
+    paletteIndexForMaskedPixels notNil ifTrue: [self writeTRNSChunk].
     self writeImageDataChunk.
     self writeEndChunk
 
-    "Modified: / 16-02-2017 / 16:35:51 / cg"
-!
+    "Modified: / 16-02-2017 / 21:18:05 / cg"
+! !
+
+!PNGReader methodsFor:'writing-private'!
+
+determinePaletteIndexForMaskedPixels
+    "/ if all masked pixels are 0, and 0 is not used elsewhere in the image,
+    "/ write it as such.
+    "/ Otherwise, find an unallocated palette index, and assign masked pixels to it.
+
+    |usedPixels freePixels pixelIdx|
+
+    colorMap size < 256 ifTrue:[
+        paletteIndexForMaskedPixels := colorMap size.
+    ] ifFalse:[    
+        usedPixels := data usedValues.
+        usedPixels size == 256 ifTrue:[
+            self error:'cannot represent image (no palette slot for mask)'
+        ].
+        freePixels := (0 to:255) asSet removeAllFoundIn:usedPixels.
+        paletteIndexForMaskedPixels := freePixels first.
+    ].    
+
+    "/ rewrite data: wherever masked, change pixel to paletteIndexForMaskedPixels
+    
+    data := data copy.
+    pixelIdx := 1.
+    0 to:height-1 do:[:y |
+        0 to:width-1 do:[:x |
+            (mask pixelAtX:x y:y) == 0 ifTrue:[
+                data at:pixelIdx put:paletteIndexForMaskedPixels.
+            ].
+            pixelIdx := pixelIdx + 1
+        ]
+    ].
+    
+    "       
+     PNGReader save:(ToolbarIconLibrary systemBrowserIcon) onFile:'/tmp/icon.png'
+     ImageEditor openOnFile:'/tmp/icon.png'
+    "
+
+    "Created: / 16-02-2017 / 19:59:39 / cg"
+    "Modified: / 17-02-2017 / 09:25:57 / cg"
+! !
+
+!PNGReader methodsFor:'writing-private chunks'!
 
 writeChunk:chunkTypeChars size:len with:aBlock
     |crc realOutStream chunkBytes|
@@ -1279,25 +1459,25 @@
         with:[ ]
 !
 
-writeFileHeader
-    outStream nextPutAll:(self pngHeader)
-!
-
 writeIHDRChunk
     |hasMask|
 
     hasMask := 0.
     
     ((photometric == #whiteIs0) or:[ (photometric == #blackIs0)]) ifTrue:[
-        colorType := 0.
+        colorType := ColorTypeGray.
         hasMask := (samplesPerPixel > 1)
     ].
-    ((photometric == #rgb) or:[(photometric == #rgba)]) ifTrue:[
-        colorType := 2.
+    (photometric == #rgb) ifTrue:[
+        colorType := ColorTypeRGB.
+        hasMask := (samplesPerPixel > 3)
+    ].
+    (photometric == #rgba) ifTrue:[
+        colorType := ColorTypeRGBAlpha.
         hasMask := (samplesPerPixel > 3)
     ].
     (photometric == #palette) ifTrue:[
-        colorType := 3.
+        colorType := ColorTypePalette.
         hasMask := (samplesPerPixel > 1)
     ].
     colorType isNil ifTrue:[
@@ -1305,12 +1485,13 @@
     ].
 
     mask notNil ifTrue:[
-        depth == 24 ifFalse:[self error:'non-depth24 images with mask are (currently) not supported'].
         hasMask := true.
     ].
-    
     hasMask ifTrue:[
-        colorType := colorType + 4.  "/ +alpha
+        "/ make it grayAlpha / rgba
+        ((colorType == ColorTypeGray) or:[colorType == ColorTypeGray]) ifTrue:[
+            colorType := colorType bitOr:4.  "/ +alpha
+        ].
     ].
     
     self 
@@ -1326,7 +1507,7 @@
             outStream nextPut: 0   "Non-interlaced"
         ]
 
-    "Modified: / 16-02-2017 / 16:35:59 / cg"
+    "Modified (format): / 16-02-2017 / 21:24:22 / cg"
 !
 
 writeImageDataChunk
@@ -1339,7 +1520,7 @@
     zlibWriter := ZipStream writeOpenAsZipStreamOn:compressedByteStream suppressHeaderAndChecksum:false.
     zlibWriter binary.
 
-    mask notNil ifTrue:[
+    (mask notNil and:[photometric ~~ #palette]) ifTrue:[
         "/ on-the-fly expand mask into the alpha channel.
         
         "/ for now - only support depth24 + mask
@@ -1390,7 +1571,7 @@
             outStream nextPutAll:compressedBytes
         ]
 
-    "Modified: / 16-02-2017 / 16:36:50 / cg"
+    "Modified: / 16-02-2017 / 21:13:26 / cg"
 !
 
 writePaletteChunk
@@ -1405,6 +1586,17 @@
                     nextPut: color blueByte
             ]
         ]
+!
+
+writeTRNSChunk
+    self 
+        writeChunk: 'tRNS'
+        size: 1
+        with:[
+            outStream nextPut: paletteIndexForMaskedPixels
+        ]
+
+    "Created: / 16-02-2017 / 19:58:06 / cg"
 ! !
 
 !PNGReader class methodsFor:'documentation'!