Description: BASH script to convert OS/2 era ICO BITMAP files to PNG. It is not an easy conversion. Otherwise use PMView in batchmode on an OS/2 machine.
Date: 2025-11-15 06:22:46
Download command: wget https://doc.laptopministry.org/uploads/1763187766_e02a99dc_os2png.sh
#!/bin/bash
# os2png - Converts OS/2 2.x / Warp .ico files to PNG
# Handles both IC (simple icon) and BA (bitmap array) formats
# Supports 1-bit, 4-bit, 8-bit, and 24-bit color depths
set -uo pipefail
# Check dependencies
for cmd in xxd convert; do
command -v "$cmd" &>/dev/null || {
echo "ERROR: Install $cmd (xxd is in vim-common, convert in imagemagick)"
exit 1
}
done
# Read little-endian values from binary data
read_le16() {
local offset=$1 file=$2
printf "%d" "0x$(xxd -s "$offset" -l 2 -p "$file" | sed 's/\(..\)\(..\)/\2\1/')"
}
read_le32() {
local offset=$1 file=$2
printf "%d" "0x$(xxd -s "$offset" -l 4 -p "$file" | sed 's/\(..\)\(..\)\(..\)\(..\)/\4\3\2\1/')"
}
# Convert single OS/2 icon to PNG
# $1 = icon file, $2 = offset to BITMAPINFOHEADER, $3 = output file, $4 = optional data offset
convert_icon() {
local ICO=$1 OFFSET=$2 BASE=$3 DATA_OFFSET_OVERRIDE=${4:-}
local TMP=$(mktemp -d)
trap "rm -rf '$TMP'" RETURN
# Parse BITMAPINFOHEADER
local cbFix=$(read_le32 "$OFFSET" "$ICO")
if (( cbFix < 12 )); then
echo "FAIL (invalid header size: $cbFix)"
return 1
fi
local width height planes bitcount
width=$(read_le16 $((OFFSET + 4)) "$ICO")
height=$(read_le16 $((OFFSET + 6)) "$ICO")
planes=$(read_le16 $((OFFSET + 8)) "$ICO")
bitcount=$(read_le16 $((OFFSET + 10)) "$ICO")
# For icons/pointers:
# - 1bpp (monochrome): height is doubled for XOR+AND masks, divide by 2
# - >1bpp (color): height is actual, use as-is (per OS/2 spec)
# Width is never doubled for any format
local actual_width actual_height
actual_width=$width
if (( bitcount == 1 )); then
# Monochrome icons have doubled height (XOR + AND mask)
actual_height=$((height / 2))
else
# Color icons store real height
actual_height=$height
fi
# Calculate color table size
local colors=$((1 << bitcount))
(( bitcount > 8 )) && colors=0
# OS/2 1.x uses RGB triples (3 bytes), OS/2 2.x uses RGBA quads (4 bytes)
local palette_bytes
if (( cbFix == 12 )); then
palette_bytes=$((colors * 3)) # OS/2 1.x: RGB
else
palette_bytes=$((colors * 4)) # OS/2 2.x: BGRA
fi
# Calculate data offset
local data_offset
if [[ -n "$DATA_OFFSET_OVERRIDE" ]]; then
data_offset=$DATA_OFFSET_OVERRIDE
else
data_offset=$((OFFSET + cbFix + palette_bytes))
fi
# Calculate row size with padding (must be multiple of 4 bytes)
local row_bits=$((actual_width * bitcount))
local row_bytes=$(( (row_bits + 31) / 32 * 4 ))
# AND mask is always 1 bpp
local and_row_bytes=$(( (actual_width + 31) / 32 * 4 ))
local xor_size=$((row_bytes * actual_height))
local and_size=$((and_row_bytes * actual_height))
# Extract palette
if (( colors > 0 )); then
dd if="$ICO" of="$TMP/palette.bin" bs=1 skip=$((OFFSET + cbFix)) count="$palette_bytes" status=none
fi
# Extract XOR mask (color data)
dd if="$ICO" of="$TMP/xor.bin" bs=1 skip="$data_offset" count="$xor_size" status=none
# Extract AND mask (transparency)
dd if="$ICO" of="$TMP/and.bin" bs=1 skip=$((data_offset + xor_size)) count="$and_size" status=none
# Convert to RGBA
> "$TMP/rgba.raw"
# Process scanlines (bottom-to-top in file, need to reverse)
for ((row = actual_height - 1; row >= 0; row--)); do
local xor_offset=$((row * row_bytes))
local and_offset=$((row * and_row_bytes))
for ((col = 0; col < actual_width; col++)); do
local r=0 g=0 b=0 a=255
# Read pixel from XOR mask
if (( bitcount == 1 )); then
local byte_offset=$((xor_offset + col / 8))
local bit_offset=$((7 - col % 8))
local byte_val=$(xxd -s "$byte_offset" -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
local pixel=$(( (0x$byte_val >> bit_offset) & 1 ))
# Read from palette
if (( cbFix == 12 )); then
# OS/2 1.x: RGB
local pal_offset=$((pixel * 3))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
else
# OS/2 2.x: BGRA
local pal_offset=$((pixel * 4))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
fi
elif (( bitcount == 4 )); then
local byte_offset=$((xor_offset + col / 2))
local byte_val=$(xxd -s "$byte_offset" -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
local pixel
if (( col % 2 == 0 )); then
pixel=$(( (0x$byte_val >> 4) & 0xF ))
else
pixel=$(( 0x$byte_val & 0xF ))
fi
# Read from palette
if (( cbFix == 12 )); then
local pal_offset=$((pixel * 3))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
else
local pal_offset=$((pixel * 4))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
fi
elif (( bitcount == 8 )); then
local byte_offset=$((xor_offset + col))
local pixel=$(xxd -s "$byte_offset" -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
pixel=$((0x$pixel))
# Read from palette
if (( cbFix == 12 )); then
local pal_offset=$((pixel * 3))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
else
local pal_offset=$((pixel * 4))
b=$(xxd -s $((pal_offset + 0)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pal_offset + 1)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pal_offset + 2)) -l 1 -p "$TMP/palette.bin" 2>/dev/null || echo "00")
fi
elif (( bitcount == 24 )); then
local pixel_offset=$((xor_offset + col * 3))
b=$(xxd -s $((pixel_offset + 0)) -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
g=$(xxd -s $((pixel_offset + 1)) -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
r=$(xxd -s $((pixel_offset + 2)) -l 1 -p "$TMP/xor.bin" 2>/dev/null || echo "00")
fi
# Read transparency from AND mask
local and_byte_offset=$((and_offset + col / 8))
local and_bit_offset=$((7 - col % 8))
local and_byte_val=$(xxd -s "$and_byte_offset" -l 1 -p "$TMP/and.bin" 2>/dev/null || echo "00")
local transparent=$(( (0x$and_byte_val >> and_bit_offset) & 1 ))
# AND mask: 1 = transparent, 0 = opaque
if (( transparent == 1 )); then
a=0
fi
# Write RGBA pixel
printf "\x$r\x$g\x$b\x$(printf '%02x' $a)" >> "$TMP/rgba.raw"
done
done
# Convert RGBA to PNG
if convert -size "${actual_width}x${actual_height}" -depth 8 rgba:"$TMP/rgba.raw" "$BASE" 2>/dev/null; then
echo "OK (${actual_width}x${actual_height}, ${bitcount}bpp)"
return 0
else
echo "FAIL (convert error)"
return 1
fi
}
# Process icons
shopt -s nullglob nocaseglob
ICO_FILES=( *.ico )
(( ${#ICO_FILES[@]} )) || { echo "No .ico files found."; exit 0; }
echo "Found ${#ICO_FILES[@]} icon file(s). Converting..."
SUCC=0 FAIL=0
for ICO in "${ICO_FILES[@]}"; do
BASE="${ICO%.*}"
printf "%-40s -> %s.png ... " "$ICO" "$BASE"
# Read magic bytes
magic=$(xxd -l 2 -p "$ICO")
if [[ $magic == "4943" ]]; then
# IC format - simple icon
# BITMAPARRAYFILEHEADER is 14 bytes, BITMAPINFOHEADER starts at offset 14
if convert_icon "$ICO" 14 "${BASE}.png"; then
((SUCC++))
else
((FAIL++))
fi
elif [[ $magic == "4241" ]]; then
# BA format - bitmap array with multiple icons (different sizes/bit depths)
# BA header is 14 bytes, then CI entries follow
# Some files have offNext field pointing to additional BA structures
file_size=$(stat -c%s "$ICO")
# Don't follow offNext - the first BA structure has the primary icons
# offNext typically points to lower-resolution versions
ba_start=0
# Find the best quality icon (highest bit depth, then largest size)
best_offset=-1
best_bitcount=0
best_width=0
best_height=0
best_offbits=0
best_ci_offset=0
offset=$((ba_start + 14))
# Scan through CI entries to find the best quality (highest bit depth, then largest size)
while (( offset < file_size - 20 )); do
entry_magic=$(xxd -s "$offset" -l 2 -p "$ICO" 2>/dev/null || echo "")
if [[ $entry_magic == "4349" ]]; then
# Found a CI entry
# Read BITMAPINFOHEADER to get bit depth and dimensions
bih_offset=$((offset + 14))
width=$(read_le16 $((bih_offset + 4)) "$ICO")
height=$(read_le16 $((bih_offset + 6)) "$ICO")
bitcount=$(read_le16 $((bih_offset + 10)) "$ICO")
offbits=$(read_le32 $((offset + 10)) "$ICO")
# Prefer higher bit depths first; if equal, prefer larger dimensions
if (( bitcount > best_bitcount )) || \
(( bitcount == best_bitcount && width * height > best_width * best_height )); then
best_bitcount=$bitcount
best_width=$width
best_height=$height
best_offset=$((offset + 14))
best_offbits=$offbits
best_ci_offset=$offset
fi
# Move to next possible CI entry (typically 32 bytes apart)
offset=$((offset + 32))
else
offset=$((offset + 2))
fi
# Safety: don't scan too far
(( offset > ba_start + 200 )) && break
done
if (( best_offset > 0 )); then
# Convert offBits (relative to CI header) to absolute file offset
# Per OS/2 spec: offBits is "from the start of the definition" (CI header)
use_offbits=""
if (( best_offbits > 0 )); then
# Calculate absolute offset: CI header start + relative offBits
abs_offbits=$((best_ci_offset + best_offbits))
# Validate the calculated offset
if (( abs_offbits < file_size - 100 )); then
test_byte=$(xxd -s "$abs_offbits" -l 1 -p "$ICO" 2>/dev/null || echo "00")
if [[ "$test_byte" != "00" ]]; then
use_offbits=$abs_offbits
else
# Fallback: try offBits as absolute (some files may not follow spec)
if (( best_offbits < file_size - 100 )); then
test_byte=$(xxd -s "$best_offbits" -l 1 -p "$ICO" 2>/dev/null || echo "00")
if [[ "$test_byte" != "00" ]]; then
use_offbits=$best_offbits
fi
fi
fi
fi
fi
if convert_icon "$ICO" "$best_offset" "${BASE}.png" "$use_offbits"; then
((SUCC++))
else
((FAIL++))
fi
else
echo "FAIL (no CI entry found)"
((FAIL++))
fi
else
echo "SKIP (not OS/2 format, magic: $magic)"
continue
fi
done
echo
echo "=================================================="
printf " SUCCESS : %4d PNG file(s)\n" "$SUCC"
printf " FAILED : %4d\n" "$FAIL"
echo "=================================================="
Back to Codes