Keith Vetter -- 2015-02-16 Here's a
tcl only png decoder. It can read almost any type of png file including grayscale, indexed and ARGB and understand all the various types of scanline filtering. The only format it cannot handle is interlaced images (which is an
ill-conceived concept anyway).
The full PNG specification is at
http://www.libpng.org/pub/png/spec/1.2.
This package lets you query the color of any pixel or get the full data of the image.
Exact usage is given in the header comments.
See also edit
#!/bin/sh
# Restart with tcl: -*- mode: tcl; tab-width: 8; -*- \
exec tclsh $0 ${1+"$@"}
##+##########################################################################
#
# pngDecode.tsh : decodes and extracts ARGB data about a png.
# by Keith Vetter 2015-02-12
#
# Handles almost all the various png types--grayscale, indexed, RGB,
# etc.--and all the filter types--sub, up, average and Paeth. all
# color types--grayscale, indexed, RGB, etc. and all scanline filters.
#
# Only type png's it cannot decode are interlaced images.
#
# Usage:
# set token [PngDecoder create pngFile ?verbosity?]
# Parses png file and returns a handle
#
# PngDecoder imageInfo token
# Returns dictionary of width, height, depth, color, compression, filter
# and interlace
#
# PngDecoder get token x y
# returns alpha, red, green and blue values for pixel at x,y
#
# PngDecoder data token how
# Returns the image data for this png as a list of scanlines. The
# how parameter can be one of:
# asARGB : alpha, red, green, blue for each pixel (32 bit)
# asRGB : red, green, blue for each pixel (32 bit)
# asIMG : #RRGGBB for each pixel -- same format as Tk's image data
#
# PngDecoder makeImage token
# Returns a Tk image object--requires Tk to be loaded
#
# PngDecoder cleanup token
# Frees resources used by token
#
# Example code:
# set token [PngDecoder create /my/pngfile.png]
#
# set imageInfo [PngDecoder imageInfo $token]
# puts "size: [dict get $imageInfo width]x[dict get $imageInfo height]"
#
# lassign [PngDecoder get $token 10 10] alpha red green blue
# puts "pixel at 10,10: $alpha/$red/$green/$blue"
#
# package require Tk
# set img [PngDecoder makeImage $token]
# pack [label .l -image $img]
#
# PngDecoder cleanup $token
namespace eval PngDecoder {
variable uid 0
variable verbose 2
namespace ensemble create \
-subcommands {create imageInfo get data makeImage cleanup}
}
##+##########################################################################
#
# Returns a PngDecoder handle for decoding this pngFile
#
proc PngDecoder::create {pngFile {verbosity 0}} {
variable uid
variable verbose
set verbose $verbosity
set token [namespace current]::[incr uid]
ParsePngFile $token $pngFile
DecodeImage $token
ShowLine 1 ""
return $token
}
##+##########################################################################
#
# Returns dictionary with keys width, height, depth, color, compression, filter
# and interlace, and the values are the associated properties of this png.
#
proc PngDecoder::imageInfo {token} {
variable $token
upvar 0 $token state
return [list width $state(width) \
height $state(height) \
depth $state(bit,depth) \
color $state(color,type) \
compression $state(compression,method) \
filter $state(filter,method) \
interlace $state(interlace)]
}
##+##########################################################################
#
# Return the alpha, red, green, blue channels for pixel at x,y
#
proc PngDecoder::get {token x y} {
variable $token
upvar 0 $token state
if {$x < 0 || $x >= $state(width) || $y < 0 || $y >= $state(height)} {
error "$x,$y is out of bounds"
}
set clr [lindex $state(image) $y $x]
foreach channel {blue green red alpha} {
set $channel [expr {$clr & 0xFF}]
set clr [expr {$clr >> 8}]
}
return [list $alpha $red $green $blue]
}
##+##########################################################################
#
# Returns the image data for this png as a list of scanlines. The
# format is one of:
# asARGB : alpha, red, green, blue for each pixel
# asRGB : red, green, blue for each pixel
# asIMG : #RRGGBB for each pixel -- same format as Tk's image data
#
proc PngDecoder::data {token {how asARGB}} {
variable $token
upvar 0 $token state
set types {asARGB asRGB asIMAGE}
if {$how ni $types} {
set emsg "usage: PngDecoder data token how"
append emsg "\n how is one of [join $types {, }]"
error $emsg
}
if {$how eq "asARGB" } {return $state(image) }
set fmt [expr {$how eq "asIMAGE" ? "#%06x" : "%d"}]
set scanlines {}
foreach raw_scanlines $state(image) {
set scanline {}
foreach pxl $raw_scanlines {
set clr [expr {$pxl & 0xFFFFFF}] ;# Remove alpha
lappend scanline [format $fmt $clr]
}
lappend scanlines $scanline
}
return $scanlines
}
##+##########################################################################
#
# Returns a Tk image from this png. Requires Tk to be loaded.
#
proc PngDecoder::makeImage {token} {
if {! [info exists ::tk_version]} {
error "makeImage requires Tk to be loaded"
}
set img [image create photo]
$img put [data $token asIMAGE]
return $img
}
##+##########################################################################
#
# Frees all memory associated with this object.
#
proc PngDecoder::cleanup {token} {
variable $token
upvar 0 $token state
if {[info exists state]} {
unset state
}
}
##+##########################################################################
#
# Private routines
#
##+##########################################################################
#
# Extracts data from all the chunks in the png file
#
proc PngDecoder::ParsePngFile {token fname} {
variable $token
upvar 0 $token state
ShowLine 1 $fname
ShowLine 1 "[string repeat = [string length $fname]]"
ShowLine 1 parsing
set fh [open $fname r]
try {
fconfigure $fh -encoding binary -translation binary -eofchar {}
if {[read $fh 8] != "\x89PNG\r\n\x1a\n"} {
ERROR "$fname is not a png file"
return
}
while {[set r [read $fh 8]] != ""} {
binary scan $r Ia4 len type
set data [read $fh $len]
set crc [read $fh 4]
set handler "Do[string toupper $type]"
if {[info procs $handler] ne ""} {
$handler $token $data
} else {
ERROR "unknown chunk type: $type"
}
}
} finally {
close $fh
}
}
proc PngDecoder::ERROR {msg} {
puts stderr $msg
}
proc PngDecoder::ShowLine {lvl msg} {
variable verbose
if {$lvl > $verbose} return
puts $msg
}
proc PngDecoder::ShowData {lvl args} {
variable verbose
if {$lvl > $verbose} return
foreach {key value} $args {
set msg [format " %-12s %s" "${key}:" $value]
puts $msg
}
}
proc PngDecoder::Adorn {value labels} {
set lbl "-"
if {$value < [llength $labels]} {
set lbl [lindex $labels $value]
}
if {$lbl eq "-"} { return $value }
return "$value -- $lbl"
}
##+##########################################################################
#
# DoXXXX : parses chunk with name XXXX, storing data in state array
#
proc PngDecoder::DoIHDR {token data} {
variable $token
upvar 0 $token state
set ctypes_ {grayscale - RGB indexed "grayscale with alpha" - RGBA}
binary scan $data IIccccc state(width) state(height) state(bit,depth) state(color,type) \
state(compression,method) state(filter,method) state(interlace)
if {$state(color,type) == 0 || $state(color,type) == 3} {
set bits [expr {$state(width) * $state(bit,depth)}]
set state(bytes,row) [expr {int(ceil($bits / 8.0))}]
set state(bytes,pixel) [expr {$state(bit,depth) > 8 ? 2 : 1}]
} elseif {$state(color,type) == 2} {
set state(bytes,row) [expr {$state(width) * 3 * $state(bit,depth) / 8}]
set state(bytes,pixel) [expr {3 * $state(bit,depth) / 8}]
} elseif {$state(color,type) == 4} {
set state(bytes,row) [expr {$state(width) * $state(bit,depth) / 8}]
set state(bytes,pixel) [expr {2 * $state(bit,depth) / 8}]
} elseif {$state(color,type) == 6} {
set state(bytes,row) [expr {$state(width) * 4 * $state(bit,depth) / 8}]
set state(bytes,pixel) [expr {4 * $state(bit,depth) / 8}]
}
ShowLine 2 "IHDR : Image header"
ShowData 2 size "$state(width)x$state(height)"
ShowData 2 "color type" [Adorn $state(color,type) $ctypes_]
ShowData 2 depth $state(bit,depth)
ShowData 3 compression $state(compression,method)
ShowData 3 filter $state(filter,method)
ShowData 2 interlace [Adorn $state(interlace) {none Adam7}]
}
proc PngDecoder::DoPLTE {token data} {
variable $token
upvar 0 $token state
ShowLine 2 "PLTE : Palette"
set alpha 0xFF
set cnt [expr {-1 + [string length $data] / 3}]
for {set i 0} {$i <= $cnt} {incr i} {
set rgb [string range $data [expr {$i * 3}] [expr {$i * 3 + 2}]]
binary scan $rgb cucucu r g b
set state(palette,$i) [expr {($alpha << 24) | ($r << 16) | ($g << 8) | $b}]
if {$i < 5} {
ShowData 3 "palette\[$i]" [format "#%08X" $state(palette,$i)]
}
}
if {$cnt >= 5} {
ShowLine 3 " ..."
ShowData 3 "palette\[$cnt]" [format "#%08X" $state(palette,$cnt)]
}
}
proc PngDecoder::DoIDAT {token data} {
variable $token
upvar 0 $token state
# Just accumulate info for summary info in IEND
incr state(idat,cnt)
append state(idat,data) $data
}
proc PngDecoder::DoIEND {token data} {
variable $token
upvar 0 $token state
# Combine multiple IDAT and display info here
binary scan $state(idat,data) cucu CMF FLG
set CM [expr {$CMF & 0xF}]
set methods_ {- - - - - - - - deflate}
set CINFO [expr {$CMF >> 4}]
set window [expr {2**($CINFO+8)}]
set FCHECK [expr {$FLG & 0x1F}]
set FDICT [expr {($FLG & 0x20) >> 5}]
set FLEVEL [expr {$FLG >> 6 }]
set flevels_ {fastest fast default maximum}
ShowLine 2 "IDAT : Image data"
ShowData 3 segments $state(idat,cnt) size [string length $state(idat,data)]
ShowData 3 method [Adorn $CM $methods_]
ShowData 3 window $window
ShowData 3 level "[Adorn $FLEVEL $flevels_] compression"
ShowLine 2 "IEND : Image trailer"
}
proc PngDecoder::DoTRNS {token data} {
variable $token
upvar 0 $token state
ShowLine 2 "tRNS : Transparency"
if {$state(color,type) == 3} { ;# Indexed color png
set cnt [expr {-1 + [string length $data]}]
for {set i 0} {$i <= $cnt} {incr i} {
binary scan [string index $data $i] cu alpha
set APALETTE($i) $alpha
set state(palette,$i) [expr {($alpha << 24) | ($state(palette,$i) & 0xFFFFFF)}]
if {$i > 4} continue
if {$alpha == 0} {
set alpha "$alpha -- transparent"
} elseif {$alpha == 255} {
set alpha "$alpha -- opaque"
}
ShowData 3 "alpha palette\[$i\]" $alpha
}
if {$cnt >= 4} {
set alpha $APALETTE($cnt)
if {$alpha == 0} {
set alpha "$alpha -- transparent"
} elseif {$alpha == 255} {
set alpha "$alpha -- opaque"
}
ShowLine 3 " ..."
ShowData 3 "alpha palette\[$cnt\]" $alpha
}
} elseif {$state(color,type) == 0} { ;# Grayscale png
binary scan $data S alpha
ShowData 3 "gray alpha" $alpha
set state(alpha,gray) $alpha
} elseif {$state(color,type) == 2} { ;# Truecolor png
binary scan $data SSS red green blue
ShowData 3 "red alpha" $red "green alpha" $green "blue alpha" $blue
set mask [expr {$state(bit,depth) == 8 ? 0xFF : 0xFFFF}]
set state(alpha,red) [expr {$red & $mask}]
set state(alpha,green) [expr {$green & $mask}]
set state(alpha,blue) [expr {$blue & $mask}]
}
}
proc PngDecoder::DoGAMA {token data} {
binary scan $data I gamma
set gamma [expr {$gamma / 100000.}]
ShowLine 2 "gAMA : Image gamma"
ShowData 3 gamma $gamma
}
proc PngDecoder::DoCHRM {token data} {
ShowLine 2 "cHRM : Primary chromaticities"
set lbls {"white x" "white y" "red x" "red y" "green x" "green y"
"blue x" "blue y"}
for {set i 0} {$i < 8} {incr i} {
set chrm [string range $data [expr {$i*4}] [expr {$i*4 + 3}]]
binary scan $chrm I val
ShowData 3 [lindex $lbls $i] $val
}
}
proc PngDecoder::DoSRGB {token data} {
binary scan $data c render
set intents_ {Perceptual "Relative colorimetric"
Saturation "Absolute colorimetric"}
ShowData 3 render [Adorn $render $intents_]
}
proc PngDecoder::DoICCP {token data} {
set name [lindex [split $data \x00] 0]
ShowLine 2 "iCCP : Embedded ICC profile"
ShowData 3 name $name
}
proc PngDecoder::DoTEXT {token data} {
ShowLine 2 "tEXt : Textual data"
lassign [split $data \x00] key value
ShowData 3 key $key value $value
}
proc PngDecoder::DoZTXT {token data} {
ShowLine 2 "zTXt : Compressed textual data"
lassign [split $data \x00] key
set keylen [expr {[string length $key] + 1}]
binary scan [string index $data $keylen] cu method
set value [string range $data $keylen+1 end]
set compressed [string range $value 2 end-4]
set uncompressed [zlib inflate $compressed]
ShowData 3 method [Adorn $method {deflate}] key $key text $uncompressed
}
proc PngDecoder::DoITXT {token data} {
ShowLine 2 "iTXt : International textual data"
lassign [split $data \x00] key
set keylen [expr {[string length $key] + 1}]
binary scan [string range $data $keylen $keylen+2] cc compress method
if {$compress == 1} {
ShowData 3 $key ...
ShowData 3 compress $compress method [Adorn $method {deflate}] text ...
} else {
set rest [string range $data $keylen+2 end]
lassign [split $rest \x00] language key2 key3 value
ShowData 3 key $key language $language key3 $key3 text $value
}
}
proc PngDecoder::DoBKGD {token data} {
ShowLine 2 "bKGD : Background color"
set len [string length $data]
if {$len == 1} {
binary scan $data cu idx
ShowData 3 "palette idx" $idx
} elseif {$len == 2} {
binary scan $data cucu gray alpha
ShowData 3 gray $gray alpha $alpha
} elseif {$len == 6} {
binary scan $data SSS red green blue
ShowData 3 red $red green $green blue $blue
}
}
proc PngDecoder::DoPHYS {token data} {
binary scan $data IIc x y units
ShowLine 2 "pHYs : Physical pixel dimensions"
ShowData 3 x-axis $x
ShowData 3 y-axis $y
ShowData 3 units [Adorn $units {"unknown" "meters"}]
}
proc PngDecoder::DoSBIT {token data} {
ShowLine 2 "sBIT : Significant bits"
set len [string length $data]
if {$len == 1} {
binary scan $data c gray
ShowData 3 gray $gray
} elseif {$len == 2} {
binary scan $data cc gray alpha
ShowData 3 gray $gray alpha $alpha
} elseif {$len == 3} {
binary scan $data ccc red green blue
ShowData 3 red $red green $green blue $blue
} elseif {$len == 4} {
binary scan $data cccc red green blue alpha
ShowData 3 red $red green $green blue $blue alpha $alpha
}
}
proc PngDecoder::DoSPLT {token data} {
ShowLine 2 "sPLT : Suggested palette"
set name [lindex [split $data \x00] 0]
ShowData 3 "palette name" $name
}
proc PngDecoder::DoSPAL {token data} {
# see ftp://ftp.simplesystems.org/pub/libpng/png-group/documents/history/png-proposed-sPLT-19961107.html
lassign [split $data \x00] name signature
ShowLine 2 "spAL : Suggested palette beta sPLT"
ShowData 3 "palette name" $name signature $signature
}
proc PngDecoder::DoHIST {token data} {
set cnt [expr {[string length $data] / 2}]
set min [expr {min(5,$cnt)}]
ShowLine 2 "hIST : Palette histogram"
ShowData 3 entries $cnt
for {set i 0} {$i < $min} {incr i} {
binary scan [string range $data [expr {2 * $i}] end] S value
ShowData 3 "hist\[$i]" $value
}
if {$min < $cnt} { ShowLine 3 " ..." }
}
proc PngDecoder::DoTIME {token data} {
binary scan $data Sccccc year month day hour minute second
ShowLine 2 "tIME : Image last-modification time"
ShowData 3 time "$year/$month/$day $hour:$minute:$second"
}
##+##########################################################################
#
# Routines to uncompress and decode the raw data
#
##+##########################################################################
#
# Runs zlib inflate on the data in the IDAT chunks
# input: state(idat,data)
# output: state(idat,uncompressed)
#
proc PngDecoder::InflateIDAT {token} {
variable $token
upvar 0 $token state
if {[info exists state(idate,uncompressed)]} return
if {! [info exists state(idat,data)]} { error "no state(idat,data)" }
# See RFC 1950 section 2.2
# https://www.ietf.org/rfc/rfc1950.txt
binary scan $state(idat,data) cucu cmf flg
set cm [expr {$cmf & 0xF}]
set cinfo [expr {$cmf >> 4}]
set fcheck [expr {$flg & 0x1F}]
set fdict [expr {($flg & 0x20) >> 5}]
set flevel [expr {$flg >> 6 }]
if {$cm != 8} { error "bad compression method $cm" }
if {$fdict} { error "cannot handle dictionary and compression" }
set compressed [string range $state(idat,data) 2 end-4]
set state(idat,uncompressed) [zlib inflate $compressed]
ShowLine 1 inflating
ShowData 2 compressed "[string length $compressed] bytes"
ShowData 2 uncompressed "[string length $state(idat,uncompressed)] bytes"
return
}
##+##########################################################################
#
# Decodes the image data stored in the IDAT chunks as a list of scanlines
# with each scanline having a 32-bit ARGB value for each pixel.
#
# The result is kept in $token(image) and accessed via ::PngDecoder::Data
#
# Ths routine is format agnostic but calls format specific functions
# to decode each scanline.
#
proc PngDecoder::DecodeImage {token} {
variable $token
upvar 0 $token state
if {[info exists state(image)]} return
if {$state(interlace)} {error "cannot handle interlaced images"}
set DecodeRowProc "_DecodeRow_$state(color,type)"
InflateIDAT $token
set state(image) {}
set last_raw_scanline {}
ShowLine 2 Scanlines
set filters {}
for {set row 0} {$row < $state(height)} {incr row} {
lassign [GetFilteredScanline $token $row] filter filtered_scanline
lappend filters $filter
set raw_scanline [UnfilterScanline $token $filter $filtered_scanline \
$last_raw_scanline]
set img_row [$DecodeRowProc $token $filter $raw_scanline]
lappend state(image) $img_row
set last_raw_scanline $raw_scanline
}
ShowData 2 filters $filters
}
##+##########################################################################
#
# Decodes a scanline for color type 0 -- grayscale
#
proc PngDecoder::_DecodeRow_0 {token filter raw_scanline} {
variable $token
upvar 0 $token state
set img_row {}
for {set col 0} {$col < $state(width)} {incr col} {
set gray [GetBits $col $state(bit,depth) $raw_scanline]
;# ALPHA : if gray matches tRNS color then alpha=0 else alpha=255
set alpha 255
if {[info exists state(alpha,gray)] && $state(alpha,gray) == $gray} {
set alpha 0
}
# Scale gray color to 0-255 range
if {$state(bit,depth) == 1} {
set gray [expr {($gray << 1) | $gray}]
set gray [expr {($gray << 2) | $gray}]
set gray [expr {($gray << 4) | $gray}]
} elseif {$state(bit,depth) == 2} {
set gray [expr {($gray << 2) | $gray}]
set gray [expr {($gray << 4) | $gray}]
} elseif {$state(bit,depth) == 4} {
set gray [expr {($gray << 4) | $gray}]
} elseif {$state(bit,depth) == 16} {
set gray [expr {($gray >> 8) & 0xFF}]
}
set clr [expr {($alpha << 24) | ($gray << 16) | ($gray << 8) | $gray}]
lappend img_row $clr
}
return $img_row
}
##+##########################################################################
#
# Decodes a scanline for color type 2 -- RGB
#
proc PngDecoder::_DecodeRow_2 {token filter raw_scanline} {
variable $token
upvar 0 $token state
set img_row {}
set alpha 255
if {$state(bit,depth) == 8} {
foreach {r g b} $raw_scanline {
;# ALPHA : if RGB matches tRNS color then alpha=0 else alpha=255
if {[info exists state(alpha,red)] && $r == $state(alpha,red) \
&& $g == $state(alpha,green) && $b == $state(alpha,blue)} {
set alpha 255
}
set clr [expr {($alpha << 24) | ($r << 16) | ($g << 8) | $b}]
lappend img_row $clr
}
} else {
foreach {r0 r1 g0 g1 b0 b1} $raw_scanline {
;# ALPHA : if RRGGBB matches tRNS color then alpha=0 else alpha=255
if {[info exists state(alpha,red)] \
&& $state(alpha,red) == ($r0 << 8 | $r1) \
&& $state(alpha,green) == ($g0 << 8 | $g1) \
&& $state(alpha,blue) == ($b0 << 8 | $b1)} {
set alpha 255
}
set clr [expr {($alpha << 24) | ($r0 << 16) | ($g0 << 8) | $b0}]
lappend img_row $clr
}
}
return $img_row
}
##+##########################################################################
#
# Decodes a scanline for color type 3 -- indexed
#
proc PngDecoder::_DecodeRow_3 {token filter raw_scanline} {
variable $token
upvar 0 $token state
set img_row {}
for {set col 0} {$col < $state(width)} {incr col} {
set idx [GetBits $col $state(bit,depth) $raw_scanline]
;# ALPHA : alpha = APALATTE(idx) if it exists, 255 otherwise
;# Note, we've already updated PALETTE with correct alpha
lappend img_row $state(palette,$idx)
}
return $img_row
}
##+##########################################################################
#
# Decodes a scanline for color type 4 -- grayscale with alpha
#
proc PngDecoder::_DecodeRow_4 {token filter raw_scanline} {
variable $token
upvar 0 $token state
set img_row {}
if {$state(bit,depth) == 8} {
foreach {gray alpha} $raw_scanline {
set clr [expr {($alpha << 24) | ($gray << 16) | ($gray << 8) | $gray}]
lappend img_row $clr
}
} else {
foreach {gray0 gray1 alpha0 alph1} $raw_scanline {
set clr [expr {($alpha0 << 24) | ($gray0 << 16) | ($gray0 << 8) | $gray0}]
lappend img_row $clr
}
}
return $img_row
}
##+##########################################################################
#
# Decodes a scanline for color type 6 - RGBA
#
proc PngDecoder::_DecodeRow_6 {token filter raw_scanline} {
variable $token
upvar 0 $token state
set img_row {}
if {$state(bit,depth) == 8} {
foreach {r g b alpha} $raw_scanline {
set clr [expr {($alpha << 24) | ($r << 16) | ($g << 8) | $b}]
lappend img_row $clr
}
} else {
foreach {r0 r1 g0 g1 b0 b1 alpha0 alpha1} $raw_scanline {
set clr [expr {($alpha0 << 24) | ($r0 << 16) | ($g0 << 8) | $b0}]
lappend img_row $clr
}
}
return $img_row
}
##+##########################################################################
#
# Extracts a single scanline from the decompressed image data. Returns list of
# the filter type and the raw bytes.
#
proc PngDecoder::GetFilteredScanline {token row} {
variable $token
upvar 0 $token state
set idx [expr {1 + $row * (1 + $state(bytes,row))}]
binary scan [string index $state(idat,uncompressed) $idx-1] cu filter
set raw_scanline {}
for {set col 0} {$col < $state(bytes,row)} {incr col} {
binary scan [string index $state(idat,uncompressed) $idx+$col] cu byte
lappend raw_scanline $byte
}
return [list $filter $raw_scanline]
}
##+##########################################################################
#
# Returns the raw scanline computed by applying the inverse filter
# algorithm to the filtered-scanline
#
proc PngDecoder::UnfilterScanline {token filter filtered_scanline last_raw_scanline} {
variable $token
upvar 0 $token state
if {$filter == 0} { return $filtered_scanline }
set raw_scanline {}
for {set idx 0} {$idx < [llength $filtered_scanline]} {incr idx} {
set item [lindex $filtered_scanline $idx]
if {$filter == 1} { ;# Sub filter
set filter_byte [SmartLindex $raw_scanline $idx-$state(bytes,pixel)]
} elseif {$filter == 2} { ;# Up filter
set filter_byte [SmartLindex $last_raw_scanline $idx]
} elseif {$filter == 3} { ;# Average filter
set sub [SmartLindex $raw_scanline $idx-$state(bytes,pixel)]
set prior [SmartLindex $last_raw_scanline $idx]
set filter_byte [expr {($sub + $prior) / 2}]
} elseif {$filter == 4} { ;# Paeth filter
set sub [SmartLindex $raw_scanline $idx-$state(bytes,pixel)]
set prior [SmartLindex $last_raw_scanline $idx]
set priorsub [SmartLindex $last_raw_scanline $idx-$state(bytes,pixel)]
set filter_byte [PaethPredictor $sub $prior $priorsub]
} else {
error "unknown filter type: $filter"
}
lappend raw_scanline [expr {($item + $filter_byte) & 0xFF}]
}
return $raw_scanline
}
##+##########################################################################
#
# Safe version of lindex which returns "" for missing items.
#
proc PngDecoder::SmartLindex {items idx} {
set value [lindex $items $idx]
if {$value eq ""} { set value 0 }
return $value
}
##+##########################################################################
#
# Computes the PaethPredictor element described in the PNG standard at
# http://www.libpng.org/pub/png/spec/1.2/png-1.2-pdg.html#Filter-type-4-Paeth
#
proc PngDecoder::PaethPredictor {a b c} {
set p [expr {$a + $b - $c}]
set pa [expr {abs($p - $a)}]
set pb [expr {abs($p - $b)}]
set pc [expr {abs($p - $c)}]
if {$pa <= $pb && $pa <= $pc} { return $a }
if {$pb <= $pc} { return $b }
return $c
}
##+##########################################################################
#
# Returns $bbp bits from $data for the $idx item.
#
proc PngDecoder::GetBits {idx bbp data} {
# Pixels are always packed into scanlines with no wasted bits
# between pixels. Pixels smaller than a byte never cross byte
# boundaries; they are packed into bytes with the leftmost pixel
# in the high-order bits of a byte, the rightmost in the low-order
# bits.
set bit_position [expr {$idx * $bbp}]
set byte_idx [expr {$bit_position / 8}]
set bit_in_byte [expr {8 - $bit_position % 8}]
# Get the byte with the bits we want
set byte [lindex $data $byte_idx]
if {$bbp == 16} {return [expr {($byte << 8) | [lindex $data $byte_idx+1]}]}
# Shift desired bits to the right and mask out unwanted bits
set byte [expr {$byte >> ($bit_in_byte - $bbp)}]
set mask [expr {2**$bbp - 1}]
set bits [expr {$byte & $mask}]
return $bits
}
proc PngDecoder::TestGetBits {} {
TestGetBits_ 0 4 0xab 0xa
TestGetBits_ 1 4 0xab 0xb
TestGetBits_ 0 2 0b11001001 0b11
TestGetBits_ 1 2 0b11001001 0b00
TestGetBits_ 2 2 0b11001001 0b10
TestGetBits_ 3 2 0b11001001 0b01
TestGetBits_ 0 1 0b10101010 1
TestGetBits_ 1 1 0b10101010 0
TestGetBits_ 2 1 0b10101010 1
TestGetBits_ 3 1 0b10101010 0
TestGetBits_ 4 1 0b10101010 1
TestGetBits_ 5 1 0b10101010 0
TestGetBits_ 6 1 0b10101010 1
TestGetBits_ 7 1 0b10101010 0
}
proc PngDecoder::TestGetBits_ {idx bbp data expected} {
set actual [GetBits $idx $bbp $data]
if {$actual == $expected} return
puts stderr "bad: GetBits $idx $bbp $data: actual $actual expected: $expected"
}
##+##########################################################################
#
# Demo code
#
if {$argc == 0} {
ERROR "usage: [file tail $argv0] image.png"
return
}
set fname [lindex $argv 0]
set token [PngDecoder create $fname]
set imageInfo [PngDecoder imageInfo $token]
puts "size: [dict get $imageInfo width]x[dict get $imageInfo height]"
lassign [PngDecoder get $token 10 10] alpha red green blue
puts "pixel at 10,10: $alpha/$red/$green/$blue"
if {[info exists tk_version]} {
set img [PngDecoder makeImage $token]
pack [label .l -image $img]
}
PngDecoder cleanup $token
return
set verbose 0
foreach fname $argv {
catch { PngDecoder cleanup $token }
if {$fname eq "-v"} { incr verbose ; continue }
if {$fname eq "-vv"} { incr verbose 2 ; continue }
if {$fname eq "-q"} { incr verbose -1 ; continue }
if {$fname eq "-qq"} { set verbose 0 ; continue }
set token [PngDecoder create $fname $verbose]
puts "token: $token"
if {$extract} {
set rootname [file rootname $fname]
set outname "${rootname}_extract[file extension $fname]"
set img [PngDecoder makeImage $token]
if {$img ne ""} {
ShowLine 1 "writing $img to $outname"
$img write $outname -format png
image delete $img
}
}
}
if {! $tcl_interactive} exit
return