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

remove metadata issue fix

Browse files
Dockerfile CHANGED
@@ -4,6 +4,8 @@ FROM python:3.10-slim
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)
 
4
  # Install system dependencies
5
  RUN apt-get update && apt-get install -y \
6
  git libheif-dev pkg-config \
7
+ libglib2.0-0 \
8
+ libasound2 libasound2-dev \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
  # Create user (Hugging Face Spaces requirement)
image/{index.html β†’ convert.html} RENAMED
@@ -5,6 +5,7 @@
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;
@@ -366,18 +367,18 @@
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
  }
@@ -492,8 +493,6 @@
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>
@@ -528,7 +527,7 @@
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>
@@ -537,7 +536,7 @@
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">
@@ -551,366 +550,8 @@
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>
 
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
+ <script src="/image/javascript/image.js"></script>
9
  <style>
10
  * {
11
  margin: 0;
 
367
  justify-content: center;
368
  }
369
 
370
+ .process-button {
371
  background: linear-gradient(135deg, #28a745, #20c997);
372
  color: white;
373
  box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
374
  }
375
 
376
+ .process-button:hover:not(:disabled) {
377
  transform: translateY(-2px);
378
  box-shadow: 0 8px 25px rgba(40, 167, 69, 0.4);
379
  }
380
 
381
+ .process-button:disabled {
382
  opacity: 0.6;
383
  cursor: not-allowed;
384
  }
 
493
  <span class="upload-icon">πŸ“</span>
494
  <div class="upload-text">
495
  <h3>Drop your images here</h3>
 
 
496
  <button class="upload-button">
497
  Choose Files
498
  </button>
 
527
 
528
  <div class="progress-section" id="progress-section">
529
  <h3 class="progress-title">
530
+ πŸ”„ Processing Images...
531
  </h3>
532
  <div class="progress-bar-container">
533
  <div class="progress-bar" id="progress-bar"></div>
 
536
  </div>
537
 
538
  <div class="action-buttons">
539
+ <button class="action-button process-button" id="process-button" disabled>
540
  πŸ”„ Convert Images
541
  </button>
542
  <button class="action-button clear-button" id="clear-button">
 
550
  </div>
551
 
552
  <script>
553
+ function secret_sauce_url() {
554
+ return '/image/convert';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  }
556
  </script>
557
  </body>
image/image_base.py CHANGED
@@ -6,14 +6,12 @@ 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',
@@ -66,69 +64,4 @@ class ImageBase(MainBase):
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}")
 
6
  from pathlib import Path
7
 
8
  class ImageBase(MainBase):
9
+ def __init__(self, feature=None):
10
  super().__init__()
11
  self.current_path = os.path.abspath(os.path.join(os.path.dirname(__file__)))
12
  self.feature = feature
 
13
  self.input_dir = os.path.join(self.current_path, "input")
14
  self.output_dir = os.path.join(self.current_path, "output")
 
15
  self.supported_formats = {
16
  'jpg': 'JPEG',
17
  'jpeg': 'JPEG',
 
64
 
65
  def upload_validate(self, image):
66
  super().upload_validate(image)
67
+ self._validate_input_file(image=image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
image/javascript/image.js ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ let selectedFiles = [];
3
+ let completedFiles = [];
4
+
5
+ // Initialize the application
6
+ document.addEventListener('DOMContentLoaded', function () {
7
+ setupEventListeners();
8
+ });
9
+
10
+ function setupEventListeners() {
11
+ const uploadSection = document.getElementById('upload-section');
12
+ const fileInput = document.getElementById('file-input');
13
+ const processButton = document.getElementById('process-button');
14
+ const clearButton = document.getElementById('clear-button');
15
+ const downloadButton = document.getElementById('download-button');
16
+
17
+ // File upload events
18
+ uploadSection.addEventListener('click', () => fileInput.click());
19
+ uploadSection.addEventListener('dragover', handleDragOver);
20
+ uploadSection.addEventListener('dragleave', handleDragLeave);
21
+ uploadSection.addEventListener('drop', handleDrop);
22
+ fileInput.addEventListener('change', handleFileSelect);
23
+
24
+ // Button events
25
+ processButton.addEventListener('click', processButtonFn);
26
+ clearButton.addEventListener('click', clearAllFiles);
27
+ downloadButton.addEventListener('click', downloadAllFiles);
28
+ }
29
+
30
+ function showMessage(message, type = 'error') {
31
+ const errorMsg = document.getElementById('error-message');
32
+ const successMsg = document.getElementById('success-message');
33
+
34
+ // Hide both messages first
35
+ errorMsg.style.display = 'none';
36
+ successMsg.style.display = 'none';
37
+
38
+ if (type === 'error') {
39
+ errorMsg.textContent = message;
40
+ errorMsg.style.display = 'block';
41
+ } else {
42
+ successMsg.textContent = message;
43
+ successMsg.style.display = 'block';
44
+ }
45
+
46
+ // Auto-hide after 5 seconds
47
+ setTimeout(() => {
48
+ errorMsg.style.display = 'none';
49
+ successMsg.style.display = 'none';
50
+ }, 5000);
51
+ }
52
+
53
+ function handleDragOver(e) {
54
+ e.preventDefault();
55
+ e.currentTarget.classList.add('dragover');
56
+ }
57
+
58
+ function handleDragLeave(e) {
59
+ e.preventDefault();
60
+ e.currentTarget.classList.remove('dragover');
61
+ }
62
+
63
+ function handleDrop(e) {
64
+ e.preventDefault();
65
+ const uploadSection = e.currentTarget;
66
+ uploadSection.classList.remove('dragover');
67
+
68
+ const files = Array.from(e.dataTransfer.files);
69
+ processFiles(files);
70
+ }
71
+
72
+ function handleFileSelect(e) {
73
+ const files = Array.from(e.target.files);
74
+ processFiles(files);
75
+ }
76
+
77
+ function generateUniqueId(file, length = 12) {
78
+ ext = "." + file.name.split(".")[file.name.split(".").length - 1]
79
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
80
+ return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('') + ext;
81
+ }
82
+
83
+ function saveFileIdToStorage(id) {
84
+ let fileIds = JSON.parse(localStorage.getItem('uploadedFileIds') || '[]');
85
+ if (!fileIds.includes(id)) {
86
+ fileIds.push(id);
87
+ localStorage.setItem('uploadedFileIds', JSON.stringify(fileIds));
88
+ }
89
+ }
90
+
91
+ function processFiles(files) {
92
+ files.forEach((file, index) => {
93
+ const id = generateUniqueId(file); // generate unique ID
94
+ const fileData = {
95
+ id: id,
96
+ file: file,
97
+ name: file.name,
98
+ size: formatFileSize(file.size),
99
+ status: 'ready',
100
+ preview: null
101
+ };
102
+
103
+ // Store file ID for potential server deletion later
104
+ saveFileIdToStorage(id);
105
+
106
+ // Create preview for images
107
+ const reader = new FileReader();
108
+ reader.onload = (e) => {
109
+ fileData.preview = e.target.result;
110
+ updateFileCard(fileData.id);
111
+ };
112
+ reader.readAsDataURL(file);
113
+
114
+ selectedFiles.push(fileData);
115
+ });
116
+
117
+ updateUI();
118
+ }
119
+
120
+ function updateUI() {
121
+ const filesPreview = document.getElementById('files-preview');
122
+ const processButton = document.getElementById('process-button');
123
+
124
+ if (selectedFiles.length > 0) {
125
+ filesPreview.style.display = 'block';
126
+ processButton.disabled = false;
127
+ renderFileCards();
128
+ } else {
129
+ filesPreview.style.display = 'none';
130
+ processButton.disabled = true;
131
+ }
132
+ }
133
+
134
+ function renderFileCards() {
135
+ const filesGrid = document.getElementById('files-grid');
136
+
137
+ filesGrid.innerHTML = selectedFiles.map(file => `
138
+ <div class="file-card" id="file-${file.id}">
139
+ <button class="remove-button" onclick="removeFile('${file.id}')">Γ—</button>
140
+ ${window.location.pathname == '/image/remove_metadata' && file.other_info ? `<button class="remove-button" style="top: 40px; background: #17a2b8;" onclick="showMetadata('${file.id}')">i</button>`: ``}
141
+
142
+ <div class="file-preview">
143
+ ${file.preview ? `<img src="${file.preview}" alt="${file.name}">` : '<span class="file-icon">πŸ–ΌοΈ</span>'}
144
+ </div>
145
+
146
+ <div class="file-info">
147
+ <h4>${file.name}</h4>
148
+ <p>${file.size}</p>
149
+
150
+ <div class="file-status">
151
+ <div class="status-indicator ${file.status === 'processing' ? 'processing' : file.status === 'error' ? 'error' : ''}"></div>
152
+ <span>${getStatusText(file.status)}</span>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ `).join('');
157
+ }
158
+
159
+ function updateFileCard(fileId) {
160
+ const file = selectedFiles.find(f => f.id === fileId);
161
+ if (!file) return;
162
+
163
+ const card = document.getElementById(`file-${fileId}`);
164
+ if (!card) return;
165
+
166
+ const preview = card.querySelector('.file-preview');
167
+ if (file.preview && preview) {
168
+ preview.innerHTML = `<img src="${file.preview}" alt="${file.name}">`;
169
+ }
170
+ }
171
+
172
+ async function processButtonFn() {
173
+ if (selectedFiles.length === 0) return;
174
+
175
+ const progressSection = document.getElementById('progress-section');
176
+ const progressBar = document.getElementById('progress-bar');
177
+ const progressText = document.getElementById('progress-text');
178
+ const processButton = document.getElementById('process-button');
179
+
180
+ // Show progress section
181
+ progressSection.style.display = 'block';
182
+ processButton.disabled = true;
183
+ const old_message = processButton.innerHTML;
184
+ processButton.innerHTML = 'πŸ”„ Processing...';
185
+ const settings = getSettingsData();
186
+ completedFiles = [];
187
+
188
+ for (let i = 0; i < selectedFiles.length; i++) {
189
+ const file = selectedFiles[i];
190
+
191
+ // Update file status
192
+ file.status = 'uploading';
193
+ renderFileCards();
194
+
195
+ try {
196
+ // Upload and process the file
197
+ await upload(file);
198
+ file.status = 'processing';
199
+ renderFileCards();
200
+ const processedFile = await secret_sauce(file, settings);
201
+ file.status = 'completed';
202
+ file.other_info = processedFile.other_info;
203
+ renderFileCards();
204
+ completedFiles.push(processedFile);
205
+ } catch (error) {
206
+ console.error('Processing error:', error);
207
+ file.status = 'error';
208
+ showMessage(`Error processing ${file.name}: ${error.message}`, 'error');
209
+ }
210
+
211
+ // Update progress
212
+ const progress = Math.round(((i + 1) / selectedFiles.length) * 100);
213
+ progressBar.style.width = progress + '%';
214
+ progressText.textContent = `${progress}% complete (${i + 1}/${selectedFiles.length})`;
215
+
216
+ renderFileCards();
217
+ }
218
+
219
+ // Completion
220
+ processButton.disabled = false;
221
+ processButton.innerHTML = old_message;
222
+
223
+ const successfulProcessing = completedFiles.length;
224
+ if (successfulProcessing > 0) {
225
+ document.getElementById('download-button').style.display = 'inline-flex';
226
+ showMessage(`Successfully processed ${successfulProcessing} image(s)!`, 'success');
227
+ }
228
+
229
+ setTimeout(() => {
230
+ progressSection.style.display = 'none';
231
+ }, 1000);
232
+ }
233
+
234
+ async function secret_sauce(file, settings) {
235
+ const formData = new FormData();
236
+ formData.append('id', file.id);
237
+ for (let key of Object.keys(settings)) {
238
+ if (settings[key]) {
239
+ formData.append('to_format', settings[key]);
240
+ }
241
+ }
242
+
243
+ try {
244
+ const response = await fetch(secret_sauce_url(), {
245
+ method: 'POST',
246
+ body: formData
247
+ });
248
+
249
+ if (!response.ok) {
250
+ const errorData = await response.json().catch(() => ({}));
251
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
252
+ }
253
+
254
+ const result = await response.json();
255
+
256
+ return {
257
+ id: file.id,
258
+ originalName: file.name,
259
+ processedName: result.new_filename,
260
+ url: "/image/download?id=" + result.new_filename,
261
+ other_info: result.other_info
262
+ };
263
+ } catch (error) {
264
+ throw new Error(`Failed to process ${file.name}: ${error.message}`);
265
+ }
266
+ }
267
+
268
+ function downloadAllFiles() {
269
+ if (completedFiles.length === 0) return;
270
+
271
+ // Download each completedFiles file
272
+ completedFiles.forEach((file, index) => {
273
+ setTimeout(() => {
274
+ const link = document.createElement('a');
275
+ link.href = file.url;
276
+ link.download = file.processedName;
277
+ link.target = '_blank';
278
+ document.body.appendChild(link);
279
+ link.click();
280
+ document.body.removeChild(link);
281
+ }, index * 100);
282
+ });
283
+ }
284
+
285
+ function formatFileSize(bytes) {
286
+ if (bytes === 0) return '0 Bytes';
287
+ const k = 1024;
288
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
289
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
290
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
291
+ }
292
+
293
+ function removeFile(fileId) {
294
+ selectedFiles = selectedFiles.filter(file => file.id !== fileId);
295
+ completedFiles = completedFiles.filter(file => file.id !== fileId);
296
+ updateUI();
297
+
298
+ if (selectedFiles.length === 0) {
299
+ document.getElementById('download-button').style.display = 'none';
300
+ }
301
+ }
302
+
303
+ async function clearAllFiles() {
304
+ const fileIds = JSON.parse(localStorage.getItem('uploadedFileIds') || '[]');
305
+
306
+ if (fileIds.length > 0) {
307
+ try {
308
+ await fetch('/image/delete', {
309
+ method: 'POST',
310
+ headers: {
311
+ 'Content-Type': 'application/json'
312
+ },
313
+ body: JSON.stringify({ ids: fileIds })
314
+ });
315
+ } catch (err) {
316
+ console.error('Error sending delete request:', err);
317
+ }
318
+ }
319
+
320
+ // Clear everything locally
321
+ selectedFiles = [];
322
+ completedFiles = [];
323
+ updateUI();
324
+ document.getElementById('download-button').style.display = 'none';
325
+ document.getElementById('progress-section').style.display = 'none';
326
+ localStorage.removeItem('uploadedFileIds');
327
+ }
328
+
329
+ async function upload(file) {
330
+ const formData = new FormData();
331
+ formData.append('image', file.file);
332
+ formData.append('id', file.id);
333
+
334
+ try {
335
+ const response = await fetch('/image/upload', {
336
+ method: 'POST',
337
+ body: formData
338
+ });
339
+
340
+ if (!response.ok) {
341
+ const errorData = await response.json().catch(() => ({}));
342
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
343
+ }
344
+ return true;
345
+ } catch (error) {
346
+ throw new Error(`Failed to upload ${file.name}: ${error.message}`);
347
+ }
348
+ }
349
+
350
+ function getSettingsData() {
351
+ return {
352
+ to_format: document.getElementById('output-format')?.value
353
+ };
354
+ }
355
+
356
+ function getStatusText(status) {
357
+ switch (status) {
358
+ case 'ready': return 'Ready';
359
+ case 'uploading': return 'Uploading...';
360
+ case 'processing': return window.location.pathname == '/image/remove_metadata' ? 'Removing metadata...' : 'Converting...';
361
+ case 'completed': return 'Processed';
362
+ case 'error': return 'Error';
363
+ default: return 'Unknown';
364
+ }
365
+ }
366
+ function showMetadata(fileId) {
367
+ const file = selectedFiles.find(f => f.id === fileId);
368
+ const contentEl = document.getElementById('metadata-content');
369
+ if (file && file.other_info) {
370
+ contentEl.textContent = JSON.stringify(file.other_info, null, 2);
371
+ document.getElementById('metadata-modal').style.display = 'block';
372
+ document.getElementById('modal-backdrop').style.display = 'block';
373
+ } else {
374
+ contentEl.textContent = 'No metadata removed or unavailable.';
375
+ document.getElementById('metadata-modal').style.display = 'block';
376
+ document.getElementById('modal-backdrop').style.display = 'block';
377
+ }
378
+ }
379
+
380
+ function closeMetadataModal() {
381
+ document.getElementById('metadata-modal').style.display = 'none';
382
+ document.getElementById('modal-backdrop').style.display = 'none';
383
+ }
image/remove_background.py CHANGED
@@ -10,50 +10,41 @@ 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
 
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
+ super().__init__("remove_background")
22
+
23
+ def remove_background(self, input_file_name: str) -> str:
24
+ try:
25
+ self.input_file_name = input_file_name
26
+ self.input_file_path = f'{self.input_dir}/{self.input_file_name}'
27
+ # Validate input
28
+ self._validate_input_file()
29
+
30
+ # Generate output path
31
+ output_path = self._generate_output_path()
32
+
33
+ logger_config.info(f"Removing background from: {self.input_file_path}")
34
+
35
+ with open(self.input_file_path, 'rb') as infile:
36
+ input_image = infile.read()
37
+
38
+ # Perform background removal
39
+ output_image = remove(input_image)
40
+
41
+ with open(output_path, 'wb') as outfile:
42
+ outfile.write(output_image)
43
+
44
+ logger_config.info(f"Background removed successfully: {output_path}")
45
+
46
+ return output_path
47
+
48
+ except Exception as e:
49
+ logger_config.error(f"Background removal failed: {str(e)}")
50
+ return None
 
 
 
 
 
 
 
 
 
image/remove_metadata.html ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Remove Metadata - Tools Collection</title>
8
+ <script src="/image/javascript/image.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+ min-height: 100vh;
20
+ padding: 20px;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1000px;
25
+ margin: 0 auto;
26
+ background: white;
27
+ border-radius: 20px;
28
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
29
+ overflow: hidden;
30
+ }
31
+
32
+ .header {
33
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
34
+ color: white;
35
+ padding: 40px 30px;
36
+ text-align: center;
37
+ position: relative;
38
+ }
39
+
40
+ .back-button {
41
+ position: absolute;
42
+ left: 30px;
43
+ top: 50%;
44
+ transform: translateY(-50%);
45
+ background: rgba(255, 255, 255, 0.2);
46
+ color: white;
47
+ border: none;
48
+ padding: 12px 20px;
49
+ border-radius: 50px;
50
+ cursor: pointer;
51
+ font-size: 1rem;
52
+ transition: all 0.3s ease;
53
+ text-decoration: none;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ }
58
+
59
+ .back-button:hover {
60
+ background: rgba(255, 255, 255, 0.3);
61
+ transform: translateY(-50%) translateX(-2px);
62
+ }
63
+
64
+ .header-content {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ gap: 20px;
69
+ margin-bottom: 20px;
70
+ }
71
+
72
+ .header-text h1 {
73
+ font-size: 2.5rem;
74
+ margin-bottom: 10px;
75
+ font-weight: 300;
76
+ }
77
+
78
+ .header-text p {
79
+ font-size: 1.1rem;
80
+ opacity: 0.9;
81
+ }
82
+
83
+ .content {
84
+ padding: 40px;
85
+ }
86
+
87
+ .upload-section {
88
+ background: #f8f9fa;
89
+ border-radius: 15px;
90
+ padding: 40px;
91
+ text-align: center;
92
+ margin-bottom: 30px;
93
+ border: 2px dashed #dee2e6;
94
+ transition: all 0.3s ease;
95
+ cursor: pointer;
96
+ }
97
+
98
+ .upload-section:hover {
99
+ border-color: #667eea;
100
+ background: #f0f4ff;
101
+ }
102
+
103
+ .upload-section.dragover {
104
+ border-color: #667eea;
105
+ background: #e8f2ff;
106
+ transform: scale(1.02);
107
+ }
108
+
109
+ .upload-icon {
110
+ font-size: 4rem;
111
+ color: #667eea;
112
+ margin-bottom: 20px;
113
+ display: block;
114
+ }
115
+
116
+ .upload-text h3 {
117
+ color: #495057;
118
+ margin-bottom: 10px;
119
+ font-size: 1.5rem;
120
+ }
121
+
122
+ .upload-text p {
123
+ color: #6c757d;
124
+ margin-bottom: 20px;
125
+ }
126
+
127
+ .upload-button {
128
+ background: linear-gradient(135deg, #667eea, #764ba2);
129
+ color: white;
130
+ border: none;
131
+ padding: 15px 30px;
132
+ border-radius: 50px;
133
+ font-size: 1.1rem;
134
+ cursor: pointer;
135
+ transition: all 0.3s ease;
136
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
137
+ }
138
+
139
+ .upload-button:hover {
140
+ transform: translateY(-2px);
141
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
142
+ }
143
+
144
+ .file-input {
145
+ display: none;
146
+ }
147
+
148
+ .settings-section {
149
+ background: white;
150
+ border-radius: 15px;
151
+ padding: 30px;
152
+ margin-bottom: 30px;
153
+ border: 1px solid #e9ecef;
154
+ }
155
+
156
+ .settings-title {
157
+ color: #495057;
158
+ font-size: 1.3rem;
159
+ margin-bottom: 20px;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 10px;
163
+ }
164
+
165
+ .settings-grid {
166
+ display: grid;
167
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
168
+ gap: 20px;
169
+ }
170
+
171
+ .setting-group {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 8px;
175
+ }
176
+
177
+ .setting-label {
178
+ color: #495057;
179
+ font-weight: 500;
180
+ font-size: 0.95rem;
181
+ }
182
+
183
+ .setting-input {
184
+ padding: 12px 15px;
185
+ border: 2px solid #e9ecef;
186
+ border-radius: 10px;
187
+ font-size: 1rem;
188
+ transition: all 0.3s ease;
189
+ }
190
+
191
+ .setting-input:focus {
192
+ outline: none;
193
+ border-color: #667eea;
194
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
195
+ }
196
+
197
+ .files-preview {
198
+ margin-top: 30px;
199
+ }
200
+
201
+ .preview-title {
202
+ color: #495057;
203
+ font-size: 1.3rem;
204
+ margin-bottom: 20px;
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 10px;
208
+ }
209
+
210
+ .files-grid {
211
+ display: grid;
212
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
213
+ gap: 20px;
214
+ }
215
+
216
+ .file-card {
217
+ background: white;
218
+ border-radius: 12px;
219
+ padding: 20px;
220
+ border: 1px solid #e9ecef;
221
+ transition: all 0.3s ease;
222
+ position: relative;
223
+ }
224
+
225
+ .file-card:hover {
226
+ transform: translateY(-2px);
227
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
228
+ }
229
+
230
+ .file-preview {
231
+ width: 100%;
232
+ height: 120px;
233
+ background: #f8f9fa;
234
+ border-radius: 8px;
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ margin-bottom: 15px;
239
+ overflow: hidden;
240
+ }
241
+
242
+ .file-preview img {
243
+ max-width: 100%;
244
+ max-height: 100%;
245
+ object-fit: cover;
246
+ border-radius: 8px;
247
+ }
248
+
249
+ .file-icon {
250
+ font-size: 2rem;
251
+ color: #667eea;
252
+ }
253
+
254
+ .file-info h4 {
255
+ color: #495057;
256
+ font-size: 0.9rem;
257
+ margin-bottom: 5px;
258
+ word-break: break-all;
259
+ }
260
+
261
+ .file-info p {
262
+ color: #6c757d;
263
+ font-size: 0.8rem;
264
+ margin-bottom: 10px;
265
+ }
266
+
267
+ .file-status {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ margin-bottom: 10px;
272
+ }
273
+
274
+ .status-indicator {
275
+ width: 8px;
276
+ height: 8px;
277
+ border-radius: 50%;
278
+ background: #28a745;
279
+ }
280
+
281
+ .status-indicator.processing {
282
+ background: #ffc107;
283
+ animation: pulse 2s infinite;
284
+ }
285
+
286
+ .status-indicator.error {
287
+ background: #dc3545;
288
+ }
289
+
290
+ .remove-button {
291
+ position: absolute;
292
+ top: 10px;
293
+ right: 10px;
294
+ background: #dc3545;
295
+ color: white;
296
+ border: none;
297
+ width: 24px;
298
+ height: 24px;
299
+ border-radius: 50%;
300
+ cursor: pointer;
301
+ font-size: 0.8rem;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ transition: all 0.3s ease;
306
+ }
307
+
308
+ .remove-button:hover {
309
+ background: #c82333;
310
+ transform: scale(1.1);
311
+ }
312
+
313
+ .progress-section {
314
+ margin-top: 30px;
315
+ display: none;
316
+ }
317
+
318
+ .progress-title {
319
+ color: #495057;
320
+ font-size: 1.2rem;
321
+ margin-bottom: 15px;
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 10px;
325
+ }
326
+
327
+ .progress-bar-container {
328
+ background: #e9ecef;
329
+ border-radius: 10px;
330
+ height: 12px;
331
+ overflow: hidden;
332
+ margin-bottom: 10px;
333
+ }
334
+
335
+ .progress-bar {
336
+ background: linear-gradient(135deg, #28a745, #20c997);
337
+ height: 100%;
338
+ width: 0%;
339
+ transition: width 0.3s ease;
340
+ border-radius: 10px;
341
+ }
342
+
343
+ .progress-text {
344
+ color: #6c757d;
345
+ font-size: 0.9rem;
346
+ text-align: center;
347
+ }
348
+
349
+ .action-buttons {
350
+ display: flex;
351
+ gap: 15px;
352
+ justify-content: center;
353
+ margin-top: 30px;
354
+ }
355
+
356
+ .action-button {
357
+ padding: 15px 30px;
358
+ border: none;
359
+ border-radius: 50px;
360
+ font-size: 1.1rem;
361
+ cursor: pointer;
362
+ transition: all 0.3s ease;
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 10px;
366
+ min-width: 150px;
367
+ justify-content: center;
368
+ }
369
+
370
+ .process-button {
371
+ background: linear-gradient(135deg, #dc3545, #c82333);
372
+ color: white;
373
+ box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
374
+ }
375
+
376
+ .process-button:hover:not(:disabled) {
377
+ transform: translateY(-2px);
378
+ box-shadow: 0 8px 25px rgba(220, 53, 69, 0.4);
379
+ }
380
+
381
+ .process-button:disabled {
382
+ opacity: 0.6;
383
+ cursor: not-allowed;
384
+ }
385
+
386
+ .clear-button {
387
+ background: #6c757d;
388
+ color: white;
389
+ }
390
+
391
+ .clear-button:hover {
392
+ background: #5a6268;
393
+ transform: translateY(-2px);
394
+ }
395
+
396
+ .download-button {
397
+ background: linear-gradient(135deg, #007bff, #0056b3);
398
+ color: white;
399
+ box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
400
+ }
401
+
402
+ .download-button:hover {
403
+ transform: translateY(-2px);
404
+ box-shadow: 0 8px 25px rgba(0, 123, 255, 0.4);
405
+ }
406
+
407
+ .error-message {
408
+ background: #f8d7da;
409
+ color: #721c24;
410
+ padding: 15px;
411
+ border-radius: 10px;
412
+ margin-bottom: 20px;
413
+ border: 1px solid #f5c6cb;
414
+ display: none;
415
+ }
416
+
417
+ .success-message {
418
+ background: #d4edda;
419
+ color: #155724;
420
+ padding: 15px;
421
+ border-radius: 10px;
422
+ margin-bottom: 20px;
423
+ border: 1px solid #c3e6cb;
424
+ display: none;
425
+ }
426
+
427
+ .info-box {
428
+ background: #d1ecf1;
429
+ color: #0c5460;
430
+ padding: 15px;
431
+ border-radius: 10px;
432
+ margin-bottom: 20px;
433
+ border: 1px solid #bee5eb;
434
+ }
435
+
436
+ .info-box h4 {
437
+ margin-bottom: 8px;
438
+ font-size: 1rem;
439
+ }
440
+
441
+ .info-box p {
442
+ font-size: 0.9rem;
443
+ margin: 0;
444
+ }
445
+
446
+ .metadata-modal {
447
+ display: none;
448
+ position: fixed;
449
+ top: 50%;
450
+ left: 50%;
451
+ transform: translate(-50%, -50%);
452
+ background: white;
453
+ padding: 30px;
454
+ border-radius: 10px;
455
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
456
+ z-index: 9999;
457
+ max-width: 90%;
458
+ max-height: 80%;
459
+ overflow: auto;
460
+ word-wrap: break-word
461
+ }
462
+
463
+ .metadata-content {
464
+ white-space: pre-wrap;
465
+ margin-top: 15px;
466
+ }
467
+
468
+ .modal-backdrop {
469
+ display: none;
470
+ position: fixed;
471
+ top: 0;
472
+ left: 0;
473
+ width: 100%;
474
+ height: 100%;
475
+ background: rgba(0, 0, 0, 0.5);
476
+ z-index: 9998;
477
+ }
478
+
479
+ @keyframes pulse {
480
+ 0% {
481
+ transform: scale(1);
482
+ opacity: 1;
483
+ }
484
+
485
+ 50% {
486
+ transform: scale(1.05);
487
+ opacity: 0.8;
488
+ }
489
+
490
+ 100% {
491
+ transform: scale(1);
492
+ opacity: 1;
493
+ }
494
+ }
495
+
496
+ @media (max-width: 768px) {
497
+ .header-content {
498
+ flex-direction: column;
499
+ gap: 15px;
500
+ }
501
+
502
+ .header-text h1 {
503
+ font-size: 2rem;
504
+ }
505
+
506
+ .back-button {
507
+ position: static;
508
+ transform: none;
509
+ margin-bottom: 20px;
510
+ align-self: flex-start;
511
+ }
512
+
513
+ .content {
514
+ padding: 20px;
515
+ }
516
+
517
+ .action-buttons {
518
+ flex-direction: column;
519
+ align-items: center;
520
+ }
521
+ }
522
+ </style>
523
+ </head>
524
+
525
+ <body>
526
+ <div class="container">
527
+ <div class="header">
528
+ <a href="/" class="back-button">
529
+ ← Back to Tools
530
+ </a>
531
+
532
+ <div class="header-content">
533
+ <div class="header-text">
534
+ <h1>Remove Metadata</h1>
535
+ <p>Strip EXIF data and metadata from your images for privacy</p>
536
+ </div>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="content">
541
+ <div class="error-message" id="error-message"></div>
542
+ <div class="success-message" id="success-message"></div>
543
+
544
+ <div class="info-box">
545
+ <h4>πŸ”’ Privacy Protection</h4>
546
+ <p>This tool removes metadata including GPS location, camera settings, timestamps, and other sensitive
547
+ information from your images while preserving image quality.</p>
548
+ </div>
549
+
550
+ <div class="upload-section" id="upload-section">
551
+ <span class="upload-icon">πŸ”’</span>
552
+ <div class="upload-text">
553
+ <h3>Drop your images here</h3>
554
+ <button class="upload-button">
555
+ Choose Files
556
+ </button>
557
+ </div>
558
+ <input type="file" id="file-input" class="file-input" multiple accept="image/*">
559
+ </div>
560
+
561
+ <div class="files-preview" id="files-preview" style="display: none;">
562
+ <h3 class="preview-title">
563
+ πŸ“‹ Selected Files
564
+ </h3>
565
+ <div class="files-grid" id="files-grid">
566
+ <!-- File cards will be inserted here -->
567
+ </div>
568
+ </div>
569
+
570
+ <div class="progress-section" id="progress-section">
571
+ <h3 class="progress-title">
572
+ πŸ”„ Processing Images...
573
+ </h3>
574
+ <div class="progress-bar-container">
575
+ <div class="progress-bar" id="progress-bar"></div>
576
+ </div>
577
+ <div class="progress-text" id="progress-text">0% complete</div>
578
+ </div>
579
+
580
+ <div class="action-buttons">
581
+ <button class="action-button process-button" id="process-button" disabled>
582
+ πŸ”’ Remove Metadata
583
+ </button>
584
+ <button class="action-button clear-button" id="clear-button">
585
+ πŸ—‘οΈ Clear All
586
+ </button>
587
+ <button class="action-button download-button" id="download-button" style="display: none;">
588
+ πŸ’Ύ Download All
589
+ </button>
590
+ </div>
591
+ </div>
592
+ </div>
593
+
594
+ <script>
595
+ function secret_sauce_url() {
596
+ return '/image/remove_metadata';
597
+ }
598
+ </script>
599
+ <div id="metadata-modal" class="metadata-modal">
600
+ <button onclick="closeMetadataModal()" class="remove-button">Γ—</button>
601
+ <h3>Removed Metadata</h3>
602
+ <pre id="metadata-content" class="metadata-content"></pre>
603
+ </div>
604
+ <div id="modal-backdrop" class="modal-backdrop" onclick="closeMetadataModal()"></div>
605
+
606
+ </body>
607
+
608
+ </html>
image/remove_metadata.py CHANGED
@@ -1,69 +1,160 @@
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
@@ -80,35 +171,41 @@ class RemoveMetadata(ImageBase):
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
@@ -120,19 +217,22 @@ class RemoveMetadata(ImageBase):
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
 
@@ -143,7 +243,7 @@ class RemoveMetadata(ImageBase):
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
@@ -153,13 +253,18 @@ class RemoveMetadata(ImageBase):
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
 
@@ -170,7 +275,7 @@ class RemoveMetadata(ImageBase):
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
@@ -181,17 +286,20 @@ class RemoveMetadata(ImageBase):
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
 
@@ -200,25 +308,30 @@ class RemoveMetadata(ImageBase):
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:
@@ -255,16 +368,25 @@ class RemoveMetadata(ImageBase):
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
@@ -275,29 +397,32 @@ class RemoveMetadata(ImageBase):
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'):
@@ -313,28 +438,23 @@ class RemoveMetadata(ImageBase):
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")
 
 
 
 
1
  """
2
  Image Metadata Remover Module
3
 
4
+ This module provides a comprehensive class-based metadata remover that strips ALL metadata
5
  from images while preserving original quality, format, and aspect ratio.
6
+ Handles EXIF, IPTC, XMP, ICC profiles, and all other embedded metadata for security/privacy.
7
  """
8
 
9
  from .image_base import ImageBase
10
  import os
11
  from custom_logger import logger_config
12
  from PIL import Image
13
+ from PIL.ExifTags import TAGS, GPSTAGS
14
+ from typing import Dict
15
+ import base64
16
 
17
  class RemoveMetadata(ImageBase):
18
  """
19
+ Comprehensive image metadata remover for security and privacy.
20
 
21
+ Removes ALL metadata types including EXIF, IPTC, XMP, ICC profiles, thumbnails,
22
+ GPS data, camera settings, and any other embedded information.
23
  """
24
  def __init__(self):
25
  super().__init__("remove_metadata")
 
26
 
27
+ def _extract_all_metadata(self, image: Image.Image):
28
  """
29
+ Extract ALL available metadata from the image for security analysis.
30
 
31
  Args:
32
  image: PIL Image object
33
 
34
  Returns:
35
+ Dictionary containing comprehensive metadata extraction
36
  """
37
+ all_metadata = {}
 
 
 
 
 
 
38
 
39
+ # 1. Extract EXIF data (most common and detailed)
40
+ try:
41
+ if hasattr(image, '_getexif') and image._getexif():
42
+ exif_dict = image._getexif()
43
+ all_metadata['exif_raw'] = exif_dict
44
+
45
+ # Decode EXIF tags to human-readable format
46
+ exif_decoded = {}
47
+ for tag_id, value in exif_dict.items():
48
+ tag = TAGS.get(tag_id, tag_id)
49
+
50
+ # Special handling for GPS data
51
+ if tag == 'GPSInfo':
52
+ gps_data = {}
53
+ for gps_tag_id, gps_value in value.items():
54
+ gps_tag = GPSTAGS.get(gps_tag_id, gps_tag_id)
55
+ gps_data[gps_tag] = gps_value
56
+ exif_decoded[tag] = gps_data
57
+ else:
58
+ exif_decoded[tag] = value
59
+
60
+ all_metadata['exif_decoded'] = exif_decoded
61
+ except Exception as e:
62
+ all_metadata['exif_error'] = str(e)
63
 
64
+ # 2. Extract all image.info metadata (includes IPTC, XMP, ICC, etc.)
65
  if hasattr(image, 'info') and image.info:
66
+ info_data = {}
67
+ for key, value in image.info.items():
68
+ # Handle binary data by encoding it
69
+ if isinstance(value, bytes):
70
+ try:
71
+ # Try to decode as text first
72
+ info_data[key] = value.decode('utf-8', errors='ignore')
73
+ except:
74
+ # If that fails, encode as base64 for viewing
75
+ info_data[key] = f"<binary_data_base64>{base64.b64encode(value[:100]).decode()}</binary_data_base64>"
76
+ else:
77
+ info_data[key] = value
78
+ all_metadata['info'] = info_data
79
+
80
+ # 3. Extract ICC Profile (color management)
81
+ try:
82
+ if hasattr(image, 'info') and 'icc_profile' in image.info:
83
+ icc_profile = image.info['icc_profile']
84
+ all_metadata['icc_profile_size'] = len(icc_profile) if icc_profile else 0
85
+ all_metadata['icc_profile_present'] = bool(icc_profile)
86
+ except Exception as e:
87
+ all_metadata['icc_error'] = str(e)
88
+
89
+ # 4. Extract XMP metadata
90
+ try:
91
+ if hasattr(image, 'info') and 'xmp' in image.info:
92
+ xmp_data = image.info['xmp']
93
+ if isinstance(xmp_data, bytes):
94
+ all_metadata['xmp'] = xmp_data.decode('utf-8', errors='ignore')
95
+ else:
96
+ all_metadata['xmp'] = xmp_data
97
+ except Exception as e:
98
+ all_metadata['xmp_error'] = str(e)
99
+
100
+ # 5. Extract IPTC metadata
101
+ try:
102
+ if hasattr(image, 'info') and any(key.startswith('iptc') for key in image.info.keys()):
103
+ iptc_data = {k: v for k, v in image.info.items() if k.startswith('iptc')}
104
+ all_metadata['iptc'] = iptc_data
105
+ except Exception as e:
106
+ all_metadata['iptc_error'] = str(e)
107
+
108
+ # 6. Extract PIL tag and tag_v2 (alternative EXIF access)
109
+ try:
110
+ if hasattr(image, 'tag') and image.tag:
111
+ all_metadata['pil_tag'] = dict(image.tag)
112
+ except Exception as e:
113
+ all_metadata['pil_tag_error'] = str(e)
114
+
115
+ try:
116
+ if hasattr(image, 'tag_v2') and image.tag_v2:
117
+ all_metadata['pil_tag_v2'] = dict(image.tag_v2)
118
+ except Exception as e:
119
+ all_metadata['pil_tag_v2_error'] = str(e)
120
 
121
+ # 7. Extract quantization tables (JPEG compression info)
122
+ try:
123
+ if hasattr(image, 'quantization') and image.quantization:
124
+ all_metadata['quantization_tables'] = len(image.quantization)
125
+ except Exception as e:
126
+ all_metadata['quantization_error'] = str(e)
127
+
128
+ # 8. Extract embedded thumbnails
129
+ try:
130
+ if hasattr(image, 'info') and 'thumbnail' in image.info:
131
+ all_metadata['thumbnail_present'] = True
132
+ except:
133
+ pass
134
+
135
+ # 9. Extract text chunks (PNG specific)
136
+ try:
137
+ if image.format == 'PNG' and hasattr(image, 'text'):
138
+ all_metadata['png_text_chunks'] = dict(image.text)
139
+ except Exception as e:
140
+ all_metadata['png_text_error'] = str(e)
141
+
142
+ # 10. Extract basic image properties that might contain metadata
143
+ basic_props = {
144
+ 'format': getattr(image, 'format', None),
145
+ 'mode': getattr(image, 'mode', None),
146
+ 'size': getattr(image, 'size', None),
147
+ 'filename': getattr(image, 'filename', None),
148
+ 'format_description': getattr(image, 'format_description', None)
149
+ }
150
+ all_metadata['basic_properties'] = basic_props
151
+
152
+ return all_metadata
153
 
154
  def _get_quality_settings_for_format(self, format_name: str, original_mode: str) -> Dict:
155
  """
156
  Get optimal save settings for maximum quality preservation per format.
157
+ ALL metadata will be stripped regardless of format.
158
 
159
  Args:
160
  format_name: PIL format name
 
171
  'optimize': False, # Don't optimize to preserve quality
172
  'progressive': False, # Standard baseline JPEG
173
  'subsampling': 0, # No chroma subsampling
174
+ 'exif': b'', # Explicitly remove EXIF
175
+ 'icc_profile': None # Remove ICC profile
176
  }
177
 
178
  elif format_name == 'PNG':
179
  settings = {
180
  'optimize': False, # Don't optimize to preserve quality
181
+ 'compress_level': 1, # Minimal compression
182
+ 'icc_profile': None, # Remove ICC profile
183
+ 'pnginfo': None # Remove PNG info chunks
184
  }
185
 
186
  elif format_name == 'WEBP':
187
  settings = {
188
  'lossless': True, # Lossless compression
189
  'quality': 100, # Maximum quality
190
+ 'method': 6, # Best compression method
191
+ 'icc_profile': None, # Remove ICC profile
192
+ 'exif': b'' # Remove EXIF
193
  }
194
 
195
  elif format_name == 'TIFF':
196
  settings = {
197
+ 'compression': None, # No compression
198
+ 'icc_profile': None # Remove ICC profile
199
  }
200
 
201
  elif format_name in ['BMP', 'GIF']:
202
+ settings = {} # These formats have limited metadata anyway
203
 
204
  return settings
205
 
206
  def _clean_png_with_transparency(self, image: Image.Image, output_path: str) -> bool:
207
  """
208
+ Clean PNG image while preserving transparency and quality, removing ALL metadata.
209
 
210
  Args:
211
  image: PIL Image object
 
217
  try:
218
  original_mode = image.mode
219
 
220
+ # Create completely new clean image preserving only visual data
221
  if original_mode == 'P':
222
+ # Palette mode - preserve only palette and pixel data
223
  clean_img = Image.new(original_mode, image.size)
224
  if image.getpalette():
225
  clean_img.putpalette(image.getpalette())
226
  clean_img.paste(image, (0, 0))
227
  else:
228
+ # RGBA or LA mode - preserve only pixel data
229
  clean_img = Image.new(original_mode, image.size, (0, 0, 0, 0))
230
  clean_img.paste(image, (0, 0))
231
 
232
+ # Ensure no metadata is carried over
233
+ clean_img.info = {}
234
+
235
+ # Save with quality preservation and NO metadata
236
  save_settings = self._get_quality_settings_for_format('PNG', original_mode)
237
  clean_img.save(output_path, format='PNG', **save_settings)
238
 
 
243
 
244
  def _clean_jpeg_image(self, image: Image.Image, output_path: str) -> bool:
245
  """
246
+ Clean JPEG image while preserving maximum quality, removing ALL metadata.
247
 
248
  Args:
249
  image: PIL Image object
 
253
  True if successful
254
  """
255
  try:
256
+ # Convert to RGB if necessary and create clean copy
257
  if image.mode != 'RGB':
258
  clean_img = image.convert('RGB')
259
  else:
260
+ # Create new image to ensure no metadata transfer
261
+ clean_img = Image.new('RGB', image.size)
262
+ clean_img.paste(image, (0, 0))
263
 
264
+ # Ensure no metadata is carried over
265
+ clean_img.info = {}
266
+
267
+ # Save with maximum quality and absolutely NO metadata
268
  save_settings = self._get_quality_settings_for_format('JPEG', image.mode)
269
  clean_img.save(output_path, format='JPEG', **save_settings)
270
 
 
275
 
276
  def _clean_other_format(self, image: Image.Image, output_path: str, original_format: str) -> bool:
277
  """
278
+ Clean other image formats while preserving quality, removing ALL metadata.
279
 
280
  Args:
281
  image: PIL Image object
 
286
  True if successful
287
  """
288
  try:
289
+ # Create completely new image without any metadata transfer
290
  clean_img = Image.new(image.mode, image.size)
291
 
292
+ # Handle palette images (preserve only palette, not metadata)
293
  if image.mode == 'P' and image.getpalette():
294
  clean_img.putpalette(image.getpalette())
295
 
296
+ # Paste only pixel data
297
  clean_img.paste(image, (0, 0))
298
+
299
+ # Ensure absolutely no metadata is carried over
300
+ clean_img.info = {}
301
 
302
+ # Save with format-specific quality settings and no metadata
303
  save_settings = self._get_quality_settings_for_format(original_format, image.mode)
304
  clean_img.save(output_path, format=original_format, **save_settings)
305
 
 
308
  except Exception as e:
309
  raise Exception(f"{original_format} cleaning failed: {e}")
310
 
311
+ def _verify_metadata_removal(self, output_path) -> bool:
312
  """
313
+ Comprehensive verification that ALL metadata has been removed.
314
  Returns:
315
+ True if ALL metadata was successfully removed
316
  """
317
  try:
318
+ with Image.open(output_path) as img:
319
+ remaining_metadata = self._extract_all_metadata(img)
320
+
321
+ # Check for any remaining metadata beyond basic properties
322
+ sensitive_keys = ['exif_raw', 'exif_decoded', 'info', 'icc_profile_present',
323
+ 'xmp', 'iptc', 'pil_tag', 'pil_tag_v2', 'quantization_tables',
324
+ 'thumbnail_present', 'png_text_chunks']
325
 
326
+ remaining_sensitive = {k: v for k, v in remaining_metadata.items()
327
+ if k in sensitive_keys and v}
328
 
329
+ if remaining_sensitive:
330
+ logger_config.warning(f"WARNING: Sensitive metadata still present: {list(remaining_sensitive.keys())}")
331
+ logger_config.debug(f"Remaining metadata: {remaining_sensitive}")
332
  return False
333
  else:
334
+ logger_config.success("ALL metadata successfully removed - image is clean")
335
  return True
336
 
337
  except Exception as e:
 
368
  except Exception as e:
369
  raise Exception(f"Quality verification failed: {e}")
370
 
371
+ def process(self, input_file_name: str):
372
  """
373
+ Remove ALL metadata from an image for complete security and privacy protection.
374
+
375
+ This method removes:
376
+ - EXIF data (camera settings, GPS coordinates, timestamps)
377
+ - IPTC data (keywords, captions, copyright)
378
+ - XMP data (Adobe metadata)
379
+ - ICC color profiles
380
+ - Embedded thumbnails
381
+ - Quantization tables
382
+ - PNG text chunks
383
+ - Any other embedded metadata
384
 
385
  Args:
386
  input_file_name: Input image name
 
387
 
388
  Returns:
389
+ Tuple containing (output_path, extracted_metadata)
390
 
391
  Raises:
392
  FileNotFoundError: If input file doesn't exist
 
397
  self.input_file_name = input_file_name
398
  self.input_file_path = f'{self.input_dir}/{self.input_file_name}'
399
  # Validate input
400
+ self._validate_input_file()
401
 
402
  # Generate output path
403
  output_path = self._generate_output_path()
404
+
405
+ logger_config.info(f"Processing: {self.input_file_path}")
406
 
407
  # Open and analyze image
408
+ with Image.open(self.input_file_path) as image:
 
409
  original_format = image.format
410
  original_mode = image.mode
411
 
412
+ # Extract ALL metadata for security analysis
413
+ extracted_metadata = self._extract_all_metadata(image)
 
414
 
415
+ # Print comprehensive metadata analysis
416
+ logger_config.info("=== COMPREHENSIVE METADATA ANALYSIS ===")
417
+ if extracted_metadata:
418
+ for key, value in extracted_metadata.items():
419
+ if value: # Only show non-empty metadata
420
+ logger_config.info(f"{key}: {value}")
421
  else:
422
+ logger_config.info("No metadata found in original image")
423
+ logger_config.info("=== END METADATA ANALYSIS ===")
424
 
425
+ # Choose appropriate cleaning method based on format
426
  success = False
427
 
428
  if original_format == 'PNG' and original_mode in ('RGBA', 'LA', 'P'):
 
438
  raise Exception("Metadata cleaning failed")
439
 
440
  # Verify results
441
+ self._verify_image_quality(self.input_file_path, output_path)
442
 
443
+ # Comprehensive metadata removal verification
444
  self._verify_metadata_removal(output_path)
445
 
446
  logger_config.success(f"Cleaned image saved: {output_path}")
447
+ logger_config.success("ALL METADATA REMOVED - Image is secure for sharing")
448
+ return output_path, extracted_metadata
449
 
450
  except Exception as e:
451
  logger_config.error(f"Failed to clean image: {str(e)}")
452
+ return None, {}
 
 
 
 
 
 
 
 
 
 
453
 
454
  # Example usage
455
  if __name__ == "__main__":
456
  cleaner = RemoveMetadata()
457
+ cleaned_file, metadata = cleaner.process("image/input/test.png")
458
+ print(f"Output: {cleaned_file}")
459
+ print(f"Extracted metadata: {metadata}")
460
+ print("Image is now secure for sharing - all metadata removed!")
main.py CHANGED
@@ -7,15 +7,13 @@ 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"]
@@ -76,7 +74,13 @@ async def index(request: Request):
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
 
@@ -137,6 +141,35 @@ async def convert_image(
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(...)
 
7
  from image.image_base import ImageBase
8
  from pydantic import BaseModel
9
  from image.converter import Converter
10
+ from image.remove_metadata import RemoveMetadata
11
  import mimetypes
12
 
13
  app = FastAPI(title="Tools Collection", description="Collection of utility tools")
14
 
15
+ # Mount static/image at /image
16
+ app.mount("/image/javascript", StaticFiles(directory="image/javascript"), name="image")
 
 
 
17
 
18
  # Templates
19
  template_dirs = [".", "./image"]
 
74
 
75
  @app.get("/image/convert", response_class=HTMLResponse)
76
  async def image_tools(request: Request):
77
+ template = env.get_template("image/convert.html") # From tool/image/
78
+ html_content = template.render(request=request)
79
+ return HTMLResponse(content=html_content)
80
+
81
+ @app.get("/image/remove_metadata", response_class=HTMLResponse)
82
+ async def image_tools(request: Request):
83
+ template = env.get_template("image/remove_metadata.html") # From tool/image/
84
  html_content = template.render(request=request)
85
  return HTMLResponse(content=html_content)
86
 
 
141
  detail=f"Internal server error: {str(e)}"
142
  )
143
 
144
+ @app.post("/image/remove_metadata")
145
+ async def remove_metadata(
146
+ id: str = Form(...)
147
+ ):
148
+ try:
149
+ removeMetadata = RemoveMetadata()
150
+ output_path, metadata = removeMetadata.process(id)
151
+
152
+ # Return success response
153
+ return JSONResponse({
154
+ "success": True,
155
+ "message": "Image uploaded successfully",
156
+ "new_filename": output_path.split("/")[-1],
157
+ "other_info": metadata
158
+ })
159
+
160
+ except ValueError as ve:
161
+ logger_config.error(f"Validation error: {str(ve)}")
162
+ raise HTTPException(
163
+ status_code=400,
164
+ detail=str(ve)
165
+ )
166
+ except Exception as e:
167
+ logger_config.error(f"Unexpected error during remove_metadata: {str(e)}")
168
+ raise HTTPException(
169
+ status_code=500,
170
+ detail=f"Internal server error: {str(e)}"
171
+ )
172
+
173
  @app.get("/image/download")
174
  async def download_converted_image(
175
  id: str = Query(...)