arguments to force 16 or 8 bit color
This commit is contained in:
167
compress_pngs.py
167
compress_pngs.py
@@ -6,14 +6,21 @@ Compresses all PNG files in subdirectories with maximum parallelism.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from PIL import Image
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
def compress_png(input_path, output_path):
|
||||
"""Compress a single PNG file."""
|
||||
def compress_png(input_path, output_path, force_bitdepth=None):
|
||||
"""Compress a single PNG file.
|
||||
|
||||
Args:
|
||||
input_path: Path to input image
|
||||
output_path: Path to output image
|
||||
force_bitdepth: None (auto-detect), '8' (force 8-bit), or '16' (force 16-bit)
|
||||
"""
|
||||
try:
|
||||
input_path = Path(input_path)
|
||||
output_path = Path(output_path)
|
||||
@@ -32,56 +39,90 @@ def compress_png(input_path, output_path):
|
||||
original_size = os.path.getsize(input_path)
|
||||
img = Image.open(input_path)
|
||||
|
||||
# Check if image is 16-bit
|
||||
# PIL represents 16-bit grayscale as 'I' mode (32-bit integer)
|
||||
# For color images, check pixel values to determine bit depth
|
||||
is_16bit = False
|
||||
original_mode = img.mode
|
||||
|
||||
if img.mode == 'I':
|
||||
# 'I' mode typically represents 16-bit grayscale
|
||||
# Determine target bit depth
|
||||
if force_bitdepth == '8':
|
||||
is_16bit = False
|
||||
elif force_bitdepth == '16':
|
||||
is_16bit = True
|
||||
elif img.mode in ('RGB', 'RGBA', 'LA'):
|
||||
# Check if max pixel value exceeds 8-bit range
|
||||
try:
|
||||
# Get a sample of pixels to check
|
||||
pixels = list(img.getdata())
|
||||
if pixels:
|
||||
# Flatten if needed (for multi-channel modes)
|
||||
if isinstance(pixels[0], (tuple, list)):
|
||||
max_val = max(max(p) for p in pixels[:1000]) # Sample first 1000 pixels
|
||||
else:
|
||||
max_val = max(pixels[:1000])
|
||||
if max_val > 255:
|
||||
is_16bit = True
|
||||
except:
|
||||
# If we can't determine, assume 8-bit to be safe
|
||||
pass
|
||||
|
||||
# Preserve 16-bit depth if present, otherwise convert as needed
|
||||
if is_16bit:
|
||||
# For 16-bit images, preserve the mode
|
||||
# PIL will save 16-bit when mode is 'I' (grayscale) or when using specific modes
|
||||
else:
|
||||
# Auto-detect bit depth
|
||||
is_16bit = False
|
||||
original_mode = img.mode
|
||||
|
||||
if img.mode == 'I':
|
||||
# Keep 16-bit grayscale
|
||||
pass
|
||||
elif img.mode in ('RGB', 'RGBA'):
|
||||
# Keep color mode - PIL may preserve 16-bit depending on how it was loaded
|
||||
# Note: PIL's PNG save may convert 16-bit RGB to 8-bit, but we preserve the mode
|
||||
pass
|
||||
else:
|
||||
# Convert other modes while trying to preserve bit depth
|
||||
if 'A' in img.mode:
|
||||
# 'I' mode typically represents 16-bit grayscale
|
||||
is_16bit = True
|
||||
elif img.mode in ('RGB', 'RGBA', 'LA'):
|
||||
# Check if max pixel value exceeds 8-bit range
|
||||
try:
|
||||
# Get a sample of pixels to check
|
||||
pixels = list(img.getdata())
|
||||
if pixels:
|
||||
# Flatten if needed (for multi-channel modes)
|
||||
if isinstance(pixels[0], (tuple, list)):
|
||||
max_val = max(max(p) for p in pixels[:1000]) # Sample first 1000 pixels
|
||||
else:
|
||||
max_val = max(pixels[:1000])
|
||||
if max_val > 255:
|
||||
is_16bit = True
|
||||
except:
|
||||
# If we can't determine, assume 8-bit to be safe
|
||||
pass
|
||||
|
||||
# Handle bit depth conversion based on target
|
||||
if is_16bit:
|
||||
# Force or preserve 16-bit
|
||||
if force_bitdepth == '16':
|
||||
# Force 16-bit: convert to appropriate 16-bit mode
|
||||
if img.mode == 'I':
|
||||
# Already 16-bit grayscale
|
||||
pass
|
||||
elif 'A' in img.mode or img.mode == 'LA':
|
||||
# Convert to 16-bit RGBA (PIL limitation: may not fully preserve 16-bit color)
|
||||
img = img.convert('RGBA')
|
||||
else:
|
||||
# Convert to 16-bit RGB
|
||||
img = img.convert('RGB')
|
||||
else:
|
||||
# Preserve existing 16-bit
|
||||
if img.mode == 'I':
|
||||
# Keep 16-bit grayscale
|
||||
pass
|
||||
elif img.mode in ('RGB', 'RGBA'):
|
||||
# Keep color mode - PIL may preserve 16-bit depending on how it was loaded
|
||||
pass
|
||||
else:
|
||||
# Convert other modes while trying to preserve bit depth
|
||||
if 'A' in img.mode:
|
||||
img = img.convert('RGBA')
|
||||
else:
|
||||
img = img.convert('RGB')
|
||||
else:
|
||||
# 8-bit images: convert as needed
|
||||
if img.mode == 'RGBA':
|
||||
# Keep RGBA for transparency support
|
||||
img = img.convert('RGBA')
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
# Force or use 8-bit
|
||||
if force_bitdepth == '8':
|
||||
# Force 8-bit: ensure we're in 8-bit mode
|
||||
if img.mode == 'I':
|
||||
# Convert 16-bit grayscale to 8-bit
|
||||
img = img.convert('L')
|
||||
elif img.mode == 'RGBA':
|
||||
# Keep RGBA for transparency support (8-bit)
|
||||
img = img.convert('RGBA')
|
||||
elif img.mode == 'RGB':
|
||||
# Already 8-bit RGB
|
||||
pass
|
||||
else:
|
||||
# Convert to 8-bit RGB
|
||||
if 'A' in img.mode:
|
||||
img = img.convert('RGBA')
|
||||
else:
|
||||
img = img.convert('RGB')
|
||||
else:
|
||||
# 8-bit images: convert as needed
|
||||
if img.mode == 'RGBA':
|
||||
# Keep RGBA for transparency support
|
||||
img = img.convert('RGBA')
|
||||
elif img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Save with maximum compression
|
||||
# PIL will preserve 16-bit for 'I' mode automatically
|
||||
@@ -139,6 +180,38 @@ def format_time(seconds):
|
||||
return f"{hours:02d}:{minutes:02d}:{secs:02d}:{centiseconds:02d}"
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Multithreaded PNG compression script with maximum parallelism.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--8bit', '-8',
|
||||
action='store_true',
|
||||
dest='force_8bit',
|
||||
help='Force 8-bit color depth for all images'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--16bit', '-16',
|
||||
action='store_true',
|
||||
dest='force_16bit',
|
||||
help='Force 16-bit color depth for all images'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine bit depth setting
|
||||
if args.force_8bit and args.force_16bit:
|
||||
print("Error: Cannot specify both --8bit and --16bit. Choose one.")
|
||||
return
|
||||
elif args.force_8bit:
|
||||
force_bitdepth = '8'
|
||||
print("Mode: Forcing 8-bit color depth")
|
||||
elif args.force_16bit:
|
||||
force_bitdepth = '16'
|
||||
print("Mode: Forcing 16-bit color depth")
|
||||
else:
|
||||
force_bitdepth = None
|
||||
print("Mode: Auto-detect bit depth (preserve 16-bit if present)")
|
||||
|
||||
input_dir = Path('input')
|
||||
output_dir = Path('output')
|
||||
|
||||
@@ -181,7 +254,7 @@ def main():
|
||||
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
||||
# Submit all tasks
|
||||
future_to_file = {
|
||||
executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir))): f
|
||||
executor.submit(compress_png, str(f), str(get_output_path(f, input_dir, output_dir)), force_bitdepth): f
|
||||
for f in png_files
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user