Browse Source
make-ico.tcl (any tclsh 8.6+, rsvg-convert on PATH) replaces make-ico.ps1 in place: pure-Tcl PNG decode (core zlib inflate + row unfilter) and DIB/ICONDIR assembly take over from System.Drawing, with an embedded sha256 (NIST-vector validated, cross-checked against Get-FileHash). Byte-identity proven before the ps1 removal: 'all -check' regenerates all seven recorded artifacts (punk-mark.ico, punk-mark-solid.ico, project-default/icon.ico on the DIB+PNG256 profile; favicon.ico all-PNG; apple-touch/192/512 rasters) byte-identical to the committed files under both mingw tclsh 8.6.11 and native Tcl 9.0.3. Every artifact now carries its <name>.assetorigin.toml sidecar (schema 1: artifact sha256, source + source hash, role, generator + invocation options), (re)written by every generator subcommand so regeneration cannot leave a stale record. The 'all' manifest embeds the invocation profiles; 'all -check' is the no-write drift probe (exit 1 on any difference). src/runtime/punkshell.ico gains a derived-copy sidecar (hash equals source_hash, source ../assets/logo/punk-mark.ico); the three vfs copies stay unrecorded pending the user call on shipping sidecars inside baked kits. src/assets/logo/AGENTS.md: assetorigin schema 1 format documentation (canonical), Tcl-generator work guidance, drift-probe verification line. favicon source recorded as web/icon.svg (renders byte-identical to punk-mark.svg at 16/32/48; the same-directory primary web icon chosen). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
11 changed files with 613 additions and 123 deletions
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "pe-resource" |
||||||
|
hash = "sha256:2e91ae70f1f48105e66e4b51a84b8206a3deeee0cadc9036637e0475c2a2ed76" |
||||||
|
source = "punk-mark-solid.svg" |
||||||
|
source_hash = "sha256:39021d4c0a170d52adcd599f55540f22e1529ecf28f7a2215ed8775aa128bda5" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "ico -sizes {16 24 32 48 64 128 256}" |
||||||
@ -1,111 +0,0 @@ |
|||||||
# Build a multi-resolution Windows .ico from an SVG. |
|
||||||
# <=128 px entries : uncompressed 32-bit BGRA DIB (BITMAPINFOHEADER + XOR + AND mask) |
|
||||||
# 256 px entry : PNG-compressed (Vista+ convention) |
|
||||||
# Both forms are legal RT_ICON payloads; the ICONDIR is built to match. |
|
||||||
# -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). |
|
||||||
param( |
|
||||||
[Parameter(Mandatory=$true)][string]$Svg, |
|
||||||
[Parameter(Mandatory=$true)][string]$Ico, |
|
||||||
[Parameter(Mandatory=$true)][string]$Work, |
|
||||||
[int[]]$Sizes = @(16,24,32,48,64,128,256), |
|
||||||
[switch]$AllPng |
|
||||||
) |
|
||||||
|
|
||||||
Set-StrictMode -Version Latest |
|
||||||
$ErrorActionPreference = 'Stop' |
|
||||||
Add-Type -AssemblyName System.Drawing |
|
||||||
|
|
||||||
$sizes = $Sizes |
|
||||||
New-Item -ItemType Directory -Force $Work | Out-Null |
|
||||||
|
|
||||||
# --- rasterize each size with librsvg (true alpha, no background) --- |
|
||||||
foreach ($s in $sizes) { |
|
||||||
$png = Join-Path $Work "$s.png" |
|
||||||
& rsvg-convert -w $s -h $s -o $png $Svg |
|
||||||
if ($LASTEXITCODE -ne 0) { throw "rsvg-convert failed at ${s}px" } |
|
||||||
} |
|
||||||
|
|
||||||
# --- build per-entry payloads --- |
|
||||||
$entries = @() |
|
||||||
foreach ($s in $sizes) { |
|
||||||
$png = Join-Path $Work "$s.png" |
|
||||||
$bmp = [System.Drawing.Bitmap]::FromFile($png) |
|
||||||
try { |
|
||||||
if ($bmp.Width -ne $s -or $bmp.Height -ne $s) { throw "rasterizer returned $($bmp.Width)x$($bmp.Height) for ${s}px" } |
|
||||||
|
|
||||||
if ($AllPng -or $s -eq 256) { |
|
||||||
# PNG entry: raw file bytes verbatim |
|
||||||
$payload = [System.IO.File]::ReadAllBytes($png) |
|
||||||
} else { |
|
||||||
# DIB entry: BITMAPINFOHEADER with doubled height, bottom-up BGRA, then AND mask |
|
||||||
$ms = New-Object System.IO.MemoryStream |
|
||||||
$bw = New-Object System.IO.BinaryWriter($ms) |
|
||||||
$bw.Write([uint32]40) # biSize |
|
||||||
$bw.Write([int32]$s) # biWidth |
|
||||||
$bw.Write([int32]($s * 2)) # biHeight = XOR + AND |
|
||||||
$bw.Write([uint16]1) # biPlanes |
|
||||||
$bw.Write([uint16]32) # biBitCount |
|
||||||
$bw.Write([uint32]0) # biCompression = BI_RGB |
|
||||||
$bw.Write([uint32]($s * $s * 4)) |
|
||||||
$bw.Write([int32]0); $bw.Write([int32]0) |
|
||||||
$bw.Write([uint32]0); $bw.Write([uint32]0) |
|
||||||
|
|
||||||
# XOR: bottom-up rows, BGRA |
|
||||||
$rect = New-Object System.Drawing.Rectangle 0,0,$s,$s |
|
||||||
$data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, |
|
||||||
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb) |
|
||||||
try { |
|
||||||
$stride = $data.Stride |
|
||||||
$buf = New-Object byte[] ($stride * $s) |
|
||||||
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $buf, 0, $buf.Length) |
|
||||||
} finally { $bmp.UnlockBits($data) } |
|
||||||
|
|
||||||
for ($y = $s - 1; $y -ge 0; $y--) { |
|
||||||
$bw.Write($buf, $y * $stride, $s * 4) |
|
||||||
} |
|
||||||
|
|
||||||
# AND mask: fully transparent pixels set; rows padded to 4 bytes |
|
||||||
$maskRow = [math]::Ceiling($s / 32.0) * 4 |
|
||||||
for ($y = $s - 1; $y -ge 0; $y--) { |
|
||||||
$row = New-Object byte[] $maskRow |
|
||||||
for ($x = 0; $x -lt $s; $x++) { |
|
||||||
$a = $buf[$y * $stride + $x * 4 + 3] |
|
||||||
if ($a -eq 0) { $row[[math]::Floor($x / 8)] = $row[[math]::Floor($x / 8)] -bor (0x80 -shr ($x % 8)) } |
|
||||||
} |
|
||||||
$bw.Write($row, 0, $maskRow) |
|
||||||
} |
|
||||||
$bw.Flush() |
|
||||||
$payload = $ms.ToArray() |
|
||||||
$bw.Dispose(); $ms.Dispose() |
|
||||||
} |
|
||||||
$entries += [pscustomobject]@{ Size = $s; Bytes = $payload } |
|
||||||
} finally { $bmp.Dispose() } |
|
||||||
} |
|
||||||
|
|
||||||
# --- assemble ICONDIR + ICONDIRENTRY[] + payloads --- |
|
||||||
$out = New-Object System.IO.MemoryStream |
|
||||||
$w = New-Object System.IO.BinaryWriter($out) |
|
||||||
$w.Write([uint16]0) # reserved |
|
||||||
$w.Write([uint16]1) # type 1 = icon |
|
||||||
$w.Write([uint16]$entries.Count) |
|
||||||
|
|
||||||
$offset = 6 + (16 * $entries.Count) |
|
||||||
foreach ($e in $entries) { |
|
||||||
$dim = if ($e.Size -ge 256) { 0 } else { $e.Size } # 0 means 256 |
|
||||||
$w.Write([byte]$dim) # width |
|
||||||
$w.Write([byte]$dim) # height |
|
||||||
$w.Write([byte]0) # colour count (0 = truecolour) |
|
||||||
$w.Write([byte]0) # reserved |
|
||||||
$w.Write([uint16]1) # planes |
|
||||||
$w.Write([uint16]32) # bit count |
|
||||||
$w.Write([uint32]$e.Bytes.Length) |
|
||||||
$w.Write([uint32]$offset) |
|
||||||
$offset += $e.Bytes.Length |
|
||||||
} |
|
||||||
foreach ($e in $entries) { $w.Write($e.Bytes, 0, $e.Bytes.Length) } |
|
||||||
$w.Flush() |
|
||||||
[System.IO.File]::WriteAllBytes($Ico, $out.ToArray()) |
|
||||||
$w.Dispose(); $out.Dispose() |
|
||||||
|
|
||||||
Write-Output ("wrote {0} ({1:N0} bytes, {2} entries: {3})" -f $Ico, (Get-Item $Ico).Length, $entries.Count, ($sizes -join ',')) |
|
||||||
@ -0,0 +1,475 @@ |
|||||||
|
# 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 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "pe-resource" |
||||||
|
hash = "sha256:e652feddf19468ad458d48584ce8af165d1594bbedb408f5a53fe68697872a33" |
||||||
|
source = "icon.svg" |
||||||
|
source_hash = "sha256:31d1e3326d479618fb3d190c409981b6a785af4d8f25845a4bcce6e27410ee30" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "ico -sizes {16 24 32 48 64 128 256}" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "pe-resource" |
||||||
|
hash = "sha256:da875dd08a7665162ead7bbb575e2cb8b82a12d6654579c1e389d97198c19a26" |
||||||
|
source = "punk-mark.svg" |
||||||
|
source_hash = "sha256:60396baad2f42747211fb43bd452179f73e9165105fd3361376a47414e65e22a" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "ico -sizes {16 24 32 48 64 128 256}" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "web" |
||||||
|
hash = "sha256:02350f65f4342785a39738c456e516125b153a17392a75a4d0e4443c6f8ff328" |
||||||
|
source = "icon-opaque.svg" |
||||||
|
source_hash = "sha256:604f07ecba198bfd299cb978587ac460929892b0b548c56a26cce5204e3e1a1e" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "raster -size 180" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "web" |
||||||
|
hash = "sha256:44ca400451f6e7ecb731dd808b88de3cadda528fe79a7bc18ceb4c7d6e19a599" |
||||||
|
source = "icon.svg" |
||||||
|
source_hash = "sha256:1834f46d39bb4c4af70bde92ca2b9c6e6cdfceb64879e366b85d6bd705b4d091" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "ico -sizes {16 32 48} -allpng" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "web" |
||||||
|
hash = "sha256:03e92d71538697a890bb52d9fff71316bd0c9125074897c76300690b90e8f63e" |
||||||
|
source = "icon-opaque.svg" |
||||||
|
source_hash = "sha256:604f07ecba198bfd299cb978587ac460929892b0b548c56a26cce5204e3e1a1e" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "raster -size 192" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
schema = 1 |
||||||
|
role = "web" |
||||||
|
hash = "sha256:4c2c3d9474a4e5de2b110dc927cb1f32fcb0c58d610864af42147fcf9bde3b1f" |
||||||
|
source = "icon-opaque.svg" |
||||||
|
source_hash = "sha256:604f07ecba198bfd299cb978587ac460929892b0b548c56a26cce5204e3e1a1e" |
||||||
|
generator = "make-ico.tcl" |
||||||
|
options = "raster -size 512" |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# assetorigin provenance sidecar (schema 1) for the asset named by this file |
||||||
|
# with the .assetorigin.toml suffix stripped. Optional and advisory. |
||||||
|
# punkshell.ico is a byte-identical derived copy of the generated logo artifact |
||||||
|
# (hash equals source_hash); it is consumed by builds, never edited here. |
||||||
|
schema = 1 |
||||||
|
role = "pe-resource" |
||||||
|
hash = "sha256:da875dd08a7665162ead7bbb575e2cb8b82a12d6654579c1e389d97198c19a26" |
||||||
|
source = "../assets/logo/punk-mark.ico" |
||||||
|
source_hash = "sha256:da875dd08a7665162ead7bbb575e2cb8b82a12d6654579c1e389d97198c19a26" |
||||||
Loading…
Reference in new issue