fix: use PIL as primary TGA reader with manual fallback

- PIL handles more TGA formats correctly
- Manual parser fixed to properly skip ID and color map fields
- Added better error messages with expected vs actual data size
- Fallback chain: PIL first, then manual parser
This commit is contained in:
LemonNexus 2026-02-11 16:00:03 +00:00
parent 46e84b39e3
commit 14bac40fdf
1 changed files with 78 additions and 37 deletions

View File

@ -188,15 +188,29 @@ class TGAConverter:
"""
try:
with open(tga_file.filepath, 'rb') as f:
# Skip header
f.seek(18)
# Read header to get ID length
header = f.read(18)
if len(header) < 18:
logger.warning(f"Invalid TGA header: {tga_file.filepath}")
return None
id_length = header[0]
color_map_type = header[1]
image_type = header[2]
# Skip ID field if present
header = f.read(18)
id_length = header[0] if len(header) >= 18 else 0
if id_length > 0:
f.seek(18 + id_length)
# Skip color map if present
if color_map_type == 1:
# Read color map spec
color_map_offset = 3 # After id_length, color_map_type, image_type
color_map_length = struct.unpack('<H', header[color_map_offset+2:color_map_offset+4])[0]
color_map_entry_size = header[color_map_offset+4]
color_map_size = color_map_length * (color_map_entry_size // 8)
f.seek(f.tell() + color_map_size)
# Calculate pixel data size
bytes_per_pixel = tga_file.pixel_depth // 8
pixel_data_size = tga_file.width * tga_file.height * bytes_per_pixel
@ -205,8 +219,13 @@ class TGAConverter:
pixel_data = f.read(pixel_data_size)
if len(pixel_data) < pixel_data_size:
logger.warning(f"Incomplete TGA pixel data: {tga_file.filepath}")
return None
logger.warning(f"Incomplete TGA pixel data: {tga_file.filepath} "
f"(got {len(pixel_data)}, expected {pixel_data_size})")
# Try to use what we have
if len(pixel_data) == 0:
return None
# Pad with zeros
pixel_data = pixel_data + bytes(pixel_data_size - len(pixel_data))
# Convert to numpy array
pixels = np.frombuffer(pixel_data, dtype=np.uint8)
@ -260,37 +279,59 @@ class TGAConverter:
return None
try:
# Read TGA
tga_file = self.read_tga_header(tga_path)
if not tga_file:
return None
logger.info(f"Converting: {tga_path.name} ({tga_file})")
pixels = self.read_tga_pixels(tga_file)
if pixels is None:
return None
# Create PIL Image
if pixels.shape[2] == 4:
# RGBA
image = Image.fromarray(pixels, 'RGBA')
else:
# RGB
image = Image.fromarray(pixels, 'RGB')
# Determine output path
if output_name is None:
output_name = tga_path.stem
png_path = self.output_dir / f"{output_name}.png"
# Save as PNG
image.save(png_path, 'PNG')
logger.info(f"Saved: {png_path}")
return png_path
# Try PIL first (handles more TGA formats)
try:
image = Image.open(tga_path)
# Convert to RGBA if necessary
if image.mode in ('RGB', 'RGBA'):
# Image is already in good format
pass
elif image.mode == 'P': # Palette
image = image.convert('RGBA')
else:
image = image.convert('RGBA')
# Determine output path
if output_name is None:
output_name = tga_path.stem
png_path = self.output_dir / f"{output_name}.png"
image.save(png_path, 'PNG')
logger.info(f"Converted (PIL): {tga_path.name} -> {png_path.name}")
return png_path
except Exception as pil_error:
logger.debug(f"PIL failed, trying manual: {pil_error}")
# Fallback to manual TGA reading
tga_file = self.read_tga_header(tga_path)
if not tga_file:
return None
logger.info(f"Converting: {tga_path.name} ({tga_file})")
pixels = self.read_tga_pixels(tga_file)
if pixels is None:
return None
# Create PIL Image
if pixels.shape[2] == 4:
image = Image.fromarray(pixels, 'RGBA')
else:
image = Image.fromarray(pixels, 'RGB')
# Determine output path
if output_name is None:
output_name = tga_path.stem
png_path = self.output_dir / f"{output_name}.png"
image.save(png_path, 'PNG')
logger.info(f"Converted (manual): {tga_path.name} -> {png_path.name}")
return png_path
except Exception as e:
logger.error(f"Error converting {tga_path}: {e}")
return None