jebin2 commited on
Commit
609d54c
·
1 Parent(s): b8d83b8

image converter added

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ venv
2
+ .env
3
+ image/__pycache__
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python image
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y \
6
+ git libheif-dev pkg-config \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Create user (Hugging Face Spaces requirement)
10
+ RUN useradd -m -u 1000 user
11
+
12
+ # Switch to user early
13
+ USER user
14
+
15
+ # Set working directory
16
+ WORKDIR /home/user/app
17
+
18
+ # Set environment variables for Hugging Face Spaces
19
+ ENV HOME=/home/user \
20
+ PATH=/home/user/.local/bin:$PATH \
21
+ BASE_PATH=/home/user/app \
22
+ IS_DOCKER=True \
23
+ SERVER_PORT=7860
24
+
25
+ COPY --chown=user requirements.txt .
26
+ RUN pip install --no-cache-dir --user -r requirements.txt
27
+
28
+ # Copy app code
29
+ COPY --chown=user . .
30
+
31
+ # Entry point
32
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
image/converter.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ High-Quality Image Converter Module
3
+
4
+ This module provides a class-based image converter that preserves original quality
5
+ and aspect ratio during format conversion. Supports all major image formats.
6
+ """
7
+
8
+ from .image_base import ImageBase
9
+ from PIL import Image
10
+ import os
11
+ from typing import Dict, Optional, Tuple
12
+ from custom_logger import logger_config
13
+ import pillow_heif
14
+ pillow_heif.register_heif_opener()
15
+
16
+
17
+ class Converter(ImageBase):
18
+ """
19
+ High-quality image format converter that preserves original quality and aspect ratio.
20
+
21
+ This class handles conversion between various image formats while maintaining
22
+ the highest possible quality and exact dimensional preservation.
23
+ """
24
+ def __init__(self):
25
+ super().__init__("converter")
26
+
27
+ def _validate_output_format(self, output_format: str) -> str:
28
+ """
29
+ Validate and normalize the output format.
30
+
31
+ Args:
32
+ output_format: Desired output format
33
+
34
+ Returns:
35
+ Normalized PIL format name
36
+
37
+ Raises:
38
+ ValueError: If format is not supported
39
+ """
40
+ if not output_format or not output_format.strip():
41
+ raise ValueError("Output format cannot be empty")
42
+
43
+ format_clean = output_format.lower().strip()
44
+
45
+ if format_clean not in self.supported_formats:
46
+ supported_list = ', '.join(self.supported_formats.keys())
47
+ raise ValueError(f"Unsupported format '{output_format}'. Supported: {supported_list}")
48
+
49
+ return self.supported_formats[format_clean]
50
+
51
+ def _get_optimal_save_settings(self, format_name: str) -> Dict:
52
+ """
53
+ Get optimal save settings for maximum quality preservation.
54
+
55
+ Args:
56
+ format_name: PIL format name (e.g., 'JPEG', 'PNG')
57
+
58
+ Returns:
59
+ Dictionary of save settings for the format
60
+ """
61
+ settings = {}
62
+
63
+ if format_name == 'JPEG':
64
+ settings = {
65
+ 'quality': 100, # Maximum quality
66
+ 'optimize': False, # Don't optimize (can reduce quality)
67
+ 'progressive': False, # Standard baseline JPEG
68
+ 'subsampling': 0 # No chroma subsampling (4:4:4)
69
+ }
70
+
71
+ elif format_name == 'PNG':
72
+ settings = {
73
+ 'optimize': False, # Don't optimize to preserve exact quality
74
+ 'compress_level': 1 # Minimal compression for maximum quality
75
+ }
76
+
77
+ elif format_name == 'WEBP':
78
+ settings = {
79
+ 'lossless': True, # Lossless compression
80
+ 'quality': 100, # Maximum quality (for fallback)
81
+ 'method': 6 # Best compression method
82
+ }
83
+
84
+ elif format_name == 'TIFF':
85
+ settings = {
86
+ 'compression': None # No compression for perfect quality
87
+ }
88
+
89
+ elif format_name == 'BMP':
90
+ settings = {} # BMP is always lossless
91
+
92
+ elif format_name == 'GIF':
93
+ settings = {
94
+ 'optimize': False # Don't optimize palette
95
+ }
96
+
97
+ return settings
98
+
99
+ def _preserve_metadata(self, original_image: Image.Image, format_name: str) -> Dict:
100
+ """
101
+ Extract metadata from original image that's compatible with target format.
102
+
103
+ Args:
104
+ original_image: Original PIL Image object
105
+ format_name: Target PIL format name
106
+
107
+ Returns:
108
+ Dictionary of metadata to preserve
109
+ """
110
+ metadata = {}
111
+
112
+ if not hasattr(original_image, 'info') or not original_image.info:
113
+ return metadata
114
+
115
+ # DPI preservation
116
+ if 'dpi' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']:
117
+ metadata['dpi'] = original_image.info['dpi']
118
+
119
+ # Color profile preservation
120
+ if 'icc_profile' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']:
121
+ metadata['icc_profile'] = original_image.info['icc_profile']
122
+
123
+ # EXIF data preservation
124
+ if 'exif' in original_image.info and format_name in ['JPEG', 'TIFF', 'WEBP']:
125
+ metadata['exif'] = original_image.info['exif']
126
+
127
+ return metadata
128
+
129
+ def _convert_color_mode(self, image: Image.Image, target_format: str) -> Image.Image:
130
+ """
131
+ Convert image color mode if required by target format, preserving quality.
132
+
133
+ Args:
134
+ image: PIL Image object
135
+ target_format: Target PIL format name
136
+
137
+ Returns:
138
+ Image with appropriate color mode
139
+ """
140
+ # Create exact copy to preserve original
141
+ converted_image = image.copy()
142
+
143
+ # Handle formats that don't support transparency
144
+ if target_format == 'JPEG' and converted_image.mode in ('RGBA', 'LA', 'P'):
145
+ logger_config.info("Converting to RGB (JPEG doesn't support transparency)")
146
+
147
+ if converted_image.mode == 'P':
148
+ # Preserve palette colors during conversion
149
+ converted_image = converted_image.convert('RGBA')
150
+
151
+ # Create white background and blend with perfect quality
152
+ background = Image.new("RGB", converted_image.size, (255, 255, 255))
153
+ if converted_image.mode in ('RGBA', 'LA'):
154
+ # Use alpha_composite for perfect quality preservation
155
+ bg_rgba = background.convert('RGBA')
156
+ composite = Image.alpha_composite(bg_rgba, converted_image.convert('RGBA'))
157
+ converted_image = composite.convert('RGB')
158
+
159
+ elif target_format == 'BMP' and converted_image.mode in ('RGBA', 'LA'):
160
+ logger_config.info("Converting to RGB (BMP doesn't support transparency)")
161
+ background = Image.new("RGB", converted_image.size, (255, 255, 255))
162
+ bg_rgba = background.convert('RGBA')
163
+ composite = Image.alpha_composite(bg_rgba, converted_image.convert('RGBA'))
164
+ converted_image = composite.convert('RGB')
165
+
166
+ return converted_image
167
+
168
+ def _verify_conversion_quality(self, original_size: Tuple[int, int], output_path: str) -> bool:
169
+ """
170
+ Verify that the converted image maintains original quality and dimensions.
171
+
172
+ Args:
173
+ original_size: Original image dimensions (width, height)
174
+ output_path: Path to converted image
175
+
176
+ Returns:
177
+ True if verification passes
178
+
179
+ Raises:
180
+ Exception: If verification fails
181
+ """
182
+ if not os.path.exists(output_path):
183
+ raise Exception("Output file was not created")
184
+
185
+ # Check file size
186
+ file_size = os.path.getsize(output_path)
187
+ logger_config.info(f"✓ Output file created: {file_size:,} bytes")
188
+
189
+ # Verify dimensions are preserved
190
+ with Image.open(output_path) as verify_img:
191
+ if verify_img.size != original_size:
192
+ raise Exception(f"Dimensions changed! Expected: {original_size}, Got: {verify_img.size}")
193
+
194
+ logger_config.info(f"✓ Aspect ratio preserved: {verify_img.size}")
195
+
196
+ return True
197
+
198
+ def convert_image(self, input_file_name: str, output_format: str) -> str:
199
+ """
200
+ Convert an image to the specified format with maximum quality and ratio preservation.
201
+
202
+ Args:
203
+ input_file: Path to the input image file
204
+ output_format: Target image format
205
+
206
+ Returns:
207
+ Path to the converted output file
208
+
209
+ Raises:
210
+ FileNotFoundError: If input file doesn't exist
211
+ ValueError: If parameters are invalid
212
+ Exception: If conversion fails
213
+ """
214
+ try:
215
+ # Validate inputs
216
+ self.input_file_name = input_file_name
217
+ self.input_file_path = f'{self.input_dir}/{self.input_file_name}'
218
+ self._validate_input_file()
219
+ target_format = self._validate_output_format(output_format)
220
+
221
+ logger_config.info(f"Starting conversion: {self.input_file_path}")
222
+
223
+ # Open and analyze original image
224
+ with Image.open(self.input_file_path) as original_image:
225
+ original_size = original_image.size
226
+ original_mode = original_image.mode
227
+ original_format = original_image.format
228
+
229
+ logger_config.info(f"Original - Format: {original_format}, Mode: {original_mode}, Size: {original_size}")
230
+
231
+ # Convert color mode if necessary
232
+ converted_image = self._convert_color_mode(original_image, target_format)
233
+
234
+ # Verify size preservation (safety check)
235
+ if converted_image.size != original_size:
236
+ raise Exception(f"Size changed during conversion! Original: {original_size}, Current: {converted_image.size}")
237
+
238
+ # Generate output path
239
+ output_path = self._generate_output_path(output_format)
240
+
241
+ # Get optimal save settings
242
+ save_settings = self._get_optimal_save_settings(target_format)
243
+
244
+ # Preserve metadata
245
+ metadata = self._preserve_metadata(original_image, target_format)
246
+ save_settings.update(metadata)
247
+
248
+ logger_config.info(f"Converting to {target_format} with maximum quality")
249
+
250
+ # Perform conversion with quality preservation
251
+ converted_image.save(output_path, format=target_format, **save_settings)
252
+
253
+ # Verify conversion quality
254
+ self._verify_conversion_quality(original_size, output_path)
255
+
256
+ logger_config.info(f"✓ Conversion completed successfully: {output_path}")
257
+ return output_path
258
+
259
+ except Exception as e:
260
+ logger_config.info(f"Image conversion failed: {str(e)}")
261
+ return None
262
+
263
+ # Example usage
264
+ if __name__ == "__main__":
265
+ converter = Converter()
266
+ converter.convert_image("image/test.HEIC", "png")
image/image_base.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from datetime import datetime, timedelta
4
+ from process_exception import ProcessAlreadyRunning
5
+ from main_base import MainBase
6
+ from pathlib import Path
7
+
8
+ class ImageBase(MainBase):
9
+ def __init__(self, feature=None, process_id="process_id"):
10
+ super().__init__()
11
+ self.current_path = os.path.abspath(os.path.join(os.path.dirname(__file__)))
12
+ self.feature = feature
13
+ self.process_id = process_id
14
+ self.input_dir = os.path.join(self.current_path, "input")
15
+ self.output_dir = os.path.join(self.current_path, "output")
16
+ self.process = os.path.join(self.current_path, "process.json")
17
+ self.supported_formats = {
18
+ 'jpg': 'JPEG',
19
+ 'jpeg': 'JPEG',
20
+ 'png': 'PNG',
21
+ 'webp': 'WEBP',
22
+ 'tiff': 'TIFF',
23
+ 'tif': 'TIFF',
24
+ 'bmp': 'BMP',
25
+ 'gif': 'GIF',
26
+ 'heic': 'HEIC'
27
+ }
28
+
29
+ # Create directories if they don't exist
30
+ os.makedirs(self.input_dir, exist_ok=True)
31
+ os.makedirs(self.output_dir, exist_ok=True)
32
+
33
+ def _validate_input_file(self, image=None):
34
+ """
35
+ Validate that the input file exists and is a supported image format.
36
+ Args:
37
+ image: UploadFile object (for upload case)
38
+ Raises:
39
+ FileNotFoundError: If input file doesn't exist
40
+ ValueError: If file format is not supported
41
+ """
42
+ if image:
43
+ # Handle upload case
44
+ if not image.filename:
45
+ raise ValueError("Uploaded file must have a filename")
46
+
47
+ extension = Path(image.filename).suffix.lower().lstrip('.')
48
+ else:
49
+ # Handle file path case
50
+ if not self.input_file_path or not self.input_file_path.strip():
51
+ raise ValueError("Image path cannot be empty")
52
+
53
+ path_obj = Path(self.input_file_path)
54
+ if not path_obj.exists():
55
+ raise FileNotFoundError(f"Image file not found: {self.input_file_path}")
56
+
57
+ if not path_obj.is_file():
58
+ raise ValueError(f"Path is not a file: {self.input_file_path}")
59
+
60
+ extension = path_obj.suffix.lower().lstrip('.')
61
+
62
+ # Check if extension is supported
63
+ if extension not in self.supported_formats:
64
+ supported_list = ', '.join(self.supported_formats.keys())
65
+ raise ValueError(f"Unsupported format '{extension}'. Supported: {supported_list}")
66
+
67
+ def upload_validate(self, image):
68
+ super().upload_validate(image)
69
+ self._validate_input_file(image=image)
70
+
71
+
72
+ # def _is_already_running(self):
73
+ # try:
74
+ # if os.path.exists(self.process):
75
+ # with open(self.process, 'r') as file:
76
+ # json_data = json.load(file)
77
+
78
+ # if "start_time" in json_data and "end_time" not in json_data:
79
+ # start_time = datetime.fromisoformat(json_data["start_time"])
80
+ # time_diff = datetime.now() - start_time
81
+
82
+ # if time_diff > timedelta(minutes=5000):
83
+ # raise ProcessAlreadyRunning(
84
+ # message=(
85
+ # f"A process named '{json_data['feature']}' is already running. "
86
+ # f"Started At: {json_data['start_time']} "
87
+ # f"(Running for {time_diff.total_seconds() // 60:.1f} minutes)"
88
+ # ),
89
+ # data=json_data
90
+ # )
91
+ # except: pass
92
+ # return True
93
+
94
+
95
+ # def _reset(self):
96
+ # if os.path.exists(self.process):
97
+ # os.remove(self.process)
98
+
99
+ # if os.path.exists(self.output_dir):
100
+ # for filename in os.listdir(self.output_dir):
101
+ # file_path = os.path.join(self.output_dir, filename)
102
+ # if os.path.isfile(file_path):
103
+ # os.remove(file_path)
104
+
105
+ # def _create_process(self):
106
+ # process_data = {
107
+ # "start_time": datetime.now().isoformat(),
108
+ # "feature": self.feature,
109
+ # "process_id": self.process_id
110
+ # }
111
+ # with open(self.process, 'w') as file:
112
+ # json.dump(process_data, file, indent=4)
113
+
114
+ # def __enter__(self):
115
+ # return self
116
+
117
+ # def __exit__(self, exc_type, exc_val, exc_tb):
118
+ # self.cleanup()
119
+
120
+ # def __del__(self):
121
+ # self.cleanup()
122
+
123
+ # def cleanup(self):
124
+ # try:
125
+ # if os.path.exists(self.process):
126
+ # with open(self.process, 'r') as file:
127
+ # json_data = json.load(file)
128
+
129
+ # json_data["end_time"] = datetime.now().isoformat()
130
+
131
+ # with open(self.process, 'w') as file:
132
+ # json.dump(json_data, file, indent=4)
133
+ # except Exception as e:
134
+ # print(f"[Process cleanup Error] {e}")
image/index.html ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Image Convert - Tools Collection</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1000px;
24
+ margin: 0 auto;
25
+ background: white;
26
+ border-radius: 20px;
27
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
28
+ overflow: hidden;
29
+ }
30
+
31
+ .header {
32
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
33
+ color: white;
34
+ padding: 40px 30px;
35
+ text-align: center;
36
+ position: relative;
37
+ }
38
+
39
+ .back-button {
40
+ position: absolute;
41
+ left: 30px;
42
+ top: 50%;
43
+ transform: translateY(-50%);
44
+ background: rgba(255, 255, 255, 0.2);
45
+ color: white;
46
+ border: none;
47
+ padding: 12px 20px;
48
+ border-radius: 50px;
49
+ cursor: pointer;
50
+ font-size: 1rem;
51
+ transition: all 0.3s ease;
52
+ text-decoration: none;
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 8px;
56
+ }
57
+
58
+ .back-button:hover {
59
+ background: rgba(255, 255, 255, 0.3);
60
+ transform: translateY(-50%) translateX(-2px);
61
+ }
62
+
63
+ .header-content {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ gap: 20px;
68
+ margin-bottom: 20px;
69
+ }
70
+
71
+ .header-text h1 {
72
+ font-size: 2.5rem;
73
+ margin-bottom: 10px;
74
+ font-weight: 300;
75
+ }
76
+
77
+ .header-text p {
78
+ font-size: 1.1rem;
79
+ opacity: 0.9;
80
+ }
81
+
82
+ .content {
83
+ padding: 40px;
84
+ }
85
+
86
+ .upload-section {
87
+ background: #f8f9fa;
88
+ border-radius: 15px;
89
+ padding: 40px;
90
+ text-align: center;
91
+ margin-bottom: 30px;
92
+ border: 2px dashed #dee2e6;
93
+ transition: all 0.3s ease;
94
+ cursor: pointer;
95
+ }
96
+
97
+ .upload-section:hover {
98
+ border-color: #667eea;
99
+ background: #f0f4ff;
100
+ }
101
+
102
+ .upload-section.dragover {
103
+ border-color: #667eea;
104
+ background: #e8f2ff;
105
+ transform: scale(1.02);
106
+ }
107
+
108
+ .upload-icon {
109
+ font-size: 4rem;
110
+ color: #667eea;
111
+ margin-bottom: 20px;
112
+ display: block;
113
+ }
114
+
115
+ .upload-text h3 {
116
+ color: #495057;
117
+ margin-bottom: 10px;
118
+ font-size: 1.5rem;
119
+ }
120
+
121
+ .upload-text p {
122
+ color: #6c757d;
123
+ margin-bottom: 20px;
124
+ }
125
+
126
+ .upload-button {
127
+ background: linear-gradient(135deg, #667eea, #764ba2);
128
+ color: white;
129
+ border: none;
130
+ padding: 15px 30px;
131
+ border-radius: 50px;
132
+ font-size: 1.1rem;
133
+ cursor: pointer;
134
+ transition: all 0.3s ease;
135
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
136
+ }
137
+
138
+ .upload-button:hover {
139
+ transform: translateY(-2px);
140
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
141
+ }
142
+
143
+ .file-input {
144
+ display: none;
145
+ }
146
+
147
+ .settings-section {
148
+ background: white;
149
+ border-radius: 15px;
150
+ padding: 30px;
151
+ margin-bottom: 30px;
152
+ border: 1px solid #e9ecef;
153
+ }
154
+
155
+ .settings-title {
156
+ color: #495057;
157
+ font-size: 1.3rem;
158
+ margin-bottom: 20px;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 10px;
162
+ }
163
+
164
+ .settings-grid {
165
+ display: grid;
166
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
167
+ gap: 20px;
168
+ }
169
+
170
+ .setting-group {
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: 8px;
174
+ }
175
+
176
+ .setting-label {
177
+ color: #495057;
178
+ font-weight: 500;
179
+ font-size: 0.95rem;
180
+ }
181
+
182
+ .setting-input {
183
+ padding: 12px 15px;
184
+ border: 2px solid #e9ecef;
185
+ border-radius: 10px;
186
+ font-size: 1rem;
187
+ transition: all 0.3s ease;
188
+ }
189
+
190
+ .setting-input:focus {
191
+ outline: none;
192
+ border-color: #667eea;
193
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
194
+ }
195
+
196
+ .files-preview {
197
+ margin-top: 30px;
198
+ }
199
+
200
+ .preview-title {
201
+ color: #495057;
202
+ font-size: 1.3rem;
203
+ margin-bottom: 20px;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 10px;
207
+ }
208
+
209
+ .files-grid {
210
+ display: grid;
211
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
212
+ gap: 20px;
213
+ }
214
+
215
+ .file-card {
216
+ background: white;
217
+ border-radius: 12px;
218
+ padding: 20px;
219
+ border: 1px solid #e9ecef;
220
+ transition: all 0.3s ease;
221
+ position: relative;
222
+ }
223
+
224
+ .file-card:hover {
225
+ transform: translateY(-2px);
226
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
227
+ }
228
+
229
+ .file-preview {
230
+ width: 100%;
231
+ height: 120px;
232
+ background: #f8f9fa;
233
+ border-radius: 8px;
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ margin-bottom: 15px;
238
+ overflow: hidden;
239
+ }
240
+
241
+ .file-preview img {
242
+ max-width: 100%;
243
+ max-height: 100%;
244
+ object-fit: cover;
245
+ border-radius: 8px;
246
+ }
247
+
248
+ .file-icon {
249
+ font-size: 2rem;
250
+ color: #667eea;
251
+ }
252
+
253
+ .file-info h4 {
254
+ color: #495057;
255
+ font-size: 0.9rem;
256
+ margin-bottom: 5px;
257
+ word-break: break-all;
258
+ }
259
+
260
+ .file-info p {
261
+ color: #6c757d;
262
+ font-size: 0.8rem;
263
+ margin-bottom: 10px;
264
+ }
265
+
266
+ .file-status {
267
+ display: flex;
268
+ align-items: center;
269
+ gap: 8px;
270
+ margin-bottom: 10px;
271
+ }
272
+
273
+ .status-indicator {
274
+ width: 8px;
275
+ height: 8px;
276
+ border-radius: 50%;
277
+ background: #28a745;
278
+ }
279
+
280
+ .status-indicator.processing {
281
+ background: #ffc107;
282
+ animation: pulse 2s infinite;
283
+ }
284
+
285
+ .status-indicator.error {
286
+ background: #dc3545;
287
+ }
288
+
289
+ .remove-button {
290
+ position: absolute;
291
+ top: 10px;
292
+ right: 10px;
293
+ background: #dc3545;
294
+ color: white;
295
+ border: none;
296
+ width: 24px;
297
+ height: 24px;
298
+ border-radius: 50%;
299
+ cursor: pointer;
300
+ font-size: 0.8rem;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ transition: all 0.3s ease;
305
+ }
306
+
307
+ .remove-button:hover {
308
+ background: #c82333;
309
+ transform: scale(1.1);
310
+ }
311
+
312
+ .progress-section {
313
+ margin-top: 30px;
314
+ display: none;
315
+ }
316
+
317
+ .progress-title {
318
+ color: #495057;
319
+ font-size: 1.2rem;
320
+ margin-bottom: 15px;
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 10px;
324
+ }
325
+
326
+ .progress-bar-container {
327
+ background: #e9ecef;
328
+ border-radius: 10px;
329
+ height: 12px;
330
+ overflow: hidden;
331
+ margin-bottom: 10px;
332
+ }
333
+
334
+ .progress-bar {
335
+ background: linear-gradient(135deg, #28a745, #20c997);
336
+ height: 100%;
337
+ width: 0%;
338
+ transition: width 0.3s ease;
339
+ border-radius: 10px;
340
+ }
341
+
342
+ .progress-text {
343
+ color: #6c757d;
344
+ font-size: 0.9rem;
345
+ text-align: center;
346
+ }
347
+
348
+ .action-buttons {
349
+ display: flex;
350
+ gap: 15px;
351
+ justify-content: center;
352
+ margin-top: 30px;
353
+ }
354
+
355
+ .action-button {
356
+ padding: 15px 30px;
357
+ border: none;
358
+ border-radius: 50px;
359
+ font-size: 1.1rem;
360
+ cursor: pointer;
361
+ transition: all 0.3s ease;
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 10px;
365
+ min-width: 150px;
366
+ justify-content: center;
367
+ }
368
+
369
+ .convert-button {
370
+ background: linear-gradient(135deg, #28a745, #20c997);
371
+ color: white;
372
+ box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
373
+ }
374
+
375
+ .convert-button:hover:not(:disabled) {
376
+ transform: translateY(-2px);
377
+ box-shadow: 0 8px 25px rgba(40, 167, 69, 0.4);
378
+ }
379
+
380
+ .convert-button:disabled {
381
+ opacity: 0.6;
382
+ cursor: not-allowed;
383
+ }
384
+
385
+ .clear-button {
386
+ background: #6c757d;
387
+ color: white;
388
+ }
389
+
390
+ .clear-button:hover {
391
+ background: #5a6268;
392
+ transform: translateY(-2px);
393
+ }
394
+
395
+ .download-button {
396
+ background: linear-gradient(135deg, #007bff, #0056b3);
397
+ color: white;
398
+ box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
399
+ }
400
+
401
+ .download-button:hover {
402
+ transform: translateY(-2px);
403
+ box-shadow: 0 8px 25px rgba(0, 123, 255, 0.4);
404
+ }
405
+
406
+ .error-message {
407
+ background: #f8d7da;
408
+ color: #721c24;
409
+ padding: 15px;
410
+ border-radius: 10px;
411
+ margin-bottom: 20px;
412
+ border: 1px solid #f5c6cb;
413
+ display: none;
414
+ }
415
+
416
+ .success-message {
417
+ background: #d4edda;
418
+ color: #155724;
419
+ padding: 15px;
420
+ border-radius: 10px;
421
+ margin-bottom: 20px;
422
+ border: 1px solid #c3e6cb;
423
+ display: none;
424
+ }
425
+
426
+ @keyframes pulse {
427
+ 0% {
428
+ transform: scale(1);
429
+ opacity: 1;
430
+ }
431
+
432
+ 50% {
433
+ transform: scale(1.05);
434
+ opacity: 0.8;
435
+ }
436
+
437
+ 100% {
438
+ transform: scale(1);
439
+ opacity: 1;
440
+ }
441
+ }
442
+
443
+ @media (max-width: 768px) {
444
+ .header-content {
445
+ flex-direction: column;
446
+ gap: 15px;
447
+ }
448
+
449
+ .header-text h1 {
450
+ font-size: 2rem;
451
+ }
452
+
453
+ .back-button {
454
+ position: static;
455
+ transform: none;
456
+ margin-bottom: 20px;
457
+ align-self: flex-start;
458
+ }
459
+
460
+ .content {
461
+ padding: 20px;
462
+ }
463
+
464
+ .action-buttons {
465
+ flex-direction: column;
466
+ align-items: center;
467
+ }
468
+ }
469
+ </style>
470
+ </head>
471
+
472
+ <body>
473
+ <div class="container">
474
+ <div class="header">
475
+ <a href="/" class="back-button">
476
+ ← Back to Tools
477
+ </a>
478
+
479
+ <div class="header-content">
480
+ <div class="header-text">
481
+ <h1>Image Convert</h1>
482
+ <p>Convert HEIC images to PNG/JPG format with ease</p>
483
+ </div>
484
+ </div>
485
+ </div>
486
+
487
+ <div class="content">
488
+ <div class="error-message" id="error-message"></div>
489
+ <div class="success-message" id="success-message"></div>
490
+
491
+ <div class="upload-section" id="upload-section">
492
+ <span class="upload-icon">📁</span>
493
+ <div class="upload-text">
494
+ <h3>Drop your images here</h3>
495
+ <p>Support for HEIC, PNG, JPG, and more formats</p>
496
+ <!-- <button class="upload-button" onclick="document.getElementById('file-input').click()"> -->
497
+ <button class="upload-button">
498
+ Choose Files
499
+ </button>
500
+ </div>
501
+ <input type="file" id="file-input" class="file-input" multiple accept="image/*">
502
+ </div>
503
+
504
+ <div class="settings-section">
505
+ <h3 class="settings-title">
506
+ ⚙️ Conversion Settings
507
+ </h3>
508
+ <div class="settings-grid">
509
+ <div class="setting-group">
510
+ <label class="setting-label">Output Format</label>
511
+ <select class="setting-input" id="output-format">
512
+ <option value="jpg">JPG</option>
513
+ <option value="png">PNG</option>
514
+ <option value="webp">WebP</option>
515
+ </select>
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <div class="files-preview" id="files-preview" style="display: none;">
521
+ <h3 class="preview-title">
522
+ 📋 Selected Files
523
+ </h3>
524
+ <div class="files-grid" id="files-grid">
525
+ <!-- File cards will be inserted here -->
526
+ </div>
527
+ </div>
528
+
529
+ <div class="progress-section" id="progress-section">
530
+ <h3 class="progress-title">
531
+ 🔄 Converting Images...
532
+ </h3>
533
+ <div class="progress-bar-container">
534
+ <div class="progress-bar" id="progress-bar"></div>
535
+ </div>
536
+ <div class="progress-text" id="progress-text">0% complete</div>
537
+ </div>
538
+
539
+ <div class="action-buttons">
540
+ <button class="action-button convert-button" id="convert-button" disabled>
541
+ 🔄 Convert Images
542
+ </button>
543
+ <button class="action-button clear-button" id="clear-button">
544
+ 🗑️ Clear All
545
+ </button>
546
+ <button class="action-button download-button" id="download-button" style="display: none;">
547
+ 💾 Download All
548
+ </button>
549
+ </div>
550
+ </div>
551
+ </div>
552
+
553
+ <script>
554
+ let selectedFiles = [];
555
+ let convertedFiles = [];
556
+
557
+ // Initialize the application
558
+ document.addEventListener('DOMContentLoaded', function () {
559
+ setupEventListeners();
560
+ });
561
+
562
+ function setupEventListeners() {
563
+ const uploadSection = document.getElementById('upload-section');
564
+ const fileInput = document.getElementById('file-input');
565
+ const convertButton = document.getElementById('convert-button');
566
+ const clearButton = document.getElementById('clear-button');
567
+ const downloadButton = document.getElementById('download-button');
568
+
569
+ // File upload events
570
+ uploadSection.addEventListener('click', () => fileInput.click());
571
+ uploadSection.addEventListener('dragover', handleDragOver);
572
+ uploadSection.addEventListener('dragleave', handleDragLeave);
573
+ uploadSection.addEventListener('drop', handleDrop);
574
+ fileInput.addEventListener('change', handleFileSelect);
575
+
576
+ // Button events
577
+ convertButton.addEventListener('click', convertImages);
578
+ clearButton.addEventListener('click', clearAllFiles);
579
+ downloadButton.addEventListener('click', downloadAllFiles);
580
+ }
581
+
582
+ function showMessage(message, type = 'error') {
583
+ const errorMsg = document.getElementById('error-message');
584
+ const successMsg = document.getElementById('success-message');
585
+
586
+ // Hide both messages first
587
+ errorMsg.style.display = 'none';
588
+ successMsg.style.display = 'none';
589
+
590
+ if (type === 'error') {
591
+ errorMsg.textContent = message;
592
+ errorMsg.style.display = 'block';
593
+ } else {
594
+ successMsg.textContent = message;
595
+ successMsg.style.display = 'block';
596
+ }
597
+
598
+ // Auto-hide after 5 seconds
599
+ setTimeout(() => {
600
+ errorMsg.style.display = 'none';
601
+ successMsg.style.display = 'none';
602
+ }, 5000);
603
+ }
604
+
605
+ function handleDragOver(e) {
606
+ e.preventDefault();
607
+ e.currentTarget.classList.add('dragover');
608
+ }
609
+
610
+ function handleDragLeave(e) {
611
+ e.preventDefault();
612
+ e.currentTarget.classList.remove('dragover');
613
+ }
614
+
615
+ function handleDrop(e) {
616
+ e.preventDefault();
617
+ const uploadSection = e.currentTarget;
618
+ uploadSection.classList.remove('dragover');
619
+
620
+ const files = Array.from(e.dataTransfer.files);
621
+ processFiles(files);
622
+ }
623
+
624
+ function handleFileSelect(e) {
625
+ const files = Array.from(e.target.files);
626
+ processFiles(files);
627
+ }
628
+
629
+ function generateUniqueId(file, length = 12) {
630
+ ext = "." + file.name.split(".")[file.name.split(".").length - 1]
631
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
632
+ return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('') + ext;
633
+ }
634
+
635
+ function saveFileIdToStorage(id) {
636
+ let fileIds = JSON.parse(localStorage.getItem('uploadedFileIds') || '[]');
637
+ if (!fileIds.includes(id)) {
638
+ fileIds.push(id);
639
+ localStorage.setItem('uploadedFileIds', JSON.stringify(fileIds));
640
+ }
641
+ }
642
+
643
+ function processFiles(files) {
644
+ files.forEach((file, index) => {
645
+ const id = generateUniqueId(file); // generate unique ID
646
+ const fileData = {
647
+ id: id,
648
+ file: file,
649
+ name: file.name,
650
+ size: formatFileSize(file.size),
651
+ status: 'ready',
652
+ preview: null
653
+ };
654
+
655
+ // Store file ID in localStorage for potential server deletion later
656
+ saveFileIdToStorage(id);
657
+
658
+ // Create preview for images
659
+ const reader = new FileReader();
660
+ reader.onload = (e) => {
661
+ fileData.preview = e.target.result;
662
+ updateFileCard(fileData.id);
663
+ };
664
+ reader.readAsDataURL(file);
665
+
666
+ selectedFiles.push(fileData);
667
+ });
668
+
669
+ updateUI();
670
+ }
671
+
672
+ function updateUI() {
673
+ const filesPreview = document.getElementById('files-preview');
674
+ const convertButton = document.getElementById('convert-button');
675
+
676
+ if (selectedFiles.length > 0) {
677
+ filesPreview.style.display = 'block';
678
+ convertButton.disabled = false;
679
+ renderFileCards();
680
+ } else {
681
+ filesPreview.style.display = 'none';
682
+ convertButton.disabled = true;
683
+ }
684
+ }
685
+
686
+ function renderFileCards() {
687
+ const filesGrid = document.getElementById('files-grid');
688
+
689
+ filesGrid.innerHTML = selectedFiles.map(file => `
690
+ <div class="file-card" id="file-${file.id}">
691
+ <button class="remove-button" onclick="removeFile('${file.id}')">×</button>
692
+
693
+ <div class="file-preview">
694
+ ${file.preview ?
695
+ `<img src="${file.preview}" alt="${file.name}">` :
696
+ '<span class="file-icon">🖼️</span>'
697
+ }
698
+ </div>
699
+
700
+ <div class="file-info">
701
+ <h4>${file.name}</h4>
702
+ <p>${file.size}</p>
703
+
704
+ <div class="file-status">
705
+ <div class="status-indicator ${file.status === 'processing' ? 'processing' : file.status === 'error' ? 'error' : ''}"></div>
706
+ <span>${getStatusText(file.status)}</span>
707
+ </div>
708
+ </div>
709
+ </div>
710
+ `).join('');
711
+ }
712
+
713
+ function updateFileCard(fileId) {
714
+ const file = selectedFiles.find(f => f.id === fileId);
715
+ if (!file) return;
716
+
717
+ const card = document.getElementById(`file-${fileId}`);
718
+ if (!card) return;
719
+
720
+ const preview = card.querySelector('.file-preview');
721
+ if (file.preview && preview) {
722
+ preview.innerHTML = `<img src="${file.preview}" alt="${file.name}">`;
723
+ }
724
+ }
725
+
726
+ function removeFile(fileId) {
727
+ selectedFiles = selectedFiles.filter(file => file.id !== fileId);
728
+ convertedFiles = convertedFiles.filter(file => file.id !== fileId);
729
+ updateUI();
730
+
731
+ if (selectedFiles.length === 0) {
732
+ document.getElementById('download-button').style.display = 'none';
733
+ }
734
+ }
735
+
736
+ async function clearAllFiles() {
737
+ const fileIds = JSON.parse(localStorage.getItem('uploadedFileIds') || '[]');
738
+
739
+ if (fileIds.length > 0) {
740
+ try {
741
+ await fetch('/image/delete', {
742
+ method: 'POST',
743
+ headers: {
744
+ 'Content-Type': 'application/json'
745
+ },
746
+ body: JSON.stringify({ ids: fileIds })
747
+ });
748
+ } catch (err) {
749
+ console.error('Error sending delete request:', err);
750
+ }
751
+ }
752
+
753
+ // Clear everything locally
754
+ selectedFiles = [];
755
+ convertedFiles = [];
756
+ updateUI();
757
+ document.getElementById('download-button').style.display = 'none';
758
+ document.getElementById('progress-section').style.display = 'none';
759
+ localStorage.removeItem('uploadedFileIds');
760
+ }
761
+
762
+
763
+ async function convertImages() {
764
+ if (selectedFiles.length === 0) return;
765
+
766
+ const progressSection = document.getElementById('progress-section');
767
+ const progressBar = document.getElementById('progress-bar');
768
+ const progressText = document.getElementById('progress-text');
769
+ const convertButton = document.getElementById('convert-button');
770
+
771
+ // Show progress section
772
+ progressSection.style.display = 'block';
773
+ convertButton.disabled = true;
774
+ convertButton.innerHTML = '🔄 Converting...';
775
+
776
+ const settings = getConversionSettings();
777
+ convertedFiles = [];
778
+
779
+ for (let i = 0; i < selectedFiles.length; i++) {
780
+ const file = selectedFiles[i];
781
+
782
+ // Update file status
783
+ file.status = 'uploading';
784
+ renderFileCards();
785
+
786
+ try {
787
+ // Upload and convert the file
788
+ await upload(file, settings);
789
+ file.status = 'converting';
790
+ renderFileCards();
791
+ const convertedFile = await convert(file, settings);
792
+ file.status = 'completed';
793
+ renderFileCards();
794
+ convertedFiles.push(convertedFile);
795
+ } catch (error) {
796
+ console.error('Conversion error:', error);
797
+ file.status = 'error';
798
+ showMessage(`Error converting ${file.name}: ${error.message}`, 'error');
799
+ }
800
+
801
+ // Update progress
802
+ const progress = Math.round(((i + 1) / selectedFiles.length) * 100);
803
+ progressBar.style.width = progress + '%';
804
+ progressText.textContent = `${progress}% complete (${i + 1}/${selectedFiles.length})`;
805
+
806
+ renderFileCards();
807
+ }
808
+
809
+ // Completion
810
+ convertButton.disabled = false;
811
+ convertButton.innerHTML = '🔄 Convert Images';
812
+
813
+ const successfulConversions = convertedFiles.length;
814
+ if (successfulConversions > 0) {
815
+ document.getElementById('download-button').style.display = 'inline-flex';
816
+ showMessage(`Successfully converted ${successfulConversions} image(s)!`, 'success');
817
+ }
818
+
819
+ setTimeout(() => {
820
+ progressSection.style.display = 'none';
821
+ }, 1000);
822
+ }
823
+
824
+ function getConversionSettings() {
825
+ return {
826
+ format: document.getElementById('output-format').value
827
+ };
828
+ }
829
+
830
+ async function upload(file, settings) {
831
+ const formData = new FormData();
832
+ formData.append('image', file.file);
833
+ formData.append('id', file.id);
834
+
835
+ try {
836
+ const response = await fetch('/image/upload', {
837
+ method: 'POST',
838
+ body: formData
839
+ });
840
+
841
+ if (!response.ok) {
842
+ const errorData = await response.json().catch(() => ({}));
843
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
844
+ }
845
+ return true
846
+ } catch (error) {
847
+ throw new Error(`Failed to upload ${file.name}: ${error.message}`);
848
+ }
849
+ }
850
+
851
+ async function convert(file, settings) {
852
+ const formData = new FormData();
853
+ formData.append('id', file.id);
854
+ formData.append('to_format', settings.format);
855
+
856
+ try {
857
+ const response = await fetch('/image/convert', {
858
+ method: 'POST',
859
+ body: formData
860
+ });
861
+
862
+ if (!response.ok) {
863
+ const errorData = await response.json().catch(() => ({}));
864
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
865
+ }
866
+
867
+ const result = await response.json();
868
+
869
+ return {
870
+ id: file.id,
871
+ originalName: file.name,
872
+ convertedName: result.new_filename,
873
+ url: "/image/download?id="+result.new_filename
874
+ };
875
+ } catch (error) {
876
+ throw new Error(`Failed to upload ${file.name}: ${error.message}`);
877
+ }
878
+ }
879
+
880
+ function downloadAllFiles() {
881
+ if (convertedFiles.length === 0) return;
882
+
883
+ // Download each converted file
884
+ convertedFiles.forEach((file, index) => {
885
+ setTimeout(() => {
886
+ const link = document.createElement('a');
887
+ link.href = file.url;
888
+ link.download = file.convertedName;
889
+ link.target = '_blank';
890
+ document.body.appendChild(link);
891
+ link.click();
892
+ document.body.removeChild(link);
893
+ }, index * 100);
894
+ });
895
+ }
896
+
897
+ function getStatusText(status) {
898
+ switch (status) {
899
+ case 'ready': return 'Ready';
900
+ case 'uploading': return 'Uploading...';
901
+ case 'converting': return 'Converting...';
902
+ case 'completed': return 'Converted';
903
+ case 'error': return 'Error';
904
+ default: return 'Unknown';
905
+ }
906
+ }
907
+
908
+ function formatFileSize(bytes) {
909
+ if (bytes === 0) return '0 Bytes';
910
+ const k = 1024;
911
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
912
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
913
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
914
+ }
915
+ </script>
916
+ </body>
917
+
918
+ </html>
image/input/temp.temp ADDED
File without changes
image/main.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from process_exception import ProcessAlreadyRunning
2
+ from .image_base import ImageBase
3
+ from .remove_metadata import RemoveMetadata
4
+
5
+ def remove_metadata():
6
+ with RemoveMetadata() as processor:
7
+ processor.remove_metadata("test.png")
8
+
9
+ def is_process_running():
10
+ try:
11
+ ImageBase()
12
+ except ProcessAlreadyRunning as e:
13
+ return e.data
14
+
15
+ return None
image/output/temp.temp ADDED
File without changes
image/process.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "start_time": "2025-06-29T13:24:50.598935",
3
+ "feature": "remove_metadata"
4
+ }
image/remove_background.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Background Remover Module
3
+
4
+ This module provides a high-quality, class-based background remover using rembg.
5
+ """
6
+
7
+ from rembg import remove
8
+ from pathlib import Path
9
+ from custom_logger import logger_config
10
+
11
+
12
+ class RemoveBackground:
13
+ """
14
+ High-quality background remover using the rembg library.
15
+
16
+ This class provides background removal functionality for supported image formats,
17
+ with proper validation, logging, and output management.
18
+ """
19
+
20
+ def __init__(self):
21
+ self.removal_history = []
22
+
23
+ def _get_output_path(self, input_path: Path, output_path: str = None) -> Path:
24
+ if output_path:
25
+ return Path(output_path)
26
+ return input_path.with_name(input_path.stem + "_nobg.png")
27
+
28
+ def remove_background(self, input_file_name: str, output_path: str = None, process_id: str = None) -> str:
29
+ try:
30
+ # Validate inputs
31
+ self.input_file_name = input_file_name
32
+ self.input_file_path = f'{self.input_dir}/{self.input_file_name}'
33
+ self._validate_input_file()
34
+ output_file = self._get_output_path(self.input_file_path, output_path)
35
+
36
+ logger_config.info(f"🧼 Removing background from: {self.input_file_path}")
37
+
38
+ with open(self.input_file_path, 'rb') as infile:
39
+ input_image = infile.read()
40
+
41
+ # Perform background removal
42
+ output_image = remove(input_image)
43
+
44
+ with open(output_file, 'wb') as outfile:
45
+ outfile.write(output_image)
46
+
47
+ logger_config.info(f"✅ Background removed successfully: {output_file}")
48
+
49
+ self.removal_history.append({
50
+ "input": self.input_file_path,
51
+ "output": str(output_file),
52
+ "success": True
53
+ })
54
+
55
+ return str(output_file)
56
+
57
+ except Exception as e:
58
+ logger_config.error(f"❌ Background removal failed: {str(e)}")
59
+ return None
image/remove_metadata.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Image Metadata Remover Module
3
+
4
+ This module provides a class-based metadata remover that strips all metadata
5
+ from images while preserving original quality, format, and aspect ratio.
6
+ """
7
+
8
+ from .image_base import ImageBase
9
+ import os
10
+ from custom_logger import logger_config
11
+ from PIL import Image
12
+ from PIL.ExifTags import TAGS
13
+ from typing import Dict, List
14
+
15
+ class RemoveMetadata(ImageBase):
16
+ """
17
+ High-quality image metadata remover that preserves original quality and aspect ratio.
18
+
19
+ This class handles removal of EXIF, IPTC, XMP and other metadata while maintaining
20
+ the highest possible image quality and exact dimensional preservation.
21
+ """
22
+ def __init__(self):
23
+ super().__init__("remove_metadata")
24
+ self.output_suffix = "no_metadata"
25
+
26
+ def _detect_metadata(self, image: Image.Image) -> Dict[str, bool]:
27
+ """
28
+ Detect various types of metadata in the image.
29
+
30
+ Args:
31
+ image: PIL Image object
32
+
33
+ Returns:
34
+ Dictionary indicating which metadata types are present
35
+ """
36
+ metadata_found = {
37
+ 'exif': False,
38
+ 'icc_profile': False,
39
+ 'xmp': False,
40
+ 'iptc': False,
41
+ 'other_info': False
42
+ }
43
+
44
+ # Check for EXIF data
45
+ if hasattr(image, '_getexif') and image._getexif():
46
+ metadata_found['exif'] = True
47
+
48
+ # Check image info for various metadata
49
+ if hasattr(image, 'info') and image.info:
50
+ if 'icc_profile' in image.info:
51
+ metadata_found['icc_profile'] = True
52
+ if 'xmp' in image.info:
53
+ metadata_found['xmp'] = True
54
+ if 'iptc' in image.info:
55
+ metadata_found['iptc'] = True
56
+
57
+ # Check for other metadata
58
+ metadata_keys = ['dpi', 'description', 'software', 'datetime', 'artist', 'copyright']
59
+ if any(key in image.info for key in metadata_keys):
60
+ metadata_found['other_info'] = True
61
+
62
+ return metadata_found
63
+
64
+ def _get_quality_settings_for_format(self, format_name: str, original_mode: str) -> Dict:
65
+ """
66
+ Get optimal save settings for maximum quality preservation per format.
67
+
68
+ Args:
69
+ format_name: PIL format name
70
+ original_mode: Original image color mode
71
+
72
+ Returns:
73
+ Dictionary of save parameters
74
+ """
75
+ settings = {}
76
+
77
+ if format_name == 'JPEG':
78
+ settings = {
79
+ 'quality': 100, # Maximum quality
80
+ 'optimize': False, # Don't optimize to preserve quality
81
+ 'progressive': False, # Standard baseline JPEG
82
+ 'subsampling': 0, # No chroma subsampling
83
+ 'exif': b'' # Explicitly remove EXIF
84
+ }
85
+
86
+ elif format_name == 'PNG':
87
+ settings = {
88
+ 'optimize': False, # Don't optimize to preserve quality
89
+ 'compress_level': 1 # Minimal compression
90
+ }
91
+
92
+ elif format_name == 'WEBP':
93
+ settings = {
94
+ 'lossless': True, # Lossless compression
95
+ 'quality': 100, # Maximum quality
96
+ 'method': 6 # Best compression method
97
+ }
98
+
99
+ elif format_name == 'TIFF':
100
+ settings = {
101
+ 'compression': None # No compression
102
+ }
103
+
104
+ elif format_name in ['BMP', 'GIF']:
105
+ settings = {} # These formats have limited options
106
+
107
+ return settings
108
+
109
+ def _clean_png_with_transparency(self, image: Image.Image, output_path: str) -> bool:
110
+ """
111
+ Clean PNG image while preserving transparency and quality.
112
+
113
+ Args:
114
+ image: PIL Image object
115
+ output_path: Path for output file
116
+
117
+ Returns:
118
+ True if successful
119
+ """
120
+ try:
121
+ original_mode = image.mode
122
+
123
+ # Create new clean image preserving transparency
124
+ if original_mode == 'P':
125
+ # Palette mode - preserve palette
126
+ clean_img = Image.new(original_mode, image.size)
127
+ if image.getpalette():
128
+ clean_img.putpalette(image.getpalette())
129
+ clean_img.paste(image, (0, 0))
130
+ else:
131
+ # RGBA or LA mode
132
+ clean_img = Image.new(original_mode, image.size, (0, 0, 0, 0))
133
+ clean_img.paste(image, (0, 0))
134
+
135
+ # Save with quality preservation
136
+ save_settings = self._get_quality_settings_for_format('PNG', original_mode)
137
+ clean_img.save(output_path, format='PNG', **save_settings)
138
+
139
+ return True
140
+
141
+ except Exception as e:
142
+ raise Exception(f"PNG transparency cleaning failed: {e}")
143
+
144
+ def _clean_jpeg_image(self, image: Image.Image, output_path: str) -> bool:
145
+ """
146
+ Clean JPEG image while preserving maximum quality.
147
+
148
+ Args:
149
+ image: PIL Image object
150
+ output_path: Path for output file
151
+
152
+ Returns:
153
+ True if successful
154
+ """
155
+ try:
156
+ # Convert to RGB if necessary
157
+ if image.mode != 'RGB':
158
+ clean_img = image.convert('RGB')
159
+ else:
160
+ clean_img = image.copy()
161
+
162
+ # Save with maximum quality and no metadata
163
+ save_settings = self._get_quality_settings_for_format('JPEG', image.mode)
164
+ clean_img.save(output_path, format='JPEG', **save_settings)
165
+
166
+ return True
167
+
168
+ except Exception as e:
169
+ raise Exception(f"JPEG cleaning failed: {e}")
170
+
171
+ def _clean_other_format(self, image: Image.Image, output_path: str, original_format: str) -> bool:
172
+ """
173
+ Clean other image formats while preserving quality.
174
+
175
+ Args:
176
+ image: PIL Image object
177
+ output_path: Path for output file
178
+ original_format: Original image format
179
+
180
+ Returns:
181
+ True if successful
182
+ """
183
+ try:
184
+ # Create new image without metadata
185
+ clean_img = Image.new(image.mode, image.size)
186
+
187
+ # Handle palette images
188
+ if image.mode == 'P' and image.getpalette():
189
+ clean_img.putpalette(image.getpalette())
190
+
191
+ # Paste original image data
192
+ clean_img.paste(image, (0, 0))
193
+
194
+ # Save with format-specific quality settings
195
+ save_settings = self._get_quality_settings_for_format(original_format, image.mode)
196
+ clean_img.save(output_path, format=original_format, **save_settings)
197
+
198
+ return True
199
+
200
+ except Exception as e:
201
+ raise Exception(f"{original_format} cleaning failed: {e}")
202
+
203
+ def _verify_metadata_removal(self) -> bool:
204
+ """
205
+ Verify that metadata has been successfully removed from the cleaned image.
206
+ Returns:
207
+ True if metadata was successfully removed
208
+ """
209
+ try:
210
+ with Image.open(self.input_file_path) as img:
211
+ metadata_found = self._detect_metadata(img)
212
+
213
+ # Check if any metadata is still present
214
+ has_metadata = any(metadata_found.values())
215
+
216
+ if has_metadata:
217
+ present_types = [k for k, v in metadata_found.items() if v]
218
+ logger_config.warning(f"Warning: Some metadata still present: {present_types}")
219
+ return False
220
+ else:
221
+ logger_config.success("No metadata found in cleaned image")
222
+ return True
223
+
224
+ except Exception as e:
225
+ raise Exception(f"Verification failed: {e}")
226
+
227
+ def _verify_image_quality(self, original_path: str, cleaned_path: str) -> bool:
228
+ """
229
+ Verify that image quality and dimensions are preserved.
230
+
231
+ Args:
232
+ original_path: Path to original image
233
+ cleaned_path: Path to cleaned image
234
+
235
+ Returns:
236
+ True if quality is preserved
237
+ """
238
+ try:
239
+ with Image.open(original_path) as original, Image.open(cleaned_path) as cleaned:
240
+ # Check dimensions
241
+ if original.size != cleaned.size:
242
+ raise Exception(f"Dimension mismatch! Original: {original.size}, Cleaned: {cleaned.size}")
243
+
244
+ # Check if file exists and has reasonable size
245
+ if not os.path.exists(cleaned_path):
246
+ return False
247
+
248
+ cleaned_size = os.path.getsize(cleaned_path)
249
+ if cleaned_size == 0:
250
+ raise Exception("Cleaned file is empty")
251
+
252
+ logger_config.success(f"Quality preserved - Size: {cleaned.size}, File size: {cleaned_size:,} bytes")
253
+ return True
254
+
255
+ except Exception as e:
256
+ raise Exception(f"Quality verification failed: {e}")
257
+
258
+ def remove_metadata(self, input_file_name: str) -> str:
259
+ """
260
+ Remove all metadata from an image while preserving quality and aspect ratio.
261
+
262
+ Args:
263
+ input_file_name: Input image name
264
+ custom_output_path: Optional custom output path (overrides default naming)
265
+
266
+ Returns:
267
+ Path to cleaned image
268
+
269
+ Raises:
270
+ FileNotFoundError: If input file doesn't exist
271
+ ValueError: If file format is not supported
272
+ Exception: If metadata removal fails
273
+ """
274
+ try:
275
+ self.input_file_name = input_file_name
276
+ self.input_file_path = f'{self.input_dir}/{self.input_file_name}'
277
+ # Validate input
278
+ input_file = self._validate_input_file()
279
+
280
+ # Generate output path
281
+ output_path = self._generate_output_path()
282
+
283
+ logger_config.info(f"Processing: {input_file.name}")
284
+
285
+ # Open and analyze image
286
+ with Image.open(str(input_file)) as image:
287
+ original_size = image.size
288
+ original_format = image.format
289
+ original_mode = image.mode
290
+
291
+ # Detect metadata
292
+ metadata_found = self._detect_metadata(image)
293
+ metadata_types = [k for k, v in metadata_found.items() if v]
294
+
295
+ if metadata_types:
296
+ logger_config.info(f"Found metadata types: {metadata_types}")
297
+ else:
298
+ logger_config.info("No metadata detected")
299
+
300
+ # Choose appropriate cleaning method based on format and characteristics
301
+ success = False
302
+
303
+ if original_format == 'PNG' and original_mode in ('RGBA', 'LA', 'P'):
304
+ success = self._clean_png_with_transparency(image, output_path)
305
+
306
+ elif original_format == 'JPEG' or output_path.lower().endswith(('.jpg', '.jpeg')):
307
+ success = self._clean_jpeg_image(image, output_path)
308
+
309
+ else:
310
+ success = self._clean_other_format(image, output_path, original_format)
311
+
312
+ if not success:
313
+ raise Exception("Metadata cleaning failed")
314
+
315
+ # Verify results
316
+ self._verify_image_quality(str(input_file), output_path)
317
+
318
+ self._verify_metadata_removal(output_path)
319
+
320
+ logger_config.success(f"Cleaned image saved: {output_path}")
321
+ return output_path
322
+
323
+ except Exception as e:
324
+ logger_config.error(f"Failed to clean image: {str(e)}")
325
+ return None
326
+
327
+ def set_output_suffix(self, suffix: str) -> None:
328
+ """
329
+ Change the output filename suffix for cleaned images.
330
+
331
+ Args:
332
+ suffix: New suffix to use (e.g., "_clean", "_no_exif")
333
+ """
334
+ self.output_suffix = suffix
335
+ logger_config.info(f"Output suffix changed to: {suffix}")
336
+
337
+ # Example usage
338
+ if __name__ == "__main__":
339
+ cleaner = RemoveMetadata()
340
+ cleaned_file = cleaner.remove_metadata("image/input/test.png")
index.html ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Tools Collection</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 40px 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 3rem;
39
+ margin-bottom: 15px;
40
+ font-weight: 300;
41
+ }
42
+
43
+ .header p {
44
+ font-size: 1.2rem;
45
+ opacity: 0.9;
46
+ margin-bottom: 30px;
47
+ }
48
+
49
+ .search-container {
50
+ max-width: 500px;
51
+ margin: 0 auto;
52
+ position: relative;
53
+ margin-bottom: 20px;
54
+ }
55
+
56
+ .search-box {
57
+ width: 100%;
58
+ padding: 15px 50px 15px 20px;
59
+ border: none;
60
+ border-radius: 50px;
61
+ font-size: 1.1rem;
62
+ background: rgba(255, 255, 255, 0.9);
63
+ backdrop-filter: blur(10px);
64
+ transition: all 0.3s ease;
65
+ }
66
+
67
+ .search-box:focus {
68
+ outline: none;
69
+ background: white;
70
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
71
+ }
72
+
73
+ .search-icon {
74
+ position: absolute;
75
+ right: 20px;
76
+ top: 50%;
77
+ transform: translateY(-50%);
78
+ font-size: 1.2rem;
79
+ color: #667eea;
80
+ }
81
+
82
+ .filter-container {
83
+ max-width: 300px;
84
+ margin: 0 auto;
85
+ }
86
+
87
+ .category-dropdown {
88
+ width: 100%;
89
+ padding: 12px 20px;
90
+ border: none;
91
+ border-radius: 50px;
92
+ font-size: 1rem;
93
+ background: rgba(255, 255, 255, 0.9);
94
+ backdrop-filter: blur(10px);
95
+ color: #495057;
96
+ cursor: pointer;
97
+ transition: all 0.3s ease;
98
+ }
99
+
100
+ .category-dropdown:focus {
101
+ outline: none;
102
+ background: white;
103
+ box-shadow: 0 5px 20px rgba(0,0,0,0.1);
104
+ }
105
+
106
+ .content {
107
+ padding: 40px;
108
+ }
109
+
110
+ .features-grid {
111
+ display: grid;
112
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
113
+ gap: 25px;
114
+ margin-top: 30px;
115
+ }
116
+
117
+ .feature-card {
118
+ background: white;
119
+ border-radius: 15px;
120
+ padding: 30px;
121
+ border: 1px solid #e9ecef;
122
+ transition: all 0.3s ease;
123
+ cursor: pointer;
124
+ text-decoration: none;
125
+ color: inherit;
126
+ display: block;
127
+ position: relative;
128
+ overflow: hidden;
129
+ }
130
+
131
+ .feature-card::before {
132
+ content: '';
133
+ position: absolute;
134
+ top: 0;
135
+ left: 0;
136
+ right: 0;
137
+ height: 4px;
138
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
139
+ transform: scaleX(0);
140
+ transition: transform 0.3s ease;
141
+ }
142
+
143
+ .feature-card:hover {
144
+ transform: translateY(-5px);
145
+ box-shadow: 0 15px 40px rgba(0,0,0,0.1);
146
+ }
147
+
148
+ .feature-card:hover::before {
149
+ transform: scaleX(1);
150
+ }
151
+
152
+ .feature-card.coming-soon {
153
+ opacity: 0.6;
154
+ cursor: not-allowed;
155
+ }
156
+
157
+ .feature-card.coming-soon:hover {
158
+ transform: none;
159
+ box-shadow: none;
160
+ }
161
+
162
+ .feature-header {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 15px;
166
+ margin-bottom: 15px;
167
+ }
168
+
169
+ .feature-icon {
170
+ font-size: 2.5rem;
171
+ width: 60px;
172
+ height: 60px;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ background: linear-gradient(135deg, #667eea15, #764ba215);
177
+ border-radius: 12px;
178
+ }
179
+
180
+ .feature-card h3 {
181
+ color: #495057;
182
+ font-size: 1.4rem;
183
+ font-weight: 600;
184
+ }
185
+
186
+ .feature-card p {
187
+ color: #6c757d;
188
+ margin-bottom: 20px;
189
+ line-height: 1.6;
190
+ }
191
+
192
+ .feature-status {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 10px;
196
+ margin-bottom: 15px;
197
+ }
198
+
199
+ .status-indicator {
200
+ width: 12px;
201
+ height: 12px;
202
+ border-radius: 50%;
203
+ background: #28a745;
204
+ }
205
+
206
+ .status-indicator.busy {
207
+ background: #ffc107;
208
+ animation: pulse 2s infinite;
209
+ }
210
+
211
+ .status-indicator.coming-soon {
212
+ background: #6c757d;
213
+ }
214
+
215
+ @keyframes pulse {
216
+ 0% { transform: scale(1); opacity: 1; }
217
+ 50% { transform: scale(1.2); opacity: 0.7; }
218
+ 100% { transform: scale(1); opacity: 1; }
219
+ }
220
+
221
+ .feature-tags {
222
+ display: flex;
223
+ flex-wrap: wrap;
224
+ gap: 8px;
225
+ margin-top: 15px;
226
+ }
227
+
228
+ .tag {
229
+ background: #f8f9fa;
230
+ color: #495057;
231
+ padding: 4px 12px;
232
+ border-radius: 20px;
233
+ font-size: 0.8rem;
234
+ border: 1px solid #e9ecef;
235
+ }
236
+
237
+ .coming-soon-badge {
238
+ position: absolute;
239
+ top: 15px;
240
+ right: 15px;
241
+ background: #6c757d;
242
+ color: white;
243
+ padding: 4px 12px;
244
+ border-radius: 20px;
245
+ font-size: 0.8rem;
246
+ font-weight: 500;
247
+ }
248
+
249
+ .stat-item h4 {
250
+ color: #667eea;
251
+ font-size: 1.5rem;
252
+ margin-bottom: 5px;
253
+ }
254
+
255
+ .stat-item p {
256
+ color: #6c757d;
257
+ font-size: 0.9rem;
258
+ }
259
+
260
+ .no-results {
261
+ text-align: center;
262
+ padding: 60px 20px;
263
+ color: #6c757d;
264
+ }
265
+
266
+ .no-results .icon {
267
+ font-size: 4rem;
268
+ margin-bottom: 20px;
269
+ opacity: 0.5;
270
+ }
271
+
272
+ .feature-path {
273
+ color: #667eea;
274
+ font-size: 0.9rem;
275
+ font-weight: 500;
276
+ margin-bottom: 5px;
277
+ }
278
+
279
+ @media (max-width: 768px) {
280
+ .header h1 {
281
+ font-size: 2rem;
282
+ }
283
+
284
+ .header p {
285
+ font-size: 1rem;
286
+ }
287
+
288
+ .content {
289
+ padding: 20px;
290
+ }
291
+
292
+ .features-grid {
293
+ grid-template-columns: 1fr;
294
+ }
295
+ }
296
+ </style>
297
+ </head>
298
+ <body>
299
+ <div class="container">
300
+ <div class="header">
301
+ <h1>🛠️ Tools Collection</h1>
302
+ <p>Simple, fast, and reliable utility tools for your daily needs</p>
303
+
304
+ <div class="search-container">
305
+ <input type="text" id="search-box" class="search-box" placeholder="Search tools... (e.g., image, pdf, convert)">
306
+ <span class="search-icon">🔍</span>
307
+ </div>
308
+
309
+ <div class="filter-container">
310
+ <select id="category-dropdown" class="category-dropdown">
311
+ <option value="">All Categories</option>
312
+ </select>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="content">
317
+ <div class="features-grid" id="features-grid">
318
+ <!-- Features will be populated by JavaScript -->
319
+ </div>
320
+
321
+ <div class="no-results" id="no-results" style="display: none;">
322
+ <div class="icon">🔍</div>
323
+ <h3>No tools found</h3>
324
+ <p>Try searching with different keywords like "image", "pdf", or "convert"</p>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ <script>
330
+ let allFeatures = {};
331
+ let expandedFeatures = [];
332
+ let currentCategory = '';
333
+ let currentSearch = '';
334
+
335
+ // Load features on page load
336
+ document.addEventListener('DOMContentLoaded', async () => {
337
+ await loadFeatures();
338
+ setupSearch();
339
+ setupCategoryFilter();
340
+ updateFeatureStatuses();
341
+
342
+ // Update statuses every 5 seconds
343
+ setInterval(updateFeatureStatuses, 5000);
344
+ });
345
+
346
+ async function loadFeatures() {
347
+ try {
348
+ const response = await fetch('/api/features');
349
+ if (response.ok) {
350
+ const data = await response.json();
351
+ allFeatures = data.features;
352
+ expandFeatures();
353
+ populateCategories();
354
+ displayFeatures(expandedFeatures);
355
+ }
356
+ } catch (error) {
357
+ console.error('Error loading features:', error);
358
+ // Fallback to mock data for demo
359
+ loadMockData();
360
+ }
361
+ }
362
+
363
+ function loadMockData() {
364
+ allFeatures = {
365
+ "image": {
366
+ "name": "Image Tools",
367
+ "description": "HEIC to PNG/JPG conversion and metadata removal",
368
+ "icon": "🖼️",
369
+ "features": ["convert", "remove_metadata"],
370
+ "folder": "image",
371
+ "tags": ["image", "heic", "png", "jpg", "convert", "metadata"]
372
+ },
373
+ "pdf": {
374
+ "name": "PDF Tools",
375
+ "description": "Convert images to PDF, merge PDFs, and more",
376
+ "icon": "📄",
377
+ "features": ["images_to_pdf"],
378
+ "folder": "pdf",
379
+ "tags": ["pdf", "merge", "convert", "images", "document"],
380
+ "coming_soon": true
381
+ },
382
+ "audio": {
383
+ "name": "Audio Tools",
384
+ "description": "Convert audio formats and compress audio files",
385
+ "icon": "🎵",
386
+ "features": ["convert_audio", "compress_audio"],
387
+ "folder": "audio",
388
+ "tags": ["audio", "music", "convert", "compress", "mp3", "wav"],
389
+ "coming_soon": true
390
+ },
391
+ "video": {
392
+ "name": "Video Tools",
393
+ "description": "Basic video editing and format conversion",
394
+ "icon": "🎬",
395
+ "features": ["convert_video", "compress_video"],
396
+ "folder": "video",
397
+ "tags": ["video", "convert", "compress", "mp4", "avi", "editing"],
398
+ "coming_soon": true
399
+ }
400
+ };
401
+ expandFeatures();
402
+ populateCategories();
403
+ displayFeatures(expandedFeatures);
404
+ }
405
+
406
+ function expandFeatures() {
407
+ expandedFeatures = [];
408
+
409
+ Object.entries(allFeatures).forEach(([categoryKey, categoryData]) => {
410
+ // Add the main category
411
+ // expandedFeatures.push({
412
+ // id: categoryKey,
413
+ // path: categoryKey,
414
+ // name: categoryData.name,
415
+ // description: categoryData.description,
416
+ // icon: categoryData.icon,
417
+ // tags: categoryData.tags || [],
418
+ // coming_soon: categoryData.coming_soon || false,
419
+ // category: categoryKey,
420
+ // type: 'category'
421
+ // });
422
+
423
+ // Add individual features
424
+ if (categoryData.features && categoryData.features.length > 0) {
425
+ categoryData.features.forEach(feature => {
426
+ expandedFeatures.push({
427
+ id: `${categoryKey}-${feature}`,
428
+ path: `${categoryKey}/${feature}`,
429
+ name: `${categoryData.name} - ${formatFeatureName(feature)}`,
430
+ description: getFeatureDescription(categoryKey, feature),
431
+ icon: categoryData.icon,
432
+ tags: [...(categoryData.tags || []), feature],
433
+ coming_soon: categoryData.coming_soon || false,
434
+ category: categoryKey,
435
+ type: 'feature'
436
+ });
437
+ });
438
+ }
439
+ });
440
+ }
441
+
442
+ function formatFeatureName(feature) {
443
+ return feature.split('_')
444
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
445
+ .join(' ');
446
+ }
447
+
448
+ function getFeatureDescription(category, feature) {
449
+ const descriptions = {
450
+ 'image': {
451
+ 'convert': 'Convert HEIC images to PNG/JPG format',
452
+ 'remove_metadata': 'Remove metadata from image files'
453
+ },
454
+ 'pdf': {
455
+ 'images_to_pdf': 'Convert multiple images into a single PDF'
456
+ },
457
+ 'audio': {
458
+ 'convert_audio': 'Convert between different audio formats',
459
+ 'compress_audio': 'Reduce audio file size while maintaining quality'
460
+ },
461
+ 'video': {
462
+ 'convert_video': 'Convert between different video formats',
463
+ 'compress_video': 'Reduce video file size while maintaining quality'
464
+ }
465
+ };
466
+
467
+ return descriptions[category]?.[feature] || `${formatFeatureName(feature)} functionality`;
468
+ }
469
+
470
+ function populateCategories() {
471
+ const dropdown = document.getElementById('category-dropdown');
472
+ const categories = Object.keys(allFeatures);
473
+
474
+ categories.forEach(category => {
475
+ const option = document.createElement('option');
476
+ option.value = category;
477
+ option.textContent = allFeatures[category].name;
478
+ dropdown.appendChild(option);
479
+ });
480
+ }
481
+
482
+ function displayFeatures(features) {
483
+ const grid = document.getElementById('features-grid');
484
+ const noResults = document.getElementById('no-results');
485
+
486
+ if (features.length === 0) {
487
+ grid.style.display = 'none';
488
+ noResults.style.display = 'block';
489
+ return;
490
+ }
491
+
492
+ grid.style.display = 'grid';
493
+ noResults.style.display = 'none';
494
+
495
+ grid.innerHTML = features.map((feature) => {
496
+ const isComingSoon = feature.coming_soon || false;
497
+ const href = isComingSoon ? '#' : `/${feature.path}`;
498
+
499
+ return `
500
+ <a href="${href}" class="feature-card ${isComingSoon ? 'coming-soon' : ''}"
501
+ ${isComingSoon ? 'onclick="return false;"' : ''}>
502
+ ${isComingSoon ? '<div class="coming-soon-badge">Coming Soon</div>' : ''}
503
+
504
+ <div class="feature-path">${feature.path}</div>
505
+
506
+ <div class="feature-header">
507
+ <div class="feature-icon">${feature.icon}</div>
508
+ <h3>${feature.name}</h3>
509
+ </div>
510
+
511
+ <p>${feature.description}</p>
512
+
513
+ <div class="feature-status">
514
+ <div class="status-indicator ${isComingSoon ? 'coming-soon' : ''}"
515
+ id="${feature.id}-status"></div>
516
+ <span id="${feature.id}-status-text">${isComingSoon ? 'Coming Soon' : 'Ready'}</span>
517
+ </div>
518
+
519
+ <div class="feature-tags">
520
+ ${feature.tags ? feature.tags.map(tag => `<span class="tag">${tag}</span>`).join('') : ''}
521
+ </div>
522
+ </a>
523
+ `;
524
+ }).join('');
525
+ }
526
+
527
+ function setupSearch() {
528
+ const searchBox = document.getElementById('search-box');
529
+ let debounceTimer;
530
+
531
+ searchBox.addEventListener('input', (e) => {
532
+ clearTimeout(debounceTimer);
533
+ debounceTimer = setTimeout(() => {
534
+ currentSearch = e.target.value.toLowerCase();
535
+ filterFeatures();
536
+ }, 300);
537
+ });
538
+ }
539
+
540
+ function setupCategoryFilter() {
541
+ const dropdown = document.getElementById('category-dropdown');
542
+
543
+ dropdown.addEventListener('change', (e) => {
544
+ currentCategory = e.target.value;
545
+ filterFeatures();
546
+ });
547
+ }
548
+
549
+ function filterFeatures() {
550
+ let filteredFeatures = expandedFeatures;
551
+
552
+ // Filter by category
553
+ if (currentCategory) {
554
+ filteredFeatures = filteredFeatures.filter(feature =>
555
+ feature.category === currentCategory
556
+ );
557
+ }
558
+
559
+ // Filter by search term
560
+ if (currentSearch) {
561
+ filteredFeatures = filteredFeatures.filter(feature =>
562
+ feature.name.toLowerCase().includes(currentSearch) ||
563
+ feature.description.toLowerCase().includes(currentSearch) ||
564
+ feature.path.toLowerCase().includes(currentSearch) ||
565
+ feature.tags.some(tag => tag.toLowerCase().includes(currentSearch))
566
+ );
567
+ }
568
+
569
+ displayFeatures(filteredFeatures);
570
+ }
571
+
572
+ async function updateFeatureStatuses() {
573
+ for (const feature of expandedFeatures) {
574
+ if (feature.coming_soon) continue;
575
+
576
+ try {
577
+ const response = await fetch(`/api/status`);
578
+ if (response.ok) {
579
+ const status = await response.json();
580
+ updateFeatureStatus(feature.id, status);
581
+ }
582
+ } catch (error) {
583
+ console.error(`Error checking status for ${feature.id}:`, error);
584
+ }
585
+ }
586
+ }
587
+
588
+ function updateFeatureStatus(featureId, status) {
589
+ const indicator = document.getElementById(`${featureId}-status`);
590
+ const text = document.getElementById(`${featureId}-status-text`);
591
+
592
+ if (!indicator || !text) return;
593
+
594
+ // For demo purposes, randomly show some as busy
595
+ const isBusy = Math.random() < 0.1;
596
+
597
+ if (isBusy) {
598
+ indicator.classList.add('busy');
599
+ text.textContent = 'Processing...';
600
+ } else {
601
+ indicator.classList.remove('busy');
602
+ text.textContent = 'Ready';
603
+ }
604
+ }
605
+ </script>
606
+ </body>
607
+ </html>
main.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, File, UploadFile, HTTPException, Form, Query
2
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
5
+ from custom_logger import logger_config
6
+ from image.main import is_process_running as is_image_process_running
7
+ from image.image_base import ImageBase
8
+ from pydantic import BaseModel
9
+ from image.converter import Converter
10
+ import mimetypes
11
+
12
+ app = FastAPI(title="Tools Collection", description="Collection of utility tools")
13
+
14
+ # Create necessary directories
15
+ # os.makedirs("static", exist_ok=True)
16
+
17
+ # Mount static files
18
+ # app.mount("/static", StaticFiles(directory="static"), name="static")
19
+
20
+ # Templates
21
+ template_dirs = [".", "./image"]
22
+ env = Environment(
23
+ loader=FileSystemLoader(template_dirs),
24
+ autoescape=select_autoescape(['html', 'xml'])
25
+ )
26
+
27
+ # Available features
28
+ FEATURES = {
29
+ "image": {
30
+ "name": "Image Tools",
31
+ "description": "HEIC to PNG/JPG conversion and metadata removal",
32
+ "icon": "🖼️",
33
+ "features": ["convert", "remove_metadata"],
34
+ "folder": "image",
35
+ "tags": ["image", "heic", "png", "jpg", "convert", "metadata"],
36
+ "is_process_running": is_image_process_running
37
+ },
38
+ "pdf": {
39
+ "name": "PDF Tools",
40
+ "description": "Convert images to PDF, merge PDFs, and more",
41
+ "icon": "📄",
42
+ "features": ["images_to_pdf"],
43
+ "folder": "pdf",
44
+ "tags": ["pdf", "merge", "convert", "images", "document"],
45
+ "coming_soon": True,
46
+ "is_process_running": is_image_process_running
47
+ },
48
+ "audio": {
49
+ "name": "Audio Tools",
50
+ "description": "Convert audio formats and compress audio files",
51
+ "icon": "🎵",
52
+ "features": ["convert_audio", "compress_audio"],
53
+ "folder": "audio",
54
+ "tags": ["audio", "music", "convert", "compress", "mp3", "wav"],
55
+ "coming_soon": True,
56
+ "is_process_running": is_image_process_running
57
+ },
58
+ "video": {
59
+ "name": "Video Tools",
60
+ "description": "Basic video editing and format conversion",
61
+ "icon": "🎬",
62
+ "features": ["convert_video", "compress_video"],
63
+ "folder": "video",
64
+ "tags": ["video", "convert", "compress", "mp4", "avi", "editing"],
65
+ "coming_soon": True,
66
+ "is_process_running": is_image_process_running
67
+ }
68
+ }
69
+
70
+ # Routes
71
+ @app.get("/", response_class=HTMLResponse)
72
+ async def index(request: Request):
73
+ template = env.get_template("index.html") # From tool/
74
+ html_content = template.render(request=request)
75
+ return HTMLResponse(content=html_content)
76
+
77
+ @app.get("/image/convert", response_class=HTMLResponse)
78
+ async def image_tools(request: Request):
79
+ template = env.get_template("image/index.html") # From tool/image/
80
+ html_content = template.render(request=request)
81
+ return HTMLResponse(content=html_content)
82
+
83
+ @app.post("/image/upload")
84
+ async def upload_image(
85
+ id: str = Form(...),
86
+ image: UploadFile = File(...)
87
+ ):
88
+ try:
89
+ image_base = ImageBase()
90
+ image_base.upload(id, image)
91
+
92
+ # Return success response
93
+ return JSONResponse({
94
+ "success": True,
95
+ "message": "Image uploaded successfully"
96
+ })
97
+
98
+ except ValueError as ve:
99
+ logger_config.error(f"Validation error: {str(ve)}")
100
+ raise HTTPException(
101
+ status_code=400,
102
+ detail=str(ve)
103
+ )
104
+ except Exception as e:
105
+ logger_config.error(f"Unexpected error during upload: {str(e)}")
106
+ raise HTTPException(
107
+ status_code=500,
108
+ detail=f"Internal server error: {str(e)}"
109
+ )
110
+
111
+ @app.post("/image/convert")
112
+ async def convert_image(
113
+ id: str = Form(...),
114
+ to_format: str = Form(...)
115
+ ):
116
+ try:
117
+ converter = Converter()
118
+ output_path = converter.convert_image(id, to_format)
119
+
120
+ # Return success response
121
+ return JSONResponse({
122
+ "success": True,
123
+ "message": "Image uploaded successfully",
124
+ "new_filename": output_path.split("/")[-1]
125
+ })
126
+
127
+ except ValueError as ve:
128
+ logger_config.error(f"Validation error: {str(ve)}")
129
+ raise HTTPException(
130
+ status_code=400,
131
+ detail=str(ve)
132
+ )
133
+ except Exception as e:
134
+ logger_config.error(f"Unexpected error during upload: {str(e)}")
135
+ raise HTTPException(
136
+ status_code=500,
137
+ detail=f"Internal server error: {str(e)}"
138
+ )
139
+
140
+ @app.get("/image/download")
141
+ async def download_converted_image(
142
+ id: str = Query(...)
143
+ ):
144
+ try:
145
+ image_base = ImageBase()
146
+ file_path = image_base.download_url(id)
147
+
148
+ mime_type, _ = mimetypes.guess_type(file_path)
149
+
150
+ return FileResponse(file_path, media_type=mime_type, filename=id)
151
+
152
+ except ValueError as ve:
153
+ logger_config.error(f"Validation error: {str(ve)}")
154
+ raise HTTPException(
155
+ status_code=400,
156
+ detail=str(ve)
157
+ )
158
+ except Exception as e:
159
+ logger_config.error(f"Unexpected error during upload: {str(e)}")
160
+ raise HTTPException(
161
+ status_code=500,
162
+ detail=f"Internal server error: {str(e)}"
163
+ )
164
+
165
+ class DeleteRequest(BaseModel):
166
+ ids: list[str]
167
+
168
+ @app.post("/image/delete")
169
+ async def delete_images(request: DeleteRequest):
170
+ try:
171
+ image_base = ImageBase()
172
+ image_base.delete(request.ids)
173
+
174
+ # Return success response
175
+ return JSONResponse({
176
+ "success": True,
177
+ "message": "Image deleted successfully"
178
+ })
179
+
180
+ except ValueError as ve:
181
+ logger_config.error(f"Validation error: {str(ve)}")
182
+ raise HTTPException(
183
+ status_code=400,
184
+ detail=str(ve)
185
+ )
186
+ except Exception as e:
187
+ logger_config.error(f"Unexpected error during upload: {str(e)}")
188
+ raise HTTPException(
189
+ status_code=500,
190
+ detail=f"Internal server error: {str(e)}"
191
+ )
192
+
193
+ @app.get("/api/features")
194
+ async def get_features():
195
+ """Get available features"""
196
+ return {"features": FEATURES}
197
+
198
+ @app.get("/api/search")
199
+ async def search_features(q: str = ""):
200
+ """Search features by name, description, or tags"""
201
+ if not q:
202
+ return {"features": FEATURES}
203
+
204
+ q = q.lower()
205
+ filtered_features = {}
206
+
207
+ for key, feature in FEATURES.items():
208
+ # Search in name, description, and tags
209
+ search_text = f"{feature['name']} {feature['description']} {' '.join(feature.get('tags', []))}".lower()
210
+ if q in search_text:
211
+ filtered_features[key] = feature
212
+
213
+ return {"features": filtered_features}
214
+
215
+ @app.get("/api/status")
216
+ async def get_feature_status():
217
+ """Get feature status"""
218
+ features_status = {}
219
+ for key in FEATURES.keys():
220
+ process_data = FEATURES[key]["is_process_running"]()
221
+ features_status[key] = {
222
+ "feature_name": process_data["feature"] if process_data else None,
223
+ "is_busy": process_data is not None,
224
+ "process_id": process_data["process_id"] if process_data else None
225
+ }
226
+
227
+ return features_status
228
+
229
+ if __name__ == "__main__":
230
+ import uvicorn
231
+ uvicorn.run(app, host="0.0.0.0", port=8000)
main_base.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ from custom_logger import logger_config
3
+ import os
4
+ from pathlib import Path
5
+
6
+ class MainBase:
7
+ def __init__(self):
8
+ self.max_file_size = 10 * 1024 * 1024
9
+
10
+ def upload_validate(self, image):
11
+ # Reset file pointer to beginning before reading
12
+ image.file.seek(0)
13
+ contents = image.file.read()
14
+ # Reset file pointer again for later use
15
+ image.file.seek(0)
16
+
17
+ if len(contents) > self.max_file_size:
18
+ raise ValueError(f"{image.filename} is too large. Max size: {self.max_file_size // (1024*1024)}MB.")
19
+
20
+ def upload(self, id, image):
21
+ self.upload_validate(image)
22
+ # Create full file path with filename
23
+ file_path = os.path.join(self.input_dir, id)
24
+
25
+ with open(file_path, "wb") as buffer:
26
+ shutil.copyfileobj(image.file, buffer)
27
+
28
+ logger_config.success(f"{image.filename} uploaded successfully to {file_path}")
29
+
30
+ def _generate_output_path(self, output_format=None) -> str:
31
+ if output_format:
32
+ new_ext = self.supported_formats.get(output_format, output_format).lower()
33
+ name_without_ext = os.path.splitext(self.input_file_name)[0]
34
+ return f"{self.output_dir}/{name_without_ext}.{new_ext}"
35
+ return f"{self.output_dir}/{self.input_file_name}"
36
+
37
+ def download_url(self, file_id):
38
+ self.delete([file_id], only_input=True)
39
+
40
+ file_path = f'{self.output_dir}/{file_id}'
41
+ if os.path.exists(file_path):
42
+ return file_path
43
+ raise ValueError("File not found")
44
+
45
+ def delete(self, ids, only_input=False):
46
+ for file_id in ids:
47
+ name_without_ext = os.path.splitext(file_id)[0]
48
+ for filename in os.listdir(self.input_dir):
49
+ if name_without_ext in filename:
50
+ file_path = os.path.join(self.input_dir, filename)
51
+ path_obj = Path(file_path)
52
+ path_obj.unlink(missing_ok=True)
53
+
54
+ if not only_input:
55
+ for filename in os.listdir(self.output_dir):
56
+ if name_without_ext in filename:
57
+ file_path = os.path.join(self.output_dir, filename)
58
+ path_obj = Path(file_path)
59
+ path_obj.unlink(missing_ok=True)
process_exception.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ class ProcessAlreadyRunning(Exception):
2
+ def __init__(self, message, data):
3
+ super().__init__(message)
4
+ self.data = data
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/jebin2/custom_logger.git
2
+ fastapi==0.104.1
3
+ uvicorn[standard]==0.24.0
4
+ python-multipart==0.0.6
5
+ pillow==10.1.0
6
+ pillow-heif==0.13.0
7
+ aiofiles==23.2.1
8
+ jinja2==3.1.2