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:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user