You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

475 lines
20 KiB

# make-ico.tcl - build multi-resolution Windows .ico files and web rasters from
# the SVG masters in this directory, with no Windows-only component (G-135).
#
# Replaces make-ico.ps1: rasterization is rsvg-convert (librsvg, true alpha,
# cross-platform); PNG decode is pure Tcl (core zlib inflate + row unfilter);
# DIB and ICONDIR assembly is pure [binary format]. ImageMagick is not used for
# assembly - its ICO writer picks its own entry formats.
#
# Entry formats (unchanged from make-ico.ps1):
# <=128 px entries : uncompressed 32-bit BGRA DIB (BITMAPINFOHEADER + XOR + AND mask)
# 256 px entry : PNG-compressed (Vista+ convention)
# -allpng makes every entry PNG-compressed (web favicons: ~10x smaller, but
# unreadable by GDI+ and tklib's ico package - never use it for a PE resource).
#
# Every artifact produced gets its <artifact>.assetorigin.toml sidecar
# (re)written - artifact and source sha256 plus the invocation options - so
# regeneration cannot leave a stale record. Format: see AGENTS.md here.
#
# usage (any tclsh 8.6+, rsvg-convert on PATH):
# tclsh make-ico.tcl all ?-check?
# Regenerate every recorded artifact from the MANIFEST below into place
# (files are rewritten only when bytes change). -check builds everything
# but writes nothing: reports match/DIFFER per artifact and sidecar,
# exit 1 on any difference - the drift probe.
# tclsh make-ico.tcl ico -svg <in.svg> -ico <out.ico> ?-sizes {16 24 32 48 64 128 256}? ?-allpng? ?-role <role>? ?-work <dir>?
# tclsh make-ico.tcl raster -svg <in.svg> -png <out.png> -size <n> ?-role <role>?
set MANIFEST {
{ico punk-mark.svg punk-mark.ico {16 24 32 48 64 128 256} 0 pe-resource}
{ico alternative/punk-mark-solid.svg alternative/punk-mark-solid.ico {16 24 32 48 64 128 256} 0 pe-resource}
{ico project-default/icon.svg project-default/icon.ico {16 24 32 48 64 128 256} 0 pe-resource}
{ico web/icon.svg web/favicon.ico {16 32 48} 1 web}
{raster web/icon-opaque.svg web/apple-touch-icon.png 180 {} web}
{raster web/icon-opaque.svg web/icon-192.png 192 {} web}
{raster web/icon-opaque.svg web/icon-512.png 512 {} web}
}
# ---------------------------------------------------------------- file helpers
proc readbin {path} {
set f [open $path rb]
set data [read $f]
close $f
return $data
}
proc writebin {path data} {
set f [open $path wb]
puts -nonewline $f $data
close $f
}
# Relative path from directory basedir to path, forward slashes.
proc relpath {basedir path} {
set base [file split [file normalize $basedir]]
set p [file split [file normalize $path]]
set caseless [expr {$::tcl_platform(platform) eq "windows"}]
set i 0
while {$i < [llength $base] && $i < [llength $p]} {
set a [lindex $base $i]
set b [lindex $p $i]
if {$caseless} {
set a [string tolower $a]
set b [string tolower $b]
}
if {$a ne $b} break
incr i
}
set rel [concat [lrepeat [expr {[llength $base] - $i}] ..] [lrange $p $i end]]
if {![llength $rel]} {return .}
return [join $rel /]
}
# ---------------------------------------------------------------- sha256 (pure Tcl)
# Self-contained so the script runs under any stock tclsh; the checker
# (scriptlib/developer/assetorigin_check.tcl) embeds the same algorithm.
namespace eval sha256 {
variable K {
0x428a2f98 0x71374491 0xb5c0fbcf 0xe9b5dba5 0x3956c25b 0x59f111f1 0x923f82a4 0xab1c5ed5
0xd807aa98 0x12835b01 0x243185be 0x550c7dc3 0x72be5d74 0x80deb1fe 0x9bdc06a7 0xc19bf174
0xe49b69c1 0xefbe4786 0x0fc19dc6 0x240ca1cc 0x2de92c6f 0x4a7484aa 0x5cb0a9dc 0x76f988da
0x983e5152 0xa831c66d 0xb00327c8 0xbf597fc7 0xc6e00bf3 0xd5a79147 0x06ca6351 0x14292967
0x27b70a85 0x2e1b2138 0x4d2c6dfc 0x53380d13 0x650a7354 0x766a0abb 0x81c2c92e 0x92722c85
0xa2bfe8a1 0xa81a664b 0xc24b8b70 0xc76c51a3 0xd192e819 0xd6990624 0xf40e3585 0x106aa070
0x19a4c116 0x1e376c08 0x2748774c 0x34b0bcb5 0x391c0cb3 0x4ed8aa4a 0x5b9cca4f 0x682e6ff3
0x748f82ee 0x78a5636f 0x84c87814 0x8cc70208 0x90befffa 0xa4506ceb 0xbef9a3f7 0xc67178f2
}
proc hex {data} {
variable K
set bitlen [expr {wide([string length $data]) * 8}]
append data \x80
set pad [expr {(56 - [string length $data] % 64 + 64) % 64}]
append data [string repeat \x00 $pad] [binary format W $bitlen]
set h0 0x6a09e667; set h1 0xbb67ae85; set h2 0x3c6ef372; set h3 0xa54ff53a
set h4 0x510e527f; set h5 0x9b05688c; set h6 0x1f83d9ab; set h7 0x5be0cd19
set n [string length $data]
for {set off 0} {$off < $n} {incr off 64} {
binary scan [string range $data $off [expr {$off + 63}]] Iu16 w
for {set t 16} {$t < 64} {incr t} {
set x [lindex $w [expr {$t - 15}]]
set s0 [expr {((($x >> 7) | ($x << 25)) ^ (($x >> 18) | ($x << 14)) ^ ($x >> 3)) & 0xffffffff}]
set x [lindex $w [expr {$t - 2}]]
set s1 [expr {((($x >> 17) | ($x << 15)) ^ (($x >> 19) | ($x << 13)) ^ ($x >> 10)) & 0xffffffff}]
lappend w [expr {([lindex $w [expr {$t - 16}]] + $s0 + [lindex $w [expr {$t - 7}]] + $s1) & 0xffffffff}]
}
set a $h0; set b $h1; set c $h2; set d $h3
set e $h4; set f $h5; set g $h6; set h $h7
for {set t 0} {$t < 64} {incr t} {
set S1 [expr {((($e >> 6) | ($e << 26)) ^ (($e >> 11) | ($e << 21)) ^ (($e >> 25) | ($e << 7))) & 0xffffffff}]
set ch [expr {(($e & $f) ^ (~$e & $g)) & 0xffffffff}]
set T1 [expr {($h + $S1 + $ch + [lindex $K $t] + [lindex $w $t]) & 0xffffffff}]
set S0 [expr {((($a >> 2) | ($a << 30)) ^ (($a >> 13) | ($a << 19)) ^ (($a >> 22) | ($a << 10))) & 0xffffffff}]
set maj [expr {($a & $b) ^ ($a & $c) ^ ($b & $c)}]
set T2 [expr {($S0 + $maj) & 0xffffffff}]
set h $g; set g $f; set f $e; set e [expr {($d + $T1) & 0xffffffff}]
set d $c; set c $b; set b $a; set a [expr {($T1 + $T2) & 0xffffffff}]
}
set h0 [expr {($h0 + $a) & 0xffffffff}]; set h1 [expr {($h1 + $b) & 0xffffffff}]
set h2 [expr {($h2 + $c) & 0xffffffff}]; set h3 [expr {($h3 + $d) & 0xffffffff}]
set h4 [expr {($h4 + $e) & 0xffffffff}]; set h5 [expr {($h5 + $f) & 0xffffffff}]
set h6 [expr {($h6 + $g) & 0xffffffff}]; set h7 [expr {($h7 + $h) & 0xffffffff}]
}
return [format %08x%08x%08x%08x%08x%08x%08x%08x $h0 $h1 $h2 $h3 $h4 $h5 $h6 $h7]
}
proc file_hex {path} {
return [hex [readbin $path]]
}
}
# ---------------------------------------------------------------- PNG decode
# Return {w h ihdr-only} without decoding pixel data.
proc png_info {data} {
if {[string range $data 0 7] ne "\x89PNG\r\n\x1a\n"} {error "not a PNG"}
binary scan [string range $data 8 15] Iua4 len type
if {$type ne "IHDR"} {error "PNG missing IHDR"}
binary scan [string range $data 16 28] IuIucucucucucu w h depth ctype comp filt inter
return [list w $w h $h depth $depth colortype $ctype interlace $inter]
}
# Decode an 8-bit RGBA non-interlaced PNG (what rsvg-convert emits) into rows of
# RGBA byte values: a list of $h rows, each a flat list of $w*4 ints, top-down.
proc png_decode_rgba_rows {data} {
set info [png_info $data]
if {[dict get $info depth] != 8 || [dict get $info colortype] != 6 || [dict get $info interlace] != 0} {
error "unsupported PNG variant (need 8-bit RGBA non-interlaced): $info"
}
set w [dict get $info w]
set h [dict get $info h]
set idat ""
set pos 8
set n [string length $data]
while {$pos < $n} {
binary scan [string range $data $pos [expr {$pos + 7}]] Iua4 len type
if {$type eq "IDAT"} {
append idat [string range $data [expr {$pos + 8}] [expr {$pos + 8 + $len - 1}]]
} elseif {$type eq "IEND"} {
break
}
incr pos [expr {12 + $len}]
}
set raw [zlib decompress $idat]
set rowbytes [expr {$w * 4}]
if {[string length $raw] != ($rowbytes + 1) * $h} {
error "PNG pixel payload length mismatch"
}
set rows {}
set prev [lrepeat $rowbytes 0]
set pos 0
for {set y 0} {$y < $h} {incr y} {
binary scan [string range $raw $pos $pos] cu f
binary scan [string range $raw [expr {$pos + 1}] [expr {$pos + $rowbytes}]] cu* cur
incr pos [expr {$rowbytes + 1}]
switch -- $f {
0 {
set recon $cur
}
1 {
set recon [lrange $cur 0 3]
for {set i 4} {$i < $rowbytes} {incr i} {
lappend recon [expr {([lindex $cur $i] + [lindex $recon [expr {$i - 4}]]) & 0xff}]
}
}
2 {
set recon {}
for {set i 0} {$i < $rowbytes} {incr i} {
lappend recon [expr {([lindex $cur $i] + [lindex $prev $i]) & 0xff}]
}
}
3 {
set recon {}
for {set i 0} {$i < 4} {incr i} {
lappend recon [expr {([lindex $cur $i] + [lindex $prev $i] / 2) & 0xff}]
}
for {set i 4} {$i < $rowbytes} {incr i} {
lappend recon [expr {([lindex $cur $i] + ([lindex $recon [expr {$i - 4}]] + [lindex $prev $i]) / 2) & 0xff}]
}
}
4 {
set recon {}
for {set i 0} {$i < $rowbytes} {incr i} {
set x [lindex $cur $i]
if {$i >= 4} {
set a [lindex $recon [expr {$i - 4}]]
set c [lindex $prev [expr {$i - 4}]]
} else {
set a 0
set c 0
}
set b [lindex $prev $i]
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} {
set pred $a
} elseif {$pb <= $pc} {
set pred $b
} else {
set pred $c
}
lappend recon [expr {($x + $pred) & 0xff}]
}
}
default {
error "PNG row $y: unknown filter type $f"
}
}
set prev $recon
lappend rows $recon
}
return $rows
}
# ---------------------------------------------------------------- ICO assembly
# DIB icon entry: BITMAPINFOHEADER (doubled height), bottom-up BGRA XOR data,
# then the 1bpp AND mask (bit set where fully transparent, rows padded to 4 bytes).
proc dib_entry {s rows} {
set hdr [binary format iiissiiiiii 40 $s [expr {$s * 2}] 1 32 0 [expr {$s * $s * 4}] 0 0 0 0]
set xor {}
set mask {}
set maskrowbytes [expr {(($s + 31) / 32) * 4}]
for {set y [expr {$s - 1}]} {$y >= 0} {incr y -1} {
set row [lindex $rows $y]
set maskrow [lrepeat $maskrowbytes 0]
set x 0
foreach {r g b a} $row {
lappend xor $b $g $r $a
if {$a == 0} {
set bi [expr {$x / 8}]
lset maskrow $bi [expr {[lindex $maskrow $bi] | (0x80 >> ($x % 8))}]
}
incr x
}
lappend mask {*}$maskrow
}
return $hdr[binary format c* $xor][binary format c* $mask]
}
# Build the .ico image (as a binary string) from a list of {size payload} pairs.
proc ico_assemble {entries} {
set dir [binary format sss 0 1 [llength $entries]]
set offset [expr {6 + 16 * [llength $entries]}]
set body ""
foreach entry $entries {
lassign $entry s payload
set dim [expr {$s >= 256 ? 0 : $s}]
set len [string length $payload]
append dir [binary format ccccssii $dim $dim 0 0 1 32 $len $offset]
append body $payload
incr offset $len
}
return $dir$body
}
proc rasterize {svg size png} {
if {[catch {exec -ignorestderr rsvg-convert -w $size -h $size -o $png $svg} err]} {
if {[string match "*couldn't execute*" $err]} {
error "rsvg-convert not found on PATH - install librsvg (any platform) to rasterize"
}
error "rsvg-convert failed at ${size}px for $svg: $err"
}
set data [readbin $png]
set info [png_info $data]
if {[dict get $info w] != $size || [dict get $info h] != $size} {
error "rasterizer returned [dict get $info w]x[dict get $info h] for ${size}px"
}
return $data
}
# Build the full .ico binary for svg at the given sizes.
proc ico_build {svg sizes allpng workdir} {
set entries {}
foreach s $sizes {
set png [file join $workdir "$s.png"]
set data [rasterize $svg $s $png]
if {$allpng || $s == 256} {
lappend entries [list $s $data]
} else {
lappend entries [list $s [dib_entry $s [png_decode_rgba_rows $data]]]
}
}
return [ico_assemble $entries]
}
# ---------------------------------------------------------------- sidecars
proc toml_escape {s} {
return [string map {\\ \\\\ \" \\\"} $s]
}
# Sidecar content for artifact (bytes given) derived from source via options.
proc sidecar_content {artifactpath artifactbytes sourcepath role options} {
set srcrel [relpath [file dirname $artifactpath] $sourcepath]
set lines {}
lappend lines "# assetorigin provenance sidecar (schema 1) for the asset named by this file"
lappend lines "# with the .assetorigin.toml suffix stripped. Optional and advisory."
lappend lines "schema = 1"
lappend lines "role = \"[toml_escape $role]\""
lappend lines "hash = \"sha256:[sha256::hex $artifactbytes]\""
lappend lines "source = \"[toml_escape $srcrel]\""
lappend lines "source_hash = \"sha256:[sha256::file_hex $sourcepath]\""
lappend lines "generator = \"make-ico.tcl\""
lappend lines "options = \"[toml_escape $options]\""
return "[join $lines \n]\n"
}
# Write content to path only when bytes differ; return unchanged|updated|created.
proc write_if_changed {path content} {
if {[file exists $path]} {
if {[readbin $path] eq $content} {
return unchanged
}
writebin $path $content
return updated
}
writebin $path $content
return created
}
# Compare content against path without writing; return match|DIFFER|MISSING.
proc check_against {path content} {
if {![file exists $path]} {return MISSING}
if {[readbin $path] eq $content} {return match}
return DIFFER
}
# ---------------------------------------------------------------- subcommands
proc opts_parse {argl spec} {
# spec: dict option -> default; boolean options have default 0 and take no value
set out $spec
for {set i 0} {$i < [llength $argl]} {incr i} {
set o [lindex $argl $i]
if {![dict exists $spec $o]} {error "unknown option $o (expected: [dict keys $spec])"}
if {[dict get $spec $o] eq "0"} {
dict set out $o 1
} else {
incr i
if {$i >= [llength $argl]} {error "option $o needs a value"}
dict set out $o [lindex $argl $i]
}
}
return $out
}
proc workdir_make {requested} {
if {$requested ne ""} {
file mkdir $requested
return [list $requested 0]
}
set base ""
foreach v {TEMP TMP TMPDIR} {
if {[info exists ::env($v)] && $::env($v) ne ""} {
set base $::env($v)
break
}
}
if {$base eq ""} {set base /tmp}
set dir [file join $base "make-ico-[pid]"]
file mkdir $dir
return [list $dir 1]
}
proc cmd_ico {argl} {
set o [opts_parse $argl {-svg {} -ico {} -work {} -sizes {16 24 32 48 64 128 256} -allpng 0 -role {}}]
if {[dict get $o -svg] eq "" || [dict get $o -ico] eq ""} {error "ico: -svg and -ico are required"}
set role [dict get $o -role]
if {$role eq ""} {set role [expr {[dict get $o -allpng] ? "web" : "pe-resource"}]}
lassign [workdir_make [dict get $o -work]] work cleanup
try {
set bytes [ico_build [dict get $o -svg] [dict get $o -sizes] [dict get $o -allpng] $work]
set state [write_if_changed [dict get $o -ico] $bytes]
set options "ico -sizes {[dict get $o -sizes]}"
if {[dict get $o -allpng]} {append options " -allpng"}
set sc [sidecar_content [dict get $o -ico] $bytes [dict get $o -svg] $role $options]
set scstate [write_if_changed "[dict get $o -ico].assetorigin.toml" $sc]
puts "wrote [dict get $o -ico] ([string length $bytes] bytes, [llength [dict get $o -sizes]] entries: [join [dict get $o -sizes] ,]) \[$state\]; sidecar \[$scstate\]"
} finally {
if {$cleanup} {file delete -force $work}
}
}
proc cmd_raster {argl} {
set o [opts_parse $argl {-svg {} -png {} -size {} -role web}]
if {[dict get $o -svg] eq "" || [dict get $o -png] eq "" || [dict get $o -size] eq ""} {
error "raster: -svg, -png and -size are required"
}
set size [dict get $o -size]
set bytes [rasterize [dict get $o -svg] $size [dict get $o -png]]
set sc [sidecar_content [dict get $o -png] $bytes [dict get $o -svg] [dict get $o -role] "raster -size $size"]
set scstate [write_if_changed "[dict get $o -png].assetorigin.toml" $sc]
puts "wrote [dict get $o -png] ([string length $bytes] bytes, ${size}px) sidecar \[$scstate\]"
}
proc cmd_all {argl} {
global MANIFEST
set o [opts_parse $argl {-check 0 -work {}}]
set check [dict get $o -check]
set here [file dirname [file normalize [info script]]]
lassign [workdir_make [dict get $o -work]] work cleanup
set drift 0
try {
foreach row $MANIFEST {
lassign $row kind src artifact sizes allpng role
set svg [file join $here $src]
set out [file join $here $artifact]
if {$kind eq "ico"} {
set subwork [file join $work [string map {/ _} $artifact]]
file mkdir $subwork
set bytes [ico_build $svg $sizes $allpng $subwork]
set options "ico -sizes {$sizes}"
if {$allpng} {append options " -allpng"}
} else {
set bytes [rasterize $svg $sizes [file join $work [file tail $artifact]]]
set options "raster -size $sizes"
}
set sc [sidecar_content $out $bytes $svg $role $options]
if {$check} {
set astate [check_against $out $bytes]
set sstate [check_against "$out.assetorigin.toml" $sc]
if {$astate ne "match"} {set drift 1}
if {$sstate ne "match"} {set drift 1}
puts [format "%-33s %-7s ([string length $bytes] bytes) sidecar: %s" $artifact $astate $sstate]
} else {
set astate [write_if_changed $out $bytes]
set sstate [write_if_changed "$out.assetorigin.toml" $sc]
puts [format "%-33s %-9s ([string length $bytes] bytes) sidecar: %s" $artifact $astate $sstate]
}
}
} finally {
if {$cleanup} {file delete -force $work}
}
if {$check && $drift} {
puts "DRIFT: one or more artifacts or sidecars do not match a fresh regeneration"
exit 1
}
}
# ---------------------------------------------------------------- dispatch
set cmd [lindex $argv 0]
set rest [lrange $argv 1 end]
switch -- $cmd {
ico {cmd_ico $rest}
raster {cmd_raster $rest}
all {cmd_all $rest}
default {
puts stderr "usage: tclsh make-ico.tcl all ?-check?"
puts stderr " tclsh make-ico.tcl ico -svg <in.svg> -ico <out.ico> ?-sizes {16 24 32 48 64 128 256}? ?-allpng? ?-role <role>? ?-work <dir>?"
puts stderr " tclsh make-ico.tcl raster -svg <in.svg> -png <out.png> -size <n> ?-role <role>?"
exit 2
}
}