Image Sizes

Keith Vetter 2016-05-25 : here's another utility I wrote a while ago that I thought I'd share here. It's a command line utility that displays the size of images, regardless of image type and not requiring Tk. It's similar to ImageMagick's identify -format "%i [%m] %w x %h\n img.png' but it's more lightweight and has better handling of filenames. For example, if you invoke it without any arguments, it will find all the image files in the current directory.

  % imgSizes
  chevrons.xbm    [xbm] : 14 x 9
  klimb.bmp       [bmp] : 50 x 50
  logo3.png       [png] : 532 x 532
  me.jpg          [jpg] : 1,024 x 683
  quito.jpg       [jpg] : 922 x 691
  sample.webp     [webp] : 256 x 22
  seascape.jpg    [jpg] : 1,024 x 683
  t_and_e.jpg     [jpg] : 691 x 922
  test.png        [png] : 566 x 611

##+##########################################################################
#
# imgSizes.tsh -- command line tool for listing image sizes
# by Keith Vetter, October 28, 2005
#

package require jpeg
package require png
package require tiff

proc GetSize {fname} {
    if {[file size $fname] == 0} {return [list $fname - -]}
    set bestGuess "try[string tolower [file extension $fname]]"

    foreach itype [concat $bestGuess [info procs try.*]] {
        if {[info procs $itype] eq ""} continue

        set try [$itype $fname]
        if {$try ne {}} { return $try }
    }
    return {? - -}
}

proc try.jpg {fname} {
    if {! [::jpeg::isJPEG $fname]} { return {} }
    return [concat jpg [::jpeg::dimensions $fname]]
}
proc try.png {fname} {
    if {! [::png::isPNG $fname]} { return {} }
    array set P [::png::imageInfo $fname]
    return [list png $P(width) $P(height)]
}
proc try.tiff {fname} {
    if {! [::tiff::isTIFF $fname]} { return {} }
    lassign [::tiff::dimensions $fname] w h
    return [list tiff $w $h]
}

proc try.gif {fname} {
    # https://wiki.tcl-lang.org/758
    set data [ReadN $fname 10]
    set sig [string toupper [string range $data 0 5]]
    if {$sig ne "GIF87A" && $sig ne "GIF89A"} { return {} }
    binary scan [string range $data 6 7] s width
    binary scan [string range $data 8 9] s height
    return [list gif $width $height]
}

proc try.ico {fname} {
    # Note, may contain multiple images so we return a list of sizes
    set f [open $fname r]
    fconfigure $f -encoding binary -translation binary

    binary scan [read $f 6] sss zero type numImages
    if {$zero != 0 || ($type != 1 && $type != 2)} { close $f ; return {} }

    set d {}
    for {set i 0} {$i < $numImages} {incr i} {
        set idata [read $f 16]
        binary scan $idata cc w h
        if {$w == 0} {set w 256}
        if {$h == 0} {set h 256}
        append d "${w}x$h "
    }
    close $f
    return [list ico $d ?]
}

proc try.ppm {fname} {
    # see http://netpbm.sourceforge.net/doc/ppm.html
    set data [ReadN $fname 256]
    set magic [string range $data 0 2]
    if {! [regexp {^P6\s$} $magic]} { return {} }

    set n [regexp {^P6\s+(\d+)\s+(\d+)} $data . width height]
    if {! $n} { error "bad ppm format" }
    return [list ppm $width $height]
}
proc try.xbm {fname} {
    # see https://en.wikipedia.org/wiki/X_PixMap#Comparison_with_other_formats
    set data [ReadN $fname 256]
    set n1 [regexp -line {^\s*\#define\s+[a-zA-Z_09]+_width\s+(\d+)} $data . width]
    set n2 [regexp -line {^\s*\#define\s+[a-zA-Z_09]+_height\s+(\d+)} $data . height]
    if {! $n1 || ! $n2} { return {} }
    return [list xbm $width $height]
}
proc try.bmp {fname} {
    # see https://en.wikipedia.org/wiki/BMP_file_format
    set data [ReadN $fname 26]
    if {[string range $data 0 1] ne "BM"} { return {} }
    binary scan [string range $data 18 21] i width
    binary scan [string range $data 22 25] i height
    return [list bmp $width $height]
}

##+##########################################################################
#
# Webp (weppy)
#
# gallery
# =======
# http://news.cnet.com/8301-1023_3-57580664-93/facebook-tries-googles-webp-image-format-users-squawk/
#
# File Format
# ===========
# https://developers.google.com/speed/webp/docs/riff_container
# VP8  :
# VP8L : https://gerrit.chromium.org/gerrit/gitweb?p=webm/libwebp.git;a=blob;f=doc/webp-lossless-bitstream-spec.txt;hb=master
proc try.webp {fname} {
    set data [ReadN $fname 30]
    set chunk0 [string range $data 0 11]
    set chunk1 [string range $data 12 end]

    binary scan $chunk0 "a4ia4" riff size id
    if {$riff ne "RIFF" || $id ne "WEBP"} {return {}}

    binary scan $chunk1 "a4" vp8
    if {$vp8 eq "VP8L"} { return [webp.VP8L $chunk1]}

    # We're assuming start code block starts 11 bytes into the VP8 chunk
    binary scan $chunk1 "a4cu7cu3cu2cu2" vp8 . startCode widthInfo heightInfo
    if {$vp8 ne "VP8 "} { error "unknown VP8 block" }
    lassign $startCode b0 b1 b2
    if {$b0 != 0x9d || $b1 != 0x01 || $b2 != 0x2a} {
        error "missing start code block"
    }
    set horizScale [expr {[lindex $widthInfo 1] >> 6}]
    lset widthInfo 1 [expr {[lindex $widthInfo 1] & 0x3f}]
    set vertScale [expr {[lindex $heightInfo 1] >> 6}]
    lset heightInfo 1 [expr {[lindex $heightInfo 1] & 0x3f}]

    binary scan [binary format cu2cu2 $widthInfo $heightInfo] tt width height
    return [list webp $width $height]
}

proc webp.VP8L {chunk1} {
    binary scan $chunk1 a4icucu4 vp8 size signature sizeInfo
    if {$signature != 0x2f} {
        error "bad VP8L signature byte: $signature"
    }
    lassign $sizeInfo b0 b1 b2 b3
    # 10001111000000010100101100010000
    # 10001111 00000001 01001011 00010000
    # 10001111.000000 01.01001011.0001  0000
    # 10001111.000000  1.01001011.0001

    # 1_webp_ll.webp
    # width: 400px    110001111
    # height: 301px   100101100

    # 2_webp_ll.webp
    # 386x295
    set width [expr {1 + ($b0 << 6) + ($b1 >> 2)}]
    set height [expr {1 + ($b1 << 12) + ($b2 << 4) + ($b3 >> 4)}]
    return [list webp $width $height]
}

proc commify number {regsub -all {\d(?=(\d{3})+($|\.))} $number {\0,}}
proc ReadN {fname n} {
    set fin [open $fname rb]
    set data [read $fin $n]
    close $fin
    return $data
}
################################################################
if {$argv == {}} {
    set argv [list *.gif *.jpg *.jpeg *.png *.ico *.webp *.bmp *.tiff *.tif *.ppm *.xbm]
}
set fnames {}
set longestName 0
foreach arg $argv {
    regsub -all {\\} $arg {/} arg
    foreach fname [glob -nocomplain $arg] {
        if {! [file isfile $fname]} continue
        lappend fnames $fname
        set longestName [expr {max($longestName, 2 + [string length $fname])}]
    }
}

if {$tcl_interactive} return

foreach fname [lsort -dictionary $fnames] {
    set sizes [GetSize $fname]
    lassign $sizes itype w h
    set type "  \[$itype\]"
    if {$itype eq "ico"} {
        puts [format "%-*s%s : %s" $longestName $fname $type $w]
    } else {
        puts [format "%-*s%s : %s x %s" $longestName $fname $type [commify $w] [commify $h]]
    }
}
exit