↓ Skip to main content
Repairing PNG Dimensions with CRC32

Repairing PNG Dimensions with CRC32

·776 words·4 mins· loading · loading ·
Author
Dawn Magnet
Tech enthusiast & daily solver.

PNG CRC32 Repair Tool
#

This is a tool for repairing PNG image CRC32 checksums, primarily by adjusting the image width and height.

Features
#

  1. CRC32 Detection

    • Automatically detect the CRC32 value of a PNG file’s IHDR chunk
    • Compare the actual CRC32 with the expected value
  2. Automatic Repair

    • Supports three repair modes:
      • Repair the height only
      • Repair the width only
      • Repair both the height and width
    • Automatically generate a repaired file with the _fix.png suffix
  3. Visual Output

    • Use colored output to show the processing steps
    • Display the current and repaired image dimensions
    • Display detailed CRC32 information

Usage
#

python break.py <png_file_path>

For example:

python break.py test.png

Output Legend
#

  • Green: success information
  • Red: error information
  • Yellow: expected values
  • Cyan: informational messages

Technical Details
#

  1. IHDR Class

    • Process the IHDR chunk of a PNG file
    • Provide methods for reading and modifying the width and height
    • Calculate the CRC32 checksum
  2. Repair Strategy

    • First try changing only the height (fastest)
    • Then try changing only the width
    • Finally try changing both the width and height (slowest)
  3. File Processing

    • Support binary file reading and writing
    • Automatically back up the original file
    • Handle errors and exceptions

Notes
#

  1. The input file must be a valid PNG file.
  2. The tool creates the repaired file in the same directory as the original file.
  3. The repair process may take some time, especially when trying to repair both the width and height.
# -*- coding: utf-8 -*-
import binascii
import struct
import sys
from pathlib import Path
from typing import Optional, Tuple


# ANSI escape codes for colors
class Colors:
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    BLUE = "\033[94m"
    CYAN = "\033[96m"
    RESET = "\033[0m"


class IHDR:
    def __init__(self, data: bytes):
        self.raw_data = data
        self._header = data[:4]  # IHDR chunk type
        self._height_bytes = data[8:12]
        self._width_bytes = data[4:8]
        self._ends = data[12:]  # bit depth, color type, etc

    @property
    def width(self) -> int:
        return int.from_bytes(self._width_bytes, "big")

    @property
    def height(self) -> int:
        return int.from_bytes(self._height_bytes, "big")

    def get_data(self, width=None, height=None) -> bytes:
        return (
            bytes(self._header)
            + (bytes(self._width_bytes) if width is None else struct.pack(">i", width))
            + (
                bytes(self._height_bytes)
                if height is None
                else struct.pack(">i", height)
            )
            + bytes(self._ends)
        )

    def crc32(self, width=None, height=None) -> int:
        return binascii.crc32(self.get_data(width, height)) & 0xFFFFFFFF

    @staticmethod
    def from_png(png_data: bytes) -> Tuple["IHDR", int]:
        """Extract IHDR from PNG file data and return (IHDR, expected_crc32)"""
        ihdr_data = png_data[12:29]
        expected_crc32 = int.from_bytes(png_data[29:33], "big")
        return IHDR(ihdr_data), expected_crc32


def try_fix_height(ihdr: IHDR, expected_crc32: int) -> Optional[int]:
    for height in range(0, 65535):
        if ihdr.crc32(height=height) == expected_crc32:
            return height
    return None


def try_fix_width(ihdr: IHDR, expected_crc32: int) -> Optional[int]:
    for width in range(0, 65535):
        if ihdr.crc32(width=width) == expected_crc32:
            return width
    return None


def try_fix_both(
    ihdr: IHDR, expected_crc32: int
) -> Tuple[Optional[int], Optional[int]]:
    for height in range(0, 65535):
        for width in range(0, 65535):
            if ihdr.crc32(width=width, height=height) == expected_crc32:
                return height, width
    return None, None


def save_fixed_png(
    original_data: bytes,
    width: Optional[int] = None,
    height: Optional[int] = None,
    output_path: Path | None = None,
) -> None:
    """Save a new PNG with fixed dimensions"""
    if output_path is None:
        output_path = Path(str(png_path).replace(".png", "_fix.png"))

    # Only modify if we have new dimensions
    if width is None and height is None:
        return

    # Copy original data
    new_data = bytearray(original_data)

    # Update width if provided (at offset 16)
    if width is not None:
        new_data[16:20] = struct.pack(">i", width)

    # Update height if provided (at offset 20)
    if height is not None:
        new_data[20:24] = struct.pack(">i", height)

    # Calculate new CRC32 for IHDR chunk
    new_crc = binascii.crc32(new_data[12:29]) & 0xFFFFFFFF
    new_data[29:33] = struct.pack(">I", new_crc)

    # Save new file
    with open(output_path, "wb") as f:
        f.write(new_data)
    print(f"{Colors.GREEN}Saved fixed PNG to: {output_path}{Colors.RESET}")


def try_fix_crc32(img_data: bytes, png_path: Path) -> None:
    ihdr, expected_crc32 = IHDR.from_png(img_data)
    real_crc32 = ihdr.crc32()

    if expected_crc32 == real_crc32:
        print(f"{Colors.GREEN}CRC32 is correct!{Colors.RESET}")
        return

    print(f"{Colors.CYAN}IHDR data:{Colors.RESET} {ihdr.get_data()}")
    print(f"{Colors.YELLOW}Expected CRC32: 0x{expected_crc32:X}")
    print(f"{Colors.RED}Current CRC32: 0x{real_crc32:X}{Colors.RESET}")
    print(f"{Colors.CYAN}Current dimensions: {ihdr.width}x{ihdr.height}{Colors.RESET}")

    # Try fixing height
    height = try_fix_height(ihdr, expected_crc32)
    if height is not None:
        print(f"{Colors.GREEN}Found height fix: {height} (0x{height:X})")
        print(f"Real dimensions: {ihdr.width}x{height}{Colors.RESET}")
        save_fixed_png(
            img_data,
            height=height,
            output_path=png_path.parent / f"{png_path.stem}_fix.png",
        )
        return

    # Try fixing width
    width = try_fix_width(ihdr, expected_crc32)
    if width is not None:
        print(f"{Colors.GREEN}Found width fix: {width} (0x{width:X})")
        print(f"Real dimensions: {width}x{ihdr.height}{Colors.RESET}")
        save_fixed_png(
            img_data,
            width=width,
            output_path=png_path.parent / f"{png_path.stem}_fix.png",
        )
        return

    # Try fixing both
    height, width = try_fix_both(ihdr, expected_crc32)
    if height is not None and width is not None:
        print(
            f"{Colors.GREEN}Found height/width fix: {height}x{width} (0x{height:X}x0x{width:X}){Colors.RESET}"
        )
        save_fixed_png(
            img_data,
            width=width,
            height=height,
            output_path=png_path.parent / f"{png_path.stem}_fix.png",
        )
        return

    print(f"{Colors.RED}No fix found{Colors.RESET}")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"{Colors.RED}Usage: python {sys.argv[0]} <png_file_path>{Colors.RESET}")
        sys.exit(1)

    png_path = Path(sys.argv[1])
    if not png_path.exists():
        print(f"{Colors.RED}Error: File {png_path} does not exist{Colors.RESET}")
        sys.exit(1)

    if not png_path.is_file():
        print(f"{Colors.RED}Error: {png_path} is not a file{Colors.RESET}")
        sys.exit(1)

    try:
        with open(png_path, "rb") as f:
            img_data = f.read()
        try_fix_crc32(img_data, png_path)
    except Exception as e:
        print(f"{Colors.RED}Error: {str(e)}{Colors.RESET}")
        sys.exit(1)
f