DOC Post Services for Page Telegram

Site Logo
Documents Code Admin Login

1763187766_e02a99dc_os2png.sh

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 File

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