arguments to force 16 or 8 bit color
This commit is contained in:
@@ -2508,3 +2508,507 @@ This format is used for elapsed time, ETA, and total time in the summary.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
_**User (2025-12-11 21:39Z)**_
|
||||||
|
|
||||||
|
let's add options to force 8bit or 16bit color: --16bit --8bit; shorthand -16, -8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_**Agent (model default, mode Agent)**_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Adding command-line options to force 8-bit or 16-bit color depth:
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="write" data-tool-name="search_replace">
|
||||||
|
|
||||||
|
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
**Chunk 1**
|
||||||
|
Lines added: 1, lines removed: undefined
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -7,4 +7,5 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
+ import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="write" data-tool-name="search_replace">
|
||||||
|
|
||||||
|
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
**Chunk 1**
|
||||||
|
Lines added: 8, lines removed: 2
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -14,6 +14,12 @@
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chunk 2**
|
||||||
|
Lines added: 30, lines removed: 26
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -34,31 +40,35 @@
|
||||||
|
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
|
||||||
|
- 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
|
||||||
|
+ # Determine target bit depth
|
||||||
|
+ if force_bitdepth == '8':
|
||||||
|
+ is_16bit = False
|
||||||
|
+ elif force_bitdepth == '16':
|
||||||
|
+ is_16bit = True
|
||||||
|
+ else:
|
||||||
|
+ # Auto-detect bit depth
|
||||||
|
+ is_16bit = False
|
||||||
|
+ original_mode = img.mode
|
||||||
|
+
|
||||||
|
+ if img.mode == 'I':
|
||||||
|
+ # '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
|
||||||
|
|
||||||
|
- # Preserve 16-bit depth if present, otherwise convert as needed
|
||||||
|
+ # Handle bit depth conversion based on target
|
||||||
|
if is_16bit:
|
||||||
|
# For 16-bit images, preserve the mode
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Updating the conversion logic for forced bit depths and adding argument parsing:
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="write" data-tool-name="search_replace">
|
||||||
|
|
||||||
|
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
**Chunk 1**
|
||||||
|
Lines added: 52, lines removed: 22
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -72,26 +72,56 @@
|
||||||
|
# Handle bit depth conversion based on target
|
||||||
|
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
|
||||||
|
- 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:
|
||||||
|
- 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 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:
|
||||||
|
+ # 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
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Updating the main function to parse arguments and pass the bitdepth option:
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="write" data-tool-name="search_replace">
|
||||||
|
|
||||||
|
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
**Chunk 1**
|
||||||
|
Lines added: 32, lines removed: undefined
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -181,4 +181,36 @@
|
||||||
|
|
||||||
|
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')
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||||
|
<details><summary>Tool use: **read_file** • Read file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="write" data-tool-name="search_replace">
|
||||||
|
|
||||||
|
<details><summary>Tool use: **search_replace** • Edit file: compress_pngs.py</summary>
|
||||||
|
|
||||||
|
**Chunk 1**
|
||||||
|
Lines added: 1, lines removed: 1
|
||||||
|
|
||||||
|
```diff
|
||||||
|
@@ -255,5 +255,5 @@
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<tool-use data-tool-type="generic" data-tool-name="read_lints">
|
||||||
|
<details>
|
||||||
|
<summary>Tool use: **read_lints** • Read lints for 1 file</summary>
|
||||||
|
|
||||||
|
Lint paths:
|
||||||
|
|
||||||
|
- `compress_pngs.py`
|
||||||
|
|
||||||
|
Lint results
|
||||||
|
|
||||||
|
|
||||||
|
**No lint errors found**
|
||||||
|
</details>
|
||||||
|
</tool-use>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Added command-line options to force 8-bit or 16-bit color depth. You can use:
|
||||||
|
|
||||||
|
- `--8bit` or `-8` to force 8-bit color depth
|
||||||
|
- `--16bit` or `-16` to force 16-bit color depth
|
||||||
|
|
||||||
|
If neither is specified, it auto-detects and preserves 16-bit when present.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `python compress_pngs.py` - Auto-detect (preserves 16-bit if present)
|
||||||
|
- `python compress_pngs.py --8bit` or `python compress_pngs.py -8` - Force all images to 8-bit
|
||||||
|
- `python compress_pngs.py --16bit` or `python compress_pngs.py -16` - Force all images to 16-bit
|
||||||
|
|
||||||
|
The script displays the selected mode at startup. The batch file will work with these options too.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_**User (2025-12-11 21:39Z)**_
|
||||||
|
|
||||||
|
Will this script work faster on a Threadripper 3970X than on an i9-13900kf?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_**Agent (model default, mode Agent)**_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Yes, it should run faster on the Threadripper 3970X, but not proportionally.
|
||||||
|
|
||||||
|
**Threadripper 3970X:**
|
||||||
|
- 32 physical cores, 64 threads
|
||||||
|
- The script uses `multiprocessing.cpu_count()`, which typically returns 64
|
||||||
|
- 64 worker processes can run in parallel
|
||||||
|
|
||||||
|
**i9-13900KF:**
|
||||||
|
- 8 P-cores + 8 E-cores = 16 physical cores, 24 threads
|
||||||
|
- The script would use 24 worker processes
|
||||||
|
|
||||||
|
**Expected performance:**
|
||||||
|
- Roughly 2–2.5x faster on the Threadripper, not 2.67x (64/24), because:
|
||||||
|
1. I/O bottlenecks: reading/writing files can limit scaling
|
||||||
|
2. Process overhead: more processes add overhead
|
||||||
|
3. PIL compression efficiency: may not scale linearly
|
||||||
|
4. Memory bandwidth: Threadripper’s higher bandwidth helps, but may not be the main factor
|
||||||
|
|
||||||
|
**To optimize further:**
|
||||||
|
- Use an NVMe SSD for input/output
|
||||||
|
- Ensure sufficient RAM (the script loads images into memory)
|
||||||
|
- Consider adjusting worker count if I/O becomes a bottleneck
|
||||||
|
|
||||||
|
The Threadripper’s higher core count should provide a clear speedup for this CPU-bound PNG compression workload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|||||||
165
compress_pngs.py
165
compress_pngs.py
@@ -6,14 +6,21 @@ Compresses all PNG files in subdirectories with maximum parallelism.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def compress_png(input_path, output_path):
|
def compress_png(input_path, output_path, force_bitdepth=None):
|
||||||
"""Compress a single PNG file."""
|
"""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:
|
try:
|
||||||
input_path = Path(input_path)
|
input_path = Path(input_path)
|
||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
@@ -32,56 +39,90 @@ def compress_png(input_path, output_path):
|
|||||||
original_size = os.path.getsize(input_path)
|
original_size = os.path.getsize(input_path)
|
||||||
img = Image.open(input_path)
|
img = Image.open(input_path)
|
||||||
|
|
||||||
# Check if image is 16-bit
|
# Determine target bit depth
|
||||||
# PIL represents 16-bit grayscale as 'I' mode (32-bit integer)
|
if force_bitdepth == '8':
|
||||||
# For color images, check pixel values to determine bit depth
|
is_16bit = False
|
||||||
is_16bit = False
|
elif force_bitdepth == '16':
|
||||||
original_mode = img.mode
|
|
||||||
|
|
||||||
if img.mode == 'I':
|
|
||||||
# 'I' mode typically represents 16-bit grayscale
|
|
||||||
is_16bit = True
|
is_16bit = True
|
||||||
elif img.mode in ('RGB', 'RGBA', 'LA'):
|
else:
|
||||||
# Check if max pixel value exceeds 8-bit range
|
# Auto-detect bit depth
|
||||||
try:
|
is_16bit = False
|
||||||
# Get a sample of pixels to check
|
original_mode = img.mode
|
||||||
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
|
|
||||||
if img.mode == 'I':
|
if img.mode == 'I':
|
||||||
# Keep 16-bit grayscale
|
# 'I' mode typically represents 16-bit grayscale
|
||||||
pass
|
is_16bit = True
|
||||||
elif img.mode in ('RGB', 'RGBA'):
|
elif img.mode in ('RGB', 'RGBA', 'LA'):
|
||||||
# Keep color mode - PIL may preserve 16-bit depending on how it was loaded
|
# Check if max pixel value exceeds 8-bit range
|
||||||
# Note: PIL's PNG save may convert 16-bit RGB to 8-bit, but we preserve the mode
|
try:
|
||||||
pass
|
# Get a sample of pixels to check
|
||||||
else:
|
pixels = list(img.getdata())
|
||||||
# Convert other modes while trying to preserve bit depth
|
if pixels:
|
||||||
if 'A' in img.mode:
|
# 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')
|
img = img.convert('RGBA')
|
||||||
else:
|
else:
|
||||||
|
# Convert to 16-bit RGB
|
||||||
img = img.convert('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:
|
else:
|
||||||
# 8-bit images: convert as needed
|
# Force or use 8-bit
|
||||||
if img.mode == 'RGBA':
|
if force_bitdepth == '8':
|
||||||
# Keep RGBA for transparency support
|
# Force 8-bit: ensure we're in 8-bit mode
|
||||||
img = img.convert('RGBA')
|
if img.mode == 'I':
|
||||||
elif img.mode != 'RGB':
|
# Convert 16-bit grayscale to 8-bit
|
||||||
img = img.convert('RGB')
|
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
|
# Save with maximum compression
|
||||||
# PIL will preserve 16-bit for 'I' mode automatically
|
# 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}"
|
return f"{hours:02d}:{minutes:02d}:{secs:02d}:{centiseconds:02d}"
|
||||||
|
|
||||||
def main():
|
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')
|
input_dir = Path('input')
|
||||||
output_dir = Path('output')
|
output_dir = Path('output')
|
||||||
|
|
||||||
@@ -181,7 +254,7 @@ def main():
|
|||||||
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
||||||
# Submit all tasks
|
# Submit all tasks
|
||||||
future_to_file = {
|
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
|
for f in png_files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user