Fix CLI encode format detection and jpegtran -trim bug

CLI encode:
- Auto-detect output format from extension (.jpg → DCT mode, .png → LSB)
- Default to JPEG output for JPEG carriers (preserves DCT benefits)
- Pass embed_mode and dct_output_format to encode function

jpegtran fix (critical for rotation fallback):
- Remove -trim flag which was dropping edge blocks and destroying stego data
- Remove -perfect flag which fails on non-MCU-aligned images
- Plain jpegtran without flags works correctly for lossless rotation

This enables: encode → external rotation → decode to work correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-11 17:06:53 -05:00
parent 38bef32750
commit afc8c93923
3 changed files with 25 additions and 17 deletions

View File

@@ -2136,7 +2136,7 @@ def api_tools_rotate():
output_path = tempfile.mktemp(suffix=".jpg") output_path = tempfile.mktemp(suffix=".jpg")
try: try:
result = subprocess.run( result = subprocess.run(
["jpegtran", "-rotate", str(rotation), "-copy", "all", "-trim", ["jpegtran", "-rotate", str(rotation), "-copy", "all",
"-outfile", output_path, input_path], "-outfile", output_path, input_path],
capture_output=True, timeout=30 capture_output=True, timeout=30
) )
@@ -2158,7 +2158,7 @@ def api_tools_rotate():
output_path = tempfile.mktemp(suffix=".jpg") output_path = tempfile.mktemp(suffix=".jpg")
try: try:
result = subprocess.run( result = subprocess.run(
["jpegtran", "-flip", "horizontal", "-copy", "all", "-trim", ["jpegtran", "-flip", "horizontal", "-copy", "all",
"-outfile", output_path, input_path], "-outfile", output_path, input_path],
capture_output=True, timeout=30 capture_output=True, timeout=30
) )
@@ -2180,7 +2180,7 @@ def api_tools_rotate():
output_path = tempfile.mktemp(suffix=".jpg") output_path = tempfile.mktemp(suffix=".jpg")
try: try:
result = subprocess.run( result = subprocess.run(
["jpegtran", "-flip", "vertical", "-copy", "all", "-trim", ["jpegtran", "-flip", "vertical", "-copy", "all",
"-outfile", output_path, input_path], "-outfile", output_path, input_path],
capture_output=True, timeout=30 capture_output=True, timeout=30
) )

View File

@@ -241,8 +241,20 @@ def encode(
with open(carrier, "rb") as f: with open(carrier, "rb") as f:
carrier_data = f.read() carrier_data = f.read()
# Determine output path # Determine output path and format
output = output or f"{Path(carrier).stem}_encoded.png" # Default to JPEG for JPEG carriers (preserves DCT mode benefits)
carrier_ext = Path(carrier).suffix.lower()
if not output:
if carrier_ext in ('.jpg', '.jpeg'):
output = f"{Path(carrier).stem}_encoded.jpg"
else:
output = f"{Path(carrier).stem}_encoded.png"
# Detect output format from extension
output_ext = Path(output).suffix.lower()
use_dct = output_ext in ('.jpg', '.jpeg')
from .steganography import EMBED_MODE_DCT, EMBED_MODE_LSB
try: try:
if file_payload: if file_payload:
@@ -253,6 +265,8 @@ def encode(
carrier_image=carrier_data, carrier_image=carrier_data,
passphrase=passphrase, passphrase=passphrase,
pin=pin, pin=pin,
embed_mode=EMBED_MODE_DCT if use_dct else EMBED_MODE_LSB,
dct_output_format="jpeg" if use_dct else "png",
) )
else: else:
# Encode message # Encode message
@@ -262,6 +276,8 @@ def encode(
carrier_image=carrier_data, carrier_image=carrier_data,
passphrase=passphrase, passphrase=passphrase,
pin=pin, pin=pin,
embed_mode=EMBED_MODE_DCT if use_dct else EMBED_MODE_LSB,
dct_output_format="jpeg" if use_dct else "png",
) )
# Write output # Write output

View File

@@ -1252,20 +1252,12 @@ def _jpegtran_rotate(image_data: bytes, rotation: int) -> bytes:
output_path = tempfile.mktemp(suffix=".jpg") output_path = tempfile.mktemp(suffix=".jpg")
try: try:
# jpegtran -rotate 90|180|270 -copy all -perfect # jpegtran -rotate 90|180|270 -copy all
# -copy all: preserve all metadata # -copy all: preserve all metadata
# -perfect: fail if there are non-transformable edge blocks (rare) # NOTE: Don't use -trim as it drops edge blocks and destroys stego data
# NOTE: Don't use -perfect as it fails on images with non-MCU-aligned edges
result = subprocess.run( result = subprocess.run(
["jpegtran", "-rotate", str(rotation), "-copy", "all", "-perfect", ["jpegtran", "-rotate", str(rotation), "-copy", "all",
"-outfile", output_path, input_path],
capture_output=True,
timeout=30
)
# If -perfect fails (edge blocks), retry without it
if result.returncode != 0:
result = subprocess.run(
["jpegtran", "-rotate", str(rotation), "-copy", "all", "-trim",
"-outfile", output_path, input_path], "-outfile", output_path, input_path],
capture_output=True, capture_output=True,
timeout=30 timeout=30