impro
Browse files- __pycache__/custom_logger.cpython-313.pyc +0 -0
- custom_logger.py +9 -0
- image/convert.html +18 -0
- image/converter.py +37 -27
- image/input/temp.temp +0 -0
- image/input/test_quality.jpg +0 -0
- image/javascript/image.js +3 -1
- image/output/temp.temp +0 -0
- image/output/test_quality.jpeg +0 -0
- image/test_quality.jpg +0 -0
- main.py +4 -2
- test_convert_options.py +41 -0
__pycache__/custom_logger.cpython-313.pyc
ADDED
|
Binary file (913 Bytes). View file
|
|
|
custom_logger.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class LoggerConfig:
|
| 2 |
+
def info(self, msg):
|
| 3 |
+
print(f"INFO: {msg}")
|
| 4 |
+
def warning(self, msg):
|
| 5 |
+
print(f"WARNING: {msg}")
|
| 6 |
+
def error(self, msg):
|
| 7 |
+
print(f"ERROR: {msg}")
|
| 8 |
+
|
| 9 |
+
logger_config = LoggerConfig()
|
image/convert.html
CHANGED
|
@@ -472,6 +472,16 @@
|
|
| 472 |
<option value="gif">GIF</option>
|
| 473 |
</select>
|
| 474 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
</div>
|
| 476 |
</div>
|
| 477 |
|
|
@@ -505,6 +515,14 @@
|
|
| 505 |
function secret_sauce_url() {
|
| 506 |
return '/image/convert';
|
| 507 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
</script>
|
| 509 |
</body>
|
| 510 |
|
|
|
|
| 472 |
<option value="gif">GIF</option>
|
| 473 |
</select>
|
| 474 |
</div>
|
| 475 |
+
<div class="setting-group">
|
| 476 |
+
<label class="setting-label">Quality: <span id="quality-value">100</span>%</label>
|
| 477 |
+
<input type="range" class="setting-input" id="quality-slider" min="1" max="100" value="100"
|
| 478 |
+
style="padding: 0;">
|
| 479 |
+
</div>
|
| 480 |
+
<div class="setting-group">
|
| 481 |
+
<label class="setting-label">Resize: <span id="scale-value">100</span>%</label>
|
| 482 |
+
<input type="range" class="setting-input" id="scale-slider" min="10" max="100" value="100"
|
| 483 |
+
step="10" style="padding: 0;">
|
| 484 |
+
</div>
|
| 485 |
</div>
|
| 486 |
</div>
|
| 487 |
|
|
|
|
| 515 |
function secret_sauce_url() {
|
| 516 |
return '/image/convert';
|
| 517 |
}
|
| 518 |
+
|
| 519 |
+
// Update slider values
|
| 520 |
+
document.getElementById('quality-slider').addEventListener('input', function (e) {
|
| 521 |
+
document.getElementById('quality-value').textContent = e.target.value;
|
| 522 |
+
});
|
| 523 |
+
document.getElementById('scale-slider').addEventListener('input', function (e) {
|
| 524 |
+
document.getElementById('scale-value').textContent = e.target.value;
|
| 525 |
+
});
|
| 526 |
</script>
|
| 527 |
</body>
|
| 528 |
|
image/converter.py
CHANGED
|
@@ -50,12 +50,13 @@ class Converter(ImageBase):
|
|
| 50 |
|
| 51 |
return self.supported_formats[format_clean]
|
| 52 |
|
| 53 |
-
def _get_optimal_save_settings(self, format_name: str) -> Dict:
|
| 54 |
"""
|
| 55 |
Get optimal save settings for maximum quality preservation.
|
| 56 |
|
| 57 |
Args:
|
| 58 |
format_name: PIL format name (e.g., 'JPEG', 'PNG')
|
|
|
|
| 59 |
|
| 60 |
Returns:
|
| 61 |
Dictionary of save settings for the format
|
|
@@ -64,28 +65,28 @@ class Converter(ImageBase):
|
|
| 64 |
|
| 65 |
if format_name == 'JPEG':
|
| 66 |
settings = {
|
| 67 |
-
'quality':
|
| 68 |
-
'optimize':
|
| 69 |
-
'progressive':
|
| 70 |
-
'subsampling': 0
|
| 71 |
}
|
| 72 |
|
| 73 |
elif format_name == 'PNG':
|
| 74 |
settings = {
|
| 75 |
-
'optimize':
|
| 76 |
-
'compress_level':
|
| 77 |
}
|
| 78 |
|
| 79 |
elif format_name == 'WEBP':
|
| 80 |
settings = {
|
| 81 |
-
'lossless':
|
| 82 |
-
'quality':
|
| 83 |
-
'method': 6
|
| 84 |
}
|
| 85 |
|
| 86 |
elif format_name == 'TIFF':
|
| 87 |
settings = {
|
| 88 |
-
'compression':
|
| 89 |
}
|
| 90 |
|
| 91 |
elif format_name == 'BMP':
|
|
@@ -93,12 +94,12 @@ class Converter(ImageBase):
|
|
| 93 |
|
| 94 |
elif format_name == 'GIF':
|
| 95 |
settings = {
|
| 96 |
-
'optimize':
|
| 97 |
}
|
| 98 |
|
| 99 |
elif format_name == 'HEIC':
|
| 100 |
settings = {
|
| 101 |
-
'quality':
|
| 102 |
}
|
| 103 |
|
| 104 |
return settings
|
|
@@ -215,13 +216,14 @@ class Converter(ImageBase):
|
|
| 215 |
file_size = os.path.getsize(output_path)
|
| 216 |
logger_config.info(f"✓ SVG file created: {file_size:,} bytes")
|
| 217 |
|
| 218 |
-
def _verify_conversion_quality(self, original_size: Tuple[int, int], output_path: str) -> bool:
|
| 219 |
"""
|
| 220 |
Verify that the converted image maintains original quality and dimensions.
|
| 221 |
|
| 222 |
Args:
|
| 223 |
original_size: Original image dimensions (width, height)
|
| 224 |
output_path: Path to converted image
|
|
|
|
| 225 |
|
| 226 |
Returns:
|
| 227 |
True if verification passes
|
|
@@ -236,22 +238,28 @@ class Converter(ImageBase):
|
|
| 236 |
file_size = os.path.getsize(output_path)
|
| 237 |
logger_config.info(f"✓ Output file created: {file_size:,} bytes")
|
| 238 |
|
|
|
|
|
|
|
|
|
|
| 239 |
# Verify dimensions are preserved
|
| 240 |
with Image.open(output_path) as verify_img:
|
| 241 |
-
|
| 242 |
-
|
|
|
|
| 243 |
|
| 244 |
-
logger_config.info(f"✓
|
| 245 |
|
| 246 |
return True
|
| 247 |
|
| 248 |
-
def convert_image(self, input_file_name: str, output_format: str) -> str:
|
| 249 |
"""
|
| 250 |
Convert an image to the specified format with maximum quality and ratio preservation.
|
| 251 |
|
| 252 |
Args:
|
| 253 |
input_file: Path to the input image file
|
| 254 |
output_format: Target image format
|
|
|
|
|
|
|
| 255 |
|
| 256 |
Returns:
|
| 257 |
Path to the converted output file
|
|
@@ -268,7 +276,7 @@ class Converter(ImageBase):
|
|
| 268 |
self._validate_input_file()
|
| 269 |
target_format = self._validate_output_format(output_format)
|
| 270 |
|
| 271 |
-
logger_config.info(f"Starting conversion: {self.input_file_path}")
|
| 272 |
|
| 273 |
# Open and analyze original image
|
| 274 |
with Image.open(self.input_file_path) as original_image:
|
|
@@ -281,9 +289,11 @@ class Converter(ImageBase):
|
|
| 281 |
# Convert color mode if necessary
|
| 282 |
converted_image = self._convert_color_mode(original_image, target_format)
|
| 283 |
|
| 284 |
-
#
|
| 285 |
-
if
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
|
| 288 |
# Generate output path
|
| 289 |
output_path = self._generate_output_path(output_format)
|
|
@@ -291,22 +301,22 @@ class Converter(ImageBase):
|
|
| 291 |
# Handle SVG conversion specially
|
| 292 |
if target_format == 'SVG':
|
| 293 |
logger_config.info(f"Converting to SVG (embedding as base64)")
|
| 294 |
-
self._convert_to_svg(converted_image, output_path,
|
| 295 |
else:
|
| 296 |
# Get optimal save settings
|
| 297 |
-
save_settings = self._get_optimal_save_settings(target_format)
|
| 298 |
|
| 299 |
# Preserve metadata
|
| 300 |
metadata = self._preserve_metadata(original_image, target_format)
|
| 301 |
save_settings.update(metadata)
|
| 302 |
|
| 303 |
-
logger_config.info(f"Converting to {target_format} with
|
| 304 |
|
| 305 |
# Perform conversion with quality preservation
|
| 306 |
converted_image.save(output_path, format=target_format, **save_settings)
|
| 307 |
|
| 308 |
# Verify conversion quality
|
| 309 |
-
self._verify_conversion_quality(original_size, output_path)
|
| 310 |
|
| 311 |
logger_config.info(f"✓ Conversion completed successfully: {output_path}")
|
| 312 |
return output_path
|
|
@@ -318,4 +328,4 @@ class Converter(ImageBase):
|
|
| 318 |
# Example usage
|
| 319 |
if __name__ == "__main__":
|
| 320 |
converter = Converter()
|
| 321 |
-
converter.convert_image("image/test.HEIC", "png")
|
|
|
|
| 50 |
|
| 51 |
return self.supported_formats[format_clean]
|
| 52 |
|
| 53 |
+
def _get_optimal_save_settings(self, format_name: str, quality: int = 100) -> Dict:
|
| 54 |
"""
|
| 55 |
Get optimal save settings for maximum quality preservation.
|
| 56 |
|
| 57 |
Args:
|
| 58 |
format_name: PIL format name (e.g., 'JPEG', 'PNG')
|
| 59 |
+
quality: Quality setting (1-100)
|
| 60 |
|
| 61 |
Returns:
|
| 62 |
Dictionary of save settings for the format
|
|
|
|
| 65 |
|
| 66 |
if format_name == 'JPEG':
|
| 67 |
settings = {
|
| 68 |
+
'quality': quality,
|
| 69 |
+
'optimize': True,
|
| 70 |
+
'progressive': True,
|
| 71 |
+
'subsampling': 0
|
| 72 |
}
|
| 73 |
|
| 74 |
elif format_name == 'PNG':
|
| 75 |
settings = {
|
| 76 |
+
'optimize': True,
|
| 77 |
+
'compress_level': 9 if quality < 100 else 1
|
| 78 |
}
|
| 79 |
|
| 80 |
elif format_name == 'WEBP':
|
| 81 |
settings = {
|
| 82 |
+
'lossless': quality == 100,
|
| 83 |
+
'quality': quality,
|
| 84 |
+
'method': 6
|
| 85 |
}
|
| 86 |
|
| 87 |
elif format_name == 'TIFF':
|
| 88 |
settings = {
|
| 89 |
+
'compression': 'tiff_lzw' if quality < 100 else None
|
| 90 |
}
|
| 91 |
|
| 92 |
elif format_name == 'BMP':
|
|
|
|
| 94 |
|
| 95 |
elif format_name == 'GIF':
|
| 96 |
settings = {
|
| 97 |
+
'optimize': True
|
| 98 |
}
|
| 99 |
|
| 100 |
elif format_name == 'HEIC':
|
| 101 |
settings = {
|
| 102 |
+
'quality': quality
|
| 103 |
}
|
| 104 |
|
| 105 |
return settings
|
|
|
|
| 216 |
file_size = os.path.getsize(output_path)
|
| 217 |
logger_config.info(f"✓ SVG file created: {file_size:,} bytes")
|
| 218 |
|
| 219 |
+
def _verify_conversion_quality(self, original_size: Tuple[int, int], output_path: str, scale: float = 1.0) -> bool:
|
| 220 |
"""
|
| 221 |
Verify that the converted image maintains original quality and dimensions.
|
| 222 |
|
| 223 |
Args:
|
| 224 |
original_size: Original image dimensions (width, height)
|
| 225 |
output_path: Path to converted image
|
| 226 |
+
scale: Scale factor used
|
| 227 |
|
| 228 |
Returns:
|
| 229 |
True if verification passes
|
|
|
|
| 238 |
file_size = os.path.getsize(output_path)
|
| 239 |
logger_config.info(f"✓ Output file created: {file_size:,} bytes")
|
| 240 |
|
| 241 |
+
# Calculate expected size
|
| 242 |
+
expected_size = (int(original_size[0] * scale), int(original_size[1] * scale))
|
| 243 |
+
|
| 244 |
# Verify dimensions are preserved
|
| 245 |
with Image.open(output_path) as verify_img:
|
| 246 |
+
# Allow for slight rounding differences (±1 pixel)
|
| 247 |
+
if abs(verify_img.size[0] - expected_size[0]) > 1 or abs(verify_img.size[1] - expected_size[1]) > 1:
|
| 248 |
+
raise Exception(f"Dimensions changed! Expected: {expected_size}, Got: {verify_img.size}")
|
| 249 |
|
| 250 |
+
logger_config.info(f"✓ Dimensions verified: {verify_img.size}")
|
| 251 |
|
| 252 |
return True
|
| 253 |
|
| 254 |
+
def convert_image(self, input_file_name: str, output_format: str, quality: int = 100, scale: float = 1.0) -> str:
|
| 255 |
"""
|
| 256 |
Convert an image to the specified format with maximum quality and ratio preservation.
|
| 257 |
|
| 258 |
Args:
|
| 259 |
input_file: Path to the input image file
|
| 260 |
output_format: Target image format
|
| 261 |
+
quality: Quality setting (1-100)
|
| 262 |
+
scale: Scale factor (0.1-1.0)
|
| 263 |
|
| 264 |
Returns:
|
| 265 |
Path to the converted output file
|
|
|
|
| 276 |
self._validate_input_file()
|
| 277 |
target_format = self._validate_output_format(output_format)
|
| 278 |
|
| 279 |
+
logger_config.info(f"Starting conversion: {self.input_file_path} (Quality: {quality}, Scale: {scale})")
|
| 280 |
|
| 281 |
# Open and analyze original image
|
| 282 |
with Image.open(self.input_file_path) as original_image:
|
|
|
|
| 289 |
# Convert color mode if necessary
|
| 290 |
converted_image = self._convert_color_mode(original_image, target_format)
|
| 291 |
|
| 292 |
+
# Apply scaling if needed
|
| 293 |
+
if scale < 1.0:
|
| 294 |
+
new_size = (int(original_size[0] * scale), int(original_size[1] * scale))
|
| 295 |
+
logger_config.info(f"Resizing image to {new_size}")
|
| 296 |
+
converted_image = converted_image.resize(new_size, Image.Resampling.LANCZOS)
|
| 297 |
|
| 298 |
# Generate output path
|
| 299 |
output_path = self._generate_output_path(output_format)
|
|
|
|
| 301 |
# Handle SVG conversion specially
|
| 302 |
if target_format == 'SVG':
|
| 303 |
logger_config.info(f"Converting to SVG (embedding as base64)")
|
| 304 |
+
self._convert_to_svg(converted_image, output_path, converted_image.size)
|
| 305 |
else:
|
| 306 |
# Get optimal save settings
|
| 307 |
+
save_settings = self._get_optimal_save_settings(target_format, quality)
|
| 308 |
|
| 309 |
# Preserve metadata
|
| 310 |
metadata = self._preserve_metadata(original_image, target_format)
|
| 311 |
save_settings.update(metadata)
|
| 312 |
|
| 313 |
+
logger_config.info(f"Converting to {target_format} with quality {quality}")
|
| 314 |
|
| 315 |
# Perform conversion with quality preservation
|
| 316 |
converted_image.save(output_path, format=target_format, **save_settings)
|
| 317 |
|
| 318 |
# Verify conversion quality
|
| 319 |
+
self._verify_conversion_quality(original_size, output_path, scale)
|
| 320 |
|
| 321 |
logger_config.info(f"✓ Conversion completed successfully: {output_path}")
|
| 322 |
return output_path
|
|
|
|
| 328 |
# Example usage
|
| 329 |
if __name__ == "__main__":
|
| 330 |
converter = Converter()
|
| 331 |
+
converter.convert_image("image/test.HEIC", "png", quality=80, scale=0.5)
|
image/input/temp.temp
DELETED
|
File without changes
|
image/input/test_quality.jpg
ADDED
|
image/javascript/image.js
CHANGED
|
@@ -414,7 +414,9 @@ async function upload(file) {
|
|
| 414 |
|
| 415 |
function getSettingsData() {
|
| 416 |
return {
|
| 417 |
-
to_format: document.getElementById('output-format')?.value
|
|
|
|
|
|
|
| 418 |
};
|
| 419 |
}
|
| 420 |
|
|
|
|
| 414 |
|
| 415 |
function getSettingsData() {
|
| 416 |
return {
|
| 417 |
+
to_format: document.getElementById('output-format')?.value,
|
| 418 |
+
quality: document.getElementById('quality-slider')?.value,
|
| 419 |
+
scale: document.getElementById('scale-slider')?.value ? document.getElementById('scale-slider').value / 100 : null
|
| 420 |
};
|
| 421 |
}
|
| 422 |
|
image/output/temp.temp
DELETED
|
File without changes
|
image/output/test_quality.jpeg
ADDED
|
image/test_quality.jpg
ADDED
|
main.py
CHANGED
|
@@ -192,7 +192,9 @@ async def upload_image(
|
|
| 192 |
@app.post("/image/convert")
|
| 193 |
async def convert_image(
|
| 194 |
id: str = Form(...),
|
| 195 |
-
to_format: str = Form(...)
|
|
|
|
|
|
|
| 196 |
):
|
| 197 |
try:
|
| 198 |
# Check if input is SVG - PIL cannot read SVG files
|
|
@@ -200,7 +202,7 @@ async def convert_image(
|
|
| 200 |
raise ValueError("SVG files cannot be converted. SVG is only available as an output format.")
|
| 201 |
|
| 202 |
converter = Converter()
|
| 203 |
-
output_path = converter.convert_image(id, to_format)
|
| 204 |
|
| 205 |
# Check if conversion failed
|
| 206 |
if output_path is None:
|
|
|
|
| 192 |
@app.post("/image/convert")
|
| 193 |
async def convert_image(
|
| 194 |
id: str = Form(...),
|
| 195 |
+
to_format: str = Form(...),
|
| 196 |
+
quality: int = Form(100),
|
| 197 |
+
scale: float = Form(1.0)
|
| 198 |
):
|
| 199 |
try:
|
| 200 |
# Check if input is SVG - PIL cannot read SVG files
|
|
|
|
| 202 |
raise ValueError("SVG files cannot be converted. SVG is only available as an output format.")
|
| 203 |
|
| 204 |
converter = Converter()
|
| 205 |
+
output_path = converter.convert_image(id, to_format, quality=quality, scale=scale)
|
| 206 |
|
| 207 |
# Check if conversion failed
|
| 208 |
if output_path is None:
|
test_convert_options.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
url = "http://localhost:8000/image/convert"
|
| 5 |
+
input_dir = "image/input"
|
| 6 |
+
file_path = os.path.join(input_dir, "test_quality.jpg")
|
| 7 |
+
output_dir = "image/output"
|
| 8 |
+
|
| 9 |
+
# Ensure directories exist
|
| 10 |
+
os.makedirs(input_dir, exist_ok=True)
|
| 11 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
# Create test image
|
| 14 |
+
from PIL import Image
|
| 15 |
+
Image.new('RGB', (100, 100), color='red').save(file_path)
|
| 16 |
+
|
| 17 |
+
# Test case 1: Quality 50, Scale 0.5
|
| 18 |
+
data = {
|
| 19 |
+
"id": "test_quality.jpg",
|
| 20 |
+
"to_format": "jpg",
|
| 21 |
+
"quality": 50,
|
| 22 |
+
"scale": 0.5
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
response = requests.post(url, data=data)
|
| 27 |
+
if response.status_code == 200:
|
| 28 |
+
result = response.json()
|
| 29 |
+
print(f"Success: {result}")
|
| 30 |
+
|
| 31 |
+
# Verify output file
|
| 32 |
+
output_file = os.path.join(output_dir, result['new_filename'])
|
| 33 |
+
if os.path.exists(output_file):
|
| 34 |
+
print(f"Output file created: {output_file}")
|
| 35 |
+
print(f"Output file size: {os.path.getsize(output_file)} bytes")
|
| 36 |
+
else:
|
| 37 |
+
print("Error: Output file not found")
|
| 38 |
+
else:
|
| 39 |
+
print(f"Failed: {response.status_code} - {response.text}")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"Error: {e}")
|