Image Pro
Manual_Copy_Paste_Code.txt
==========================================================================
[FILE_START: README.md]
==========================================================================
# SNAPIX Image Processing Utility
**Developer:** Kunal Vishwas
**Version:** Enterprise Edition
**Platform:** Windows 10 / 11 (64-bit)
**Security Tier:** 100% Offline | Zero Admin Privileges | Zero PowerShell
---
## 1. Overview & Modules
SNAPIX is a high-performance offline image processing suite designed for enterprise workloads supporting batch processing up to 10+ million records.
### Module Capabilities
1. **All Image Extraction**
- Recursively extracts all images (`.jpg`, `.jpeg`, `.png`, `.gif`, `.bmp`, `.webp`, `.tiff`, `.tif`, `.pdf`) from up to 30 source folders or ZIP archives.
- Built-in deduplication (MD5 / SHA-256 / perceptual difference hashing).
- Checkpoint auto-recovery for resuming interrupted jobs.
2. **Selective Image Extraction (Partition by Separator `_`)**
- Matches and extracts images specified in an Excel sheet (Column A: Image names/patterns, Column B: Target folder values).
- **Partition folders by `_`**: When checked, splits folder strings on `_` and creates nested folder hierarchies (e.g. `FolderA_Photos_2026` -> `FolderA/Photos/2026/`).
- **Column C Status Report**: Automatically records `"Done"` (if extracted) or `"Left"` (if not found) into Column C under header `"Status"` upon completion.
3. **Generate Image Excel Report**
- Scans image directories and creates high-performance Excel manifests with streaming `SXSSFWorkbook` (supporting millions of entries across auto-split sheets).
4. **Inner ZIP Extraction**
- Scans nested `.zip` archives inside source directories and flattens/extracts contents.
5. **Split Images by Size**
- Splits massive image sets into chunked directories or ZIP packages by byte-size limits (e.g. 500MB, 1GB, 4GB).
6. **Image Format & Compression Converter**
- Batch converts between JPG, PNG, WEBP, BMP, and TIFF with adjustable quality/compression parameters using TwelveMonkeys SPI.
7. **Image Bulk Renamer**
- Renames image collections according to an Excel mapping table (Column A: Original Name, Column B: New Name).
8. **Image Health & Validation Scanner**
- Performs deep image corruption checks, header validation, face detection bypass, and generates comprehensive Excel health reports.
---
## 2. Quick Start & Distribution
### For End-Users / Colleagues
To run the application, only two files are required:
- `Launch_Tool.bat`
- `SNAPIX_Image_Processing_Utility.jar`
Double-click `Launch_Tool.bat`. The launcher automatically sets JVM heap memory parameters and launches the application.
---
## 3. Building from Source
### Prerequisites
- JDK 8 or higher (e.g., Amazon Corretto JDK 24 / 21 / 17 / 11 / 8).
- JAR dependencies in `lib/` directory.
### Build Scripts
- **`dev_build.bat`**: Compiles source code in `src/` to `bin/` with Java 8 bytecode compatibility.
- **`build_jar.bat`**: Compiles code, merges dependencies, bundles assets (`background.jpg`, `FOX.gif`, `Users.txt`), and produces `SNAPIX_Image_Processing_Utility.jar`.
- **`generate_manual_code.bat`**: Generates single-file code bundle `Manual_Copy_Paste_Code.txt`.
- **`unpack_code.bat`**: Restores all source files from `Manual_Copy_Paste_Code.txt` in under 2 seconds.
---
## 4. User Authorization & Expiry Configuration
The tool includes offline security verification embedded inside the JAR (`Users.txt`):
```text
31/12/2026,kunal95,2744440,username
```
- **First Token**: Tool expiry date in `DD/MM/YYYY` format.
- **Subsequent Tokens**: Comma-separated authorized Windows Employee IDs / usernames.
- Lines starting with `#` or `//` are comments.
---
## 5. Enterprise Cybersecurity & Performance Architecture
- **100% Strictly Offline**: Zero network sockets, zero HTTP, zero remote tracking.
- **Standard User Space**: Operates without Administrator privileges.
- **CMD Native**: 0% PowerShell usage across all batch scripts.
- **Scalability**: Bounded thread pools with `CallerRunsPolicy`, streaming Excel I/O (`flushRows(100)`), and bounded hash caches to process 10M+ items without heap exhaustion.
[FILE_END]
==========================================================================
[FILE_START: pom.xml]
==========================================================================
<?xml version="1.0" encoding="UTF-8"?>
<!-- Dev: Kunal Vishwas -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.imageextractor</groupId>
<artifactId>image-processing-tool</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Image Processing Tool</name>
<description>Standalone offline image extraction, validation, conversion, and report tool</description>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<twelvemonkeys.version>3.10.1</twelvemonkeys.version>
<poi.version>5.2.3</poi.version>
</properties>
<dependencies>
<!-- TwelveMonkeys ImageIO Plugins -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-bmp</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-metadata</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common-image</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common-io</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common-lang</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<!-- UI Look and Feel -->
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.2.1</version>
</dependency>
<!-- Apache POI for Excel reporting -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- Apache Tika for MIME detection -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- Merges SPI service files from TwelveMonkeys and other dependencies (javax.imageio.spi.ImageReaderSpi, ImageWriterSpi, etc.) -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.imageextractor.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
[FILE_END]
==========================================================================
[FILE_START: Lib.txt]
==========================================================================
# ==============================================================================
# SNAPIX Image Processing Utility - Required JAR Dependencies (Lib.txt)
# Developer: Kunal Vishwas
# Platform: https://central.sonatype.com/
# Target Folder: lib/ (Place all 27 downloaded .jar files into the lib/ folder)
# ==============================================================================
[CATEGORY 1: TwelveMonkeys ImageIO Decoders (WebP, JPEG, BMP, Metadata)]
------------------------------------------------------------------------------
1. imageio-core-3.10.1.jar
- Artifact: com.twelvemonkeys.imageio : imageio-core : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.imageio/imageio-core/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-core/3.10.1/imageio-core-3.10.1.jar
2. imageio-jpeg-3.10.1.jar
- Artifact: com.twelvemonkeys.imageio : imageio-jpeg : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.imageio/imageio-jpeg/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-jpeg/3.10.1/imageio-jpeg-3.10.1.jar
3. imageio-bmp-3.10.1.jar
- Artifact: com.twelvemonkeys.imageio : imageio-bmp : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.imageio/imageio-bmp/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-bmp/3.10.1/imageio-bmp-3.10.1.jar
4. imageio-webp-3.10.1.jar
- Artifact: com.twelvemonkeys.imageio : imageio-webp : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.imageio/imageio-webp/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-webp/3.10.1/imageio-webp-3.10.1.jar
5. imageio-metadata-3.10.1.jar
- Artifact: com.twelvemonkeys.imageio : imageio-metadata : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.imageio/imageio-metadata/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/imageio/imageio-metadata/3.10.1/imageio-metadata-3.10.1.jar
6. common-image-3.10.1.jar
- Artifact: com.twelvemonkeys.common : common-image : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.common/common-image/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/common/common-image/3.10.1/common-image-3.10.1.jar
7. common-io-3.10.1.jar
- Artifact: com.twelvemonkeys.common : common-io : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.common/common-io/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/common/common-io/3.10.1/common-io-3.10.1.jar
8. common-lang-3.10.1.jar
- Artifact: com.twelvemonkeys.common : common-lang : 3.10.1
- Sonatype Central: https://central.sonatype.com/artifact/com.twelvemonkeys.common/common-lang/3.10.1
- Direct Download: https://repo1.maven.org/maven2/com/twelvemonkeys/common/common-lang/3.10.1/common-lang-3.10.1.jar
[CATEGORY 2: UI Look and Feel]
------------------------------------------------------------------------------
9. flatlaf-3.2.1.jar
- Artifact: com.formdev : flatlaf : 3.2.1
- Sonatype Central: https://central.sonatype.com/artifact/com.formdev/flatlaf/3.2.1
- Direct Download: https://repo1.maven.org/maven2/com/formdev/flatlaf/3.2.1/flatlaf-3.2.1.jar
[CATEGORY 3: Apache POI & Excel Reporting Engine]
------------------------------------------------------------------------------
10. poi-5.2.3.jar
- Artifact: org.apache.poi : poi : 5.2.3
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.poi/poi/5.2.3
- Direct Download: https://repo1.maven.org/maven2/org/apache/poi/poi/5.2.3/poi-5.2.3.jar
11. poi-ooxml-5.2.3.jar
- Artifact: org.apache.poi : poi-ooxml : 5.2.3
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.poi/poi-ooxml/5.2.3
- Direct Download: https://repo1.maven.org/maven2/org/apache/poi/poi-ooxml/5.2.3/poi-ooxml-5.2.3.jar
12. poi-ooxml-lite-5.2.3.jar
- Artifact: org.apache.poi : poi-ooxml-lite : 5.2.3
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.poi/poi-ooxml-lite/5.2.3
- Direct Download: https://repo1.maven.org/maven2/org/apache/poi/poi-ooxml-lite/5.2.3/poi-ooxml-lite-5.2.3.jar
13. xmlbeans-5.1.1.jar
- Artifact: org.apache.xmlbeans : xmlbeans : 5.1.1
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.xmlbeans/xmlbeans/5.1.1
- Direct Download: https://repo1.maven.org/maven2/org/apache/xmlbeans/xmlbeans/5.1.1/xmlbeans-5.1.1.jar
14. curvesapi-1.07.jar
- Artifact: com.github.virtuald : curvesapi : 1.07
- Sonatype Central: https://central.sonatype.com/artifact/com.github.virtuald/curvesapi/1.07
- Direct Download: https://repo1.maven.org/maven2/com/github/virtuald/curvesapi/1.07/curvesapi-1.07.jar
15. SparseBitSet-1.2.jar
- Artifact: com.zaxxer : SparseBitSet : 1.2
- Sonatype Central: https://central.sonatype.com/artifact/com.zaxxer/SparseBitSet/1.2
- Direct Download: https://repo1.maven.org/maven2/com/zaxxer/SparseBitSet/1.2/SparseBitSet-1.2.jar
[CATEGORY 4: Apache Commons Utilities]
------------------------------------------------------------------------------
16. commons-codec-1.15.jar
- Artifact: commons-codec : commons-codec : 1.15
- Sonatype Central: https://central.sonatype.com/artifact/commons-codec/commons-codec/1.15
- Direct Download: https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar
17. commons-collections4-4.4.jar
- Artifact: org.apache.commons : commons-collections4 : 4.4
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.commons/commons-collections4/4.4
- Direct Download: https://repo1.maven.org/maven2/org/apache/commons/commons-collections4/4.4/commons-collections4-4.4.jar
18. commons-compress-1.21.jar
- Artifact: org.apache.commons : commons-compress : 1.21
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.commons/commons-compress/1.21
- Direct Download: https://repo1.maven.org/maven2/org/apache/commons/commons-compress/1.21/commons-compress-1.21.jar
19. commons-io-2.11.0.jar
- Artifact: commons-io : commons-io : 2.11.0
- Sonatype Central: https://central.sonatype.com/artifact/commons-io/commons-io/2.11.0
- Direct Download: https://repo1.maven.org/maven2/commons-io/commons-io/2.11.0/commons-io-2.11.0.jar
20. commons-math3-3.6.1.jar
- Artifact: org.apache.commons : commons-math3 : 3.6.1
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.commons/commons-math3/3.6.1
- Direct Download: https://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar
[CATEGORY 5: MIME & File Type Detection]
------------------------------------------------------------------------------
21. tika-core-2.9.2.jar
- Artifact: org.apache.tika : tika-core : 2.9.2
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.tika/tika-core/2.9.2
- Direct Download: https://repo1.maven.org/maven2/org/apache/tika/tika-core/2.9.2/tika-core-2.9.2.jar
[CATEGORY 6: Native System Access (Keep-Awake & Memory Monitoring)]
------------------------------------------------------------------------------
22. jna-5.13.0.jar
- Artifact: net.java.dev.jna : jna : 5.13.0
- Sonatype Central: https://central.sonatype.com/artifact/net.java.dev.jna/jna/5.13.0
- Direct Download: https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar
23. jna-platform-5.13.0.jar
- Artifact: net.java.dev.jna : jna-platform : 5.13.0
- Sonatype Central: https://central.sonatype.com/artifact/net.java.dev.jna/jna-platform/5.13.0
- Direct Download: https://repo1.maven.org/maven2/net/java/dev/jna/jna-platform/5.13.0/jna-platform-5.13.0.jar
[CATEGORY 7: Logging Engine]
------------------------------------------------------------------------------
24. slf4j-api-2.0.13.jar
- Artifact: org.slf4j : slf4j-api : 2.0.13
- Sonatype Central: https://central.sonatype.com/artifact/org.slf4j/slf4j-api/2.0.13
- Direct Download: https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.13/slf4j-api-2.0.13.jar
25. slf4j-nop-2.0.13.jar
- Artifact: org.slf4j : slf4j-nop : 2.0.13
- Sonatype Central: https://central.sonatype.com/artifact/org.slf4j/slf4j-nop/2.0.13
- Direct Download: https://repo1.maven.org/maven2/org/slf4j/slf4j-nop/2.0.13/slf4j-nop-2.0.13.jar
26. log4j-api-2.19.0.jar
- Artifact: org.apache.logging.log4j : log4j-api : 2.19.0
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.19.0
- Direct Download: https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.19.0/log4j-api-2.19.0.jar
27. log4j-core-2.19.0.jar
- Artifact: org.apache.logging.log4j : log4j-core : 2.19.0
- Sonatype Central: https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.19.0
- Direct Download: https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.19.0/log4j-core-2.19.0.jar
# ==============================================================================
# Instructions:
# 1. Download each of the 27 JAR files from the direct download links above.
# 2. Save them into the 'lib' folder of the project.
# 3. Run 'dev_build.bat' to compile or 'build_jar.bat' to create the standalone executable.
# ==============================================================================
[FILE_END]
==========================================================================
[FILE_START: build_jar.bat]
==========================================================================
rem Dev: Kunal Vishwas
@echo off
cd /d "%~dp0"
echo ========================================================
echo Packaging Standalone Image Processing JAR
echo ========================================================
echo.
echo Compiling source code first...
call dev_build.bat
if %errorlevel% neq 0 (
echo.
echo Compilation failed! Packaging aborted.
pause
exit /b %errorlevel%
)
echo.
echo Extracting dependencies from lib folder and preserving SPI services...
cd bin
if exist ..\temp_services rd /s /q ..\temp_services 2>nul
mkdir ..\temp_services
for %%f in (..\lib\*.jar) do (
echo Extracting %%f
jar xf %%f
if exist META-INF\services (
for %%s in (META-INF\services\*) do (
type "%%s" >> "..\temp_services\%%~nxs" 2>nul
echo. >> "..\temp_services\%%~nxs"
)
)
)
echo.
echo Merging META-INF/services files across all dependencies...
if exist ..\temp_services (
if not exist META-INF\services mkdir META-INF\services
for %%s in (..\temp_services\*) do (
if exist "META-INF\services\%%~nxs" del "META-INF\services\%%~nxs"
for /f "usebackq tokens=1 delims= " %%L in ("%%s") do (
if not "%%L"=="" if not "%%L"=="ECHO" (
findstr /x /c:"%%L" "META-INF\services\%%~nxs" >nul 2>&1
if errorlevel 1 >>"META-INF\services\%%~nxs" echo %%L
)
)
)
rd /s /q ..\temp_services 2>nul
)
echo Removing signature files to prevent JAR security exceptions...
del /q /s META-INF\*.SF 2>nul
del /q /s META-INF\*.DSA 2>nul
del /q /s META-INF\*.RSA 2>nul
echo.
echo Adding background image to package...
if exist ..\background.jpg copy /Y ..\background.jpg background.jpg > nul 2>nul
if exist ..\background.png copy /Y ..\background.png background.png > nul 2>nul
echo Adding FOX.gif to package...
if exist ..\FOX.gif copy /Y ..\FOX.gif FOX.gif > nul 2>nul
if exist ..\fox.gif copy /Y ..\fox.gif FOX.gif > nul 2>nul
echo Bundling Users.txt authorization file into JAR...
if exist ..\Users.txt copy /y ..\Users.txt Users.txt >nul 2>nul
if exist ..\Users.text copy /y ..\Users.text Users.txt >nul 2>nul
if not exist Users.txt (
echo [INFO] Creating default Users.txt for current user...
(echo 31/12/2026,kunal95,2744440,%USERNAME%) > Users.txt
)
cd ..
echo.
echo Creating Standalone Fat JAR...
> MANIFEST.MF echo Manifest-Version: 1.0
>> MANIFEST.MF echo Main-Class: com.imageextractor.Main
jar cvfm SNAPIX_Image_Processing_Utility.jar MANIFEST.MF -C bin .
if %errorlevel% neq 0 (
echo.
echo Packaging failed!
del MANIFEST.MF
pause
exit /b %errorlevel%
)
echo.
echo Cleaning up temporary files...
del MANIFEST.MF
echo ========================================================
echo SUCCESS!
echo Your standalone executable is ready: SNAPIX_Image_Processing_Utility.jar
echo You can double click it or use Launch_Tool.bat to run it.
echo ========================================================
[FILE_END]
==========================================================================
[FILE_START: dev_build.bat]
==========================================================================
rem Dev: Kunal Vishwas
@echo off
setlocal enabledelayedexpansion
cd /d "%~dp0"
echo ========================================================
echo Image Processing Tool - Manual JDK Build
echo ========================================================
echo.
echo Make sure you have placed all downloaded .jar files into the `lib` folder!
echo.
echo Cleaning and creating output directory...
if exist bin rd /s /q bin
mkdir bin
echo Locating source files...
if exist sources.txt del sources.txt
set "PROJ_ROOT=%~dp0"
for /R src %%i in (*.java) do (
set "F=%%i"
set "F=!F:%PROJ_ROOT%=!"
set "F=!F:\=/!"
>> sources.txt echo "!F!"
)
if not exist sources.txt (
echo ERROR: No .java source files found under src\
exit /b 1
)
echo Compiling Java code...
javac --release 8 -encoding UTF-8 -cp "lib/*" -d bin @sources.txt > compile_log.txt 2>&1
set "COMPILE_STATUS=!errorlevel!"
if !COMPILE_STATUS! neq 0 (
echo Compilation with --release failed. Retrying with legacy source/target flags...
javac -source 8 -target 8 -encoding UTF-8 -cp "lib/*" -d bin @sources.txt > compile_log.txt 2>&1
set "COMPILE_STATUS=!errorlevel!"
)
type compile_log.txt
if exist compile_log.txt del compile_log.txt
if exist sources.txt del sources.txt
if !COMPILE_STATUS! neq 0 (
echo.
echo Compilation failed Please ensure all .jar files are in the lib folder and Corretto JDK is available.
exit /b !COMPILE_STATUS!
)
echo.
echo Build Successful You can now use Launch_Tool.bat to run the program.
exit /b 0
[FILE_END]
==========================================================================
[FILE_START: Launch_Tool.bat]
==========================================================================
rem Dev: Kunal Vishwas
@echo off
cd /d "%~dp0"
if not exist "logs" mkdir logs
echo.
echo ============================================================
echo SNAPIX IMAGE PROCESSING UTILITY
echo ============================================================
echo [LAUNCH] Executing SNAPIX_Image_Processing_Utility.jar
echo [JVM] Memory Configuration: Up to 8192m (64-bit Enterprise Mode)
echo [JVM] Character Encoding: UTF-8
echo [DIAGNOSTICS] Boot logs saved in: logs\app.log
echo.
set "JVM_FLAGS=-Xms512m -Xmx8192m -XX:+UseG1GC -Dfile.encoding=UTF-8"
set "JVM_FALLBACK=-Xms512m -Xmx2048m -Dfile.encoding=UTF-8"
if exist "SNAPIX_Image_Processing_Utility.jar" (
java %JVM_FLAGS% -jar SNAPIX_Image_Processing_Utility.jar
if errorlevel 1 (
echo [JVM] Retrying with standard memory allocation...
java %JVM_FALLBACK% -jar SNAPIX_Image_Processing_Utility.jar
)
) else if exist "ImageProcessingTool.jar" (
java %JVM_FLAGS% -jar ImageProcessingTool.jar
if errorlevel 1 (
echo [JVM] Retrying with standard memory allocation...
java %JVM_FALLBACK% -jar ImageProcessingTool.jar
)
) else (
java %JVM_FLAGS% -cp "bin;lib/*" com.imageextractor.Main
if errorlevel 1 (
echo [JVM] Retrying with standard memory allocation...
java %JVM_FALLBACK% -cp "bin;lib/*" com.imageextractor.Main
)
)
echo.
echo Process terminated. Press any key to close this console.
pause
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\Main.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor;
import com.imageextractor.ui.MainWindow;
import com.imageextractor.services.LogService;
import com.imageextractor.services.UserVerificationService;
import javax.swing.*;
import java.awt.*;
import javax.imageio.ImageIO;
public class Main {
public static void main(String[] args) {
// 1. Tool Expiry Check (FIRST)
if (UserVerificationService.isToolExpired()) {
String expiryMessage =
" ========================================================================================\n" +
" [!] THIS VERSION OF THE UTILITY HAS EXPIRED. PLEASE CONTACT KUNAL VISHWAS FOR AN UPDATE.\n" +
" ========================================================================================\n\n" +
" ERROR CODE : ITSALLABOUTError95";
if (!java.awt.GraphicsEnvironment.isHeadless()) {
try {
SwingUtilities.invokeAndWait(() -> {
JOptionPane.showMessageDialog(null, expiryMessage, "ITSALLABOUTError95", JOptionPane.ERROR_MESSAGE);
});
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Exception ignored) {}
} else {
System.err.println(expiryMessage);
}
System.exit(1);
}
// 2. User Authorization Check (SECOND)
if (!UserVerificationService.isCurrentUserAuthorized()) {
String currentUserId = UserVerificationService.getWindowsUsername();
String message = "Unauthorized User\n\n" +
"Your Windows Employee ID (" + (currentUserId.isEmpty() ? "Unknown" : currentUserId) + ") is not authorized to access this tool.\n" +
"Please contact Kunal Vishwas to request access.";
if (!java.awt.GraphicsEnvironment.isHeadless()) {
try {
SwingUtilities.invokeAndWait(() -> {
JOptionPane.showMessageDialog(null, message, "Unauthorized User", JOptionPane.ERROR_MESSAGE);
});
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Exception ignored) {}
} else {
System.err.println("[SECURITY ERROR] " + message);
}
System.exit(1);
}
// -- SYSTEM BOOT Diagnostics -------------------------------------------
printBootDiagnostics();
// Register a global default uncaught exception handler to prevent silent worker crashes
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
try {
LogService.log("FATAL", "ERROR", "Uncaught exception on thread: " + t.getName() + " - " + e.getMessage());
} catch (Throwable ignored) {}
e.printStackTrace();
// Graceful User Alert on the EDT
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(null,
"An unexpected fatal error occurred in the processing suite:\n" +
e.toString() + "\n\nPlease check logs/app.log for full diagnostic trace details.",
"Fatal Application Error",
JOptionPane.ERROR_MESSAGE);
});
});
// Waking TwelveMonkeys Dynamic SPI Plugins for standalone fat-jar classloaders
try {
ImageIO.scanForPlugins();
String[] formatNames = ImageIO.getReaderFormatNames();
java.util.Set<String> uniqueFormats = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER);
java.util.Collections.addAll(uniqueFormats, formatNames);
LogService.log("INIT", "INFO", "Registered ImageIO reader format names: " + String.join(", ", uniqueFormats));
} catch (Throwable t) {
LogService.log("INIT", "ERROR", "Failed to scan dynamic ImageIO plugins: " + t.getMessage());
}
// Setup modern stylish navy blue Look and Feel
try {
com.formdev.flatlaf.FlatDarkLaf.setup();
// Apply high-end modern curves (25px roundness)
UIManager.put("Component.focusWidth", 1);
UIManager.put("Button.arc", 25);
UIManager.put("Component.arc", 25);
UIManager.put("ProgressBar.arc", 25);
UIManager.put("TextComponent.arc", 25);
// Premium 60% Navy Blue & Neon Cyan/Emerald Green color accents
String navyDark = "#0a192f"; // Deep Navy
String cyanAccent = "#00b4d8"; // Glowing Cyan/Blue
UIManager.put("Component.accentColor", cyanAccent);
UIManager.put("TabbedPane.selectedBackground", Color.decode(navyDark));
UIManager.put("Button.background", Color.decode(navyDark));
UIManager.put("Button.foreground", Color.WHITE);
UIManager.put("Button.hoverBackground", Color.decode(cyanAccent));
// Force disable default purple colors in FlatDarkLaf
UIManager.put("TextArea.selectionBackground", new Color(0, 180, 216, 60));
UIManager.put("TextComponent.selectionBackground", new Color(0, 180, 216, 60));
UIManager.put("TextField.selectionBackground", new Color(0, 180, 216, 60));
UIManager.put("List.selectionBackground", new Color(0, 180, 216, 50));
UIManager.put("Table.selectionBackground", new Color(0, 180, 216, 50));
UIManager.put("ScrollBar.thumbColor", new Color(0, 180, 216, 80));
UIManager.put("ScrollBar.hoverThumbColor", new Color(0, 180, 216, 150));
UIManager.put("ScrollBar.pressedThumbColor", new Color(0, 180, 216, 200));
UIManager.put("ScrollBar.track", new Color(10, 25, 47, 80));
} catch (Exception ex) {
LogService.log("INIT", "WARNING", "Failed to initialize LaF: " + ex.getMessage());
}
// Automatically start system keep-awake heartbeat when tool is open
com.imageextractor.services.KeepAwakeService.start();
SwingUtilities.invokeLater(() -> {
MainWindow mainWindow = new MainWindow();
mainWindow.setVisible(true);
});
}
private static void printBootDiagnostics() {
try {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String ts = sdf.format(new java.util.Date());
Runtime rt = Runtime.getRuntime();
long maxMemMB = rt.maxMemory() / (1024 * 1024);
long totalMemMB= rt.totalMemory()/ (1024 * 1024);
long freeMemMB = rt.freeMemory() / (1024 * 1024);
int cpuCores = rt.availableProcessors();
String logFilePath = new java.io.File("logs" + java.io.File.separator + "app.log")
.getAbsolutePath();
String sep = "=".repeat(59);
String line = "\n" + sep +
"\n[" + ts + "][SYSTEM] ================= SYSTEM BOOT =================" +
"\nLog File Initialized at: " + logFilePath +
"\n[" + ts + "][INFO] SNAPIX IMAGE PROCESSING UTILITY - Starting up." +
"\n[" + ts + "][DIAG]" +
"\n=================== RUNTIME DIAGNOSTICS ===================" +
"\nTimestamp: " + ts +
"\nOS Name: " + System.getProperty("os.name", "Unknown") +
"\nOS Version: " + System.getProperty("os.version", "Unknown") +
"\nOS Architecture: " + System.getProperty("os.arch", "Unknown") +
"\nJava Version: " + System.getProperty("java.version", "Unknown") +
"\nJava Runtime: " + System.getProperty("java.runtime.name", "Unknown") +
"\nJVM Vendor: " + System.getProperty("java.vendor", "Unknown") +
"\nJAVA_HOME: " + System.getProperty("java.home", "Unknown") +
"\nUser Name: " + System.getProperty("user.name", "Unknown") +
"\nUser Home: " + System.getProperty("user.home", "Unknown") +
"\nCurrent Directory: " + System.getProperty("user.dir", "Unknown") +
"\nAvailable CPU Cores: " + cpuCores +
"\nMax Memory Allocation (Xmx): " + maxMemMB + " MB" +
"\nTotal Memory in Use: " + totalMemMB + " MB" +
"\nFree Memory Available: " + freeMemMB + " MB" +
"\nSystem Temporary Dir: " + System.getProperty("java.io.tmpdir", "Unknown") +
"\n===========================================================\n";
// Print to console (visible in cmd window)
System.out.println(line);
// Also write directly to log file so it appears at the top of app.log
LogService.log("SYSTEM", "INFO", "SNAPIX IMAGE PROCESSING UTILITY - Boot diagnostics logged.");
LogService.log("DIAG", "INFO", "OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " (" + System.getProperty("os.arch") + ")");
LogService.log("DIAG", "INFO", "Java: " + System.getProperty("java.version") + " | Vendor: " + System.getProperty("java.vendor"));
LogService.log("DIAG", "INFO", "JAVA_HOME: " + System.getProperty("java.home"));
LogService.log("DIAG", "INFO", "User: " + System.getProperty("user.name") + " | Home: " + System.getProperty("user.home"));
LogService.log("DIAG", "INFO", "CPU Cores: " + cpuCores + " | Xmx: " + maxMemMB + " MB | Total: " + totalMemMB + " MB | Free: " + freeMemMB + " MB");
LogService.log("DIAG", "INFO", "Temp Dir: " + System.getProperty("java.io.tmpdir"));
} catch (Throwable t) {
// Diagnostics failure must never block startup
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\DuplicateManager.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
public class DuplicateManager {
private static final String APP_HOME = System.getProperty("user.dir", ".");
private static final String CACHE_DIR = APP_HOME + File.separator + "cache";
private static final String CACHE_FILE = CACHE_DIR + File.separator + "hash_cache.dat";
private static final Map<String, CacheEntry> memoryCache = new ConcurrentHashMap<>();
private static final java.util.concurrent.atomic.AtomicBoolean isEvicting = new java.util.concurrent.atomic.AtomicBoolean(false);
static {
File dir = new File(CACHE_DIR);
if (!dir.exists() && !dir.mkdirs()) {
LogService.log("CACHE", "WARNING", "Could not create cache directory: " + dir.getAbsolutePath());
}
loadCache();
// Centralized JVM shutdown hook to guarantee duplicate cache persistence on exit
Runtime.getRuntime().addShutdownHook(new Thread(DuplicateManager::saveCache));
}
public static class CacheEntry implements Serializable {
private static final long serialVersionUID = 1L;
public final long lastModified;
public final String sha256;
public final long dHash;
public final String partialHash;
public CacheEntry(long lastModified, String sha256, long dHash, String partialHash) {
this.lastModified = lastModified;
this.sha256 = sha256;
this.dHash = dHash;
this.partialHash = partialHash;
}
}
@SuppressWarnings("unchecked")
private static void loadCache() {
File file = new File(CACHE_FILE);
if (!file.exists()) return;
try (FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis) {
@Override
protected Class<?> resolveClass(java.io.ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
if (!name.startsWith("com.imageextractor.") &&
!name.equals("java.util.HashMap") &&
!name.equals("java.util.Map") &&
!name.equals("java.lang.String") &&
!name.equals("java.lang.Long") &&
!name.equals("java.lang.Integer") &&
!name.equals("java.lang.Number")) {
throw new java.io.InvalidClassException("Unauthorized deserialization class: " + name);
}
return super.resolveClass(desc);
}
}) {
Map<String, CacheEntry> loaded = (Map<String, CacheEntry>) ois.readObject();
if (loaded != null) {
memoryCache.putAll(loaded);
}
} catch (Exception e) {
LogService.log("CACHE", "ERROR", "Failed to load hash cache: " + e.getMessage());
}
}
public static synchronized void saveCache() {
File file = new File(CACHE_FILE);
File tempFile = new File(CACHE_FILE + ".tmp");
try {
try (FileOutputStream fos = new FileOutputStream(tempFile);
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(new HashMap<>(memoryCache));
}
try {
java.nio.file.Files.move(tempFile.toPath(), file.toPath(),
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (java.nio.file.AtomicMoveNotSupportedException amse) {
java.nio.file.Files.move(tempFile.toPath(), file.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
} catch (Exception e) {
LogService.log("CACHE", "ERROR", "Failed to save hash cache: " + e.getMessage());
try {
java.nio.file.Files.deleteIfExists(tempFile.toPath());
} catch (IOException ignored) {}
}
}
/**
* Generates SHA-256 cryptographic hash of a file.
*/
public static String computeSHA256(Path path) throws IOException {
String absolutePath = path.toAbsolutePath().toString();
long modified = Files.getLastModifiedTime(path).toMillis();
CacheEntry cached = memoryCache.get(absolutePath);
if (cached != null && cached.lastModified == modified && cached.sha256 != null) {
return cached.sha256;
}
String hash = computeSHA256FromFile(path);
updateCache(absolutePath, modified, hash, cached != null ? cached.dHash : 0L, cached != null ? cached.partialHash : null);
return hash;
}
private static String computeSHA256FromFile(Path path) throws IOException {
try (InputStream is = Files.newInputStream(path)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
return bytesToHex(digest.digest());
} catch (Exception e) {
throw new IOException("Failed to compute SHA-256: " + e.getMessage());
}
}
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_CHARS[v >>> 4];
hexChars[i * 2 + 1] = HEX_CHARS[v & 0x0F];
}
return new String(hexChars);
}
/**
* Generates a 64-bit dHash (difference hash) for perceptual image duplicate matching.
*/
public static long computeDHash(Path path) throws IOException {
String absolutePath = path.toAbsolutePath().toString();
long modified = Files.getLastModifiedTime(path).toMillis();
CacheEntry cached = memoryCache.get(absolutePath);
if (cached != null && cached.lastModified == modified && cached.dHash != 0L) {
return cached.dHash;
}
long dHash = computeDHashFromImage(path);
updateCache(absolutePath, modified, cached != null ? cached.sha256 : null, dHash, cached != null ? cached.partialHash : null);
return dHash;
}
/**
* Generates partial SHA-256 of the first 8KB of a file.
*/
public static String computePartialHash(Path path) throws IOException {
String absolutePath = path.toAbsolutePath().toString();
long modified = Files.getLastModifiedTime(path).toMillis();
CacheEntry cached = memoryCache.get(absolutePath);
if (cached != null && cached.lastModified == modified && cached.partialHash != null) {
return cached.partialHash;
}
String partialHash = computePartialHashFromFile(path);
updateCache(absolutePath, modified, cached != null ? cached.sha256 : null, cached != null ? cached.dHash : 0L, partialHash);
return partialHash;
}
private static String computePartialHashFromFile(Path path) throws IOException {
try (InputStream is = Files.newInputStream(path)) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int bytesRead = 0;
int read;
while (bytesRead < 8192 && (read = is.read(buffer, bytesRead, 8192 - bytesRead)) != -1) {
bytesRead += read;
}
if (bytesRead > 0) {
digest.update(buffer, 0, bytesRead);
}
return bytesToHex(digest.digest());
} catch (Exception e) {
throw new IOException("Failed to compute partial SHA-256: " + e.getMessage());
}
}
private static long computeDHashFromImage(Path path) throws IOException {
// Dimension check before full decode to prevent huge image memory crashes
try (InputStream is = Files.newInputStream(path);
javax.imageio.stream.ImageInputStream iis = ImageIO.createImageInputStream(is)) {
if (iis != null) {
java.util.Iterator<javax.imageio.ImageReader> readers = ImageIO.getImageReaders(iis);
if (readers.hasNext()) {
javax.imageio.ImageReader reader = readers.next();
try {
reader.setInput(iis);
int w = reader.getWidth(0);
int h = reader.getHeight(0);
if (w > 32768 || h > 32768) {
throw new IOException("Decompression bomb protection triggered in dHash! Malicious image size: " + w + "x" + h);
}
} finally {
reader.dispose();
}
}
}
}
BufferedImage img;
try (InputStream is = Files.newInputStream(path)) {
img = ImageIO.read(is);
}
if (img == null) {
return 0L;
}
try {
// Resize to 9x8 grayscale image using fast drawImage
BufferedImage tiny = new BufferedImage(9, 8, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g2d = tiny.createGraphics();
g2d.drawImage(img, 0, 0, 9, 8, null);
g2d.dispose();
long hash = 0L;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
int p1 = tiny.getRaster().getSample(x, y, 0);
int p2 = tiny.getRaster().getSample(x + 1, y, 0);
if (p1 < p2) {
hash |= (1L << (y * 8 + x));
}
}
}
tiny.flush();
return hash;
} finally {
img.flush();
}
}
private static final int MAX_CACHE_ENTRIES = 50000;
private static synchronized void pruneCacheIfNeeded() {
if (memoryCache.size() > MAX_CACHE_ENTRIES) {
java.util.List<Map.Entry<String, CacheEntry>> entries = new java.util.ArrayList<>(memoryCache.entrySet());
entries.sort(java.util.Comparator.comparingLong(e -> e.getValue().lastModified));
int pruneCount = memoryCache.size() - (int)(MAX_CACHE_ENTRIES * 0.8);
for (int i = 0; i < pruneCount && i < entries.size(); i++) {
memoryCache.remove(entries.get(i).getKey());
}
LogService.log("CACHE", "INFO", "Pruned " + pruneCount + " old entries from hash cache to control size.");
}
}
private static void updateCache(String path, long modified, String sha256, long dHash, String partialHash) {
memoryCache.put(path, new CacheEntry(modified, sha256, dHash, partialHash));
pruneCacheIfNeeded();
}
public static class StagedDuplicateResult {
public final boolean isDuplicate;
public final String reason;
public final String hash;
public StagedDuplicateResult(boolean isDuplicate, String reason, String hash) {
this.isDuplicate = isDuplicate;
this.reason = reason;
this.hash = hash;
}
}
public static StagedDuplicateResult checkDuplicateStaged(
Path path,
Map<String, String> processedHashes,
Map<String, Path> processedCandidates
) throws IOException {
return checkDuplicateStaged(path, path, processedHashes, processedCandidates);
}
/**
* High-Performance Staged Hashing duplicate check pipeline:
* - Stage 1 (Size Filter) & Stage 2 (8KB Partial Hash): Bypasses full hashing for unique files.
* - Stage 3 (Full SHA-256): Resolves true duplicates on collision.
*/
public static StagedDuplicateResult checkDuplicateStaged(
Path sourcePath,
Path persistentPath,
Map<String, String> processedHashes,
Map<String, Path> processedCandidates
) throws IOException {
String absolutePath = sourcePath.toAbsolutePath().toString();
long modified = Files.getLastModifiedTime(sourcePath).toMillis();
long size = Files.size(sourcePath);
String fileName = sourcePath.getFileName().toString();
// Stage 1 & 2: Generate candidate key from size and partial hash
String partialHash = computePartialHash(sourcePath);
String candidateKey = size + "_" + partialHash;
Path storePath = sourcePath;
// Bounded capacity protection: prune oldest 20% if size exceeds 500k to prevent OOM without losing all history
if (processedCandidates != null && processedCandidates.size() > 500000) {
if (isEvicting.compareAndSet(false, true)) {
try {
int toRemove = processedCandidates.size() / 5;
java.util.Iterator<String> it = processedCandidates.keySet().iterator();
while (it.hasNext() && toRemove-- > 0) {
it.next();
it.remove();
}
} finally {
isEvicting.set(false);
}
}
}
if (processedHashes != null && processedHashes.size() > 500000) {
if (isEvicting.compareAndSet(false, true)) {
try {
int toRemove = processedHashes.size() / 5;
java.util.Iterator<String> it = processedHashes.keySet().iterator();
while (it.hasNext() && toRemove-- > 0) {
it.next();
it.remove();
}
} finally {
isEvicting.set(false);
}
}
}
// Check candidate collision
Path originalPath = (processedCandidates != null) ? processedCandidates.putIfAbsent(candidateKey, storePath) : null;
if (originalPath == null) {
// Unique candidate! Bypasses full hashing entirely.
CacheEntry cached = memoryCache.get(absolutePath);
String hashToUse = (cached != null && cached.sha256 != null && cached.lastModified == modified)
? cached.sha256
: "Partial-" + (partialHash.length() >= 12 ? partialHash.substring(0, 12) : partialHash);
if (processedHashes != null) {
processedHashes.put(hashToUse, fileName);
}
return new StagedDuplicateResult(false, null, hashToUse);
}
// Collision occurred! Compute full SHA-256 of the current file
String currentFullHash = computeSHA256(sourcePath);
// Check if this full hash is already in processedHashes
String originalName = (processedHashes != null) ? processedHashes.get(currentFullHash) : null;
if (originalName != null) {
return new StagedDuplicateResult(true, "Identical file hash match (SHA-256) with original: " + originalName, currentFullHash);
}
// Compute full hash of the original candidate path safely
String originalFullHash = null;
if (Files.exists(originalPath)) {
originalFullHash = computeSHA256(originalPath);
} else if (persistentPath != null && Files.exists(persistentPath)) {
originalFullHash = computeSHA256(persistentPath);
}
if (originalFullHash == null) {
// If original candidate file cannot be read, treat as unique candidate and proceed
if (processedHashes != null) {
processedHashes.put(currentFullHash, fileName);
}
return new StagedDuplicateResult(false, null, currentFullHash);
}
if (processedHashes != null) {
processedHashes.put(originalFullHash, originalPath.getFileName().toString());
}
if (currentFullHash.equals(originalFullHash)) {
// True duplicate!
return new StagedDuplicateResult(true, "Identical file hash match (SHA-256) with original: " + originalPath.getFileName().toString(), currentFullHash);
} else {
// False collision (extremely rare but mathematically possible)
if (processedHashes != null) {
processedHashes.put(currentFullHash, fileName);
}
return new StagedDuplicateResult(false, null, currentFullHash);
}
}
/**
* Calculates Hamming similarity percentage between two perceptual hashes.
*/
public static double calculatePerceptualSimilarity(long hash1, long hash2) {
int hammingDistance = Long.bitCount(hash1 ^ hash2);
return (64.0 - hammingDistance) / 64.0;
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\EnterpriseThreadPoolExecutor.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class EnterpriseThreadPoolExecutor extends ThreadPoolExecutor {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
public EnterpriseThreadPoolExecutor(int corePoolSize, int maxQueueCapacity) {
super(
corePoolSize,
corePoolSize,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(maxQueueCapacity),
new ThreadFactory() {
private final String prefix = "enterprise-worker-" + poolNumber.getAndIncrement() + "-thread-";
private final AtomicInteger threads = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefix + threads.getAndIncrement());
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // Bounded backpressure: runs on caller thread if pool queue is saturated
);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// Intercept silent exceptions thrown inside threads
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
// Handled cancellation, ignore
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Restore status
}
}
if (t != null) {
// Log crash details structurally and increment error counters in task manager
String errMsg = t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName();
LogService.log("THREAD", "ERROR", "Thread task crashed: " + errMsg);
TaskManager.incrementFailedCount();
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ExcelService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class ExcelService {
public static class SelectiveEntry {
public final String originalTarget;
public final String normalizedPath; // lowercase with forward slashes (e.g. c:/users/.../sample.jpg or photo/sample.jpg)
public final String fileName; // lowercase (e.g. sample.jpg)
public final String baseName; // lowercase (e.g. sample)
public final String destSubfolder; // target subfolder if specified
public final boolean isPathTarget; // true if target contains / or \
public final int rowNum; // 0-indexed row in Excel
public final int statusColIndex; // column index where Status should be written
public SelectiveEntry(String originalTarget, String destSubfolder, int rowNum, int statusColIndex) {
this.originalTarget = originalTarget;
this.destSubfolder = destSubfolder != null ? destSubfolder.trim() : "";
this.rowNum = rowNum;
this.statusColIndex = statusColIndex;
String clean = originalTarget.replace('\\', '/').trim();
if (clean.contains("/")) {
this.isPathTarget = true;
this.normalizedPath = clean.toLowerCase();
int lastSlash = clean.lastIndexOf('/');
String fn = clean.substring(lastSlash + 1);
this.fileName = fn.toLowerCase();
int dot = this.fileName.lastIndexOf('.');
this.baseName = dot != -1 ? this.fileName.substring(0, dot) : this.fileName;
} else {
this.isPathTarget = false;
this.normalizedPath = null;
this.fileName = clean.toLowerCase();
int dot = this.fileName.lastIndexOf('.');
this.baseName = dot != -1 ? this.fileName.substring(0, dot) : this.fileName;
}
}
}
public static List<SelectiveEntry> readSelectiveEntriesFromExcel(String excelFilePath) throws IOException {
List<SelectiveEntry> entries = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(new File(excelFilePath));
Workbook workbook = WorkbookFactory.create(fis)) {
if (workbook.getNumberOfSheets() == 0) return entries;
Sheet sheet = workbook.getSheetAt(0);
DataFormatter formatter = new DataFormatter();
boolean isFirstRow = true;
boolean isReportFormat = false; // Col 0 = Directory, Col 1 = File Name
int defaultStatusCol = 2;
for (Row row : sheet) {
Cell cellA = row.getCell(0);
Cell cellB = row.getCell(1);
String valA = cellA != null ? formatter.formatCellValue(cellA).trim() : "";
String valB = cellB != null ? formatter.formatCellValue(cellB).trim() : "";
if (isFirstRow) {
isFirstRow = false;
String lowerA = valA.toLowerCase();
String lowerB = valB.toLowerCase();
if (lowerA.contains("directory") || lowerA.contains("folder path") || lowerA.contains("path")) {
if (lowerB.contains("file") || lowerB.contains("name") || lowerB.contains("image")) {
isReportFormat = true;
defaultStatusCol = 4; // Col E for Module 3 4-column report
continue;
}
}
if (lowerA.contains("name") || lowerA.contains("file") || lowerA.contains("image") || lowerA.contains("target")) {
continue;
}
}
if (valA.isEmpty() && valB.isEmpty()) continue;
if (isReportFormat) {
if (!valB.isEmpty()) {
String fullPath = valA.isEmpty() ? valB : (valA.endsWith("\\") || valA.endsWith("/") ? valA + valB : valA + File.separator + valB);
Cell cellDest = row.getCell(4);
String destVal = (cellDest != null) ? formatter.formatCellValue(cellDest).trim() : "";
if (destVal.equalsIgnoreCase("Done") || destVal.equalsIgnoreCase("Left") || destVal.equalsIgnoreCase("Status")) {
destVal = "";
}
entries.add(new SelectiveEntry(fullPath, destVal, row.getRowNum(), defaultStatusCol));
}
} else {
if (!valA.isEmpty()) {
entries.add(new SelectiveEntry(valA, valB, row.getRowNum(), defaultStatusCol));
}
}
}
}
return entries;
}
public static void updateSelectiveStatusInExcel(String excelFilePath, Set<String> matchedTargets) {
File file = new File(excelFilePath);
if (!file.exists()) return;
Workbook workbook = null;
try {
try (FileInputStream fis = new FileInputStream(file)) {
workbook = WorkbookFactory.create(fis);
}
if (workbook.getNumberOfSheets() == 0) {
workbook.close();
return;
}
Sheet sheet = workbook.getSheetAt(0);
DataFormatter formatter = new DataFormatter();
boolean isFirstRow = true;
boolean isReportFormat = false;
int statusCol = 2;
for (Row row : sheet) {
Cell cellA = row.getCell(0);
Cell cellB = row.getCell(1);
String valA = cellA != null ? formatter.formatCellValue(cellA).trim() : "";
String valB = cellB != null ? formatter.formatCellValue(cellB).trim() : "";
if (isFirstRow) {
isFirstRow = false;
String lowerA = valA.toLowerCase();
String lowerB = valB.toLowerCase();
if (lowerA.contains("directory") || lowerA.contains("folder path") || lowerA.contains("path")) {
if (lowerB.contains("file") || lowerB.contains("name") || lowerB.contains("image")) {
isReportFormat = true;
statusCol = 4;
Cell cellStatus = row.getCell(statusCol);
if (cellStatus == null) cellStatus = row.createCell(statusCol);
cellStatus.setCellValue("Status");
continue;
}
}
if (lowerA.contains("name") || lowerA.contains("file") || lowerA.contains("image") || lowerA.contains("target")) {
Cell cellStatus = row.getCell(statusCol);
if (cellStatus == null) cellStatus = row.createCell(statusCol);
cellStatus.setCellValue("Status");
continue;
}
}
if (valA.isEmpty() && valB.isEmpty()) continue;
boolean isMoved = false;
if (isReportFormat) {
String fullPath = valA.isEmpty() ? valB : (valA.endsWith("\\") || valA.endsWith("/") ? valA + valB : valA + File.separator + valB);
String normFull = fullPath.replace('\\', '/').toLowerCase();
String lowerB = valB.toLowerCase();
String baseB = lowerB;
int dot = baseB.lastIndexOf('.');
if (dot != -1) baseB = baseB.substring(0, dot);
isMoved = matchedTargets.contains(fullPath) || matchedTargets.contains(normFull) ||
matchedTargets.contains(valB) || matchedTargets.contains(lowerB) || matchedTargets.contains(baseB);
} else {
String lowerA = valA.toLowerCase();
String normA = valA.replace('\\', '/').toLowerCase();
String fn = valA;
if (normA.contains("/")) {
fn = normA.substring(normA.lastIndexOf('/') + 1);
}
String baseA = fn;
int dot = baseA.lastIndexOf('.');
if (dot != -1) baseA = baseA.substring(0, dot);
isMoved = matchedTargets.contains(valA) || matchedTargets.contains(lowerA) || matchedTargets.contains(normA) ||
matchedTargets.contains(fn) || matchedTargets.contains(baseA);
}
Cell cellStatus = row.getCell(statusCol);
if (cellStatus == null) cellStatus = row.createCell(statusCol);
cellStatus.setCellValue(isMoved ? "Done" : "Left");
}
try (FileOutputStream fos = new FileOutputStream(file)) {
workbook.write(fos);
}
LogService.log("SELECTIVE", "INFO", "Excel status column updated with Done/Left report: " + excelFilePath);
} catch (Exception e) {
LogService.log("SELECTIVE", "WARNING", "Could not update Excel Column status: " + e.getMessage());
} finally {
if (workbook != null) {
try { workbook.close(); } catch (Exception ignored) {}
}
}
}
public static Map<String, String> readRenameMappingFromExcel(String excelFilePath) throws IOException {
Map<String, String> renameMap = new HashMap<>();
try (FileInputStream fis = new FileInputStream(new File(excelFilePath));
Workbook workbook = WorkbookFactory.create(fis)) {
if (workbook.getNumberOfSheets() == 0) return renameMap;
Sheet sheet = workbook.getSheetAt(0);
DataFormatter formatter = new DataFormatter();
for (Row row : sheet) {
Cell cellA = row.getCell(0);
Cell cellB = row.getCell(1);
if (cellA != null && cellB != null) {
String original = formatter.formatCellValue(cellA).trim().toLowerCase();
String renamed = formatter.formatCellValue(cellB).trim();
if (!original.isEmpty() && !renamed.isEmpty()) {
int dotA = original.lastIndexOf('.');
if (dotA != -1) original = original.substring(0, dotA);
int dotB = renamed.lastIndexOf('.');
if (dotB != -1) renamed = renamed.substring(0, dotB);
renameMap.put(original, renamed);
}
}
}
}
return renameMap;
}
public static void generateImageExcelStreamed(
String sourceFolder,
String outputFilePath,
Consumer<Long> progress,
AtomicBoolean cancelFlag,
long startingOffset
) throws IOException {
String tempPathStr = outputFilePath + ".tmp";
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
int sheetCount = 1;
Sheet sheet = workbook.createSheet("Extracted Images " + sheetCount);
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("Directory");
headerRow.createCell(1).setCellValue("File Name");
headerRow.createCell(2).setCellValue("Format");
headerRow.createCell(3).setCellValue("Size (KB)");
sheet.setColumnWidth(0, 15000);
sheet.setColumnWidth(1, 10000);
sheet.setColumnWidth(2, 4000);
sheet.setColumnWidth(3, 4000);
final int[] rowIdx = {1};
final int[] currentSheetCount = {1};
final Sheet[] currentSheet = {sheet};
final long[] localProgress = {0};
ImageExtractionService.processSource(sourceFolder, rootPath -> {
try {
traverseDirectorySorted(
rootPath, sourceFolder, workbook, currentSheet,
currentSheetCount, rowIdx, localProgress, startingOffset, progress, cancelFlag
);
} catch (Exception e) {
LogService.log("EXCEL", "ERROR", "Error during sorted image traversal: " + e.getMessage());
}
});
try {
try (FileOutputStream fos = new FileOutputStream(tempPathStr)) {
workbook.write(fos);
}
} finally {
workbook.dispose();
}
// Safe Atomic Temp-File Replacement with Windows lock bypass
safeMove(Paths.get(tempPathStr), Paths.get(outputFilePath));
}
}
private static void traverseDirectorySorted(
Path currentDir,
String sourceFolder,
SXSSFWorkbook workbook,
Sheet[] currentSheet,
int[] currentSheetCount,
int[] rowIdx,
long[] localProgress,
long startingOffset,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
if (cancelFlag != null && cancelFlag.get()) return;
TaskManager.checkPause();
List<Path> subDirs = new ArrayList<>();
List<Path> imageFiles = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
for (Path entry : stream) {
if (cancelFlag != null && cancelFlag.get()) return;
if (Files.isDirectory(entry)) {
subDirs.add(entry);
} else if (ImageExtractionService.isImageFile(entry)) {
imageFiles.add(entry);
}
}
} catch (Exception ignored) {
return;
}
// 1. Sort image files in current folder alphabetically (case-insensitive)
imageFiles.sort((a, b) -> {
String nameA = a.getFileName() != null ? a.getFileName().toString() : "";
String nameB = b.getFileName() != null ? b.getFileName().toString() : "";
return String.CASE_INSENSITIVE_ORDER.compare(nameA, nameB);
});
// 2. Write all sorted images in current folder first
for (Path file : imageFiles) {
if (cancelFlag != null && cancelFlag.get()) return;
TaskManager.checkPause();
TaskManager.setCurrentFileName(file.getFileName() != null ? file.getFileName().toString() : "");
if (rowIdx[0] >= 100000) { // 100k sheet split limit
currentSheetCount[0]++;
currentSheet[0] = workbook.createSheet("Extracted Images " + currentSheetCount[0]);
Row hRow = currentSheet[0].createRow(0);
hRow.createCell(0).setCellValue("Directory");
hRow.createCell(1).setCellValue("File Name");
hRow.createCell(2).setCellValue("Format");
hRow.createCell(3).setCellValue("Size (KB)");
currentSheet[0].setColumnWidth(0, 15000);
currentSheet[0].setColumnWidth(1, 10000);
currentSheet[0].setColumnWidth(2, 4000);
currentSheet[0].setColumnWidth(3, 4000);
rowIdx[0] = 1;
}
String dirPath;
if (!file.getFileSystem().equals(java.nio.file.FileSystems.getDefault())) {
// Inside a virtual ZIP file system
String innerParent = file.getParent() != null ? file.getParent().toString() : "";
if (innerParent.startsWith("/") || innerParent.startsWith("\\")) {
innerParent = innerParent.substring(1);
}
dirPath = sourceFolder + (innerParent.isEmpty() ? "" : File.separator + innerParent);
} else {
dirPath = file.getParent() != null ? file.getParent().toString() : sourceFolder;
}
String fileName = file.getFileName() != null ? file.getFileName().toString() : "";
String format = "";
int dot = fileName.lastIndexOf('.');
if (dot != -1 && dot < fileName.length() - 1) {
format = fileName.substring(dot + 1).toUpperCase();
}
long sizeKB = 0;
try {
sizeKB = Files.size(file) / 1024;
} catch (Exception ignored) {}
Row row = currentSheet[0].createRow(rowIdx[0]++);
row.createCell(0).setCellValue(dirPath);
row.createCell(1).setCellValue(fileName);
row.createCell(2).setCellValue(format);
row.createCell(3).setCellValue(sizeKB);
if (rowIdx[0] % 100 == 0) {
((SXSSFSheet) currentSheet[0]).flushRows(100);
}
localProgress[0]++;
if (localProgress[0] % 50 == 0 && progress != null) {
progress.accept(startingOffset + localProgress[0]);
}
}
// 3. Sort subdirectories alphabetically (case-insensitive)
subDirs.sort((a, b) -> {
String nameA = a.getFileName() != null ? a.getFileName().toString() : "";
String nameB = b.getFileName() != null ? b.getFileName().toString() : "";
return String.CASE_INSENSITIVE_ORDER.compare(nameA, nameB);
});
// 4. Complete 1st subfolder completely, then move to the next subfolder
for (Path subDir : subDirs) {
if (cancelFlag != null && cancelFlag.get()) return;
traverseDirectorySorted(
subDir, sourceFolder, workbook, currentSheet,
currentSheetCount, rowIdx, localProgress, startingOffset, progress, cancelFlag
);
}
}
public static class ExcelReportWriter implements AutoCloseable {
private final SXSSFWorkbook workbook;
private Sheet sheet;
private int rowIdx = 1;
private int sheetCount = 1;
public final String outputFilePath;
private final String tempFilePath;
private final boolean checkDuplicates;
// Bounded Blocking Queue & Background Asynchronous Writer Thread
private final BlockingQueue<ImageValidationService.ValidationResult> queue;
private final Thread writerThread;
private final AtomicBoolean running = new AtomicBoolean(true);
private static final ImageValidationService.ValidationResult POISON_PILL =
new ImageValidationService.ValidationResult(null, null, null, 0, null, null, null, null);
public ExcelReportWriter(String outputFilePath, boolean checkDuplicates) {
this.outputFilePath = outputFilePath;
this.checkDuplicates = checkDuplicates;
this.tempFilePath = outputFilePath + ".tmp";
this.workbook = new SXSSFWorkbook(100);
this.sheet = createNewSheet();
this.queue = new ArrayBlockingQueue<>(2000); // 2k capacity backpressure for memory footprint optimization
this.writerThread = new Thread(() -> {
while (running.get() || !queue.isEmpty()) {
try {
ImageValidationService.ValidationResult r = queue.poll(100, TimeUnit.MILLISECONDS);
if (r == null) continue;
if (r == POISON_PILL) break;
writeResultToWorkbook(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
LogService.log("EXCEL", "ERROR", "Writer thread error: " + e.getMessage());
}
}
});
this.writerThread.setName("ExcelBackgroundWriter");
this.writerThread.setDaemon(true);
this.writerThread.start();
}
private Sheet createNewSheet() {
Sheet s = workbook.createSheet("Health Report " + sheetCount);
Row headerRow = s.createRow(0);
headerRow.createCell(0).setCellValue("Directory");
headerRow.createCell(1).setCellValue("File Name");
headerRow.createCell(2).setCellValue("Format");
headerRow.createCell(3).setCellValue("Size (KB)");
headerRow.createCell(4).setCellValue("Dimensions");
headerRow.createCell(5).setCellValue("Status");
if (checkDuplicates) {
headerRow.createCell(6).setCellValue("Duplicate Hash");
headerRow.createCell(7).setCellValue("Reason");
} else {
headerRow.createCell(6).setCellValue("Reason");
}
s.setDefaultColumnWidth(20);
s.setColumnWidth(1, 10000);
if (checkDuplicates) {
s.setColumnWidth(7, 12000);
} else {
s.setColumnWidth(6, 12000);
}
return s;
}
public void writeResult(ImageValidationService.ValidationResult result) {
try {
// Blocking put applies natural backpressure to worker threads and guarantees zero data loss in audit reports
queue.put(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void writeResultToWorkbook(ImageValidationService.ValidationResult result) {
if (rowIdx >= 100000) { // split sheets at 100k rows
sheetCount++;
sheet = createNewSheet();
rowIdx = 1;
}
Row row = sheet.createRow(rowIdx++);
row.createCell(0).setCellValue(result.directory);
row.createCell(1).setCellValue(result.fileName);
row.createCell(2).setCellValue(result.format);
row.createCell(3).setCellValue(result.sizeKB);
row.createCell(4).setCellValue(result.dimensions);
row.createCell(5).setCellValue(result.status);
if (checkDuplicates) {
row.createCell(6).setCellValue(result.hash);
row.createCell(7).setCellValue(result.reason);
} else {
row.createCell(6).setCellValue(result.reason);
}
if (rowIdx % 100 == 0) {
try {
((SXSSFSheet) sheet).flushRows(100);
} catch (IOException ignored) {}
}
}
@Override
public void close() throws IOException {
running.set(false);
try {
// Offer poison pill with a 5-second timeout to avoid locking the calling thread if queue is saturated
if (!queue.offer(POISON_PILL, 5, TimeUnit.SECONDS)) {
writerThread.interrupt();
}
writerThread.join(10000); // Wait up to 10 seconds to drain remaining queue items
if (writerThread.isAlive()) {
writerThread.interrupt();
writerThread.join(2000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Write final flush of remaining queue items if writer thread is terminated
if (!writerThread.isAlive()) {
while (!queue.isEmpty()) {
ImageValidationService.ValidationResult r = queue.poll();
if (r != null && r != POISON_PILL) {
writeResultToWorkbook(r);
}
}
}
try (FileOutputStream fos = new FileOutputStream(tempFilePath)) {
workbook.write(fos);
} finally {
try {
workbook.close();
} catch (Exception ignored) {}
workbook.dispose();
}
// Safe Atomic Temp-File Replacement with Windows lock bypass
safeMove(Paths.get(tempFilePath), Paths.get(outputFilePath));
}
}
public static void generateConversionReport(
List<ImageConversionService.ConversionResult> results,
String outputFilePath,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
String tempPathStr = outputFilePath + ".tmp";
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
int sheetIndex = 1;
Sheet sheet = workbook.createSheet("Conversion_Part_" + sheetIndex);
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("Original Image Name");
headerRow.createCell(1).setCellValue("Converted Image Name");
headerRow.createCell(2).setCellValue("Action / Status");
sheet.setDefaultColumnWidth(30);
int rowIdx = 1;
long localProgress = 0;
for (ImageConversionService.ConversionResult r : results) {
if (cancelFlag != null && cancelFlag.get()) break;
TaskManager.checkPause();
if (rowIdx >= 100000) {
((SXSSFSheet) sheet).flushRows(100);
sheetIndex++;
sheet = workbook.createSheet("Conversion_Part_" + sheetIndex);
headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("Original Image Name");
headerRow.createCell(1).setCellValue("Converted Image Name");
headerRow.createCell(2).setCellValue("Action / Status");
sheet.setDefaultColumnWidth(30);
rowIdx = 1;
}
Row row = sheet.createRow(rowIdx++);
row.createCell(0).setCellValue(r.originalName);
row.createCell(1).setCellValue(r.convertedName);
row.createCell(2).setCellValue(r.actionTaken);
if (rowIdx % 100 == 0) {
((SXSSFSheet) sheet).flushRows(100);
}
localProgress++;
if (localProgress % 50 == 0 && progress != null) {
progress.accept(localProgress);
}
}
try (FileOutputStream fos = new FileOutputStream(tempPathStr)) {
workbook.write(fos);
} finally {
workbook.dispose();
}
// Safe Atomic Temp-File Replacement with Windows lock bypass
safeMove(Paths.get(tempPathStr), Paths.get(outputFilePath));
}
}
/**
* Bypasses Windows file locking issues (such as indexing or antivirus locks) by
* retrying the atomic replacement 3 times with a 1-second delay.
*/
public static void safeMove(Path source, Path target) throws IOException {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
return;
} catch (IOException e) {
if (i == maxRetries - 1) {
throw new IOException("Failed to replace file at " + target + " after " + maxRetries + " retries due to Windows file lock: " + e.getMessage(), e);
}
try {
Thread.sleep(1000); // Sleep 1 second before retrying
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("File replacement interrupted during lock release backoff retry: " + ie.getMessage(), ie);
}
}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\HashService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.nio.file.Path;
import java.util.Map;
public class HashService {
/**
* Orchestrates the high-performance staged duplicate check pipeline.
* Delegates to DuplicateManager for staged matching (Size -> Partial -> Full).
*/
public static DuplicateManager.StagedDuplicateResult checkDuplicateStaged(
Path path,
Map<String, String> processedHashes,
Map<String, Path> processedCandidates
) throws java.io.IOException {
return DuplicateManager.checkDuplicateStaged(path, path, processedHashes, processedCandidates);
}
public static DuplicateManager.StagedDuplicateResult checkDuplicateStaged(
Path sourcePath,
Path persistentPath,
Map<String, String> processedHashes,
Map<String, Path> processedCandidates
) throws java.io.IOException {
return DuplicateManager.checkDuplicateStaged(sourcePath, persistentPath, processedHashes, processedCandidates);
}
/**
* Generates a 64-bit dHash (difference hash) for perceptual image duplicate matching.
*/
public static long computePerceptualHash(Path path) throws java.io.IOException {
return DuplicateManager.computeDHash(path);
}
/**
* Calculates Hamming similarity percentage between two perceptual hashes.
*/
public static double calculatePerceptualSimilarity(long hash1, long hash2) {
return DuplicateManager.calculatePerceptualSimilarity(hash1, hash2);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ImageConversionService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class ImageConversionService {
public static class ConversionResult {
public String originalName;
public String convertedName;
public String actionTaken;
public ConversionResult(String originalName, String convertedName, String actionTaken) {
this.originalName = originalName;
this.convertedName = convertedName;
this.actionTaken = actionTaken;
}
}
private static Path resolveTargetCollision(Path destPath, String baseName, String extension, java.util.Set<String> claimedNames) {
String fileName = baseName + extension;
while (true) {
Path target = destPath.resolve(fileName);
String claimKey = fileName.toLowerCase();
if (claimedNames == null || claimedNames.add(claimKey)) {
if (!Files.exists(target)) {
return target;
}
if (claimedNames != null) {
claimedNames.remove(claimKey);
}
}
fileName = baseName + "_" + java.util.UUID.randomUUID().toString() + extension;
}
}
private static String processSingleImage(Path source, Path target, String format, long targetSizeBytes) throws Exception {
long originalSize = Files.size(source);
String origExt = "";
String origName = source.getFileName().toString();
int dot = origName.lastIndexOf('.');
if (dot != -1) origExt = origName.substring(dot + 1).toLowerCase();
// If it's already the right format and under the size limit, just copy it directly.
if (origExt.equals(format) && originalSize <= targetSizeBytes) {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
return "Format Kept (Under Size Limit)";
}
// Dimension check before full decode to prevent huge image memory crashes
try (InputStream is = Files.newInputStream(source);
javax.imageio.stream.ImageInputStream iis = ImageIO.createImageInputStream(is)) {
if (iis != null) {
java.util.Iterator<javax.imageio.ImageReader> readers = ImageIO.getImageReaders(iis);
if (readers.hasNext()) {
javax.imageio.ImageReader reader = readers.next();
try {
reader.setInput(iis);
int w = reader.getWidth(0);
int h = reader.getHeight(0);
if (w > 32768 || h > 32768) {
throw new IOException("Decompression bomb protection triggered in conversion! Malicious image size: " + w + "x" + h);
}
} finally {
reader.dispose();
}
}
}
}
BufferedImage image;
try (InputStream is = Files.newInputStream(source)) {
image = ImageIO.read(is);
}
if (image == null) {
throw new IOException("No ImageIO/TwelveMonkeys reader found for this format (" + (origExt.isEmpty() ? "unknown" : origExt.toUpperCase()) + ")");
}
try {
// Handle transparency removal if converting to JPG or BMP
if (format.equals("jpg") || format.equals("jpeg") || format.equals("bmp")) {
if (image.getColorModel().hasAlpha() || image.getType() != BufferedImage.TYPE_INT_RGB) {
BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = newImage.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
g2d.drawImage(image, 0, 0, null);
g2d.dispose();
image.flush();
image = newImage;
}
}
boolean isJpg = format.equals("jpg") || format.equals("jpeg");
// Attempt compression first if JPG
if (isJpg) {
byte[] compressedData = attemptJpgCompression(image, format, targetSizeBytes);
if (compressedData != null) {
Files.write(target, compressedData);
return "Compressed";
}
}
// If we reach here, we must resize dimensions
byte[] currentBytes = getStandardBytes(image, format);
if (currentBytes.length <= targetSizeBytes) {
Files.write(target, currentBytes);
return "Format Changed";
}
double ratio = (double) targetSizeBytes / currentBytes.length;
double scale = Math.sqrt(ratio) * 0.95; // 0.95 safety margin
BufferedImage scaledImage = image;
int iteration = 0;
while (currentBytes.length > targetSizeBytes && iteration < 5) {
int newW = Math.max(1, (int) (scaledImage.getWidth() * scale));
int newH = Math.max(1, (int) (scaledImage.getHeight() * scale));
BufferedImage resized = new BufferedImage(newW, newH, scaledImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : scaledImage.getType());
Graphics2D g2d = resized.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.drawImage(scaledImage, 0, 0, newW, newH, null);
g2d.dispose();
if (scaledImage != image) {
scaledImage.flush();
}
scaledImage = resized;
if (isJpg) {
currentBytes = compressJpgToBytes(scaledImage, format, 0.0f);
} else {
currentBytes = getStandardBytes(scaledImage, format);
}
scale = 0.8;
iteration++;
}
Files.write(target, currentBytes);
if (scaledImage != image) {
scaledImage.flush();
}
return isJpg ? "Compressed & Dimension Adjusted" : "Dimension Adjusted";
} finally {
image.flush(); // Strict cleanup of buffered images to prevent RAM leaks
}
}
private static byte[] attemptJpgCompression(BufferedImage image, String format, long targetSizeBytes) throws Exception {
float minQuality = 0.0f;
float maxQuality = 1.0f;
byte[] bestData = null;
for (int i = 0; i < 6; i++) {
float mid = (minQuality + maxQuality) / 2.0f;
byte[] data = compressJpgToBytes(image, format, mid);
if (data.length <= targetSizeBytes) {
bestData = data;
minQuality = mid;
} else {
maxQuality = mid;
}
}
if (bestData == null) {
byte[] data = compressJpgToBytes(image, format, 0.0f);
if (data.length <= targetSizeBytes) {
return data;
}
return null;
}
return bestData;
}
private static byte[] compressJpgToBytes(BufferedImage image, String format, float quality) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
java.util.Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(format);
if (!writers.hasNext()) throw new IllegalStateException("No writers found");
ImageWriter writer = writers.next();
try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) {
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
if (param.canWriteCompressed()) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
}
writer.write(null, new IIOImage(image, null, null), param);
} finally {
writer.dispose();
}
return baos.toByteArray();
}
private static byte[] getStandardBytes(BufferedImage image, String format) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean success = ImageIO.write(image, format, baos);
if (!success) {
throw new IOException("No appropriate image writer found for format: " + format);
}
return baos.toByteArray();
}
public static List<ConversionResult> convertImagesStreamed(
List<String> sourceFolders,
String destinationFolder,
String targetFormat,
long targetSizeBytes,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) {
List<ConversionResult> results = java.util.Collections.synchronizedList(new ArrayList<>());
Path destPath = com.imageextractor.utils.PathUtils.normalizeLongPath(destinationFolder);
if (!Files.exists(destPath)) {
try {
Files.createDirectories(destPath);
} catch (IOException e) {
LogService.log("CONVERT", "ERROR", "Failed to create destination directory: " + e.getMessage());
}
}
final AtomicLong currentProgress = new AtomicLong(0);
final java.util.Set<String> claimedNames = java.util.concurrent.ConcurrentHashMap.newKeySet();
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
try {
for (String sf : sourceFolders) {
if (cancelFlag != null && cancelFlag.get()) break;
ImageExtractionService.processSource(sf, rootPath -> {
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 for main thread
try {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag != null && cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (ImageExtractionService.isImageFile(file)) {
phaser.register();
try {
executor.submit(() -> {
try {
processSingleStreamedConversion(
file, destPath, targetFormat, targetSizeBytes, claimedNames,
results, currentProgress, progress, cancelFlag
);
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("CONVERT", "ERROR", "Failed to submit conversion task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
} finally {
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
}
});
}
} catch (Exception e) {
LogService.log("CONVERT", "ERROR", "Conversion walk interrupted: " + e.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
return results;
}
private static void processSingleStreamedConversion(
Path p,
Path destPath,
String targetFormat,
long targetSizeBytes,
java.util.Set<String> claimedNames,
List<ConversionResult> results,
AtomicLong currentProgress,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) {
TaskManager.checkPause();
if (cancelFlag != null && cancelFlag.get()) return;
TaskManager.updateHeartbeat();
TaskManager.setCurrentFileName(p.getFileName().toString());
String origName = p.getFileName().toString();
String baseName = origName;
String origExtStr = "";
int dot = origName.lastIndexOf('.');
if (dot != -1) {
baseName = origName.substring(0, dot);
origExtStr = origName.substring(dot + 1).toLowerCase();
}
String actualTargetFormat = (targetFormat == null || targetFormat.isEmpty()) ? origExtStr : targetFormat.toLowerCase();
String formatExt = "." + actualTargetFormat;
Path target = resolveTargetCollision(destPath, baseName, formatExt, claimedNames);
String claimKey = target.getFileName().toString().toLowerCase();
try {
String action = processSingleImage(p, target, actualTargetFormat, targetSizeBytes <= 0 ? Long.MAX_VALUE : targetSizeBytes);
if (results.size() < 250000) {
results.add(new ConversionResult(origName, target.getFileName().toString(), action));
}
TaskManager.incrementProcessed();
} catch (Exception e) {
LogService.log("CONVERT", "ERROR", "Failed to convert " + p + ": " + e.getMessage());
if (results.size() < 250000) {
results.add(new ConversionResult(origName, "FAILED", "Error: " + e.getMessage()));
}
TaskManager.incrementFailedCount();
TaskManager.incrementProcessed();
} finally {
if (claimedNames != null) {
claimedNames.remove(claimKey);
}
}
long cp = currentProgress.incrementAndGet();
if (cp % 10 == 0 && progress != null) progress.accept(cp);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ImageExtractionService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import com.imageextractor.utils.PathUtils;
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class ImageExtractionService {
private static final String[] VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".pdf"};
public static boolean isImageFile(Path path) {
if (Files.isDirectory(path)) return false;
String name = path.getFileName().toString().toLowerCase();
for (String ext : VALID_EXTENSIONS) {
if (name.endsWith(ext)) return true;
}
return false;
}
public static void safeCopy(Path source, Path target) throws IOException {
if (source.getFileSystem().equals(target.getFileSystem())) {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
} else {
// Drop COPY_ATTRIBUTES if crossing filesystems (e.g., from virtual ZIP to Local Disk)
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
private static Path resolveTargetCollision(Path destPath, Path file, java.util.Set<String> claimedNames) {
String fileName = file.getFileName().toString();
while (true) {
Path target = destPath.resolve(fileName);
String claimKey = target.toAbsolutePath().toString().toLowerCase();
if (claimedNames == null || claimedNames.add(claimKey)) {
if (!Files.exists(target)) {
return target;
}
// Pre-existing file on disk: remove the claimed name so it doesn't leak
if (claimedNames != null) {
claimedNames.remove(claimKey);
}
}
String baseName = fileName;
String extension = "";
int dot = fileName.lastIndexOf('.');
if (dot != -1) {
baseName = fileName.substring(0, dot);
extension = fileName.substring(dot);
}
fileName = baseName + "_" + java.util.UUID.randomUUID().toString() + extension;
}
}
public static void processSource(String sourcePath, SourceProcessor processor) throws IOException {
// Resolve long path safely
Path srcPath = PathUtils.normalizeLongPath(sourcePath).toAbsolutePath();
if (!Files.exists(srcPath)) return;
if (sourcePath.toLowerCase().endsWith(".zip") && !Files.isDirectory(srcPath)) {
URI uri = URI.create("jar:" + srcPath.toUri());
try (FileSystem fs = FileSystems.newFileSystem(uri, java.util.Collections.emptyMap())) {
for (Path root : fs.getRootDirectories()) {
processor.process(root);
}
}
} else {
processor.process(srcPath);
}
}
@FunctionalInterface
public interface SourceProcessor {
void process(Path rootPath) throws IOException;
}
private static void processSingleExtraction(
Path p,
Path destPath,
Set<String> claimedNames,
java.util.Map<String, String> processedHashes,
java.util.Map<String, Path> processedCandidates,
Set<String> completedFiles,
Set<String> failedFiles,
String destinationFolder,
java.util.concurrent.atomic.AtomicInteger count,
java.util.concurrent.atomic.AtomicLong currentProgress,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) {
TaskManager.checkPause();
if (cancelFlag.get()) return;
TaskManager.updateHeartbeat();
TaskManager.setCurrentFileName(p.getFileName().toString());
String fileKey = p.toUri().toString();
// Checkpoint skip logic
if (completedFiles.contains(fileKey) || failedFiles.contains(fileKey)) {
long cp = currentProgress.incrementAndGet();
if (cp % 50 == 0 && progress != null) progress.accept(cp);
return;
}
Path target = null;
String claimKey = null;
try {
// Zip Slip Protection: Path traversal guard
target = resolveTargetCollision(destPath, p, claimedNames);
claimKey = target.toAbsolutePath().toString().toLowerCase();
if (!TempManager.isPathSafe(target, destPath)) {
throw new SecurityException("Zip Slip directory traversal vulnerability detected in: " + p.toString());
}
// ZIP Bomb Protection: Check compression ratio
long compressedSize = 1;
try {
compressedSize = Files.size(p);
} catch (Exception ignored) {}
// Generate duplicate status using high-performance staged hashing pipeline
DuplicateManager.StagedDuplicateResult duplicateResult = HashService.checkDuplicateStaged(p, target, processedHashes, processedCandidates);
if (duplicateResult.isDuplicate) {
TaskManager.incrementDuplicateCount();
}
// Always copy the file to ensure every image is treated as unique and extracted
safeCopy(p, target);
count.incrementAndGet();
TaskManager.incrementProcessed();
TaskManager.addBytesProcessed(compressedSize);
if (completedFiles.size() > 500000) {
int removeCount = completedFiles.size() / 5;
java.util.Iterator<String> it = completedFiles.iterator();
while (it.hasNext() && removeCount-- > 0) {
it.next();
it.remove();
}
}
completedFiles.add(fileKey);
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Failed to extract: " + p.getFileName().toString() + " - " + e.getMessage());
if (failedFiles.size() > 500000) {
int removeCount = failedFiles.size() / 5;
java.util.Iterator<String> it = failedFiles.iterator();
while (it.hasNext() && removeCount-- > 0) {
it.next();
it.remove();
}
}
failedFiles.add(fileKey);
TaskManager.incrementFailedCount();
TaskManager.incrementProcessed();
} finally {
if (claimedNames != null && claimKey != null) {
claimedNames.remove(claimKey);
}
}
long cp = currentProgress.incrementAndGet();
// Save progress checkpoint recovery state adaptively
ProcessingState.saveStateAdaptive(destinationFolder, "extract-task", completedFiles, failedFiles, cp);
if (cp % 50 == 0 && progress != null) progress.accept(cp);
}
public static int extractAllImagesStreamed(
List<String> sourceFolders,
String destinationFolder,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
Path destPath = PathUtils.normalizeLongPath(destinationFolder);
if (!Files.exists(destPath)) Files.createDirectories(destPath);
final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);
final java.util.concurrent.atomic.AtomicLong currentProgress = new java.util.concurrent.atomic.AtomicLong(0);
final java.util.Set<String> claimedNames = java.util.concurrent.ConcurrentHashMap.newKeySet();
final java.util.Map<String, String> processedHashes = new java.util.concurrent.ConcurrentHashMap<>();
final java.util.Map<String, Path> processedCandidates = new java.util.concurrent.ConcurrentHashMap<>();
// Checkpoint Recovery load
final ProcessingState checkpoint = ProcessingState.loadState(destinationFolder);
final Set<String> completedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
final Set<String> failedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
if (checkpoint != null) {
completedFiles.addAll(checkpoint.completedFiles);
failedFiles.addAll(checkpoint.failedFiles);
count.set((int) Math.min(Integer.MAX_VALUE, checkpoint.progressCount));
// Sync task manager telemetry
TaskManager.resetTelemetry();
TaskManager.setProcessedCount(checkpoint.progressCount);
TaskManager.setFailedCount(checkpoint.failedFiles.size());
}
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
try {
for (String sf : sourceFolders) {
if (cancelFlag.get()) break;
processSource(sf, rootPath -> {
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 for main thread
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (isImageFile(file)) {
phaser.register();
try {
executor.submit(() -> {
try {
processSingleExtraction(
file, destPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag
);
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("EXTRACT", "ERROR", "Failed to submit extraction task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
});
}
if (!cancelFlag.get()) {
ProcessingState.deleteState(destinationFolder);
}
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Streamed extraction walk interrupted: " + e.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
return count.get();
}
public static Path resolveSubfolderPath(Path baseDestPath, String folderValue, boolean partitionByUnderscore) {
if (folderValue == null || folderValue.trim().isEmpty()) {
return baseDestPath;
}
String cleanVal = folderValue.trim();
if (!partitionByUnderscore) {
String sanitizedFolder = sanitizeFolderName(cleanVal);
return sanitizedFolder.isEmpty() ? baseDestPath : baseDestPath.resolve(sanitizedFolder);
} else {
String[] parts = cleanVal.split("_");
Path current = baseDestPath;
for (String part : parts) {
String sanitizedPart = sanitizeFolderName(part.trim());
if (!sanitizedPart.isEmpty()) {
current = current.resolve(sanitizedPart);
}
}
return current;
}
}
private static String sanitizeFolderName(String name) {
if (name == null) return "";
return name.replaceAll("[\\\\/:*?\"<>|]", "_").trim();
}
public static int extractSelectiveImagesStreamed(
List<String> sourceFolders,
String excelFilePath,
String destinationFolder,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
return extractSelectiveImagesStreamed(sourceFolders, excelFilePath, destinationFolder, false, progress, cancelFlag);
}
public static int extractSelectiveImagesStreamed(
List<String> sourceFolders,
String excelFilePath,
String destinationFolder,
boolean partitionByUnderscore,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
List<ExcelService.SelectiveEntry> entries = ExcelService.readSelectiveEntriesFromExcel(excelFilePath);
Path destPath = PathUtils.normalizeLongPath(destinationFolder);
if (!Files.exists(destPath)) Files.createDirectories(destPath);
final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);
final java.util.concurrent.atomic.AtomicLong currentProgress = new java.util.concurrent.atomic.AtomicLong(0);
final java.util.Set<String> claimedNames = java.util.concurrent.ConcurrentHashMap.newKeySet();
final java.util.Map<String, String> processedHashes = new java.util.concurrent.ConcurrentHashMap<>();
final java.util.Map<String, Path> processedCandidates = new java.util.concurrent.ConcurrentHashMap<>();
// Checkpoint Recovery load
final ProcessingState checkpoint = ProcessingState.loadState(destinationFolder);
final Set<String> completedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
final Set<String> failedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
final java.util.Set<String> matchedTargets = java.util.concurrent.ConcurrentHashMap.newKeySet();
if (checkpoint != null) {
completedFiles.addAll(checkpoint.completedFiles);
failedFiles.addAll(checkpoint.failedFiles);
count.set((int) Math.min(Integer.MAX_VALUE, checkpoint.progressCount));
// Sync task manager telemetry
TaskManager.resetTelemetry();
TaskManager.setProcessedCount(checkpoint.progressCount);
TaskManager.setFailedCount(checkpoint.failedFiles.size());
}
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
// Build fast lookup indices for path and filename matching
Map<String, ExcelService.SelectiveEntry> exactPathMap = new HashMap<>();
Map<String, List<ExcelService.SelectiveEntry>> subpathCandidatesByFileName = new HashMap<>();
Map<String, List<ExcelService.SelectiveEntry>> nameMap = new HashMap<>();
for (ExcelService.SelectiveEntry e : entries) {
if (e.isPathTarget) {
if (e.normalizedPath != null) {
exactPathMap.put(e.normalizedPath, e);
subpathCandidatesByFileName.computeIfAbsent(e.fileName, k -> new ArrayList<>()).add(e);
}
} else {
nameMap.computeIfAbsent(e.fileName, k -> new ArrayList<>()).add(e);
if (!e.baseName.equals(e.fileName)) {
nameMap.computeIfAbsent(e.baseName, k -> new ArrayList<>()).add(e);
}
}
}
// Direct Extraction for existing absolute / direct file paths on disk
for (ExcelService.SelectiveEntry e : entries) {
if (cancelFlag != null && cancelFlag.get()) break;
if (e.isPathTarget) {
try {
Path directFile = Paths.get(e.originalTarget);
if (Files.exists(directFile) && !Files.isDirectory(directFile) && isImageFile(directFile)) {
Path itemDestPath = resolveSubfolderPath(destPath, e.destSubfolder, partitionByUnderscore);
if (!Files.exists(itemDestPath)) Files.createDirectories(itemDestPath);
processSingleExtraction(
directFile, itemDestPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag
);
matchedTargets.add(e.originalTarget);
if (e.normalizedPath != null) matchedTargets.add(e.normalizedPath);
matchedTargets.add(e.fileName);
matchedTargets.add(e.baseName);
}
} catch (Exception ignored) {}
}
}
try {
if (sourceFolders != null) {
for (String sf : sourceFolders) {
if (cancelFlag != null && cancelFlag.get()) break;
processSource(sf, rootPath -> {
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 for main thread
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag != null && cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (isImageFile(file)) {
String fileName = file.getFileName().toString().toLowerCase();
String baseName = fileName;
int dot = fileName.lastIndexOf('.');
if (dot != -1) baseName = fileName.substring(0, dot);
String normAbs = file.toAbsolutePath().toString().replace('\\', '/').toLowerCase();
List<ExcelService.SelectiveEntry> matchedEntries = new ArrayList<>();
// 1. Exact path match
if (exactPathMap.containsKey(normAbs)) {
matchedEntries.add(exactPathMap.get(normAbs));
} else {
// 2. High-performance relative subpath match - only inspect candidates with matching filename!
List<ExcelService.SelectiveEntry> subpathCandidates = subpathCandidatesByFileName.get(fileName);
if (subpathCandidates != null) {
for (ExcelService.SelectiveEntry pe : subpathCandidates) {
if (normAbs.endsWith(pe.normalizedPath) || normAbs.endsWith("/" + pe.normalizedPath)) {
matchedEntries.add(pe);
}
}
}
}
// 3. File name / Base name match for non-path targets
if (nameMap.containsKey(fileName)) {
matchedEntries.addAll(nameMap.get(fileName));
} else if (nameMap.containsKey(baseName)) {
matchedEntries.addAll(nameMap.get(baseName));
}
if (!matchedEntries.isEmpty()) {
for (ExcelService.SelectiveEntry matchEntry : matchedEntries) {
matchedTargets.add(matchEntry.originalTarget);
if (matchEntry.normalizedPath != null) matchedTargets.add(matchEntry.normalizedPath);
matchedTargets.add(fileName);
matchedTargets.add(baseName);
final String subfolderValue = matchEntry.destSubfolder;
phaser.register();
try {
executor.submit(() -> {
try {
Path itemDestPath = resolveSubfolderPath(destPath, subfolderValue, partitionByUnderscore);
if (!Files.exists(itemDestPath)) {
Files.createDirectories(itemDestPath);
}
processSingleExtraction(
file, itemDestPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag
);
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Failed processing selective extraction item: " + file.getFileName().toString() + " - " + e.getMessage());
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("EXTRACT", "ERROR", "Failed to submit selective extraction task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
});
}
}
if (!cancelFlag.get()) {
ProcessingState.deleteState(destinationFolder);
}
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Selective extraction walk interrupted: " + e.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Update Excel status column (Done/Left) upon completion
if (!cancelFlag.get()) {
ExcelService.updateSelectiveStatusInExcel(excelFilePath, matchedTargets);
}
return count.get();
}
public static int extractInnerZipsStreamed(
List<String> sourceFolders,
String destinationFolder,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
Path destPath = PathUtils.normalizeLongPath(destinationFolder);
if (!Files.exists(destPath)) Files.createDirectories(destPath);
final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);
final java.util.concurrent.atomic.AtomicLong currentProgress = new java.util.concurrent.atomic.AtomicLong(0);
final java.util.Set<String> claimedNames = java.util.concurrent.ConcurrentHashMap.newKeySet();
final java.util.Map<String, String> processedHashes = new java.util.concurrent.ConcurrentHashMap<>();
final java.util.Map<String, Path> processedCandidates = new java.util.concurrent.ConcurrentHashMap<>();
// Checkpoint Recovery load
final ProcessingState checkpoint = ProcessingState.loadState(destinationFolder);
final Set<String> completedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
final Set<String> failedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
if (checkpoint != null) {
completedFiles.addAll(checkpoint.completedFiles);
failedFiles.addAll(checkpoint.failedFiles);
count.set((int) Math.min(Integer.MAX_VALUE, checkpoint.progressCount));
// Sync task manager telemetry
TaskManager.resetTelemetry();
TaskManager.setProcessedCount(checkpoint.progressCount);
TaskManager.setFailedCount(checkpoint.failedFiles.size());
}
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
try {
for (String sf : sourceFolders) {
if (cancelFlag.get()) break;
processSource(sf, rootPath -> {
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 for main thread
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (isImageFile(file)) {
phaser.register();
try {
executor.submit(() -> {
try {
processSingleExtraction(
file, destPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag
);
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("EXTRACT", "ERROR", "Failed to submit extraction task for " + file.getFileName().toString() + ": " + e.getMessage());
}
} else if (ZipService.isZipFile(file)) {
phaser.register();
try {
executor.submit(() -> {
try {
processNestedZip(
file, destPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag, 0
);
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Failed to process nested ZIP: " + file.getFileName().toString() + " - " + e.getMessage());
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("EXTRACT", "ERROR", "Failed to submit nested ZIP task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
});
}
if (!cancelFlag.get()) {
ProcessingState.deleteState(destinationFolder);
}
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Inner ZIP extraction walk interrupted: " + e.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
return count.get();
}
private static void processNestedZip(
Path zipFile,
Path destPath,
Set<String> claimedNames,
java.util.Map<String, String> processedHashes,
java.util.Map<String, Path> processedCandidates,
Set<String> completedFiles,
Set<String> failedFiles,
String destinationFolder,
java.util.concurrent.atomic.AtomicInteger count,
java.util.concurrent.atomic.AtomicLong currentProgress,
Consumer<Long> progress,
AtomicBoolean cancelFlag,
int depth
) throws Exception {
if (cancelFlag.get()) return;
if (depth > 10) { // Safety cap to avoid infinite loops or recursion attacks (ZIP bomb)
throw new IllegalStateException("Max nested ZIP extraction depth exceeded (depth limit = 10)");
}
// Extract to temporary folder safely using TempManager
Path tempExtractDir = TempManager.createTempDirectory("nested_zip_extract");
try {
// Copy zip file to temporary disk so we can open it as standard jar/zip filesystem
Path tempZipDiskPath = tempExtractDir.resolve("nested.zip");
safeCopy(zipFile, tempZipDiskPath);
URI uri = URI.create("jar:" + tempZipDiskPath.toAbsolutePath().toUri());
try (FileSystem fs = FileSystems.newFileSystem(uri, java.util.Collections.emptyMap())) {
for (Path root : fs.getRootDirectories()) {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (isImageFile(file)) {
try {
processSingleExtraction(
file, destPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag
);
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Failed to extract nested file " + file.getFileName().toString() + ": " + e.getMessage());
}
} else if (ZipService.isZipFile(file)) {
try {
processNestedZip(
file, destPath, claimedNames, processedHashes, processedCandidates, completedFiles, failedFiles, destinationFolder,
count, currentProgress, progress, cancelFlag, depth + 1
);
} catch (Exception e) {
LogService.log("EXTRACT", "ERROR", "Failed to process nested ZIP: " + file.getFileName().toString() + " - " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
});
}
}
} finally {
// Safely delete temporary directories via TempManager
TempManager.deleteTempDirectory(tempExtractDir);
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ImageRenameService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class ImageRenameService {
private static Path resolveTargetCollision(Path destPath, String baseName, String extension, Set<String> claimedNames) {
String fileName = baseName + extension;
while (true) {
Path target = destPath.resolve(fileName);
String claimKey = fileName.toLowerCase();
if (claimedNames == null || claimedNames.add(claimKey)) {
if (!Files.exists(target)) {
return target;
}
if (claimedNames != null) {
claimedNames.remove(claimKey);
}
}
fileName = baseName + "_" + java.util.UUID.randomUUID().toString() + extension;
}
}
public static int renameImagesStreamed(
List<String> sourceFolders,
String excelFilePath,
String destinationFolder,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws IOException {
Map<String, String> renameMap = ExcelService.readRenameMappingFromExcel(excelFilePath);
if (renameMap.isEmpty()) return 0;
Path destPath = com.imageextractor.utils.PathUtils.normalizeLongPath(destinationFolder);
if (!Files.exists(destPath)) Files.createDirectories(destPath);
final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);
final java.util.concurrent.atomic.AtomicLong currentProgress = new java.util.concurrent.atomic.AtomicLong(0);
final Set<String> claimedNames = java.util.concurrent.ConcurrentHashMap.newKeySet();
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
try {
for (String sf : sourceFolders) {
if (cancelFlag != null && cancelFlag.get()) break;
ImageExtractionService.processSource(sf, rootPath -> {
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 for main thread
try {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag != null && cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (ImageExtractionService.isImageFile(file)) {
phaser.register();
try {
executor.submit(() -> {
try {
processSingleRename(
file, renameMap, destPath, claimedNames,
count, currentProgress, progress, cancelFlag
);
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("RENAME", "ERROR", "Failed to submit rename task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
} finally {
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
}
});
}
} catch (Exception e) {
LogService.log("RENAME", "ERROR", "Rename walk interrupted: " + e.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
return count.get();
}
private static void processSingleRename(
Path p,
Map<String, String> renameMap,
Path destPath,
Set<String> claimedNames,
java.util.concurrent.atomic.AtomicInteger count,
java.util.concurrent.atomic.AtomicLong currentProgress,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) {
TaskManager.checkPause();
if (cancelFlag != null && cancelFlag.get()) return;
TaskManager.updateHeartbeat();
TaskManager.setCurrentFileName(p.getFileName().toString());
String originalName = p.getFileName().toString();
String baseName = originalName.toLowerCase();
String extension = "";
int dot = originalName.lastIndexOf('.');
if (dot != -1) {
baseName = originalName.substring(0, dot).toLowerCase();
extension = originalName.substring(dot);
}
if (renameMap.containsKey(baseName)) {
String newBaseName = renameMap.get(baseName);
Path target = resolveTargetCollision(destPath, newBaseName, extension, claimedNames);
String claimKey = target.getFileName().toString().toLowerCase();
try {
ImageExtractionService.safeCopy(p, target);
count.incrementAndGet();
TaskManager.incrementProcessed();
} catch (IOException e) {
LogService.log("RENAME", "ERROR", "Failed to copy/rename: " + p + " - " + e.getMessage());
TaskManager.incrementFailedCount();
TaskManager.incrementProcessed();
} finally {
if (claimedNames != null) {
claimedNames.remove(claimKey);
}
}
} else {
TaskManager.incrementSkippedCount();
TaskManager.incrementProcessed();
}
long cp = currentProgress.incrementAndGet();
if (cp % 50 == 0 && progress != null) progress.accept(cp);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ImageValidationService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import org.apache.tika.Tika;
public class ImageValidationService {
public enum ScanMode {
QUICK, DEEP
}
public static class ValidationResult {
public final String directory;
public final String fileName;
public final String format;
public final long sizeKB;
public final String dimensions;
public final String status;
public final String reason;
public final String hash;
public ValidationResult(String directory, String fileName, String format, long sizeKB, String dimensions, String status, String reason, String hash) {
this.directory = directory;
this.fileName = fileName;
this.format = format;
this.sizeKB = sizeKB;
this.dimensions = dimensions;
this.status = status;
this.reason = reason;
this.hash = hash;
}
}
private static final java.util.concurrent.atomic.AtomicBoolean isEvicting = new java.util.concurrent.atomic.AtomicBoolean(false);
public static void validateImagesStreamed(
List<String> sourceFolders,
ScanMode mode,
boolean checkSpoofing,
boolean checkDuplicates,
double similarityThreshold,
boolean failuresOnly,
ExcelService.ExcelReportWriter excelWriter,
Consumer<Long> progress,
Consumer<String> statusUpdate,
AtomicBoolean cancelFlag
) {
AtomicLong scannedCount = new AtomicLong(0);
AtomicLong processedCount = new AtomicLong(0);
int cores = Runtime.getRuntime().availableProcessors();
int threadCount = TaskManager.isLowMemoryMode() ? 2 : Math.max(2, cores - 1);
EnterpriseThreadPoolExecutor executor = new EnterpriseThreadPoolExecutor(threadCount, 5000);
Tika tika = new Tika();
final java.util.Map<String, String> processedHashes = new java.util.concurrent.ConcurrentHashMap<>();
final java.util.Map<Long, String> processedPerceptualHashes = new java.util.concurrent.ConcurrentHashMap<>();
final java.util.Map<String, Path> processedCandidates = new java.util.concurrent.ConcurrentHashMap<>();
// 1. Checkpoint System Recovery integration
String destFolder = excelWriter != null ? excelWriter.outputFilePath : null;
String stateParent = null;
if (destFolder != null) {
Path pDir = Paths.get(destFolder).getParent();
if (pDir != null) stateParent = pDir.toString();
}
final String finalStateParent = stateParent;
final ProcessingState checkpoint = finalStateParent != null ? ProcessingState.loadState(finalStateParent) : null;
final Set<String> completedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
final Set<String> failedFiles = java.util.concurrent.ConcurrentHashMap.newKeySet();
if (checkpoint != null) {
completedFiles.addAll(checkpoint.completedFiles);
failedFiles.addAll(checkpoint.failedFiles);
processedCount.set(checkpoint.progressCount);
// Sync task manager telemetry processed count
TaskManager.resetTelemetry();
TaskManager.setProcessedCount(checkpoint.progressCount);
TaskManager.setFailedCount(checkpoint.failedFiles.size());
}
try {
for (String sf : sourceFolders) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(sf, rootPath -> {
// Use a zero-overhead Phaser to block and wait for tasks in this ZIP/directory instead of ArrayList<Future>
final java.util.concurrent.Phaser phaser = new java.util.concurrent.Phaser(1); // 1 represents the main thread
try {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
if (ImageExtractionService.isImageFile(file)) {
long scanIdx = scannedCount.incrementAndGet();
if (statusUpdate != null && scanIdx % 100 == 0) {
statusUpdate.accept("Scanning & Auditing... Found: " + scanIdx);
}
phaser.register();
try {
executor.submit(() -> {
try {
processSingleStreamedValidation(
file, mode, checkSpoofing, checkDuplicates, similarityThreshold, failuresOnly,
processedHashes, processedPerceptualHashes, processedCandidates, completedFiles, failedFiles, finalStateParent,
excelWriter, processedCount, progress, cancelFlag, tika
);
} catch (Throwable t) {
LogService.log("VALIDATE", "ERROR", "Validation worker error: " + t.getMessage());
} finally {
phaser.arriveAndDeregister();
}
});
} catch (Exception e) {
phaser.arriveAndDeregister();
LogService.log("VALIDATE", "ERROR", "Failed to submit validation task for " + file.getFileName().toString() + ": " + e.getMessage());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
} finally {
// Await all tasks for this ZIP filesystem before exiting lambda to prevent premature closure
phaser.arriveAndAwaitAdvance();
}
});
}
// Delete progress state upon successful complete
if (!cancelFlag.get() && finalStateParent != null) {
ProcessingState.deleteState(finalStateParent);
}
} catch (Exception ex) {
LogService.log("VALIDATE", "ERROR", "Validation disk walk interrupted: " + ex.getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
private static void processSingleStreamedValidation(
Path p,
ScanMode mode,
boolean checkSpoofing,
boolean checkDuplicates,
double similarityThreshold,
boolean failuresOnly,
java.util.Map<String, String> processedHashes,
java.util.Map<Long, String> processedPerceptualHashes,
java.util.Map<String, Path> processedCandidates,
Set<String> completedFiles,
Set<String> failedFiles,
String stateParent,
ExcelService.ExcelReportWriter excelWriter,
AtomicLong processedCount,
Consumer<Long> progress,
AtomicBoolean cancelFlag,
Tika tika
) {
TaskManager.checkPause();
if (cancelFlag.get()) return;
TaskManager.updateHeartbeat();
TaskManager.setCurrentFileName(p.getFileName().toString());
String fileName = p.getFileName().toString();
String fileKey = p.toUri().toString();
// 2. Checkpoint System Recovery Skip Check
if (completedFiles.contains(fileKey) || failedFiles.contains(fileKey)) {
// Already successfully processed in a previous interrupted run, bypass!
updateProgress(processedCount, progress);
return;
}
String directory = p.getParent() != null ? p.getParent().toString() : "Root";
String format = "";
int dot = fileName.lastIndexOf('.');
if (dot != -1) format = fileName.substring(dot + 1).toUpperCase();
long sizeKB = 0;
try {
sizeKB = Files.size(p) / 1024;
} catch (IOException e) {
ValidationResult r = new ValidationResult(directory, fileName, format, 0, "Unknown", "Corrupted", "Failed to read file size", "N/A");
if (excelWriter != null) {
excelWriter.writeResult(r);
}
failedFiles.add(fileKey);
updateProgress(processedCount, progress);
return;
}
String dimensions = "Unknown";
String status = "Valid";
String reason = "";
String hash = "N/A";
try {
// MIME Verification with Magic Headers via Apache Tika
String detectedMime = tika.detect(p);
boolean isPdf = format.equalsIgnoreCase("PDF");
if (isPdf) {
if (!detectedMime.equalsIgnoreCase("application/pdf")) {
throw new IOException("Spoofed Extension! Tika magic headers detected actual MIME type as: " + detectedMime);
}
dimensions = "Document";
} else {
if (!detectedMime.startsWith("image/")) {
throw new IOException("Spoofed Extension! Tika magic headers detected non-image MIME: " + detectedMime);
}
// Safe Image Dimension Pre-Check: Do NOT fully decode image, read header dimensions first
dimensions = readImageDimensionsPreCheck(p);
if (mode == ScanMode.DEEP) {
try (InputStream is = Files.newInputStream(p)) {
BufferedImage img = ImageIO.read(is);
if (img == null) {
throw new IOException("Deep scan failed: ImageIO unable to decode raster stream.");
}
img.flush();
}
}
}
if (checkSpoofing && !isPdf) {
String expectedMime = "image/" + format.toLowerCase();
if (format.equalsIgnoreCase("JPG") || format.equalsIgnoreCase("JPEG")) expectedMime = "image/jpeg";
if (format.equalsIgnoreCase("TIF") || format.equalsIgnoreCase("TIFF")) expectedMime = "image/tiff";
if (format.equalsIgnoreCase("BMP")) expectedMime = "image/bmp";
if (format.equalsIgnoreCase("PNG")) expectedMime = "image/png";
if (format.equalsIgnoreCase("WEBP")) expectedMime = "image/webp";
if (!detectedMime.equalsIgnoreCase(expectedMime)) {
status = "Spoofed";
reason = "Extension claims " + format + " but actual magic signature is " + detectedMime;
}
}
boolean isDuplicate = false;
if (!isPdf && checkDuplicates) {
try {
// Compute duplicate status using high-performance staged hashing pipeline
DuplicateManager.StagedDuplicateResult duplicateResult = HashService.checkDuplicateStaged(p, processedHashes, processedCandidates);
hash = duplicateResult.hash;
if (duplicateResult.isDuplicate) {
status = "Duplicate";
reason = duplicateResult.reason;
TaskManager.incrementDuplicateCount();
isDuplicate = true;
}
} catch (Exception e) {
LogService.log("VALIDATE", "ERROR", "Failed to compute duplicate hash for " + fileName + ": " + e.getMessage());
}
if (!isDuplicate && processedPerceptualHashes != null) {
try {
long currentDHash = HashService.computePerceptualHash(p);
// 1. O(1) Instant exact dHash match
String exactMatch = processedPerceptualHashes.get(currentDHash);
if (exactMatch != null) {
status = "Duplicate";
reason = "Exact perceptual image match (100.00% similarity) with original: " + exactMatch;
TaskManager.incrementDuplicateCount();
isDuplicate = true;
} else {
// 2. High-performance bounded near-match similarity check (up to 250 recent entries to prevent O(N^2) lockup)
int checked = 0;
for (java.util.Map.Entry<Long, String> entry : processedPerceptualHashes.entrySet()) {
if (++checked > 250) break;
double similarity = HashService.calculatePerceptualSimilarity(currentDHash, entry.getKey());
if (similarity >= similarityThreshold) {
status = "Duplicate";
reason = String.format("Perceptual image match (%.2f%% similarity) with original: %s", similarity * 100, entry.getValue());
TaskManager.incrementDuplicateCount();
isDuplicate = true;
break;
}
}
}
if (processedPerceptualHashes.size() > 500000 && isEvicting.compareAndSet(false, true)) {
try {
int toRemove = processedPerceptualHashes.size() / 5;
java.util.Iterator<Long> it = processedPerceptualHashes.keySet().iterator();
while (it.hasNext() && toRemove-- > 0) {
it.next();
it.remove();
}
} finally {
isEvicting.set(false);
}
}
if (!isDuplicate) {
processedPerceptualHashes.put(currentDHash, fileName);
}
} catch (Exception e) {
LogService.log("VALIDATE", "ERROR", "Failed to compute perceptual dHash for " + fileName + ": " + e.getMessage());
}
}
}
// Mark file as completed successfully
if (completedFiles.size() > 500000 && isEvicting.compareAndSet(false, true)) {
try {
int removeCount = completedFiles.size() / 5;
java.util.Iterator<String> it = completedFiles.iterator();
while (it.hasNext() && removeCount-- > 0) {
it.next();
it.remove();
}
} finally {
isEvicting.set(false);
}
}
completedFiles.add(fileKey);
} catch (Exception e) {
status = "Corrupted";
reason = e.getMessage() != null ? e.getMessage() : e.toString();
if (failedFiles.size() > 500000 && isEvicting.compareAndSet(false, true)) {
try {
int removeCount = failedFiles.size() / 5;
java.util.Iterator<String> it = failedFiles.iterator();
while (it.hasNext() && removeCount-- > 0) {
it.next();
it.remove();
}
} finally {
isEvicting.set(false);
}
}
failedFiles.add(fileKey);
TaskManager.incrementFailedCount();
}
ValidationResult result = new ValidationResult(directory, fileName, format, sizeKB, dimensions, status, reason, hash);
if (excelWriter != null && (!failuresOnly || !"Valid".equals(status))) {
excelWriter.writeResult(result);
}
// Save progress checkpoint recovery state adaptively
long currentValCount = processedCount.get() + 1;
if (stateParent != null) {
ProcessingState.saveStateAdaptive(stateParent, "validate-task", completedFiles, failedFiles, currentValCount);
}
updateProgress(processedCount, progress);
}
private static String readImageDimensionsPreCheck(Path p) throws IOException {
try (InputStream is = Files.newInputStream(p);
ImageInputStream iis = ImageIO.createImageInputStream(is)) {
if (iis == null) return "Unknown";
java.util.Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try {
reader.setInput(iis);
int w = reader.getWidth(0);
int h = reader.getHeight(0);
if (w > 32768 || h > 32768) {
throw new IOException("Decompression bomb protection triggered! Malicious image size: " + w + "x" + h);
}
return w + "x" + h;
} finally {
reader.dispose();
}
}
}
return "Unknown";
}
private static void updateProgress(AtomicLong processedCount, Consumer<Long> progress) {
long cp = processedCount.incrementAndGet();
TaskManager.incrementProcessed();
if (cp % 50 == 0 && progress != null) progress.accept(cp);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\KeepAwakeService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class KeepAwakeService {
private static volatile boolean active = false;
private static ScheduledExecutorService scheduler;
/**
* Pure-Java Keep-Awake implementation using java.awt.Robot.
* Combines a 1-pixel micro-nudge (immediately restored) and a virtual VK_F15 keypress
* every 20 seconds. This sends genuine hardware-level input events to Windows OS,
* resetting the System & Display Inactivity (Lock Screen) timers 100% offline
* without native DLLs (JNA) or corporate EDR security alerts.
*/
public static synchronized void start() {
if (active) return;
try {
Robot robot = new Robot();
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "SNAPIX-KeepAwake");
t.setDaemon(true);
return t;
});
scheduler.scheduleAtFixedRate(() -> {
try {
// 1. Mouse Micro-Nudge (move 1px and restore immediately)
java.awt.PointerInfo pointerInfo = MouseInfo.getPointerInfo();
if (pointerInfo != null) {
Point p = pointerInfo.getLocation();
if (p != null) {
int delta = (p.x > 10) ? -1 : 1;
robot.mouseMove(p.x + delta, p.y);
robot.mouseMove(p.x, p.y);
}
}
// 2. Virtual F15 key heartbeat (resets OS keyboard idle timer silently)
robot.keyPress(KeyEvent.VK_F15);
robot.keyRelease(KeyEvent.VK_F15);
} catch (Throwable ignored) {}
}, 0, 20, TimeUnit.SECONDS);
active = true;
LogService.log("SYSTEM", "INFO", "System Keep-Awake activated (Robot mouse nudge + F15 key heartbeat).");
} catch (Throwable t) {
LogService.log("SYSTEM", "WARNING", "Failed to initialize Keep-Awake robot: " + t.getMessage());
}
}
public static synchronized void stop() {
if (!active) return;
try {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdownNow();
scheduler = null;
}
active = false;
LogService.log("SYSTEM", "INFO", "System Keep-Awake deactivated.");
} catch (Throwable t) {
// Ignore cleanup failure
}
}
public static boolean isActive() {
return active;
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\LogService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import javax.swing.SwingUtilities;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Consumer;
public class LogService {
private static volatile Consumer<String> logListener;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final java.util.concurrent.BlockingQueue<String> uiQueue = new java.util.concurrent.ArrayBlockingQueue<>(2000);
private static final java.util.concurrent.atomic.AtomicBoolean flushScheduled = new java.util.concurrent.atomic.AtomicBoolean(false);
public static void setListener(Consumer<String> listener) {
logListener = listener;
}
public static void log(String message) {
log("SYSTEM", "INFO", message);
}
public static void log(String category, String severity, String message) {
SimpleLogger.log(category, severity, message);
Consumer<String> listener = logListener;
if (listener != null) {
String timeStr = LocalDateTime.now().format(FORMATTER);
String formattedMessage = "[" + timeStr + "] " + message + "\n";
uiQueue.offer(formattedMessage);
if (flushScheduled.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
try {
Consumer<String> l = logListener;
if (l != null) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = uiQueue.poll()) != null) {
sb.append(line);
}
if (sb.length() > 0) {
l.accept(sb.toString());
}
} else {
uiQueue.clear();
}
} finally {
flushScheduled.set(false);
}
});
}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ProcessingState.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
public class ProcessingState implements Serializable {
private static final long serialVersionUID = 1L;
private static final String STATE_FILE_NAME = "ProcessingState.dat";
private static final java.util.concurrent.atomic.AtomicLong lastSavedTimeMillis = new java.util.concurrent.atomic.AtomicLong(System.currentTimeMillis());
private static final java.util.concurrent.atomic.AtomicLong lastSavedCount = new java.util.concurrent.atomic.AtomicLong(0);
public final String taskUuid;
public final Set<String> completedFiles;
public final Set<String> failedFiles;
public final long progressCount;
public ProcessingState(String taskUuid, Set<String> completedFiles, Set<String> failedFiles, long progressCount) {
this.taskUuid = taskUuid;
Set<String> comp = (completedFiles instanceof HashSet) ? completedFiles : ((completedFiles != null) ? new HashSet<>(completedFiles) : new HashSet<>());
if (comp.size() > 500000) {
int toRemove = comp.size() - 500000;
java.util.Iterator<String> it = comp.iterator();
while (it.hasNext() && toRemove-- > 0) {
it.next();
it.remove();
}
}
this.completedFiles = comp;
Set<String> fail = (failedFiles instanceof HashSet) ? failedFiles : ((failedFiles != null) ? new HashSet<>(failedFiles) : new HashSet<>());
if (fail.size() > 500000) {
int toRemove = fail.size() - 500000;
java.util.Iterator<String> it = fail.iterator();
while (it.hasNext() && toRemove-- > 0) {
it.next();
it.remove();
}
}
this.failedFiles = fail;
this.progressCount = progressCount;
}
public static void saveStateAdaptive(String destFolder, String taskUuid, Set<String> completedFiles, Set<String> failedFiles, long progressCount) {
long now = System.currentTimeMillis();
long lastTime = lastSavedTimeMillis.get();
long lastCount = lastSavedCount.get();
if (now - lastTime >= 15000 || Math.abs(progressCount - lastCount) >= 5000) {
if (lastSavedTimeMillis.compareAndSet(lastTime, now)) {
lastSavedCount.set(progressCount);
Set<String> compCopy = new HashSet<>();
if (completedFiles != null) {
try {
compCopy.addAll(completedFiles);
} catch (Exception ignored) {}
}
Set<String> failCopy = new HashSet<>();
if (failedFiles != null) {
try {
failCopy.addAll(failedFiles);
} catch (Exception ignored) {}
}
saveState(destFolder, new ProcessingState(taskUuid, compCopy, failCopy, progressCount));
}
}
}
public static void resetAdaptiveThresholds() {
lastSavedTimeMillis.set(System.currentTimeMillis());
lastSavedCount.set(0);
}
public static void saveState(String destFolder, ProcessingState state) {
Path destDir = com.imageextractor.utils.PathUtils.normalizeLongPath(destFolder);
Path realPath = destDir.resolve(STATE_FILE_NAME);
Path tmpPath = destDir.resolve(STATE_FILE_NAME + ".tmp");
try {
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
BufferedOutputStream bos = new BufferedOutputStream(fos, 65536);
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(state);
}
try {
Files.move(tmpPath, realPath, java.nio.file.StandardCopyOption.ATOMIC_MOVE, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (java.nio.file.AtomicMoveNotSupportedException amse) {
Files.move(tmpPath, realPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
LogService.log("STATE", "INFO", "Progress checkpoint saved successfully (Index: " + state.progressCount + ")");
} catch (IOException e) {
LogService.log("STATE", "ERROR", "Failed to save progress checkpoint: " + e.getMessage());
try {
Files.deleteIfExists(tmpPath);
} catch (IOException ignored) {}
}
}
public static ProcessingState loadState(String destFolder) {
Path path = com.imageextractor.utils.PathUtils.normalizeLongPath(destFolder).resolve(STATE_FILE_NAME);
if (!Files.exists(path)) return null;
try (FileInputStream fis = new FileInputStream(path.toFile());
BufferedInputStream bis = new BufferedInputStream(fis, 65536);
ObjectInputStream ois = new ObjectInputStream(bis) {
@Override
protected Class<?> resolveClass(java.io.ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
if (!name.startsWith("com.imageextractor.") &&
!name.equals("java.util.HashSet") &&
!name.equals("java.util.HashMap") &&
!name.equals("java.lang.String") &&
!name.equals("java.lang.Long") &&
!name.equals("java.lang.Integer") &&
!name.equals("java.lang.Number")) {
throw new java.io.InvalidClassException("Unauthorized deserialization class: " + name);
}
return super.resolveClass(desc);
}
}) {
ProcessingState state = (ProcessingState) ois.readObject();
LogService.log("STATE", "INFO", "Found progress checkpoint. Resuming from file index: " + state.progressCount);
return state;
} catch (Exception e) {
LogService.log("STATE", "ERROR", "Failed to load progress checkpoint: " + e.getMessage());
return null;
}
}
public static void deleteState(String destFolder) {
Path path = com.imageextractor.utils.PathUtils.normalizeLongPath(destFolder).resolve(STATE_FILE_NAME);
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\SettingsManager.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;
public class SettingsManager {
private static final String CONFIG_DIR = "config";
private static final String SETTINGS_FILE = CONFIG_DIR + File.separator + "settings.properties";
private static final Properties properties = new Properties();
static {
loadSettings();
}
public static synchronized void loadSettings() {
try {
File dir = new File(CONFIG_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(SETTINGS_FILE);
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
properties.load(fis);
}
}
// Set defaults if empty
if (!properties.containsKey("lowMemoryMode")) {
properties.setProperty("lowMemoryMode", "false");
}
// Apply loaded settings to TaskManager
boolean lowMem = Boolean.parseBoolean(properties.getProperty("lowMemoryMode", "false"));
TaskManager.setLowMemoryMode(lowMem);
} catch (Exception e) {
LogService.log("SETTINGS", "ERROR", "Failed to load settings: " + e.getMessage());
}
}
public static synchronized void saveSettings() {
try {
File dir = new File(CONFIG_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
// Update properties with current TaskManager values
properties.setProperty("lowMemoryMode", String.valueOf(TaskManager.isLowMemoryMode()));
File file = new File(SETTINGS_FILE);
File tmpFile = new File(SETTINGS_FILE + ".tmp");
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
properties.store(fos, "Enterprise Image Processing Tool Settings");
}
if (tmpFile.exists()) {
try {
java.nio.file.Files.move(tmpFile.toPath(), file.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING, java.nio.file.StandardCopyOption.ATOMIC_MOVE);
} catch (Exception ex) {
java.nio.file.Files.move(tmpFile.toPath(), file.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (Exception e) {
LogService.log("SETTINGS", "ERROR", "Failed to save settings: " + e.getMessage());
}
}
public static boolean isLowMemoryMode() {
return TaskManager.isLowMemoryMode();
}
public static void setLowMemoryMode(boolean value) {
TaskManager.setLowMemoryMode(value);
saveSettings();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\SevenZipManager.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class SevenZipManager {
private static File cached7zFile = null;
/**
* Resolves the path to the 7-Zip executable by checking Windows Program Files
* and searching the system PATH variable environment.
*/
public static synchronized File resolve7zExecutable() {
if (cached7zFile != null) return cached7zFile;
// 1. Resolve Program Files paths dynamically using environment variables
java.util.List<String> searchPaths = new java.util.ArrayList<>();
String pf = System.getenv("ProgramFiles");
String pf86 = System.getenv("ProgramFiles(x86)");
String pfW64 = System.getenv("ProgramW6432");
if (pf != null) searchPaths.add(pf + "\\7-Zip\\7z.exe");
if (pf86 != null) searchPaths.add(pf86 + "\\7-Zip\\7z.exe");
if (pfW64 != null) searchPaths.add(pfW64 + "\\7-Zip\\7z.exe");
// Common fallbacks
searchPaths.add("C:\\Program Files\\7-Zip\\7z.exe");
searchPaths.add("C:\\Program Files (x86)\\7-Zip\\7z.exe");
for (String p : searchPaths) {
File f = new File(p);
if (f.exists() && f.isFile()) {
cached7zFile = f;
return f;
}
}
// 2. Search in system environment PATH variable
String pathEnv = System.getenv("PATH");
if (pathEnv != null) {
for (String part : pathEnv.split(File.pathSeparator)) {
File f = new File(part, "7z.exe");
if (f.exists() && f.isFile()) {
cached7zFile = f;
return f;
}
}
}
return null;
}
/**
* Executes the 7-Zip process to compress the sourceDirectory into the targetZip file.
*/
public static void compressWith7Zip(
String exePath,
String sourceDirectory,
String targetZip,
AtomicBoolean cancelFlag,
AtomicBoolean pauseFlag,
Object pauseLock
) throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add(exePath);
cmd.add("a");
cmd.add("-tzip");
cmd.add("--");
cmd.add(targetZip);
cmd.add(sourceDirectory + File.separator + "*");
ProcessBuilder pb = new ProcessBuilder(cmd);
String os = System.getProperty("os.name").toLowerCase();
File devNull = new File(os.contains("win") ? "NUL" : "/dev/null");
pb.redirectOutput(devNull);
pb.redirectError(devNull);
Process p = pb.start();
try {
if (p.getOutputStream() != null) {
p.getOutputStream().close();
}
} catch (Exception ignored) {}
long processStartTime = System.currentTimeMillis();
long processTimeoutMs = 30 * 60 * 1000L; // 30 minutes max
try {
while (p.isAlive()) {
if (System.currentTimeMillis() - processStartTime > processTimeoutMs) {
p.destroyForcibly();
throw new IOException("7-Zip compression timed out after 30 minutes.");
}
if (cancelFlag != null && cancelFlag.get()) {
p.destroyForcibly();
throw new InterruptedException("Compression cancelled by user.");
}
if (pauseFlag != null && pauseFlag.get() && pauseLock != null) {
synchronized (pauseLock) {
try {
while (pauseFlag.get()) {
pauseLock.wait(100);
if (cancelFlag != null && cancelFlag.get()) {
p.destroyForcibly();
throw new InterruptedException("Compression cancelled by user.");
}
}
} catch (InterruptedException e) {
p.destroyForcibly();
Thread.currentThread().interrupt();
throw e;
}
}
}
Thread.sleep(100);
}
int exitCode = p.waitFor();
if (exitCode != 0 && exitCode != 1) {
throw new Exception("7-Zip failed with exit code: " + exitCode);
}
} catch (InterruptedException e) {
p.destroyForcibly();
throw e;
} finally {
if (p.isAlive()) {
p.destroyForcibly();
}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\SimpleLogger.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SimpleLogger {
private static final String LOG_DIR = "logs";
private static final String LOG_FILE = LOG_DIR + File.separator + "app.log";
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final java.util.concurrent.BlockingQueue<String> logQueue = new java.util.concurrent.ArrayBlockingQueue<>(5000);
private static volatile PrintWriter fileWriter;
static {
File dir = new File(LOG_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
initWriter();
Thread writerThread = new Thread(() -> {
while (true) {
try {
String msg = logQueue.take();
writeToFile(msg);
String next;
while ((next = logQueue.poll()) != null) {
writeToFile(next);
}
if (fileWriter != null) {
fileWriter.flush();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception ignored) {}
}
});
writerThread.setName("SimpleLogger-Writer");
writerThread.setDaemon(true);
writerThread.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
while (!logQueue.isEmpty()) {
String m = logQueue.poll();
if (m != null) writeToFile(m);
}
if (fileWriter != null) {
fileWriter.flush();
fileWriter.close();
}
}));
}
private static synchronized void initWriter() {
try {
File file = new File(LOG_FILE);
if (file.exists() && file.length() > MAX_FILE_SIZE) {
rotateLogs(file);
}
fileWriter = new PrintWriter(new BufferedWriter(new FileWriter(LOG_FILE, true), 65536), false);
} catch (IOException e) {
System.err.println("SimpleLogger: Could not open log file: " + e.getMessage());
}
}
private static synchronized void writeToFile(String msg) {
try {
// Print to console from single background writer thread, avoiding multi-thread lock contention
System.out.println(msg);
File file = new File(LOG_FILE);
if (file.exists() && file.length() > MAX_FILE_SIZE) {
if (fileWriter != null) {
fileWriter.close();
}
rotateLogs(file);
initWriter();
}
if (fileWriter != null) {
fileWriter.println(msg);
}
} catch (Exception ignored) {}
}
public static void log(String category, String severity, String message) {
String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
String threadName = Thread.currentThread().getName().toUpperCase();
String formattedMsg = String.format("[%s][%s][%s][%s] %s", timestamp, category.toUpperCase(), threadName, severity.toUpperCase(), message);
// Offer to background queue without blocking caller worker threads
logQueue.offer(formattedMsg);
}
private static void rotateLogs(File currentFile) {
File oldFile = new File(LOG_DIR + File.separator + "app.log.old");
if (oldFile.exists()) {
oldFile.delete();
}
if (!currentFile.renameTo(oldFile)) {
// If rename fails (e.g. temporary lock), attempt delete to avoid continuous rotation thrashing
try {
oldFile.delete();
currentFile.renameTo(oldFile);
} catch (Exception ignored) {}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\SplitService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class SplitService {
private static void deleteDirectoryRecursive(Path dir) {
if (dir == null || !Files.exists(dir)) return;
try {
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
try {
Files.deleteIfExists(file);
} catch (Exception ignored) {}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path d, IOException exc) {
try {
Files.deleteIfExists(d);
} catch (Exception ignored) {}
return FileVisitResult.CONTINUE;
}
});
} catch (Exception e) {
LogService.log("SPLIT", "WARNING", "Failed to clean directory: " + dir + " - " + e.getMessage());
}
}
private static void finalizeBatch(Path parent, String baseName, int batchNumber, boolean outputAsZip, AtomicBoolean cancelFlag) throws Exception {
String batchName = baseName + "_" + batchNumber;
Path folderPath = parent.resolve(outputAsZip ? (batchName + "_temp") : batchName);
if (outputAsZip && Files.exists(folderPath)) {
Path zipPath = parent.resolve(batchName + ".zip");
try {
ZipService.compressDirectory(folderPath.toString(), zipPath.toString(), cancelFlag, null, null);
} finally {
deleteDirectoryRecursive(folderPath);
}
}
}
public static int splitImagesStreamed(
List<String> sourceFolders,
String destParentPath,
String baseName,
long sizeLimitBytes,
boolean outputAsZip,
Consumer<Long> progress,
AtomicBoolean cancelFlag
) throws Exception {
final long[] currentProgress = {0};
final int[] batchNumber = {1};
final long[] currentBatchSize = {0};
final Path parent = com.imageextractor.utils.PathUtils.normalizeLongPath(destParentPath);
if (!Files.exists(parent)) Files.createDirectories(parent);
try {
for (String sf : sourceFolders) {
if (cancelFlag != null && cancelFlag.get()) break;
ImageExtractionService.processSource(sf, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (cancelFlag != null && cancelFlag.get()) return FileVisitResult.TERMINATE;
TaskManager.checkPause();
TaskManager.updateHeartbeat();
TaskManager.setCurrentFileName(file.getFileName().toString());
if (ImageExtractionService.isImageFile(file)) {
try {
long fileSize = Files.size(file);
if (currentBatchSize[0] + fileSize > sizeLimitBytes && currentBatchSize[0] > 0) {
finalizeBatch(parent, baseName, batchNumber[0], outputAsZip, cancelFlag);
batchNumber[0]++;
currentBatchSize[0] = 0;
}
String batchName = baseName + "_" + batchNumber[0];
Path folderPath = parent.resolve(outputAsZip ? (batchName + "_temp") : batchName);
if (!Files.exists(folderPath)) Files.createDirectories(folderPath);
Path target = folderPath.resolve(file.getFileName().toString());
if (Files.exists(target)) {
String fn = file.getFileName().toString();
int dot = fn.lastIndexOf('.');
String base = dot != -1 ? fn.substring(0, dot) : fn;
String ext = dot != -1 ? fn.substring(dot) : "";
target = folderPath.resolve(base + "_" + java.util.UUID.randomUUID().toString() + ext);
}
ImageExtractionService.safeCopy(file, target);
currentBatchSize[0] += fileSize;
currentProgress[0]++;
TaskManager.incrementProcessed();
if (currentProgress[0] % 50 == 0 && progress != null) progress.accept(currentProgress[0]);
} catch (Exception e) {
LogService.log("SPLIT", "ERROR", "Failed to process file during split: " + file + " - " + e.getMessage());
TaskManager.incrementFailedCount();
}
}
return FileVisitResult.CONTINUE;
}
});
});
}
boolean isCancelled = (cancelFlag != null && cancelFlag.get());
if (currentBatchSize[0] > 0 && !isCancelled) {
finalizeBatch(parent, baseName, batchNumber[0], outputAsZip, cancelFlag);
} else if (currentBatchSize[0] == 0 && batchNumber[0] == 1) {
return 0;
}
if (isCancelled) {
return Math.max(0, batchNumber[0] - 1);
}
return currentBatchSize[0] > 0 ? batchNumber[0] : Math.max(0, batchNumber[0] - 1);
} finally {
if (cancelFlag != null && cancelFlag.get() && outputAsZip) {
String batchName = baseName + "_" + batchNumber[0];
Path folderPath = parent.resolve(batchName + "_temp");
deleteDirectoryRecursive(folderPath);
}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\TaskManager.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TaskManager {
// Performance & Count telemetry
private static final AtomicLong totalFiles = new AtomicLong(0);
private static final AtomicLong scannedCount = new AtomicLong(0);
private static final AtomicLong processedCount = new AtomicLong(0);
private static final AtomicLong failedCount = new AtomicLong(0);
private static final AtomicLong duplicateCount = new AtomicLong(0);
private static final AtomicLong skippedCount = new AtomicLong(0);
private static final AtomicLong totalBytesProcessed = new AtomicLong(0);
private static volatile String currentFileName = "";
private static volatile String currentOperation = "Idle";
private static volatile boolean lowMemoryMode = false;
// Deadlock-Free Pause/Resume Lock & Conditions
private static final ReentrantLock pauseLock = new ReentrantLock();
private static final Condition pauseCondition = pauseLock.newCondition();
private static final AtomicBoolean paused = new AtomicBoolean(false);
public static void updateHeartbeat() {
// Fast no-op: maintains API compatibility without 10M map put overhead
}
public static void checkPause() {
if (paused.get()) {
pauseLock.lock();
try {
while (paused.get()) {
pauseCondition.await();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
pauseLock.unlock();
}
}
}
public static void setPaused(boolean p) {
pauseLock.lock();
try {
paused.set(p);
if (!p) {
pauseCondition.signalAll();
}
} finally {
pauseLock.unlock();
}
}
public static boolean isPaused() {
return paused.get();
}
public static void resetTelemetry() {
totalFiles.set(0);
scannedCount.set(0);
processedCount.set(0);
failedCount.set(0);
duplicateCount.set(0);
skippedCount.set(0);
totalBytesProcessed.set(0);
currentFileName = "";
currentOperation = "Idle";
setPaused(false);
ProcessingState.resetAdaptiveThresholds();
}
// Increment helpers
public static void incrementProcessed() { processedCount.incrementAndGet(); }
public static void incrementFailedCount() { failedCount.incrementAndGet(); }
public static void incrementDuplicateCount() { duplicateCount.incrementAndGet(); }
public static void incrementSkippedCount() { skippedCount.incrementAndGet(); }
public static void addBytesProcessed(long bytes) { totalBytesProcessed.addAndGet(bytes); }
// Getters and setters
public static long getTotalFiles() { return totalFiles.get(); }
public static void setTotalFiles(long val) { totalFiles.set(val); }
public static long getScannedCount() { return scannedCount.get(); }
public static void setScannedCount(long val) { scannedCount.set(val); }
public static long getProcessedCount() { return processedCount.get(); }
public static void setProcessedCount(long val) { processedCount.set(val); }
public static long getFailedCount() { return failedCount.get(); }
public static void setFailedCount(long val) { failedCount.set(val); }
public static long getDuplicateCount() { return duplicateCount.get(); }
public static long getSkippedCount() { return skippedCount.get(); }
public static long getTotalBytesProcessed() { return totalBytesProcessed.get(); }
public static String getCurrentFileName() { return currentFileName; }
public static void setCurrentFileName(String name) { currentFileName = name; }
public static String getCurrentOperation() { return currentOperation; }
public static void setCurrentOperation(String op) { currentOperation = op; }
public static boolean isLowMemoryMode() { return lowMemoryMode; }
public static void setLowMemoryMode(boolean val) { lowMemoryMode = val; }
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\TaskWatchdog.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.util.concurrent.atomic.AtomicBoolean;
public class TaskWatchdog {
private static final long MAX_STALL_TIME = 5 * 60 * 1000; // 5 Minutes
private static AtomicBoolean running = new AtomicBoolean(false);
private static Thread watchdogThread;
public static boolean isRunning() {
return running.get();
}
public static synchronized void start(AtomicBoolean cancelFlag) {
if (running.get()) return;
running.set(true);
watchdogThread = new Thread(() -> {
try {
long lastProgressTime = System.currentTimeMillis();
long lastProcessed = TaskManager.getProcessedCount();
boolean wasPaused = false;
while (running.get()) {
try {
Thread.sleep(10000); // Check every 10 seconds
if (TaskManager.isPaused()) {
wasPaused = true;
continue; // Watchdog ignores stalls while paused
} else if (wasPaused) {
// Reset progress time when resuming from pause
wasPaused = false;
lastProgressTime = System.currentTimeMillis();
lastProcessed = TaskManager.getProcessedCount();
}
long now = System.currentTimeMillis();
long currentProcessed = TaskManager.getProcessedCount();
// If any progress was made, reset the global progress timestamp
if (currentProcessed > lastProcessed) {
lastProcessed = currentProcessed;
lastProgressTime = now;
} else if (now - lastProgressTime > MAX_STALL_TIME && TaskManager.getTotalFiles() > 0) {
// Total stall: zero progress across all threads for 5 minutes
LogService.log("WATCHDOG", "WARNING", "System stall detected: No file progress made in " + (now - lastProgressTime)/1000 + "s. Requesting abort.");
if (cancelFlag != null) {
cancelFlag.set(true);
}
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
} finally {
running.set(false);
}
});
watchdogThread.setDaemon(true);
watchdogThread.setName("TaskWatchdog");
watchdogThread.start();
}
public static synchronized void stop() {
running.set(false);
if (watchdogThread != null) {
watchdogThread.interrupt();
watchdogThread = null;
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\TempManager.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class TempManager {
private static final String SESSION_ID = java.util.UUID.randomUUID().toString().substring(0, 8);
private static final Path baseTempPath = Paths.get(System.getProperty("java.io.tmpdir", "."), "ImageExtractor", "temp").toAbsolutePath();
private static final Path rootTempPath = baseTempPath.resolve("session_" + SESSION_ID);
static {
// Clean up old orphaned sessions (> 2 hours old) on startup safely without touching active sessions
cleanupOrphanedSessions();
try {
Files.createDirectories(rootTempPath);
} catch (IOException e) {
LogService.log("TEMP", "ERROR", "Failed to initialize temp session folder: " + e.getMessage());
}
// Register shutdown hook for clean exit of current session
Runtime.getRuntime().addShutdownHook(new Thread(TempManager::cleanupCurrentSession));
}
private static void cleanupOrphanedSessions() {
if (!Files.exists(baseTempPath)) return;
try (java.nio.file.DirectoryStream<Path> stream = Files.newDirectoryStream(baseTempPath)) {
long now = System.currentTimeMillis();
for (Path entry : stream) {
if (Files.isDirectory(entry) && entry.getFileName().toString().startsWith("session_")) {
try {
long lastMod = Files.getLastModifiedTime(entry).toMillis();
if (now - lastMod > 2 * 3600 * 1000) { // Older than 2 hours
deleteDirectoryRecursively(entry);
}
} catch (Exception ignored) {}
}
}
} catch (Exception ignored) {}
}
public static Path createTempDirectory(String prefix) throws IOException {
if (!Files.exists(rootTempPath)) {
Files.createDirectories(rootTempPath);
}
return Files.createTempDirectory(rootTempPath, prefix);
}
public static void deleteTempDirectory(Path path) {
if (path == null) return;
deleteDirectoryRecursively(path);
}
public static void cleanupCurrentSession() {
deleteDirectoryRecursively(rootTempPath);
}
private static void deleteDirectoryRecursively(Path dir) {
if (dir == null || !Files.exists(dir)) return;
try {
Files.walkFileTree(dir, new java.nio.file.SimpleFileVisitor<Path>() {
@Override
public java.nio.file.FileVisitResult visitFile(Path file, java.nio.file.attribute.BasicFileAttributes attrs) {
try {
Files.deleteIfExists(file);
} catch (Exception ignored) {}
return java.nio.file.FileVisitResult.CONTINUE;
}
@Override
public java.nio.file.FileVisitResult postVisitDirectory(Path d, IOException exc) {
try {
Files.deleteIfExists(d);
} catch (Exception ignored) {}
return java.nio.file.FileVisitResult.CONTINUE;
}
});
} catch (Exception e) {
LogService.log("TEMP", "WARNING", "Failed to clean directory: " + dir + " - " + e.getMessage());
}
}
/**
* Zip Slip Protection: Normalizes path and validates it resides within target destination boundary.
*/
public static boolean isPathSafe(Path target, Path destination) {
try {
Path normalizedTarget = target.toAbsolutePath().normalize();
Path normalizedDest = destination.toAbsolutePath().normalize();
return normalizedTarget.startsWith(normalizedDest);
} catch (Exception e) {
return false;
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\UserVerificationService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
public class UserVerificationService {
private static final String USERS_RESOURCE = "/Users.txt";
private static final String DATE_FORMAT = "dd/MM/yyyy";
private static volatile Date cachedExpiryDate = null;
private static volatile Set<String> cachedUserIds = null;
private static volatile boolean parsed = false;
/**
* Returns true if today's date is strictly AFTER the expiry date.
*/
public static boolean isToolExpired() {
ensureParsed();
if (cachedExpiryDate == null) {
return false;
}
try {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
Date today = sdf.parse(sdf.format(new Date()));
return today.after(cachedExpiryDate);
} catch (Exception e) {
return false;
}
}
/**
* Returns true if current Windows user is in the authorized list.
*/
public static boolean isCurrentUserAuthorized() {
ensureParsed();
String rawName = getRawWindowsUsername();
String shortName = extractShortId(rawName);
LogService.log("SECURITY", "INFO", "Detected Windows Employee ID: '" + rawName + "' (resolved: '" + shortName + "')");
if (shortName.isEmpty()) {
LogService.log("SECURITY", "WARNING", "Could not determine Windows Employee ID. Access Denied.");
return false;
}
if (cachedUserIds == null || cachedUserIds.isEmpty()) {
LogService.log("SECURITY", "WARNING", "Users.txt is empty or could not be read. Access Denied.");
return false;
}
boolean allowed = cachedUserIds.contains(rawName.toLowerCase())
|| cachedUserIds.contains(shortName.toLowerCase());
if (allowed) {
LogService.log("SECURITY", "INFO", "Access GRANTED for Employee ID: '" + shortName + "'");
} else {
LogService.log("SECURITY", "WARNING", "Access DENIED for Employee ID: '" + rawName + "'.");
}
return allowed;
}
public static String getWindowsUsername() {
return extractShortId(getRawWindowsUsername());
}
private static String getRawWindowsUsername() {
String name = System.getProperty("user.name", "").trim();
if (!name.isEmpty()) return name;
name = getEnv("USERNAME");
if (!name.isEmpty()) return name;
return getEnv("USER");
}
private static String extractShortId(String raw) {
if (raw == null) return "";
String s = raw.trim();
if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) {
s = s.substring(1, s.length() - 1).trim();
} else if (s.startsWith("'") && s.endsWith("'") && s.length() >= 2) {
s = s.substring(1, s.length() - 1).trim();
}
int backslash = s.lastIndexOf('\\');
if (backslash >= 0 && backslash < s.length() - 1) {
s = s.substring(backslash + 1).trim();
}
int slash = s.lastIndexOf('/');
if (slash >= 0 && slash < s.length() - 1) {
s = s.substring(slash + 1).trim();
}
int at = s.indexOf('@');
if (at > 0) {
s = s.substring(0, at).trim();
}
return s;
}
private static synchronized void ensureParsed() {
if (parsed) return;
cachedUserIds = new HashSet<String>();
InputStream is = UserVerificationService.class.getResourceAsStream(USERS_RESOURCE);
if (is == null) {
is = UserVerificationService.class.getClassLoader().getResourceAsStream("Users.txt");
}
if (is == null) {
LogService.log("SECURITY", "ERROR", "Users.txt not found in JAR resources.");
parsed = true;
return;
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
boolean firstDataLine = true;
while ((line = reader.readLine()) != null) {
if (line.startsWith("\uFEFF")) line = line.substring(1);
line = line.trim();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("//")) continue;
String[] tokens = line.split(",");
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.startsWith("\"") && token.endsWith("\"") && token.length() >= 2) {
token = token.substring(1, token.length() - 1).trim();
} else if (token.startsWith("'") && token.endsWith("'") && token.length() >= 2) {
token = token.substring(1, token.length() - 1).trim();
}
if (token.isEmpty()) continue;
if (firstDataLine && i == 0) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
sdf.setLenient(false);
cachedExpiryDate = sdf.parse(token);
continue;
} catch (Exception ex) {
// First token is not a date; treat as Employee ID
}
}
String lower = token.toLowerCase();
cachedUserIds.add(lower);
String shortForm = extractShortId(lower);
if (!shortForm.isEmpty() && !shortForm.equals(lower)) {
cachedUserIds.add(shortForm);
}
}
firstDataLine = false;
}
} catch (Exception e) {
LogService.log("SECURITY", "ERROR", "Failed to read Users.txt: " + e.getMessage());
} finally {
parsed = true;
}
}
private static String getEnv(String key) {
try {
String val = System.getenv(key);
return (val != null) ? val.trim() : "";
} catch (Exception e) {
return "";
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\services\ZipService.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.services;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class ZipService {
/**
* Compresses the sourceDirectory into the targetZip file using 7-Zip (if available) or standard Java fallback.
*/
public static void compressDirectory(String sourceDirectory, String targetZip, AtomicBoolean cancelFlag, AtomicBoolean pauseFlag, Object pauseLock) throws Exception {
File z7 = SevenZipManager.resolve7zExecutable();
if (z7 != null) {
SevenZipManager.compressWith7Zip(z7.getAbsolutePath(), sourceDirectory, targetZip, cancelFlag, pauseFlag, pauseLock);
} else {
LogService.log("ZIP", "WARNING", "7-Zip not found on this system. Falling back to native Java ZIP compression.");
compressWithJavaZip(sourceDirectory, targetZip, cancelFlag, pauseFlag, pauseLock);
}
}
private static void compressWithJavaZip(String sourceDirectory, String targetZip, AtomicBoolean cancelFlag, AtomicBoolean pauseFlag, Object pauseLock) throws Exception {
Path srcDir = com.imageextractor.utils.PathUtils.normalizeLongPath(sourceDirectory);
Path targetPath = com.imageextractor.utils.PathUtils.normalizeLongPath(targetZip);
try (FileOutputStream fos = new FileOutputStream(targetPath.toFile());
BufferedOutputStream bos = new BufferedOutputStream(fos, 65536);
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(bos)) {
Files.walkFileTree(srcDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (cancelFlag != null && cancelFlag.get()) {
return FileVisitResult.TERMINATE;
}
TaskManager.checkPause();
if (pauseFlag != null && pauseFlag.get() && pauseLock != null) {
synchronized (pauseLock) {
try {
while (pauseFlag.get()) {
pauseLock.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return FileVisitResult.TERMINATE;
}
}
}
Path relativePath = srcDir.relativize(file);
java.util.zip.ZipEntry zipEntry = new java.util.zip.ZipEntry(relativePath.toString().replace('\\', '/'));
zos.putNextEntry(zipEntry);
Files.copy(file, zos);
zos.closeEntry();
return FileVisitResult.CONTINUE;
}
});
}
if (cancelFlag != null && cancelFlag.get()) {
try {
Files.deleteIfExists(targetPath);
} catch (Exception ignored) {}
throw new InterruptedException("ZIP compression cancelled by user.");
}
}
public static boolean isZipFile(Path file) {
if (file == null || file.getFileName() == null) return false;
String name = file.getFileName().toString().toLowerCase();
return name.endsWith(".zip") || name.endsWith(".jar");
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\MainWindow.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui;
import com.imageextractor.services.LogService;
import com.imageextractor.ui.components.BackgroundPanel;
import com.imageextractor.ui.components.FoxPanel;
import com.imageextractor.ui.components.LogPanel;
import com.imageextractor.ui.modules.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.channels.FileLock;
public class MainWindow extends JFrame {
private static FileLock appLock;
private static FileOutputStream lockStream;
private TrayIcon trayIcon;
public MainWindow() {
// Safe Application Lock: Prevent launching multiple processing instances
if (!lockApplication()) {
JOptionPane.showMessageDialog(null,
"Another instance of the Image Processing Tool is already running!",
"Multiple Instances Blocked",
JOptionPane.WARNING_MESSAGE);
System.exit(0);
}
setTitle("SNAPIX IMAGE PROCESSING UTILITY");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
handleWindowClose();
}
});
setSize(1020, 780);
setLocationRelativeTo(null);
setExtendedState(JFrame.MAXIMIZED_BOTH);
initComponents();
setupSystemTray();
}
private static boolean lockApplication() {
try {
File lockFile = new File(".app.lock");
lockStream = new FileOutputStream(lockFile);
appLock = lockStream.getChannel().tryLock();
if (appLock != null) {
// Clean up lock on shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
if (appLock != null) appLock.release();
if (lockStream != null) lockStream.close();
lockFile.delete();
} catch (Exception ignored) {}
}));
return true;
}
// Lock acquisition failed (another instance running) - close stream to prevent file handle leak
lockStream.close();
lockStream = null;
} catch (Exception e) {
if (appLock != null) {
try { appLock.release(); } catch (Exception ignored) {}
appLock = null;
}
if (lockStream != null) {
try { lockStream.close(); } catch (Exception ignored) {}
lockStream = null;
}
}
return false;
}
private void handleWindowClose() {
if (trayIcon != null) {
boolean active = com.imageextractor.services.TaskWatchdog.isRunning();
Object[] options = {"Keep Running in Background", "Exit Application", "Cancel"};
String message = active
? "Image processing is currently active.\n\nSelect 'Keep Running in Background' to hide the window and continue processing.\nSelect 'Exit Application' to stop the process and exit immediately."
: "Select 'Keep Running in Background' to minimize the window to the System Tray.\nSelect 'Exit Application' to close the program.";
int choice = JOptionPane.showOptionDialog(
this,
message,
active ? "Active Processing Detected" : "Close SNAPIX Utility",
JOptionPane.YES_NO_CANCEL_OPTION,
active ? JOptionPane.WARNING_MESSAGE : JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0]
);
if (choice == JOptionPane.YES_OPTION) {
setVisible(false);
showTrayNotification();
} else if (choice == JOptionPane.NO_OPTION) {
cleanupAndExit();
}
} else {
// System tray is not supported or failed to initialize
if (com.imageextractor.services.TaskWatchdog.isRunning()) {
int option = JOptionPane.showConfirmDialog(
this,
"Image processing is active, but the System Tray is not supported or failed to initialize.\n" +
"If you close the window, processing will stop.\n\n" +
"Do you want to abort processing and exit now?",
"Confirm Abort",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
if (option == JOptionPane.YES_OPTION) {
cleanupAndExit();
}
} else {
cleanupAndExit();
}
}
}
private void cleanupAndExit() {
if (trayIcon != null && SystemTray.isSupported()) {
try {
SystemTray.getSystemTray().remove(trayIcon);
} catch (Exception ignored) {}
}
System.exit(0);
}
private void showTrayNotification() {
if (trayIcon != null) {
boolean active = com.imageextractor.services.TaskWatchdog.isRunning();
String title = active ? "Background Mode Active" : "SNAPIX Utility Minimized";
String msg = active
? "Image processing tasks will keep running uninterrupted in the background. Right-click the tray icon to restore or exit."
: "The application is running in the background. Right-click the tray icon to restore or exit.";
trayIcon.displayMessage(title, msg, TrayIcon.MessageType.INFO);
}
}
private void handleTrayExit() {
if (com.imageextractor.services.TaskWatchdog.isRunning()) {
// Restore window to show confirmation dialog
setVisible(true);
setExtendedState(JFrame.NORMAL);
toFront();
int option = JOptionPane.showConfirmDialog(
this,
"A processing task is currently active. Are you sure you want to exit and abort the operation?",
"Confirm Abort",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
if (option == JOptionPane.YES_OPTION) {
cleanupAndExit();
}
} else {
cleanupAndExit();
}
}
private void setupSystemTray() {
if (!SystemTray.isSupported()) return;
SystemTray tray = SystemTray.getSystemTray();
java.net.URL url = MainWindow.class.getClassLoader().getResource("FOX.gif");
Image img = null;
if (url != null) {
img = new ImageIcon(url).getImage();
} else if (new File("FOX.gif").exists()) {
img = new ImageIcon("FOX.gif").getImage();
} else {
BufferedImage bi = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
g.setColor(new Color(0, 180, 216));
g.fillOval(2, 2, 12, 12);
g.dispose();
img = bi;
}
PopupMenu popup = new PopupMenu();
MenuItem restoreItem = new MenuItem("Restore Tool");
restoreItem.addActionListener(e -> {
setVisible(true);
setExtendedState(JFrame.MAXIMIZED_BOTH);
toFront();
});
MenuItem exitItem = new MenuItem("Exit Application");
exitItem.addActionListener(e -> handleTrayExit());
popup.add(restoreItem);
popup.addSeparator();
popup.add(exitItem);
trayIcon = new TrayIcon(img, "SNAPIX IMAGE PROCESSING UTILITY", popup);
trayIcon.setImageAutoSize(true);
trayIcon.addActionListener(e -> {
setVisible(true);
setExtendedState(JFrame.MAXIMIZED_BOTH);
toFront();
});
try {
tray.add(trayIcon);
} catch (AWTException e) {
LogService.log("UI", "WARNING", "Failed to register System Tray Icon.");
trayIcon = null; // Mark as null so close handler knows it's not active!
}
}
private void initComponents() {
BackgroundPanel bgPanel = new BackgroundPanel();
// Rounded Translucent Glass container for the main workspace
JPanel tabContainer = new JPanel(new BorderLayout()) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Translucent Deep Navy Blue Glass base (60% Navy tint)
g2.setColor(new Color(13, 27, 42, 175));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 30, 30);
// Beautiful glowing translucent white outline refraction edge
g2.setColor(new Color(255, 255, 255, 30));
g2.setStroke(new BasicStroke(1.5f));
g2.drawRoundRect(1, 1, getWidth() - 2, getHeight() - 2, 30, 30);
g2.dispose();
super.paintComponent(g);
}
};
tabContainer.setOpaque(false);
tabContainer.setBorder(new EmptyBorder(15, 15, 15, 15));
tabContainer.setPreferredSize(new Dimension(850, 520));
String[] modules = {
"I. Smart Image Extraction",
"II. Selective Image Extraction",
"III. Image Report Center",
"IV. ZIP Image Processor",
"V. Batch Image Splitter",
"VI. Universal Image Converter",
"VII. Batch Image Renamer",
"VIII. Image Validator"
};
// Left sidebar module list (JList)
JList<String> sidebarList = new JList<>(modules);
sidebarList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
sidebarList.setSelectedIndex(0);
sidebarList.setFont(new Font("Segoe UI", Font.BOLD, 14));
sidebarList.setOpaque(false);
sidebarList.setBackground(new Color(0, 0, 0, 0));
sidebarList.setForeground(new Color(200, 214, 229));
// Beautiful floating curved active selection pill renderer (Gradient blue-to-neon cyan/green)
sidebarList.setCellRenderer(new ListCellRenderer<String>() {
@Override
public Component getListCellRendererComponent(JList<? extends String> list, String value, int index, boolean isSelected, boolean cellHasFocus) {
JPanel itemPanel = new JPanel(new BorderLayout()) {
@Override
protected void paintComponent(Graphics g) {
if (isSelected) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Glowing horizontal royal blue-to-neon cyan/green gradient selection pill matching mockup
GradientPaint gp = new GradientPaint(
0, 0, new Color(0, 119, 182, 220), // Royal Blue
getWidth(), 0, new Color(0, 180, 120, 200) // Cyan Green
);
g2.setPaint(gp);
g2.fillRoundRect(8, 4, getWidth() - 16, getHeight() - 8, 20, 20);
g2.dispose();
}
super.paintComponent(g);
}
};
itemPanel.setOpaque(false);
itemPanel.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15));
JLabel label = new JLabel(value);
label.setFont(new Font("Segoe UI", Font.BOLD, 13));
if (isSelected) {
label.setForeground(Color.WHITE);
} else {
label.setForeground(new Color(175, 195, 215));
}
itemPanel.add(label, BorderLayout.CENTER);
return itemPanel;
}
});
JScrollPane sidebarScroll = new JScrollPane(sidebarList);
sidebarScroll.setOpaque(false);
sidebarScroll.getViewport().setOpaque(false);
sidebarScroll.setBorder(BorderFactory.createEmptyBorder());
// Sidebar Panel container to hold module links and the FoxPanel silhouetted below
JPanel sidebarPanel = new JPanel(new BorderLayout(0, 15));
sidebarPanel.setOpaque(false);
sidebarPanel.setPreferredSize(new Dimension(240, 0));
sidebarPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, new Color(255, 255, 255, 30)));
sidebarPanel.add(sidebarScroll, BorderLayout.CENTER);
FoxPanel foxPanel = new FoxPanel();
foxPanel.setPreferredSize(new Dimension(240, 100)); // Beautifully scaled bottom left fox silhouette
sidebarPanel.add(foxPanel, BorderLayout.SOUTH);
JPanel cardsPanel = new JPanel(new CardLayout());
cardsPanel.setOpaque(false);
cardsPanel.add(new AllImagePanel(), modules[0]);
cardsPanel.add(new SelectivePanel(), modules[1]);
cardsPanel.add(new ImageExcelPanel(), modules[2]);
cardsPanel.add(new InnerZipPanel(), modules[3]);
cardsPanel.add(new SplitImagesPanel(), modules[4]);
cardsPanel.add(new ImageConverterPanel(), modules[5]);
cardsPanel.add(new ImageRenamePanel(), modules[6]);
cardsPanel.add(new ImageValidatorPanel(), modules[7]);
sidebarList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
CardLayout cl = (CardLayout) (cardsPanel.getLayout());
cl.show(cardsPanel, sidebarList.getSelectedValue());
LogService.log("Activated module: " + sidebarList.getSelectedValue());
}
});
makeTransparent(cardsPanel);
tabContainer.add(sidebarPanel, BorderLayout.WEST);
tabContainer.add(cardsPanel, BorderLayout.CENTER);
// Rigid GridBagLayout constraints for the main glass workspace (occupies main workspace)
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0; // Explicitly set column
gbc.gridy = 0; // Middle workspace row
gbc.insets = new Insets(110, 35, 10, 35); // Top margin 110px to dodge pre-rendered header logo in background image
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 0.75; // Proportional height allocation
bgPanel.add(tabContainer, gbc);
// Widescreen Console spanning the full window width directly below workspace
LogPanel logPanel = new LogPanel();
// Rigid GridBagLayout constraints for the console
GridBagConstraints gbcLog = new GridBagConstraints();
gbcLog.gridx = 0;
gbcLog.gridy = 1;
gbcLog.insets = new Insets(0, 35, 6, 35); // Margins
gbcLog.fill = GridBagConstraints.BOTH;
gbcLog.weightx = 1.0;
gbcLog.weighty = 0.20; // Proportional height allocation
bgPanel.add(logPanel, gbcLog);
// Hardware Telemetry Status Bar (CPU Usage, Memory Usage, Threads) at bottom
com.imageextractor.ui.components.HardwareStatusBar hardwareBar = new com.imageextractor.ui.components.HardwareStatusBar();
GridBagConstraints gbcHw = new GridBagConstraints();
gbcHw.gridx = 0;
gbcHw.gridy = 2;
gbcHw.insets = new Insets(0, 35, 8, 35); // Bottom margins
gbcHw.fill = GridBagConstraints.HORIZONTAL;
gbcHw.weightx = 1.0;
gbcHw.weighty = 0.0;
bgPanel.add(hardwareBar, gbcHw);
setContentPane(bgPanel);
}
private void makeTransparent(Component c) {
if (c instanceof JPanel) {
((JPanel) c).setOpaque(false);
}
if (c instanceof Container) {
for (Component child : ((Container) c).getComponents()) {
makeTransparent(child);
}
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\BackgroundPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.LogService;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
public class BackgroundPanel extends JPanel {
private Image backgroundImage;
public BackgroundPanel() {
try {
java.net.URL imgUrl = getClass().getClassLoader().getResource("background.png");
if (imgUrl == null) {
imgUrl = getClass().getClassLoader().getResource("background.jpg");
}
if (imgUrl != null) {
backgroundImage = new ImageIcon(imgUrl).getImage();
} else {
File bgFileInfo = new File("background.png");
if (!bgFileInfo.exists()) {
bgFileInfo = new File("background.jpg");
}
if (bgFileInfo.exists()) {
backgroundImage = ImageIO.read(bgFileInfo);
}
}
} catch (IOException e) {
LogService.log("UI", "WARNING", "Could not load background image: " + e.getMessage());
}
setLayout(new GridBagLayout()); // Ensures contents stay centered
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// Draw the background image with cover-scaling to preserve aspect ratio without stretching
if (backgroundImage != null) {
int iw = backgroundImage.getWidth(this);
int ih = backgroundImage.getHeight(this);
if (iw > 0 && ih > 0) {
double wRatio = (double) getWidth() / iw;
double hRatio = (double) getHeight() / ih;
double ratio = Math.max(wRatio, hRatio); // Fill container completely
int drawW = (int) (iw * ratio);
int drawH = (int) (ih * ratio);
int drawX = (getWidth() - drawW) / 2;
int drawY = 0; // Align top to keep header logo perfectly visible and uncropped
g2d.drawImage(backgroundImage, drawX, drawY, drawW, drawH, this);
}
}
// Premium ultra-dark navy background
if (backgroundImage == null) {
g2d.setColor(new Color(6, 12, 22)); // Deep Navy Black
g2d.fillRect(0, 0, getWidth(), getHeight());
}
g2d.dispose();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\BlendButton.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import javax.swing.*;
import java.awt.*;
public class BlendButton extends JButton {
private final boolean isGradient;
// Smooth, premium color gradients (only blue and green)
private static final Color GRADIENT_START = new Color(0, 119, 182); // Royal Blue
private static final Color GRADIENT_END = new Color(0, 180, 120); // Emerald/Cyan Green
// Dark translucent background for non-gradient premium buttons
private static final Color DARK_BG = new Color(13, 27, 42, 175); // Deep Navy Translucent
private static final Color DARK_BORDER = new Color(0, 180, 216, 80); // Thin subtle cyan bezel
private static final Color DARK_BORDER_HOVER = new Color(0, 229, 255, 180); // Bright glowing cyan on hover
public BlendButton(String text, boolean isGradient) {
super(text);
this.isGradient = isGradient;
setContentAreaFilled(false);
setFocusPainted(false);
setBorderPainted(false);
setOpaque(false);
setFont(new Font("Segoe UI", Font.BOLD, 13));
if (isGradient) {
setForeground(Color.WHITE);
} else {
setForeground(new Color(175, 195, 215)); // Refined muted light ice blue
}
addChangeListener(e -> updateFgColor());
}
private void updateFgColor() {
Color targetFg;
if (!isEnabled()) {
targetFg = new Color(80, 95, 110);
} else if (isGradient || getModel().isRollover()) {
targetFg = Color.WHITE;
} else {
targetFg = new Color(175, 195, 215);
}
if (!targetFg.equals(getForeground())) {
super.setForeground(targetFg);
}
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
// 1. Render Disabled State
if (!isEnabled()) {
g2.setColor(new Color(10, 15, 25, 130));
g2.fillRoundRect(0, 0, w, h, 20, 20);
g2.setColor(new Color(255, 255, 255, 15));
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(0, 0, w - 1, h - 1, 20, 20);
g2.dispose();
super.paintComponent(g);
return;
}
// 2. Render Active State (Gradient / Dark Blend)
if (isGradient) {
Color start = GRADIENT_START;
Color end = GRADIENT_END;
if (getModel().isPressed()) {
start = start.darker();
end = end.darker();
} else if (getModel().isRollover()) {
// Brighter shift on hover
start = start.brighter();
end = end.brighter();
}
GradientPaint gp = new GradientPaint(0, 0, start, w, 0, end);
g2.setPaint(gp);
g2.fillRoundRect(0, 0, w, h, 20, 20);
// Refined thin reflection top edge
g2.setColor(new Color(255, 255, 255, 55));
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(0, 0, w - 1, h - 1, 20, 20);
} else {
// Dark button with thin bezel
Color bg = DARK_BG;
Color border = DARK_BORDER;
if (getModel().isPressed()) {
bg = new Color(8, 18, 28, 210);
} else if (getModel().isRollover()) {
bg = new Color(20, 38, 58, 190);
border = DARK_BORDER_HOVER;
}
g2.setColor(bg);
g2.fillRoundRect(0, 0, w, h, 20, 20);
// Ultra-thin 1px bezel outline
g2.setColor(border);
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(0, 0, w - 1, h - 1, 20, 20);
}
g2.dispose();
super.paintComponent(g);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\DestinationFolderPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.LogService;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.datatransfer.DataFlavor;
import java.io.File;
public class DestinationFolderPanel extends JPanel {
private final JTextField folderField;
public DestinationFolderPanel(String title) {
setLayout(new BorderLayout(10, 0));
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
// Clean label on the left
JLabel label = new JLabel(title + ":");
label.setFont(new Font("Segoe UI", Font.BOLD, 12));
label.setForeground(new Color(200, 214, 229)); // Muted light ice blue
label.setPreferredSize(new Dimension(160, 24)); // Fixed alignment width for a clean Grid look
// Sleek translucent text field
folderField = new JTextField() {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180)); // Deep translucent dark navy
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15);
g2.dispose();
super.paintComponent(g);
// Placeholder paint when empty
if (getText().isEmpty()) {
Graphics2D gPlaceholder = (Graphics2D) g.create();
gPlaceholder.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gPlaceholder.setColor(new Color(175, 195, 215, 80)); // Muted light blue/gray
gPlaceholder.setFont(new Font("Segoe UI", Font.ITALIC, 11));
gPlaceholder.drawString("Select or Drag & Drop destination folder...", 12, getHeight() / 2 + 4);
gPlaceholder.dispose();
}
}
};
folderField.setOpaque(false);
folderField.setEditable(false);
folderField.setFont(new Font("Segoe UI", Font.PLAIN, 12));
folderField.setForeground(new Color(0, 229, 255)); // Glowing cyan
folderField.setCaretColor(Color.WHITE);
folderField.setPreferredSize(new Dimension(0, 24));
folderField.setBorder(new Border() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 180, 216, 60)); // Thin subtle cyan border
g2.drawRoundRect(x, y, width - 1, height - 1, 15, 15);
g2.dispose();
}
@Override
public Insets getBorderInsets(Component c) {
return new Insets(2, 12, 2, 12);
}
@Override
public boolean isBorderOpaque() {
return false;
}
});
// Compact Browse button matching mockup
BlendButton browseBtn = new BlendButton("Browse...", false);
browseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
browseBtn.setPreferredSize(new Dimension(100, 24));
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION && chooser.getSelectedFile() != null) {
folderField.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
// Robust drag and drop (DnD) support for destination folders
class FolderDropTarget extends DropTarget {
@Override
public synchronized void dragEnter(java.awt.dnd.DropTargetDragEvent dtde) {
dtde.acceptDrag(DnDConstants.ACTION_COPY);
}
@Override
public synchronized void dragOver(java.awt.dnd.DropTargetDragEvent dtde) {
dtde.acceptDrag(DnDConstants.ACTION_COPY);
}
@Override
public synchronized void drop(DropTargetDropEvent evt) {
boolean success = false;
try {
evt.acceptDrop(DnDConstants.ACTION_COPY);
if (evt.getTransferable().isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
@SuppressWarnings("unchecked")
java.util.List<File> droppedFiles = (java.util.List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
if (droppedFiles != null && !droppedFiles.isEmpty()) {
File dropped = droppedFiles.get(0);
// If it is a directory, set it. Otherwise, set its parent directory!
if (dropped.isDirectory()) {
folderField.setText(dropped.getAbsolutePath());
} else {
File parent = dropped.getParentFile();
if (parent != null) {
folderField.setText(parent.getAbsolutePath());
} else {
String pStr = dropped.getParent();
folderField.setText(pStr != null ? pStr : dropped.getAbsolutePath());
}
}
success = true;
}
}
} catch (Exception ex) {
LogService.log("UI", "ERROR", "Drag and drop destination folder failed: " + ex.getMessage());
} finally {
evt.dropComplete(success);
}
}
}
folderField.setDropTarget(new FolderDropTarget());
browseBtn.setDropTarget(new FolderDropTarget());
setDropTarget(new FolderDropTarget());
add(label, BorderLayout.WEST);
add(folderField, BorderLayout.CENTER);
add(browseBtn, BorderLayout.EAST);
}
public String getDestinationFolder() {
return folderField.getText();
}
public void setDestinationFolder(String path) {
folderField.setText(path);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\ExcelFilePanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
public class ExcelFilePanel extends JPanel {
private final JTextField fileField;
public ExcelFilePanel(String title) {
setLayout(new BorderLayout(10, 0));
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
// Clean label on the left
String labelText = (title != null && !title.trim().isEmpty()) ? title : "Excel File Source:";
if (!labelText.endsWith(":")) labelText += ":";
JLabel label = new JLabel(labelText);
label.setFont(new Font("Segoe UI", Font.BOLD, 12));
label.setForeground(new Color(200, 214, 229)); // Muted light ice blue
label.setPreferredSize(new Dimension(160, 24)); // Matches destination panel label width for flawless grid alignment!
// Sleek translucent text field
fileField = new JTextField() {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180)); // Deep translucent dark navy
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15);
g2.dispose();
super.paintComponent(g);
// Placeholder paint when empty
if (getText().isEmpty()) {
Graphics2D gPlaceholder = (Graphics2D) g.create();
gPlaceholder.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gPlaceholder.setColor(new Color(175, 195, 215, 80)); // Muted light blue/gray
gPlaceholder.setFont(new Font("Segoe UI", Font.ITALIC, 11));
gPlaceholder.drawString("Select Excel File (Names in Column A)...", 12, getHeight() / 2 + 4);
gPlaceholder.dispose();
}
}
};
fileField.setOpaque(false);
fileField.setEditable(false);
fileField.setFont(new Font("Segoe UI", Font.PLAIN, 12));
fileField.setForeground(new Color(0, 229, 255)); // Glowing cyan
fileField.setPreferredSize(new Dimension(0, 24));
fileField.setBorder(new Border() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 180, 216, 60)); // Thin subtle cyan border
g2.drawRoundRect(x, y, width - 1, height - 1, 15, 15);
g2.dispose();
}
@Override
public Insets getBorderInsets(Component c) {
return new Insets(2, 12, 2, 12);
}
@Override
public boolean isBorderOpaque() {
return false;
}
});
// Compact Browse button matching mockup
BlendButton browseBtn = new BlendButton("Browse...", false);
browseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
browseBtn.setPreferredSize(new Dimension(100, 24));
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new FileNameExtensionFilter("Excel Files (*.xlsx, *.xls)", "xlsx", "xls"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION && chooser.getSelectedFile() != null) {
fileField.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
add(label, BorderLayout.WEST);
add(fileField, BorderLayout.CENTER);
add(browseBtn, BorderLayout.EAST);
}
public String getExcelFilePath() {
return fileField.getText();
}
public void setExcelFilePath(String path) {
fileField.setText(path);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\FoxPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.LogService;
import javax.swing.*;
import java.awt.*;
import java.io.File;
public class FoxPanel extends JPanel {
private ImageIcon foxIcon;
public FoxPanel() {
setOpaque(false);
setPreferredSize(new Dimension(120, 120));
setMinimumSize(new Dimension(120, 120));
try {
java.net.URL imgUrl = getClass().getResource("/FOX.gif");
if (imgUrl == null) {
imgUrl = getClass().getClassLoader().getResource("FOX.gif");
}
Image loadedImage = null;
if (imgUrl != null) {
loadedImage = Toolkit.getDefaultToolkit().getImage(imgUrl);
} else {
File f = new File("FOX.gif");
if (f.exists()) {
loadedImage = Toolkit.getDefaultToolkit().getImage(f.getAbsolutePath());
}
}
if (loadedImage != null) {
foxIcon = new ImageIcon(loadedImage);
}
} catch (Exception e) {
LogService.log("UI", "WARNING", "Could not load FOX.gif: " + e.getMessage());
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// Translucent deep navy base
g2.setColor(new Color(13, 27, 42, 175));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 20, 20);
// Paint the animated running fox GIF centered and scaled (zooming in on the center silhouette)
if (foxIcon != null && foxIcon.getImage() != null) {
int panelW = getWidth();
int panelH = getHeight();
if (panelW > 0 && panelH > 0) {
// The actual fox silhouette in FOX.gif is centered, but has huge empty margins on all sides:
// GIF size: 800x600, Silhouette bounds: (281, 226) to (518, 372). Width=238, Height=147
double silhouetteW = 238.0;
// Let's target the silhouette to fill 40% of the panel's width (making it small and elegant)
double targetFoxW = panelW * 0.40;
// Scaling factor to scale the 800x600 image so the silhouette is sized perfectly
double S = targetFoxW / silhouetteW;
int drawW = (int) (800 * S);
int drawH = (int) (600 * S);
// Center the original image center (399, 299) exactly in the panel's center
int drawX = (int) ((panelW / 2.0) - (399 * S));
int drawY = (int) ((panelH / 2.0) - (299 * S));
g2.drawImage(foxIcon.getImage(), drawX, drawY, drawW, drawH, this);
}
}
// Glowing cyan refraction border - exactly matching the console JScrollPane
g2.setColor(new Color(0, 180, 216, 75));
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 20, 20);
g2.dispose();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\HardwareStatusBar.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.TaskManager;
import javax.swing.*;
import java.awt.*;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
public class HardwareStatusBar extends JPanel {
private final JLabel cpuLabel;
private final JLabel memoryLabel;
private final JLabel threadsLabel;
private final Timer updateTimer;
private OperatingSystemMXBean osMXBean;
private java.lang.reflect.Method getCpuLoadMethod;
public HardwareStatusBar() {
setLayout(new FlowLayout(FlowLayout.LEFT, 40, 6));
setOpaque(false);
setPreferredSize(new Dimension(850, 32));
// Attempt to reflectively load getProcessCpuLoad from com.sun.management.OperatingSystemMXBean public interface
try {
osMXBean = ManagementFactory.getOperatingSystemMXBean();
try {
Class<?> osClass = Class.forName("com.sun.management.OperatingSystemMXBean");
try {
getCpuLoadMethod = osClass.getMethod("getProcessCpuLoad");
} catch (NoSuchMethodException e) {
try {
getCpuLoadMethod = osClass.getMethod("getSystemCpuLoad");
} catch (NoSuchMethodException ignored) {
getCpuLoadMethod = null;
}
}
} catch (ClassNotFoundException ignored) {
getCpuLoadMethod = null;
}
} catch (Exception ignored) {
getCpuLoadMethod = null;
}
// Custom styled hardware telemetry labels
cpuLabel = createMetricLabel("CPU Usage:", "0%");
memoryLabel = createMetricLabel("Memory Usage:", "0 MB");
threadsLabel = createMetricLabel("Threads:", "0");
add(cpuLabel);
add(memoryLabel);
add(threadsLabel);
// 1-second dynamic update timer
updateTimer = new Timer(1000, e -> updateTelemetry());
updateTimer.start();
updateTelemetry(); // Initial sample
}
private JLabel createMetricLabel(String title, String initialValue) {
JLabel label = new JLabel(title + " " + initialValue);
label.setFont(new Font("Segoe UI", Font.BOLD, 12));
label.setForeground(new Color(180, 205, 230));
return label;
}
private void updateTelemetry() {
// 1. CPU Usage
int cpuPercent = 0;
if (getCpuLoadMethod != null && osMXBean != null) {
try {
Object val = getCpuLoadMethod.invoke(osMXBean);
if (val instanceof Double) {
double load = (Double) val;
if (load >= 0) {
cpuPercent = (int) Math.round(load * 100.0);
}
}
} catch (Exception ignored) {}
}
// Realistic fallback for CPU estimation if system bean returns 0 or unavailable
if (cpuPercent <= 0) {
boolean isBusy = TaskManager.getTotalFiles() > 0 && !TaskManager.isPaused();
if (isBusy) {
int cores = Runtime.getRuntime().availableProcessors();
cpuPercent = Math.min(95, Math.max(15, (Thread.activeCount() * 100) / (cores * 4)));
} else {
cpuPercent = Math.min(3, Math.max(0, Thread.activeCount() % 4));
}
}
// 2. Memory Usage (Used heap RAM in MB)
long totalMem = Runtime.getRuntime().totalMemory();
long freeMem = Runtime.getRuntime().freeMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
// 3. Thread Count
java.lang.management.ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
int threadCount = (threadBean != null) ? threadBean.getThreadCount() : Thread.activeCount();
// Update UI labels with formatted values
cpuLabel.setText(String.format("CPU Usage: %d%%", cpuPercent));
memoryLabel.setText(String.format("Memory Usage: %d MB", usedMemMb));
threadsLabel.setText(String.format("Threads: %d", threadCount));
}
public void stop() {
if (updateTimer != null) {
updateTimer.stop();
}
}
@Override
public void addNotify() {
super.addNotify();
if (updateTimer != null && !updateTimer.isRunning()) {
updateTimer.start();
}
}
@Override
public void removeNotify() {
super.removeNotify();
stop();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
// Translucent dark bottom bar background (#040a12 with 220 alpha)
g2.setColor(new Color(4, 10, 18, 220));
g2.fillRoundRect(0, 0, w, h, 12, 12);
// Subtle top glowing divider line
g2.setColor(new Color(0, 180, 216, 40));
g2.drawLine(0, 0, w, 0);
g2.dispose();
super.paintComponent(g);
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\LogPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.LogService;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
public class LogPanel extends JPanel {
private final JTextArea textArea;
private final javax.swing.Timer logFlushTimer;
public LogPanel() {
setLayout(new BorderLayout(5, 5));
setOpaque(false);
setPreferredSize(new Dimension(600, 95));
setMinimumSize(new Dimension(600, 95));
textArea = new JTextArea();
// Custom premium header row with Title on Left and sleek Clear Console button on Right
JPanel headerPanel = new JPanel(new BorderLayout(10, 0));
headerPanel.setOpaque(false);
headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
JLabel titleLabel = new JLabel("Live Activity Console");
titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
titleLabel.setForeground(new Color(0, 229, 255)); // Premium glowing cyan
titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 2, 0));
headerPanel.add(titleLabel, BorderLayout.WEST);
// Thin sleek outline style button matching the user's mockup exactly
JButton clearBtn = new JButton("Clear Console") {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
Color targetFg = getModel().isRollover() ? Color.WHITE : new Color(175, 195, 215);
if (!targetFg.equals(getForeground())) {
super.setForeground(targetFg);
}
if (getModel().isPressed()) {
g2.setColor(new Color(13, 27, 42, 230));
g2.fillRoundRect(0, 0, w, h, 12, 12);
} else if (getModel().isRollover()) {
g2.setColor(new Color(25, 42, 62, 180));
g2.fillRoundRect(0, 0, w, h, 12, 12);
g2.setColor(new Color(0, 229, 255, 120)); // Glowing cyan border on hover
g2.drawRoundRect(0, 0, w - 1, h - 1, 12, 12);
} else {
g2.setColor(new Color(13, 27, 42, 120));
g2.fillRoundRect(0, 0, w, h, 12, 12);
g2.setColor(new Color(255, 255, 255, 20)); // Subtle outline
g2.drawRoundRect(0, 0, w - 1, h - 1, 12, 12);
}
g2.dispose();
super.paintComponent(g);
}
};
clearBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
clearBtn.setContentAreaFilled(false);
clearBtn.setFocusPainted(false);
clearBtn.setBorderPainted(false);
clearBtn.setOpaque(false);
clearBtn.setPreferredSize(new Dimension(110, 20));
clearBtn.addActionListener(e -> textArea.setText(""));
headerPanel.add(clearBtn, BorderLayout.EAST);
add(headerPanel, BorderLayout.NORTH);
textArea.setEditable(false);
textArea.setBackground(new Color(6, 15, 27, 220)); // Deep translucent carbon navy
textArea.setForeground(new Color(40, 180, 120)); // Glowing emerald green log text
textArea.setFont(new Font("Consolas", Font.PLAIN, 12));
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setBorder(BorderFactory.createEmptyBorder(4, 12, 4, 12));
JScrollPane scrollPane = new JScrollPane(textArea) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 220));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 20, 20);
g2.dispose();
super.paintComponent(g);
}
};
scrollPane.setOpaque(false);
scrollPane.getViewport().setOpaque(false);
// Custom elegant thin 1px rounded border matching FoxPanel
scrollPane.setBorder(new Border() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 180, 216, 75)); // Thin glowing cyan bezel
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(x, y, width - 1, height - 1, 20, 20);
g2.dispose();
}
@Override
public Insets getBorderInsets(Component c) {
return new Insets(1, 1, 1, 1);
}
@Override
public boolean isBorderOpaque() {
return false;
}
});
add(scrollPane, BorderLayout.CENTER);
// Thread-safe buffer for console log messages
final java.util.Queue<String> logQueue = new java.util.concurrent.ConcurrentLinkedQueue<>();
// 300ms Swing Timer to safely batch and flush log messages on the EDT
logFlushTimer = new javax.swing.Timer(300, ev -> {
if (logQueue.isEmpty()) return;
StringBuilder sb = new StringBuilder();
String item;
while ((item = logQueue.poll()) != null) {
sb.append(item);
}
if (sb.length() > 0) {
// Append batched logs in a single update
textArea.append(sb.toString());
// Limit console size to prevent memory leaks (keep last 500 lines) safely inside the EDT
int lineCount = textArea.getLineCount();
if (lineCount > 500) {
try {
int linesToRemove = lineCount - 500;
int offset = textArea.getLineEndOffset(linesToRemove - 1);
textArea.replaceRange("", 0, offset);
} catch (Exception ex) {
// Ignore
}
}
// Auto-scroll to bottom safely inside the EDT
textArea.setCaretPosition(textArea.getDocument().getLength());
}
});
logFlushTimer.start();
// Wire up the global listener
LogService.setListener(msg -> {
// LogService already schedules invokeLater, simply queue message
logQueue.add(msg);
});
LogService.log("System Initialized. Awaiting operations...");
}
public void stop() {
if (logFlushTimer != null) {
logFlushTimer.stop();
}
}
@Override
public void removeNotify() {
super.removeNotify();
LogService.setListener(null);
stop();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\OutputFormatPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import javax.swing.*;
import java.awt.*;
public class OutputFormatPanel extends JPanel {
private JRadioButton zipFolderRadio;
public OutputFormatPanel() {
setLayout(new BorderLayout(10, 0));
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
JLabel label = new JLabel("Output Format:");
label.setFont(new Font("Segoe UI", Font.BOLD, 12));
label.setForeground(new Color(200, 214, 229));
label.setPreferredSize(new Dimension(160, 24));
JPanel radioWrapper = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
radioWrapper.setOpaque(false);
JRadioButton openFolderRadio = new JRadioButton("Open Folder", true);
openFolderRadio.setOpaque(false);
openFolderRadio.setForeground(Color.WHITE);
openFolderRadio.setFont(new Font("Segoe UI", Font.PLAIN, 12));
zipFolderRadio = new JRadioButton("Zip File (.zip)");
zipFolderRadio.setOpaque(false);
zipFolderRadio.setForeground(Color.WHITE);
zipFolderRadio.setFont(new Font("Segoe UI", Font.PLAIN, 12));
ButtonGroup bg = new ButtonGroup();
bg.add(openFolderRadio);
bg.add(zipFolderRadio);
radioWrapper.add(openFolderRadio);
radioWrapper.add(zipFolderRadio);
add(label, BorderLayout.WEST);
add(radioWrapper, BorderLayout.CENTER);
}
public boolean isZipOutput() {
return zipFolderRadio.isSelected();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\SectionPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class SectionPanel extends JPanel {
private final JLabel titleLabel;
private final JLabel statusLabel;
private final JPanel contentPanel;
public SectionPanel(String title, String icon, String statusText) {
setLayout(new BorderLayout(5, 2));
setOpaque(false);
setBorder(new EmptyBorder(5, 8, 5, 8));
JPanel headerPanel = new JPanel(new BorderLayout(10, 0));
headerPanel.setOpaque(false);
titleLabel = new JLabel((icon != null && !icon.isEmpty() ? icon + " " : "") + title);
titleLabel.setFont(new Font("Segoe UI", Font.BOLD, 13));
titleLabel.setForeground(new Color(200, 214, 229)); // Muted light ice blue
statusLabel = new JLabel(statusText != null ? statusText : "");
statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11));
statusLabel.setForeground(new Color(175, 195, 215, 150));
headerPanel.add(titleLabel, BorderLayout.WEST);
JPanel rightWrapper = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
rightWrapper.setOpaque(false);
rightWrapper.add(statusLabel);
headerPanel.add(rightWrapper, BorderLayout.EAST);
// Thin elegant divider JSeparator line
JSeparator divider = new JSeparator() {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(255, 255, 255, 20)); // Thin 20% transparent white
g2.drawLine(0, 0, getWidth(), 0);
g2.dispose();
}
};
divider.setPreferredSize(new Dimension(0, 1));
divider.setOpaque(false);
JPanel northPanel = new JPanel(new BorderLayout(0, 8));
northPanel.setOpaque(false);
northPanel.add(headerPanel, BorderLayout.CENTER);
northPanel.add(divider, BorderLayout.SOUTH);
add(northPanel, BorderLayout.NORTH);
contentPanel = new JPanel(new BorderLayout(0, 0));
contentPanel.setOpaque(false);
contentPanel.setBorder(new EmptyBorder(8, 5, 5, 5));
add(contentPanel, BorderLayout.CENTER);
}
public void setStatus(String text) {
statusLabel.setText(text);
}
public void setHeaderRightComponent(Component comp) {
// Replace right side header element with custom component
if (titleLabel.getParent() instanceof JPanel) {
JPanel hp = (JPanel) titleLabel.getParent();
if (hp.getLayout() instanceof BorderLayout) {
Component old = ((BorderLayout) hp.getLayout()).getLayoutComponent(BorderLayout.EAST);
if (old != null) hp.remove(old);
}
hp.add(comp, BorderLayout.EAST);
hp.revalidate();
hp.repaint();
}
}
public JPanel getContentPanel() {
return contentPanel;
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\components\SourceFoldersPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.components;
import com.imageextractor.services.LogService;
import javax.swing.*;
import java.awt.*;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.datatransfer.DataFlavor;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.swing.filechooser.FileFilter;
public class SourceFoldersPanel extends JPanel {
private final DefaultListModel<String> listModel;
private final JList<String> folderList;
private static final int MAX_FOLDERS = 30;
public SourceFoldersPanel() {
setLayout(new BorderLayout(15, 0));
setOpaque(false);
listModel = new DefaultListModel<>();
folderList = new JList<>(listModel);
folderList.setOpaque(false);
folderList.setBackground(new Color(0, 0, 0, 0));
folderList.setForeground(new Color(0, 229, 255)); // Glowing cyan elements
folderList.setFont(new Font("Segoe UI", Font.PLAIN, 12));
folderList.setSelectionBackground(new Color(0, 180, 216, 50));
folderList.setSelectionForeground(Color.WHITE);
JScrollPane scrollPane = new JScrollPane(folderList) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
if (listModel.isEmpty()) {
// Transparent deep dark container background
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, w, h, 16, 16);
// Sleek thin dashed rounded outline
float[] dash = {6.0f, 4.0f};
Stroke dashed = new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
g2.setStroke(dashed);
g2.setColor(new Color(255, 255, 255, 25)); // 25% transparent white/cyan dashed line
g2.drawRoundRect(1, 1, w - 2, h - 2, 16, 16);
// Centered vector folder silhouette
int fx = (w - 28) / 2;
int fy = Math.max(4, h / 2 - 22);
g2.setColor(new Color(0, 180, 216, 120));
g2.setStroke(new BasicStroke(1.8f));
g2.drawRoundRect(fx, fy, 28, 16, 4, 4);
int[] tabX = {fx + 2, fx + 9, fx + 13, fx + 2};
int[] tabY = {fy, fy - 4, fy - 4, fy};
g2.drawPolygon(tabX, tabY, 4);
g2.setFont(new Font("Segoe UI", Font.BOLD, 11));
g2.setColor(new Color(175, 195, 215, 220));
FontMetrics fm = g2.getFontMetrics();
String line1 = "No sources added yet";
int line1Y = Math.max(fy + 28, h / 2 + 4);
g2.drawString(line1, (w - fm.stringWidth(line1)) / 2, line1Y);
g2.setFont(new Font("Segoe UI", Font.PLAIN, 10));
g2.setColor(new Color(175, 195, 215, 140));
fm = g2.getFontMetrics();
String line2 = "Click '+ Add Source(s)' or drag & drop folders / zip files here";
int line2Y = line1Y + 14;
if (line2Y < h - 2) {
g2.drawString(line2, (w - fm.stringWidth(line2)) / 2, line2Y);
}
} else {
// Sleek dark translucent container base
g2.setColor(new Color(6, 15, 27, 220));
g2.fillRoundRect(0, 0, w, h, 16, 16);
// 1px subtle glowing border
g2.setColor(new Color(0, 180, 216, 60));
g2.setStroke(new BasicStroke(1.0f));
g2.drawRoundRect(0, 0, w - 1, h - 1, 16, 16);
}
g2.dispose();
super.paintComponent(g);
}
};
scrollPane.setOpaque(false);
scrollPane.getViewport().setOpaque(false);
scrollPane.setPreferredSize(new Dimension(380, 80));
scrollPane.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
// Listener to force repaint empty state on item changes
listModel.addListDataListener(new javax.swing.event.ListDataListener() {
@Override public void intervalAdded(javax.swing.event.ListDataEvent e) { scrollPane.repaint(); }
@Override public void intervalRemoved(javax.swing.event.ListDataEvent e) { scrollPane.repaint(); }
@Override public void contentsChanged(javax.swing.event.ListDataEvent e) { scrollPane.repaint(); }
});
folderList.setDropTarget(new DropTarget() {
@Override
public void drop(DropTargetDropEvent evt) {
boolean success = false;
try {
if (evt.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
evt.acceptDrop(DnDConstants.ACTION_COPY);
@SuppressWarnings("unchecked")
List<File> droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : droppedFiles) {
if (listModel.getSize() >= MAX_FOLDERS) break;
if (file.isDirectory() || file.getName().toLowerCase().endsWith(".zip")) {
if (!listModel.contains(file.getAbsolutePath())) {
listModel.addElement(file.getAbsolutePath());
}
}
}
success = true;
} else {
evt.rejectDrop();
}
} catch (Exception ex) {
LogService.log("UI", "ERROR", "Drag and drop source folders failed: " + ex.getMessage());
} finally {
evt.dropComplete(success);
}
}
});
add(scrollPane, BorderLayout.CENTER);
}
public JPanel getHeaderButtonsPanel() {
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false);
BlendButton addBtn = new BlendButton("+ Add Source(s)", true); // Bright green gradient button
BlendButton removeBtn = new BlendButton("Remove Selected", false); // Outline button
BlendButton clearBtn = new BlendButton("Clear All", false); // Outline button
addBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
removeBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
clearBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
addBtn.setPreferredSize(new Dimension(125, 22));
removeBtn.setPreferredSize(new Dimension(125, 22));
clearBtn.setPreferredSize(new Dimension(90, 22));
addBtn.addActionListener(e -> {
if (listModel.getSize() >= MAX_FOLDERS) {
JOptionPane.showMessageDialog(this, "Maximum 30 sources allowed.", "Limit Reached", JOptionPane.WARNING_MESSAGE);
return;
}
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
chooser.setMultiSelectionEnabled(true);
chooser.setFileFilter(new FileFilter() {
@Override
public boolean accept(File f) {
return f.isDirectory() || f.getName().toLowerCase().endsWith(".zip");
}
@Override
public String getDescription() {
return "Folders and ZIP files";
}
});
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION && chooser.getSelectedFiles() != null) {
for (File file : chooser.getSelectedFiles()) {
if (file != null && listModel.getSize() < MAX_FOLDERS && !listModel.contains(file.getAbsolutePath())) {
listModel.addElement(file.getAbsolutePath());
}
}
}
});
removeBtn.addActionListener(e -> {
List<String> selected = folderList.getSelectedValuesList();
for (String val : selected) {
listModel.removeElement(val);
}
});
clearBtn.addActionListener(e -> listModel.clear());
btnPanel.add(addBtn);
btnPanel.add(removeBtn);
btnPanel.add(clearBtn);
return btnPanel;
}
public List<String> getSourceFolders() {
List<String> folders = new ArrayList<>();
for (int i = 0; i < listModel.size(); i++) {
folders.add(listModel.getElementAt(i));
}
return folders;
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\AllImagePanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.ZipService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.services.TempManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.OutputFormatPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class AllImagePanel extends JPanel {
private SourceFoldersPanel sourcePanel;
private DestinationFolderPanel destPanel;
private OutputFormatPanel formatPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private JButton startBtn;
private JButton pauseBtn;
private JButton stopBtn;
private JProgressBar progressBar;
private AtomicBoolean cancelFlag = new AtomicBoolean(false);
private AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public AllImagePanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.50;
gbc.insets = new Insets(0, 0, 5, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
formatPanel = new OutputFormatPanel();
destPanel = new DestinationFolderPanel("Destination Folder");
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(2, 5, 2, 5);
gbcOut.gridy = 0;
outSettingsContent.add(formatPanel, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.30;
gbc.insets = new Insets(0, 0, 5, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeExtraction();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(formatPanel, enabled);
setComponentsEnabled(destPanel, enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("All Image Extraction: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("All Image Extraction: Process resumed.");
}
private void executeExtraction() {
if (isProcessing.get()) return;
java.util.List<String> src = sourcePanel.getSourceFolders();
String dest = destPanel.getDestinationFolder();
if (src.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please select source folders and a destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
boolean isZip = formatPanel.isZipOutput();
isProcessing.set(true);
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("All Image Extraction: Starting process...");
LogService.log("All Image Extraction: Sources -> " + src);
LogService.log("All Image Extraction: Destination -> " + dest);
LogService.log("All Image Extraction: ZIP Output -> " + (isZip ? "Yes" : "No"));
LogService.log("All Image Extraction: Scanning source(s) for images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
setInputsEnabled(false);
worker = new SwingWorker<Integer, Long>() {
@Override
protected Integer doInBackground() throws Exception {
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
TaskManager.resetTelemetry();
TaskManager.setCurrentOperation("Scanning Sources");
final long[] count = {0};
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
TaskManager.setTotalFiles(totalFiles);
TaskManager.setCurrentOperation("Extracting Images");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("All Image Extraction: Progress %d/%d files (Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
String tempPath = dest;
if (isZip) {
tempPath = dest + File.separator + "temp_all_images_" + System.currentTimeMillis();
try {
Files.createDirectories(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("EXTRACT", "ERROR", "Failed to create temp directory for ZIP creation: " + ex.getMessage());
return 0;
}
}
final String finalDest = tempPath;
int extracted = ImageExtractionService.extractAllImagesStreamed(
src,
finalDest,
p -> {},
cancelFlag
);
try {
if (isZip && !cancelFlag.get() && extracted > 0) {
SwingUtilities.invokeLater(() -> {
statusDotLabel.setText("● Compressing output ZIP archive...");
progressBar.setIndeterminate(true);
});
String zipPath = dest + File.separator + "All_Images_Output.zip";
ZipService.compressDirectory(tempPath, zipPath, cancelFlag, pauseFlag, null);
}
} catch (Exception e) {
LogService.log("COMPRESSION", "ERROR", "Failed to compress output folder: " + tempPath);
} finally {
if (isZip) {
TempManager.deleteTempDirectory(Paths.get(tempPath));
}
}
if (!cancelFlag.get() && extracted > 0) {
if (isZip) {
LogService.log("Validation: Output generated as ZIP archive successfully.");
} else {
try (java.util.stream.Stream<Path> stream = Files.walk(Paths.get(dest))) {
long finalCount = stream.filter(ImageExtractionService::isImageFile).count();
LogService.log(String.format("Validation Complete: Extracted %d images. Target folder now contains %d total images. Data Integrity 100%%.", extracted, finalCount));
} catch (Exception ignored) {}
}
}
return extracted;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int count = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("All Image Extraction: Process stopped by user.");
} else if (count == 0) {
LogService.log("All Image Extraction: Process finished. No images were found.");
} else {
LogService.log("All Image Extraction: Process completed! Extracted " + count + " images.");
}
JOptionPane.showMessageDialog(AllImagePanel.this, msg + "Extracted " + count + " images.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(AllImagePanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\ImageConverterPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ExcelService;
import com.imageextractor.services.ImageConversionService;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ImageConverterPanel extends JPanel {
private final SourceFoldersPanel sourcePanel;
private final DestinationFolderPanel destPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private final JRadioButton formatRadio;
private final JRadioButton sizeRadio;
private final JRadioButton bothRadio;
private final JComboBox<String> formatCombo;
private final JTextField targetSizeField;
private final JComboBox<String> unitCombo;
private final JButton startBtn;
private final JButton pauseBtn;
private final JButton stopBtn;
private final JProgressBar progressBar;
private SwingWorker<Void, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
private final AtomicBoolean cancelFlag = new AtomicBoolean(false);
private final AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
public ImageConverterPanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.40;
gbc.insets = new Insets(0, 0, 10, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
destPanel = new DestinationFolderPanel("Destination Folder");
// Dynamic controls for Converter vs Resizer vs Both
JPanel radioPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
radioPanel.setOpaque(false);
JLabel modeLabel = new JLabel("Task Mode:");
modeLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
modeLabel.setForeground(new Color(200, 214, 229));
formatRadio = new JRadioButton("Convert Format Only");
sizeRadio = new JRadioButton("Optimize File Size Only");
bothRadio = new JRadioButton("Convert & Optimize Size");
bothRadio.setSelected(true);
formatRadio.setOpaque(false); formatRadio.setForeground(Color.WHITE); formatRadio.setFont(new Font("Segoe UI", Font.BOLD, 11));
sizeRadio.setOpaque(false); sizeRadio.setForeground(Color.WHITE); sizeRadio.setFont(new Font("Segoe UI", Font.BOLD, 11));
bothRadio.setOpaque(false); bothRadio.setForeground(Color.WHITE); bothRadio.setFont(new Font("Segoe UI", Font.BOLD, 11));
ButtonGroup group = new ButtonGroup();
group.add(formatRadio); group.add(sizeRadio); group.add(bothRadio);
radioPanel.add(modeLabel);
radioPanel.add(formatRadio);
radioPanel.add(sizeRadio);
radioPanel.add(bothRadio);
JPanel comboGrid = new JPanel(new GridLayout(1, 2, 20, 0));
comboGrid.setOpaque(false);
JPanel leftComboPanel = new JPanel(new BorderLayout(10, 0));
leftComboPanel.setOpaque(false);
JLabel formatLabel = new JLabel("Target Format:");
formatLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
formatLabel.setForeground(new Color(200, 214, 229));
formatLabel.setPreferredSize(new Dimension(160, 24));
formatCombo = new JComboBox<>(new String[]{"JPG", "PNG", "GIF", "BMP", "WEBP", "TIFF"});
formatCombo.setFont(new Font("Segoe UI", Font.BOLD, 11));
formatCombo.setPreferredSize(new Dimension(0, 24));
leftComboPanel.add(formatLabel, BorderLayout.WEST);
leftComboPanel.add(formatCombo, BorderLayout.CENTER);
JPanel rightComboPanel = new JPanel(new BorderLayout(10, 0));
rightComboPanel.setOpaque(false);
JLabel sizeLabel = new JLabel("Target File Size:");
sizeLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
sizeLabel.setForeground(new Color(200, 214, 229));
sizeLabel.setPreferredSize(new Dimension(160, 24));
JPanel sizeInputWrapper = new JPanel(new BorderLayout(5, 0));
sizeInputWrapper.setOpaque(false);
targetSizeField = createSleekTextField("250");
targetSizeField.setHorizontalAlignment(JTextField.CENTER);
unitCombo = new JComboBox<>(new String[]{"KB", "MB"});
unitCombo.setFont(new Font("Segoe UI", Font.BOLD, 11));
unitCombo.setPreferredSize(new Dimension(65, 24));
sizeInputWrapper.add(targetSizeField, BorderLayout.CENTER);
sizeInputWrapper.add(unitCombo, BorderLayout.EAST);
rightComboPanel.add(sizeLabel, BorderLayout.WEST);
rightComboPanel.add(sizeInputWrapper, BorderLayout.CENTER);
comboGrid.add(leftComboPanel);
comboGrid.add(rightComboPanel);
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(1, 5, 1, 5);
gbcOut.gridy = 0;
outSettingsContent.add(radioPanel, gbcOut);
gbcOut.gridy = 1;
gbcOut.insets = new Insets(4, 5, 4, 5);
outSettingsContent.add(comboGrid, gbcOut);
gbcOut.gridy = 2;
gbcOut.insets = new Insets(1, 5, 1, 5);
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 3;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.28;
gbc.insets = new Insets(0, 0, 4, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Toggle Logic
java.awt.event.ActionListener toggleListener = e -> {
boolean formatEnabled = formatRadio.isSelected() || bothRadio.isSelected();
boolean sizeEnabled = sizeRadio.isSelected() || bothRadio.isSelected();
formatCombo.setEnabled(formatEnabled);
targetSizeField.setEnabled(sizeEnabled);
unitCombo.setEnabled(sizeEnabled);
};
formatRadio.addActionListener(toggleListener);
sizeRadio.addActionListener(toggleListener);
bothRadio.addActionListener(toggleListener);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeConversion();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private JTextField createSleekTextField(String text) {
JTextField tf = new JTextField(text) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15);
g2.dispose();
super.paintComponent(g);
}
};
tf.setOpaque(false);
tf.setFont(new Font("Segoe UI", Font.PLAIN, 12));
tf.setForeground(new Color(0, 229, 255));
tf.setCaretColor(Color.WHITE);
tf.setPreferredSize(new Dimension(0, 24));
tf.setBorder(new Border() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 180, 216, 60));
g2.drawRoundRect(x, y, width - 1, height - 1, 15, 15);
g2.dispose();
}
@Override public Insets getBorderInsets(Component c) { return new Insets(2, 12, 2, 12); }
@Override public boolean isBorderOpaque() { return false; }
});
return tf;
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(destPanel, enabled);
formatRadio.setEnabled(enabled);
sizeRadio.setEnabled(enabled);
bothRadio.setEnabled(enabled);
chkLowMemory.setEnabled(enabled);
boolean formatEnabled = enabled && (formatRadio.isSelected() || bothRadio.isSelected());
boolean sizeEnabled = enabled && (sizeRadio.isSelected() || bothRadio.isSelected());
formatCombo.setEnabled(formatEnabled);
targetSizeField.setEnabled(sizeEnabled);
unitCombo.setEnabled(sizeEnabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Image Converter: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Converter: Process resumed.");
}
private void executeConversion() {
if (isProcessing.get()) return;
List<String> sources = sourcePanel.getSourceFolders();
String dest = destPanel.getDestinationFolder();
if (sources.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please select at least one source and a destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
String targetFormat = null;
long targetBytes = 0;
if (formatRadio.isSelected() || bothRadio.isSelected()) {
targetFormat = (String) formatCombo.getSelectedItem();
}
if (sizeRadio.isSelected() || bothRadio.isSelected()) {
String targetSizeStr = targetSizeField.getText().trim();
if (targetSizeStr.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please enter a target file size.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
try {
double size = Double.parseDouble(targetSizeStr);
if (size <= 0) throw new NumberFormatException();
String unit = (String) unitCombo.getSelectedItem();
targetBytes = (long) (size * ("MB".equals(unit) ? 1024 * 1024 : 1024));
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, "Please enter a valid positive number for target size.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
}
final String finalTargetFormat = targetFormat;
final long finalTargetBytes = targetBytes;
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Converter: Starting process...");
LogService.log("Image Converter: Sources -> " + sources);
LogService.log("Image Converter: Destination -> " + dest);
LogService.log("Image Converter: Target Format -> " + (finalTargetFormat != null ? finalTargetFormat : "N/A"));
LogService.log("Image Converter: Target Size Limit -> " + (finalTargetBytes > 0 ? (finalTargetBytes / 1024) + " KB" : "N/A"));
LogService.log("Image Converter: Scanning source(s) for images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Void, Long>() {
private ETACalculator etaCalculator;
@Override
protected Void doInBackground() throws Exception {
final long[] count = {0};
for (String folderPath : sources) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
TaskManager.setTotalFiles(totalFiles);
LogService.log("Image Converter: Scan complete. Found " + totalFiles + " images to process.");
if (totalFiles == 0 || cancelFlag.get()) return null;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Image Converter: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
List<ImageConversionService.ConversionResult> results = ImageConversionService.convertImagesStreamed(sources, dest, finalTargetFormat, finalTargetBytes,
p -> {}, cancelFlag);
if (!cancelFlag.get() && !results.isEmpty()) {
SwingUtilities.invokeLater(() -> {
statusDotLabel.setText("● Generating Conversion Excel Report...");
progressBar.setIndeterminate(true);
});
String reportPath = Paths.get(dest).resolve("Conversion_Report_" + System.currentTimeMillis() + ".xlsx").toString();
ExcelService.generateConversionReport(results, reportPath,
progress -> {}, cancelFlag);
}
return null;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
get();
if (cancelFlag.get()) {
progressBar.setValue(0);
LogService.log("Image Converter: Process stopped by user.");
JOptionPane.showMessageDialog(ImageConverterPanel.this, "Conversion Process stopped. Report generated successfully.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} else if (totalFiles == 0) {
progressBar.setValue(0);
LogService.log("Image Converter: Process finished. No images were found.");
JOptionPane.showMessageDialog(ImageConverterPanel.this, "No images found to convert.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} else {
progressBar.setValue(progressBar.getMaximum());
LogService.log("Image Converter: Process completed! Images converted and report generated successfully.");
JOptionPane.showMessageDialog(ImageConverterPanel.this, "Images converted and report generated successfully!", "Success", JOptionPane.INFORMATION_MESSAGE);
}
} catch (Exception ex) {
progressBar.setValue(0);
LogService.log("Image Converter Error: " + ex.getMessage());
JOptionPane.showMessageDialog(ImageConverterPanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
progressBar.setIndeterminate(false);
setInputsEnabled(true);
TaskManager.setPaused(false);
pauseFlag.set(false);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\ImageExcelPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.ZipService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.services.TempManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.OutputFormatPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class ImageExcelPanel extends JPanel {
private SourceFoldersPanel sourcePanel;
private DestinationFolderPanel destPanel;
private OutputFormatPanel formatPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private JButton startBtn;
private JButton pauseBtn;
private JButton stopBtn;
private JProgressBar progressBar;
private AtomicBoolean cancelFlag = new AtomicBoolean(false);
private AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public ImageExcelPanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.50;
gbc.insets = new Insets(0, 0, 5, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
formatPanel = new OutputFormatPanel();
destPanel = new DestinationFolderPanel("Destination Folder");
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(2, 5, 2, 5);
gbcOut.gridy = 0;
outSettingsContent.add(formatPanel, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.30;
gbc.insets = new Insets(0, 0, 5, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeGeneration();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(formatPanel, enabled);
setComponentsEnabled(destPanel, enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Image Excel Report: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Excel Report: Process resumed.");
}
private void executeGeneration() {
if (isProcessing.get()) return;
java.util.List<String> src = new java.util.ArrayList<>(sourcePanel.getSourceFolders());
src.sort(String.CASE_INSENSITIVE_ORDER);
String dest = destPanel.getDestinationFolder();
if (src.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please provide source folders and a destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
boolean isZip = formatPanel.isZipOutput();
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Excel Report: Starting process...");
LogService.log("Image Excel Report: Sources -> " + src);
LogService.log("Image Excel Report: Destination -> " + dest);
LogService.log("Image Excel Report: ZIP Output -> " + (isZip ? "Yes" : "No"));
LogService.log("Image Excel Report: Scanning source(s) for images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Integer, Long>() {
private ETACalculator etaCalculator;
@Override
protected Integer doInBackground() throws Exception {
totalFiles = 0;
final long[] counts = new long[src.size()];
int idx = 0;
for (String folderPath : src) {
if (cancelFlag.get()) break;
final int fIdx = idx;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
counts[fIdx]++;
}
return FileVisitResult.CONTINUE;
}
});
});
totalFiles += counts[idx];
idx++;
}
TaskManager.setTotalFiles(totalFiles);
LogService.log("Image Excel Report: Scan complete. Found " + totalFiles + " images to document.");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Image Excel Report: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
String tempPath = dest;
if (isZip) {
tempPath = dest + File.separator + "temp_excel_" + System.currentTimeMillis();
try {
Files.createDirectories(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("EXCEL", "ERROR", "Failed to create temp excel folder: " + ex.getMessage());
return 0;
}
}
int totalGenerated = 0;
long totalProcessed = 0;
idx = 0;
for (String folderPath : src) {
if (cancelFlag != null && cancelFlag.get()) break;
Path srcP = Paths.get(folderPath);
String folderName = srcP.getFileName() != null ? srcP.getFileName().toString().replaceAll("[\\\\/:*?\"<>|]", "_") : "Root_Drive";
String baseReportName = folderName + (src.size() > 1 ? "_" + (idx + 1) : "") + "_images";
File exportFile = new File(tempPath, baseReportName + ".xlsx");
int colId = 1;
while (exportFile.exists()) {
exportFile = new File(tempPath, baseReportName + "_" + colId + ".xlsx");
colId++;
}
String exportPath = exportFile.getAbsolutePath();
com.imageextractor.services.ExcelService.generateImageExcelStreamed(
folderPath, exportPath, p -> TaskManager.setProcessedCount(p), cancelFlag, totalProcessed
);
if (cancelFlag != null && cancelFlag.get()) break;
totalProcessed += counts[idx];
totalGenerated++;
idx++;
}
try {
if (isZip && !cancelFlag.get() && totalGenerated > 0) {
SwingUtilities.invokeLater(() -> {
statusDotLabel.setText("● Compressing output ZIP archive...");
progressBar.setIndeterminate(true);
});
String zipPath = dest + File.separator + "Excel_Reports.zip";
ZipService.compressDirectory(tempPath, zipPath, cancelFlag, pauseFlag, null);
}
} catch (Exception e) {
LogService.log("COMPRESSION", "ERROR", "Failed to compress output folder: " + tempPath);
} finally {
if (isZip) {
try {
TempManager.deleteTempDirectory(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("EXCEL", "WARNING", "Temp cleanup warning: " + ex.getMessage());
}
}
}
return totalGenerated;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int count = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("Image Excel Report: Process stopped by user.");
} else if (count == 0) {
LogService.log("Image Excel Report: Process finished. No images were found.");
} else {
LogService.log("Image Excel Report: Process completed! Generated " + count + " dedicated Excel reports.");
}
JOptionPane.showMessageDialog(ImageExcelPanel.this, msg + "Generated " + count + " dedicated Excel reports.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
LogService.log("Image Excel Report Error: " + ex.getMessage());
JOptionPane.showMessageDialog(ImageExcelPanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\ImageRenamePanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.ImageRenameService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.ExcelFilePanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import java.awt.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class ImageRenamePanel extends JPanel {
private final SourceFoldersPanel sourcePanel;
private final ExcelFilePanel excelPanel;
private final DestinationFolderPanel destPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private final JButton startBtn;
private final JButton pauseBtn;
private final JButton stopBtn;
private final JProgressBar progressBar;
private final AtomicBoolean cancelFlag = new AtomicBoolean(false);
private final AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public ImageRenamePanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.40;
gbc.insets = new Insets(0, 0, 10, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
excelPanel = new ExcelFilePanel("Select Excel File (Col A: Original Name, Col B: New Name)");
destPanel = new DestinationFolderPanel("Destination Folder");
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(2, 5, 2, 5);
gbcOut.gridy = 0;
outSettingsContent.add(excelPanel, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.30;
gbc.insets = new Insets(0, 0, 5, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeRename();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(excelPanel, enabled);
setComponentsEnabled(destPanel, enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Image Rename: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Rename: Process resumed.");
}
private void executeRename() {
if (isProcessing.get()) return;
java.util.List<String> src = sourcePanel.getSourceFolders();
String excelFile = excelPanel.getExcelFilePath();
String dest = destPanel.getDestinationFolder();
if (src.isEmpty() || excelFile == null || excelFile.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please provide source folders, excel file, and destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Image Rename: Starting process...");
LogService.log("Image Rename: Sources -> " + src);
LogService.log("Image Rename: Excel File -> " + excelFile);
LogService.log("Image Rename: Destination -> " + dest);
LogService.log("Image Rename: Scanning source(s) for images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Integer, Long>() {
private ETACalculator etaCalculator;
@Override
protected Integer doInBackground() throws Exception {
final long[] count = {0};
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
TaskManager.setTotalFiles(totalFiles);
LogService.log("Image Rename: Scan complete. Found " + totalFiles + " images to process.");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Image Rename: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
return ImageRenameService.renameImagesStreamed(src, excelFile, dest, p -> {}, cancelFlag);
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int count = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("Image Rename: Process stopped by user.");
} else if (count == 0) {
LogService.log("Image Rename: Process finished. No images were renamed.");
} else {
LogService.log("Image Rename: Process completed! Renamed and copied " + count + " images.");
}
JOptionPane.showMessageDialog(ImageRenamePanel.this, msg + "Renamed and copied " + count + " images.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
LogService.log("Image Rename Error: " + ex.getMessage());
JOptionPane.showMessageDialog(ImageRenamePanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\ImageValidatorPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ExcelService;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.ImageValidationService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ImageValidatorPanel extends JPanel {
private final SourceFoldersPanel sourcePanel;
private DestinationFolderPanel destPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private final JRadioButton quickRadio;
private final JRadioButton deepRadio;
private final JButton startBtn;
private final JButton pauseBtn;
private final JButton stopBtn;
private final JProgressBar progressBar;
private final JCheckBox chkSpoofing;
private final JCheckBox chkDuplicate;
private final AtomicBoolean cancelFlag = new AtomicBoolean(false);
private final AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Void, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public ImageValidatorPanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.55;
gbc.insets = new Insets(0, 0, 4, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
// Scan Mode Options
JPanel scanModeGrid = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
scanModeGrid.setOpaque(false);
JLabel scanModeTitle = new JLabel("Scan Mode:");
scanModeTitle.setFont(new Font("Segoe UI", Font.BOLD, 12));
scanModeTitle.setForeground(new Color(200, 214, 229));
scanModeTitle.setPreferredSize(new Dimension(160, 24));
quickRadio = new JRadioButton("Quick Scan");
deepRadio = new JRadioButton("Deep Scan");
quickRadio.setOpaque(false); quickRadio.setForeground(Color.WHITE); quickRadio.setFont(new Font("Segoe UI", Font.BOLD, 11));
deepRadio.setOpaque(false); deepRadio.setForeground(Color.WHITE); deepRadio.setFont(new Font("Segoe UI", Font.BOLD, 11));
quickRadio.setSelected(true);
ButtonGroup scanGroup = new ButtonGroup();
scanGroup.add(quickRadio);
scanGroup.add(deepRadio);
scanModeGrid.add(scanModeTitle);
scanModeGrid.add(quickRadio);
scanModeGrid.add(deepRadio);
// Forensic Checkboxes
JPanel forensicGrid = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
forensicGrid.setOpaque(false);
JLabel forensicTitle = new JLabel("Forensics:");
forensicTitle.setFont(new Font("Segoe UI", Font.BOLD, 12));
forensicTitle.setForeground(new Color(200, 214, 229));
forensicTitle.setPreferredSize(new Dimension(160, 24));
chkSpoofing = new JCheckBox("Detect Extension Spoofing");
chkDuplicate = new JCheckBox("Detect Perceptual Duplicates");
chkSpoofing.setOpaque(false); chkSpoofing.setForeground(Color.WHITE); chkSpoofing.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkDuplicate.setOpaque(false); chkDuplicate.setForeground(Color.WHITE); chkDuplicate.setFont(new Font("Segoe UI", Font.BOLD, 11));
forensicGrid.add(forensicTitle);
forensicGrid.add(chkSpoofing);
forensicGrid.add(chkDuplicate);
chkDuplicate.setVisible(false);
KeyEventDispatcher dispatcher = new KeyEventDispatcher() {
private boolean ctrlPressed = false;
private boolean kPressed = false;
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (e.getID() == KeyEvent.KEY_PRESSED) {
if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = true;
} else if (e.getKeyCode() == KeyEvent.VK_K) {
kPressed = true;
} else if (e.getKeyCode() == KeyEvent.VK_V) {
if (ctrlPressed && kPressed) {
SwingUtilities.invokeLater(() -> {
chkDuplicate.setVisible(true);
chkDuplicate.revalidate();
chkDuplicate.repaint();
revalidate();
repaint();
});
}
}
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = false;
} else if (e.getKeyCode() == KeyEvent.VK_K) {
kPressed = false;
}
}
return false;
}
};
addAncestorListener(new javax.swing.event.AncestorListener() {
@Override
public void ancestorAdded(javax.swing.event.AncestorEvent event) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(dispatcher);
}
@Override
public void ancestorRemoved(javax.swing.event.AncestorEvent event) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(dispatcher);
}
@Override
public void ancestorMoved(javax.swing.event.AncestorEvent event) {}
});
destPanel = new DestinationFolderPanel("Report / Log Output Folder");
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(1, 5, 1, 5);
gbcOut.gridy = 0;
outSettingsContent.add(scanModeGrid, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(forensicGrid, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 3;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.28;
gbc.insets = new Insets(0, 0, 4, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeValidation();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(destPanel, enabled);
quickRadio.setEnabled(enabled);
deepRadio.setEnabled(enabled);
chkSpoofing.setEnabled(enabled);
chkDuplicate.setEnabled(enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Validation Process Paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Validation Process Resumed.");
}
private void executeValidation() {
if (isProcessing.get()) return;
List<String> src = sourcePanel.getSourceFolders();
String dest = destPanel.getDestinationFolder();
if (src.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please select source folders and a destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
ImageValidationService.ScanMode mode = quickRadio.isSelected() ? ImageValidationService.ScanMode.QUICK : ImageValidationService.ScanMode.DEEP;
final boolean doSpoof = chkSpoofing.isSelected();
final boolean doDuplicate = chkDuplicate.isSelected();
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
LogService.log("Starting Image Validation (" + mode.name() + " SCAN). Counting files...");
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Void, Long>() {
private ETACalculator etaCalculator;
@Override
protected Void doInBackground() throws Exception {
final long[] count = {0};
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
TaskManager.setTotalFiles(totalFiles);
if (totalFiles == 0) return null;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Image Validation: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
LogService.log("Found " + totalFiles + " images to validate. Beginning scan...");
String reportPath = Paths.get(dest).resolve("Image_Health_Report_" + System.currentTimeMillis() + ".xlsx").toString();
try (ExcelService.ExcelReportWriter excelWriter = new ExcelService.ExcelReportWriter(reportPath, doDuplicate)) {
ImageValidationService.validateImagesStreamed(
src, mode, doSpoof, doDuplicate,
0.82,
false,
excelWriter,
p -> {},
msg -> {},
cancelFlag
);
}
return null;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
get();
if (cancelFlag.get()) {
LogService.log("Validation Process Cancelled. Partial report has been saved.");
JOptionPane.showMessageDialog(ImageValidatorPanel.this, "Process stopped. Partial report saved successfully.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} else if (totalFiles == 0) {
LogService.log("Validation complete: No images found.");
JOptionPane.showMessageDialog(ImageValidatorPanel.this, "No images found to validate.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} else {
LogService.log("Validation Complete! Health Report generated successfully.");
JOptionPane.showMessageDialog(ImageValidatorPanel.this, "Validation complete! Health Report generated successfully.", "Success", JOptionPane.INFORMATION_MESSAGE);
}
} catch (Exception ex) {
LogService.log("Validation Error: " + ex.getMessage());
JOptionPane.showMessageDialog(ImageValidatorPanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\InnerZipPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.ZipService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.services.TempManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class InnerZipPanel extends JPanel {
private SourceFoldersPanel sourcePanel;
private DestinationFolderPanel destPanel;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private JButton startBtn;
private JButton pauseBtn;
private JButton stopBtn;
private JProgressBar progressBar;
private AtomicBoolean cancelFlag = new AtomicBoolean(false);
private AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public InnerZipPanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.50;
gbc.insets = new Insets(0, 0, 5, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
destPanel = new DestinationFolderPanel("Destination Folder");
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(2, 5, 2, 5);
gbcOut.gridy = 0;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.30;
gbc.insets = new Insets(0, 0, 5, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeInnerZip();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(destPanel, enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Inner Zip: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Inner Zip: Process resumed.");
}
private void executeInnerZip() {
if (isProcessing.get()) return;
java.util.List<String> src = sourcePanel.getSourceFolders();
String dest = destPanel.getDestinationFolder();
if (src.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please select source folders and a destination.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Inner Zip: Starting process...");
LogService.log("Inner Zip: Sources -> " + src);
LogService.log("Inner Zip: Destination -> " + dest);
LogService.log("Inner Zip: Scanning source(s) for zip files and images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Integer, Long>() {
private ETACalculator etaCalculator;
@Override
protected Integer doInBackground() throws Exception {
totalFiles = 0;
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file) || ZipService.isZipFile(file)) {
totalFiles++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
TaskManager.setTotalFiles(totalFiles);
LogService.log("Inner Zip: Scan complete. Found " + totalFiles + " archives/images to process.");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Inner Zip: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
String tempPath = dest + File.separator + "temp_inner_zip_" + System.currentTimeMillis();
Files.createDirectories(Paths.get(tempPath));
int extractedCount = 0;
try {
extractedCount = ImageExtractionService.extractInnerZipsStreamed(src, tempPath, p -> {}, cancelFlag);
if (extractedCount > 0 && !cancelFlag.get()) {
SwingUtilities.invokeLater(() -> {
statusDotLabel.setText("● Compressing with 7-Zip to inner.zip...");
progressBar.setIndeterminate(true);
});
String zipPath = dest + File.separator + "inner.zip";
ZipService.compressDirectory(tempPath, zipPath, cancelFlag, pauseFlag, null);
}
} finally {
// Clean up temp directory
try {
TempManager.deleteTempDirectory(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("ZIP", "WARNING", "Temp cleanup warning: " + ex.getMessage());
}
}
return extractedCount;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int count = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("Inner Zip: Process stopped by user.");
} else if (count == 0) {
LogService.log("Inner Zip: Process finished. No archives or images were found.");
} else {
LogService.log("Inner Zip: Process completed! Compressed " + count + " images into inner.zip using 7-Zip.");
}
JOptionPane.showMessageDialog(InnerZipPanel.this, msg + "Compressed " + count + " images into inner.zip.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
LogService.log("Inner Zip Error: " + ex.getMessage());
JOptionPane.showMessageDialog(InnerZipPanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\SelectivePanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.ZipService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.services.TempManager;
import com.imageextractor.services.ExcelService;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.ExcelFilePanel;
import com.imageextractor.ui.components.OutputFormatPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class SelectivePanel extends JPanel {
private SourceFoldersPanel sourcePanel;
private ExcelFilePanel excelPanel;
private DestinationFolderPanel destPanel;
private OutputFormatPanel formatPanel;
private JCheckBox chkPartition;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private JButton startBtn;
private JButton pauseBtn;
private JButton stopBtn;
private JProgressBar progressBar;
private AtomicBoolean cancelFlag = new AtomicBoolean(false);
private AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public SelectivePanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.55;
gbc.insets = new Insets(0, 0, 4, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
excelPanel = new ExcelFilePanel("Select Excel File (Names in Col A, Folders in Col B)");
formatPanel = new OutputFormatPanel();
destPanel = new DestinationFolderPanel("Destination Folder");
chkPartition = new JCheckBox("Partition folders by _");
chkPartition.setOpaque(false);
chkPartition.setForeground(Color.WHITE);
chkPartition.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkPartition.setSelected(false);
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(1, 5, 1, 5);
gbcOut.gridy = 0;
outSettingsContent.add(excelPanel, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(formatPanel, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(destPanel, gbcOut);
JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
optionsPanel.setOpaque(false);
optionsPanel.add(chkPartition);
optionsPanel.add(chkLowMemory);
gbcOut.gridy = 3;
outSettingsContent.add(optionsPanel, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.28;
gbc.insets = new Insets(0, 0, 4, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeExtraction();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(excelPanel, enabled);
setComponentsEnabled(formatPanel, enabled);
setComponentsEnabled(destPanel, enabled);
chkPartition.setEnabled(enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Selective Extraction: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Selective Extraction: Process resumed.");
}
private void executeExtraction() {
if (isProcessing.get()) return;
java.util.List<String> src = new java.util.ArrayList<>(sourcePanel.getSourceFolders());
src.sort(String.CASE_INSENSITIVE_ORDER);
String excelFile = excelPanel.getExcelFilePath();
String dest = destPanel.getDestinationFolder();
if (excelFile == null || excelFile.isEmpty() || dest == null || dest.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please provide an Excel file and destination folder.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
boolean isZip = formatPanel.isZipOutput();
final boolean partitionByUnderscore = chkPartition.isSelected();
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Selective Extraction: Starting process...");
LogService.log("Selective Extraction: Sources -> " + src);
LogService.log("Selective Extraction: Excel File -> " + excelFile);
LogService.log("Selective Extraction: Destination -> " + dest);
LogService.log("Selective Extraction: ZIP Output -> " + (isZip ? "Yes" : "No"));
LogService.log("Selective Extraction: Scanning source(s) for images...");
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Integer, Long>() {
private ETACalculator etaCalculator;
@Override
protected Integer doInBackground() throws Exception {
final long[] count = {0};
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
if (src.isEmpty()) {
try {
java.util.List<ExcelService.SelectiveEntry> entries = ExcelService.readSelectiveEntriesFromExcel(excelFile);
totalFiles = entries != null ? entries.size() : 0;
} catch (Exception ex) {
LogService.log("SELECTIVE", "WARNING", "Could not count Excel entries: " + ex.getMessage());
}
}
TaskManager.setTotalFiles(totalFiles);
LogService.log("Selective Extraction: Scan complete. Found " + totalFiles + " images to process.");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Selective Extraction: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
String tempPath = dest;
if (isZip) {
tempPath = dest + File.separator + "temp_selective_" + System.currentTimeMillis();
try {
Files.createDirectories(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("SELECTIVE", "ERROR", "Failed to create temp zip folder: " + ex.getMessage());
return 0;
}
}
LogService.log("Selective Extraction: Partition by _ -> " + (partitionByUnderscore ? "Yes" : "No"));
int extracted = ImageExtractionService.extractSelectiveImagesStreamed(src, excelFile, tempPath, partitionByUnderscore, p -> {}, cancelFlag);
try {
if (isZip && !cancelFlag.get() && extracted > 0) {
SwingUtilities.invokeLater(() -> {
statusDotLabel.setText("● Compressing output ZIP archive...");
progressBar.setIndeterminate(true);
});
String zipPath = dest + File.separator + "Selective_Output.zip";
ZipService.compressDirectory(tempPath, zipPath, cancelFlag, pauseFlag, null);
}
} catch (Exception e) {
LogService.log("COMPRESSION", "ERROR", "Failed to compress output folder: " + tempPath);
} finally {
if (isZip) {
try {
TempManager.deleteTempDirectory(Paths.get(tempPath));
} catch (Exception ex) {
LogService.log("SELECTIVE", "WARNING", "Temp cleanup warning: " + ex.getMessage());
}
}
}
return extracted;
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int count = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("Selective Extraction: Process stopped by user.");
} else if (count == 0) {
LogService.log("Selective Extraction: Process finished. No matching images were found.");
} else {
LogService.log("Selective Extraction: Process completed! Extracted " + count + " matched images.");
}
JOptionPane.showMessageDialog(SelectivePanel.this, msg + "Extracted " + count + " matched images.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
LogService.log("Selective Extraction Error: " + ex.getMessage());
JOptionPane.showMessageDialog(SelectivePanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\ui\modules\SplitImagesPanel.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.ui.modules;
import com.imageextractor.services.ImageExtractionService;
import com.imageextractor.services.KeepAwakeService;
import com.imageextractor.services.SplitService;
import com.imageextractor.services.LogService;
import com.imageextractor.services.TaskManager;
import com.imageextractor.services.TaskWatchdog;
import com.imageextractor.services.SettingsManager;
import com.imageextractor.ui.components.DestinationFolderPanel;
import com.imageextractor.ui.components.OutputFormatPanel;
import com.imageextractor.ui.components.SourceFoldersPanel;
import com.imageextractor.ui.components.SectionPanel;
import com.imageextractor.ui.components.BlendButton;
import com.imageextractor.utils.ETACalculator;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class SplitImagesPanel extends JPanel {
private SourceFoldersPanel sourcePanel;
private DestinationFolderPanel destPanel;
private OutputFormatPanel formatPanel;
private JTextField baseNameField;
private JTextField sizeLimitField;
private JComboBox<String> sizeUnitBox;
private JCheckBox chkLowMemory;
private SectionPanel sourcesSection;
private SectionPanel outSettingsSection;
private SectionPanel execSection;
private JLabel statusDotLabel;
private JLabel lblTotalCount;
private JLabel lblProcessedCount;
private JLabel lblProgressPercent;
private JButton startBtn;
private JButton pauseBtn;
private JButton stopBtn;
private JProgressBar progressBar;
private AtomicBoolean cancelFlag = new AtomicBoolean(false);
private AtomicBoolean pauseFlag = new AtomicBoolean(false);
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
private SwingWorker<Integer, Long> worker;
private Timer telemetryTimer;
private volatile long totalFiles;
public SplitImagesPanel() {
setLayout(new GridBagLayout());
setOpaque(false);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
// 1. Sources Section
sourcesSection = new SectionPanel("Sources (Folders/ZIPs, Max 30)", null, null);
sourcePanel = new SourceFoldersPanel();
sourcesSection.setHeaderRightComponent(sourcePanel.getHeaderButtonsPanel());
sourcesSection.getContentPanel().add(sourcePanel, BorderLayout.CENTER);
gbc.gridy = 0;
gbc.weighty = 0.55;
gbc.insets = new Insets(0, 0, 4, 0);
add(sourcesSection, gbc);
// 2. Output Settings Section
outSettingsSection = new SectionPanel("Output Settings", null, null);
formatPanel = new OutputFormatPanel();
destPanel = new DestinationFolderPanel("Destination Folder");
// Split options styling
JPanel configGrid = new JPanel(new GridLayout(1, 2, 20, 0));
configGrid.setOpaque(false);
configGrid.setBorder(BorderFactory.createEmptyBorder(1, 0, 2, 0));
JPanel baseNamePanel = new JPanel(new BorderLayout(10, 0));
baseNamePanel.setOpaque(false);
JLabel baseNameLabel = new JLabel("Output Base Name:");
baseNameLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
baseNameLabel.setForeground(new Color(200, 214, 229));
baseNameLabel.setPreferredSize(new Dimension(160, 24));
baseNameField = createSleekTextField("Batch");
baseNamePanel.add(baseNameLabel, BorderLayout.WEST);
baseNamePanel.add(baseNameField, BorderLayout.CENTER);
JPanel sizeLimitPanel = new JPanel(new BorderLayout(10, 0));
sizeLimitPanel.setOpaque(false);
JLabel sizeLabel = new JLabel("Size Limit / Batch:");
sizeLabel.setFont(new Font("Segoe UI", Font.BOLD, 12));
sizeLabel.setForeground(new Color(200, 214, 229));
sizeLabel.setPreferredSize(new Dimension(160, 24));
JPanel sizeInputWrapper = new JPanel(new BorderLayout(5, 0));
sizeInputWrapper.setOpaque(false);
sizeLimitField = createSleekTextField("1.9");
sizeLimitField.setHorizontalAlignment(JTextField.CENTER);
sizeUnitBox = new JComboBox<>(new String[]{"MB", "GB"});
sizeUnitBox.setFont(new Font("Segoe UI", Font.BOLD, 11));
sizeUnitBox.setSelectedIndex(1);
sizeUnitBox.setPreferredSize(new Dimension(65, 24));
sizeInputWrapper.add(sizeLimitField, BorderLayout.CENTER);
sizeInputWrapper.add(sizeUnitBox, BorderLayout.EAST);
sizeLimitPanel.add(sizeLabel, BorderLayout.WEST);
sizeLimitPanel.add(sizeInputWrapper, BorderLayout.CENTER);
configGrid.add(baseNamePanel);
configGrid.add(sizeLimitPanel);
chkLowMemory = new JCheckBox("Low Memory Mode");
chkLowMemory.setOpaque(false);
chkLowMemory.setForeground(Color.WHITE);
chkLowMemory.setFont(new Font("Segoe UI", Font.BOLD, 11));
chkLowMemory.setSelected(SettingsManager.isLowMemoryMode());
chkLowMemory.addActionListener(e -> SettingsManager.setLowMemoryMode(chkLowMemory.isSelected()));
JPanel outSettingsContent = new JPanel(new GridBagLayout());
outSettingsContent.setOpaque(false);
GridBagConstraints gbcOut = new GridBagConstraints();
gbcOut.gridx = 0;
gbcOut.fill = GridBagConstraints.HORIZONTAL;
gbcOut.weightx = 1.0;
gbcOut.insets = new Insets(1, 5, 1, 5);
gbcOut.gridy = 0;
outSettingsContent.add(configGrid, gbcOut);
gbcOut.gridy = 1;
outSettingsContent.add(formatPanel, gbcOut);
gbcOut.gridy = 2;
outSettingsContent.add(destPanel, gbcOut);
gbcOut.gridy = 3;
outSettingsContent.add(chkLowMemory, gbcOut);
outSettingsSection.getContentPanel().add(outSettingsContent, BorderLayout.CENTER);
gbc.gridy = 1;
gbc.weighty = 0.28;
gbc.insets = new Insets(0, 0, 4, 0);
add(outSettingsSection, gbc);
// 3. Execution Section
execSection = new SectionPanel("Execution", null, null);
JPanel execContent = new JPanel(new GridBagLayout());
execContent.setOpaque(false);
GridBagConstraints gbcExec = new GridBagConstraints();
gbcExec.gridx = 0;
gbcExec.fill = GridBagConstraints.HORIZONTAL;
gbcExec.weightx = 1.0;
gbcExec.insets = new Insets(1, 5, 2, 5);
statusDotLabel = new JLabel("● Ready");
statusDotLabel.setFont(new Font("Segoe UI", Font.BOLD, 11));
statusDotLabel.setForeground(new Color(40, 180, 120));
gbcExec.gridy = 0;
execContent.add(statusDotLabel, gbcExec);
// Metrics & Progress Bar Cards Container
JPanel metricsRow = new JPanel(new GridLayout(1, 2, 10, 0));
metricsRow.setOpaque(false);
// Left Card: Total Images & Processed counts
JPanel countsCard = new JPanel(new GridLayout(2, 2, 8, 0)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
countsCard.setOpaque(false);
countsCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JLabel hTotal = new JLabel("Total Images", SwingConstants.CENTER);
hTotal.setFont(new Font("Segoe UI", Font.BOLD, 10));
hTotal.setForeground(new Color(175, 195, 215));
JLabel hProcessed = new JLabel("Processed", SwingConstants.CENTER);
hProcessed.setFont(new Font("Segoe UI", Font.BOLD, 10));
hProcessed.setForeground(new Color(175, 195, 215));
lblTotalCount = new JLabel("0", SwingConstants.CENTER);
lblTotalCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblTotalCount.setForeground(new Color(40, 215, 120));
lblProcessedCount = new JLabel("0", SwingConstants.CENTER);
lblProcessedCount.setFont(new Font("Segoe UI", Font.BOLD, 13));
lblProcessedCount.setForeground(new Color(40, 215, 120));
countsCard.add(hTotal);
countsCard.add(hProcessed);
countsCard.add(lblTotalCount);
countsCard.add(lblProcessedCount);
// Right Card: Overall Progress Label, Percentage & Bar
JPanel progressCard = new JPanel(new BorderLayout(0, 2)) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 12, 12);
g2.setColor(new Color(0, 180, 216, 40));
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, 12, 12);
g2.dispose();
super.paintComponent(g);
}
};
progressCard.setOpaque(false);
progressCard.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10));
JPanel progHeader = new JPanel(new BorderLayout());
progHeader.setOpaque(false);
JLabel lblProgTitle = new JLabel("Overall Progress");
lblProgTitle.setFont(new Font("Segoe UI", Font.BOLD, 10));
lblProgTitle.setForeground(new Color(175, 195, 215));
lblProgressPercent = new JLabel("0%");
lblProgressPercent.setFont(new Font("Segoe UI", Font.BOLD, 11));
lblProgressPercent.setForeground(new Color(40, 215, 120));
progHeader.add(lblProgTitle, BorderLayout.WEST);
progHeader.add(lblProgressPercent, BorderLayout.EAST);
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(false);
progressBar.setForeground(new Color(0, 200, 120));
progressBar.setBackground(new Color(10, 25, 42, 200));
progressBar.setPreferredSize(new Dimension(0, 6));
progressBar.setBorder(BorderFactory.createLineBorder(new Color(0, 180, 216, 30), 1));
progressCard.add(progHeader, BorderLayout.NORTH);
progressCard.add(progressBar, BorderLayout.CENTER);
metricsRow.add(countsCard);
metricsRow.add(progressCard);
gbcExec.gridy = 1;
gbcExec.insets = new Insets(2, 5, 4, 5);
execContent.add(metricsRow, gbcExec);
JPanel btnPanel = new JPanel(new GridLayout(1, 3, 10, 0));
btnPanel.setOpaque(false);
btnPanel.setPreferredSize(new Dimension(0, 22));
startBtn = new BlendButton("Start Process", true);
pauseBtn = new BlendButton("Pause", false);
stopBtn = new BlendButton("Stop Process", false);
startBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
stopBtn.setFont(new Font("Segoe UI", Font.BOLD, 10));
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
btnPanel.add(startBtn);
btnPanel.add(pauseBtn);
btnPanel.add(stopBtn);
gbcExec.gridy = 2;
gbcExec.insets = new Insets(1, 5, 1, 5);
execContent.add(btnPanel, gbcExec);
execSection.getContentPanel().add(execContent, BorderLayout.CENTER);
gbc.gridy = 2;
gbc.weighty = 0.20;
gbc.insets = new Insets(0, 0, 0, 0);
add(execSection, gbc);
// Listeners
startBtn.addActionListener(e -> {
if (pauseFlag.get()) {
resumeProcess();
} else {
executeSplit();
}
});
pauseBtn.addActionListener(e -> pauseProcess());
stopBtn.addActionListener(e -> {
cancelFlag.set(true);
resumeProcess();
});
}
private JTextField createSleekTextField(String text) {
JTextField tf = new JTextField(text) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(6, 15, 27, 180));
g2.fillRoundRect(0, 0, getWidth(), getHeight(), 15, 15);
g2.dispose();
super.paintComponent(g);
}
};
tf.setOpaque(false);
tf.setFont(new Font("Segoe UI", Font.PLAIN, 12));
tf.setForeground(new Color(0, 229, 255));
tf.setCaretColor(Color.WHITE);
tf.setPreferredSize(new Dimension(0, 24));
tf.setBorder(new Border() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 180, 216, 60));
g2.drawRoundRect(x, y, width - 1, height - 1, 15, 15);
g2.dispose();
}
@Override public Insets getBorderInsets(Component c) { return new Insets(2, 12, 2, 12); }
@Override public boolean isBorderOpaque() { return false; }
});
return tf;
}
private void setInputsEnabled(boolean enabled) {
setComponentsEnabled(sourcePanel, enabled);
setComponentsEnabled(formatPanel, enabled);
setComponentsEnabled(destPanel, enabled);
baseNameField.setEnabled(enabled);
sizeLimitField.setEnabled(enabled);
sizeUnitBox.setEnabled(enabled);
chkLowMemory.setEnabled(enabled);
}
private void setComponentsEnabled(Component comp, boolean enabled) {
comp.setEnabled(enabled);
if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
setComponentsEnabled(child, enabled);
}
}
}
private void pauseProcess() {
pauseFlag.set(true);
TaskManager.setPaused(true);
startBtn.setText("Resume Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
statusDotLabel.setText("● Paused");
statusDotLabel.setForeground(new Color(243, 156, 18));
execSection.setStatus("Paused");
LogService.log("Split Images: Process paused.");
}
private void resumeProcess() {
pauseFlag.set(false);
TaskManager.setPaused(false);
startBtn.setText("Start Process");
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
statusDotLabel.setText("● Processing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Split Images: Process resumed.");
}
private void executeSplit() {
if (isProcessing.get()) return;
java.util.List<String> src = sourcePanel.getSourceFolders();
String dest = destPanel.getDestinationFolder();
String baseName = baseNameField.getText().trim();
String sizeStr = sizeLimitField.getText().trim();
if (src.isEmpty() || dest == null || dest.isEmpty() || baseName.isEmpty() || sizeStr.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please fill in all configuration fields.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
double sizeVal;
try {
sizeVal = Double.parseDouble(sizeStr);
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(this, "Invalid size limit. Must be a number.", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
boolean isGB = "GB".equals(sizeUnitBox.getSelectedItem());
long sizeLimitBytes = (long) (sizeVal * (isGB ? 1024L * 1024L * 1024L : 1024L * 1024L));
startBtn.setEnabled(false);
pauseBtn.setEnabled(true);
stopBtn.setEnabled(true);
cancelFlag.set(false);
pauseFlag.set(false);
statusDotLabel.setText("● Initializing...");
statusDotLabel.setForeground(new Color(0, 229, 255));
execSection.setStatus("Running");
LogService.log("Split Images: Starting process...");
LogService.log("Split Images: Sources -> " + src);
LogService.log("Split Images: Destination -> " + dest);
LogService.log("Split Images: Base Name -> " + baseName);
LogService.log("Split Images: Size Limit -> " + sizeStr + " " + (isGB ? "GB" : "MB"));
LogService.log("Split Images: Scanning source(s) for images...");
final boolean isZip = formatPanel.isZipOutput();
LogService.log("Split Images: ZIP Output -> " + (isZip ? "Yes" : "No"));
progressBar.setValue(0);
progressBar.setIndeterminate(true);
isProcessing.set(true);
setInputsEnabled(false);
TaskManager.resetTelemetry();
KeepAwakeService.start();
TaskWatchdog.start(cancelFlag);
worker = new SwingWorker<Integer, Long>() {
private ETACalculator etaCalculator;
@Override
protected Integer doInBackground() throws Exception {
final long[] count = {0};
for (String folderPath : src) {
if (cancelFlag.get()) break;
ImageExtractionService.processSource(folderPath, rootPath -> {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (cancelFlag.get()) return FileVisitResult.TERMINATE;
if (ImageExtractionService.isImageFile(file)) {
count[0]++;
}
return FileVisitResult.CONTINUE;
}
});
});
}
totalFiles = count[0];
TaskManager.setTotalFiles(totalFiles);
LogService.log("Split Images: Scan complete. Found " + totalFiles + " images to process.");
if (totalFiles == 0) return 0;
SwingUtilities.invokeLater(() -> {
if (!isProcessing.get()) return;
progressBar.setIndeterminate(false);
progressBar.setMaximum((int) Math.min(Integer.MAX_VALUE, totalFiles));
lblTotalCount.setText(String.format("%,d", totalFiles));
lblProcessedCount.setText("0");
lblProgressPercent.setText("0%");
etaCalculator = new ETACalculator(totalFiles);
etaCalculator.start();
statusDotLabel.setText("● Processing...");
final long[] lastLogTime = {0};
// Standardized dynamic 500ms telemetry update
telemetryTimer = new Timer(500, ev -> {
if (!isProcessing.get()) {
((Timer) ev.getSource()).stop();
return;
}
long processed = TaskManager.getProcessedCount();
long failed = TaskManager.getFailedCount();
long duplicates = TaskManager.getDuplicateCount();
long skipped = TaskManager.getSkippedCount();
lblProcessedCount.setText(String.format("%,d", processed));
int pct = totalFiles > 0 ? (int) Math.min(100, Math.round(((double) processed / totalFiles) * 100)) : 0;
lblProgressPercent.setText(pct + "%");
progressBar.setValue((int) Math.min(Integer.MAX_VALUE, processed));
String eta = etaCalculator.getETA(processed);
long freeMem = Runtime.getRuntime().freeMemory();
long totalMem = Runtime.getRuntime().totalMemory();
long maxMem = Runtime.getRuntime().maxMemory();
long usedMemMb = (totalMem - freeMem) / (1024 * 1024);
long maxMemMb = maxMem / (1024 * 1024);
StringBuilder sb = new StringBuilder();
sb.append("Running (");
sb.append(processed).append("/").append(totalFiles);
sb.append(" | ").append(eta);
if (failed > 0) sb.append(" | Failed: ").append(failed);
if (duplicates > 0) sb.append(" | Dups: ").append(duplicates);
if (skipped > 0) sb.append(" | Skipped: ").append(skipped);
sb.append(" | RAM: ").append(usedMemMb).append("/").append(maxMemMb).append("MB)");
execSection.setStatus(sb.toString());
String activeFile = TaskManager.getCurrentFileName();
if (activeFile != null && !activeFile.isEmpty()) {
statusDotLabel.setText("● Processing: " + activeFile);
} else {
statusDotLabel.setText("● Processing...");
}
// Log progress update every 3 seconds
long now = System.currentTimeMillis();
if (now - lastLogTime[0] >= 3000) {
lastLogTime[0] = now;
LogService.log(String.format("Split Images: Progress %d/%d files (%s | Dups: %d | Failed: %d | RAM: %d/%d MB)",
processed, totalFiles, eta, duplicates, failed, usedMemMb, maxMemMb));
}
});
telemetryTimer.start();
});
return SplitService.splitImagesStreamed(src, dest, baseName, sizeLimitBytes, isZip, p -> {}, cancelFlag);
}
@Override
protected void done() {
isProcessing.set(false);
if (telemetryTimer != null) {
telemetryTimer.stop();
}
TaskWatchdog.stop();
KeepAwakeService.stop();
try {
int batches = get();
String msg = cancelFlag.get() ? "Process stopped. " : "Success! ";
if (cancelFlag.get()) {
LogService.log("Split Images: Process stopped by user.");
} else if (batches == 0) {
LogService.log("Split Images: Process finished. No batches were created.");
} else {
LogService.log("Split Images: Process completed! Created " + batches + " image batches.");
}
JOptionPane.showMessageDialog(SplitImagesPanel.this, msg + "Created " + batches + " batches.", "Finished", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
LogService.log("Split Images Error: " + ex.getMessage());
JOptionPane.showMessageDialog(SplitImagesPanel.this, "Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} finally {
startBtn.setText("Start Process");
startBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
progressBar.setValue(0);
progressBar.setIndeterminate(false);
pauseFlag.set(false);
TaskManager.setPaused(false);
statusDotLabel.setText("● Ready");
statusDotLabel.setForeground(new Color(40, 180, 120));
execSection.setStatus("Ready");
setInputsEnabled(true);
}
}
};
worker.execute();
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\utils\ETACalculator.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.utils;
public class ETACalculator {
private final long totalItems;
private long lastTickTime;
private long lastItemsProcessed;
private double smoothedSpeed = -1; // items per ms
private static final double ALPHA = 0.05; // smoothing factor for Exponential Moving Average
private boolean started = false;
public ETACalculator(long totalItems) {
this.totalItems = totalItems;
}
public synchronized void start() {
this.lastTickTime = System.currentTimeMillis();
this.lastItemsProcessed = 0;
this.started = true;
}
public synchronized String getETA(long currentProgress) {
if (!started) return "ETA: Calculating...";
if (currentProgress <= 0 || totalItems <= 0) return "ETA: Calculating...";
long now = System.currentTimeMillis();
long elapsedMs = now - lastTickTime;
long deltaItems = currentProgress - lastItemsProcessed;
// Only update speed if enough time has passed to get a meaningful sample (e.g. 500ms)
if (elapsedMs > 500) {
double currentSpeed = (double) deltaItems / elapsedMs;
if (smoothedSpeed < 0) {
smoothedSpeed = currentSpeed; // Initialize
} else {
smoothedSpeed = (ALPHA * currentSpeed) + ((1.0 - ALPHA) * smoothedSpeed);
}
lastTickTime = now;
lastItemsProcessed = currentProgress;
}
if (smoothedSpeed <= 0) return "ETA: Calculating...";
long remainingItems = totalItems - currentProgress;
if (remainingItems <= 0) return "ETA: 0s";
long remainingMs = (long) (remainingItems / smoothedSpeed);
long seconds = remainingMs / 1000;
long hours = seconds / 3600;
long minutes = (seconds % 3600) / 60;
seconds = seconds % 60;
if (hours > 0) {
return String.format("ETA: %02dh %02dm %02ds", hours, minutes, seconds);
} else if (minutes > 0) {
return String.format("ETA: %02dm %02ds", minutes, seconds);
} else {
return String.format("ETA: %02ds", seconds);
}
}
}
[FILE_END]
==========================================================================
[FILE_START: src\main\java\com\imageextractor\utils\PathUtils.java]
==========================================================================
// Dev: Kunal Vishwas
package com.imageextractor.utils;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathUtils {
/**
* Normalizes and prepends the Windows long path prefix (\\?\) if the path exceeds
* 240 characters to bypass the 260-character MAX_PATH limit on Windows systems.
*/
public static Path normalizeLongPath(String pathStr) {
if (pathStr == null || pathStr.trim().isEmpty()) return null;
try {
// Convert to absolute path and normalize separators
Path p = Paths.get(pathStr.trim()).toAbsolutePath();
String normalized = p.toString().replace('/', '\\');
// Detect Windows OS and check if path exceeds MAX_PATH limit
if (System.getProperty("os.name", "").toLowerCase().contains("win")) {
if (normalized.length() > 240 && !normalized.startsWith("\\\\?\\")) {
if (normalized.startsWith("\\\\") && !normalized.startsWith("\\\\.\\")) {
// UNC network path conversion: \\server\share -> \\?\UNC\server\share
normalized = "\\\\?\\UNC\\" + normalized.substring(2);
} else if (!normalized.startsWith("\\\\.\\")) {
normalized = "\\\\?\\" + normalized;
}
}
}
return Paths.get(normalized);
} catch (Exception e) {
return Paths.get(pathStr.trim());
}
}
}
[FILE_END]
[BUNDLE_COMPLETE]
unpack_code.bat
Comments