From 612066eb1d14b972011504859470210903849c72 Mon Sep 17 00:00:00 2001 From: hardikjumnani Date: Tue, 7 Jan 2025 11:54:57 +0530 Subject: [PATCH] addded a new image compression utility to reduce the size of images before uploading them to the server --- frontend/src-tauri/Cargo.lock | 198 +++++++++++++++++++- frontend/src-tauri/Cargo.toml | 3 + frontend/src-tauri/src/commands/image.rs | 137 ++++++++++++++ frontend/src-tauri/src/commands/mod.rs | 1 + frontend/src-tauri/src/main.rs | 6 +- frontend/src/components/Media/MediaView.tsx | 36 +++- frontend/src/pages/Dashboard/Dashboard.tsx | 8 +- frontend/src/utils/imageCompression.ts | 56 ++++++ 8 files changed, 438 insertions(+), 7 deletions(-) create mode 100644 frontend/src-tauri/src/commands/image.rs create mode 100644 frontend/src-tauri/src/commands/mod.rs create mode 100644 frontend/src/utils/imageCompression.ts diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 768ad00c..84ccda91 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Pictopy" @@ -8,6 +8,8 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", + "image", + "serde", "serde_json", "tauri", "tauri-build", @@ -15,6 +17,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-shell", "tauri-plugin-store", + "urlencoding", "walkdir", ] @@ -33,6 +36,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -260,7 +269,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.3", "object", "rustc-demangle", ] @@ -277,6 +286,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -534,6 +549,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -626,12 +647,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -844,6 +890,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "embed-resource" version = "2.4.2" @@ -947,6 +999,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.2", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -979,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.3", ] [[package]] @@ -1272,6 +1339,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.29.0" @@ -1426,6 +1503,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1616,6 +1703,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1738,6 +1843,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1788,6 +1902,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1956,6 +2076,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -2565,7 +2694,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.3", ] [[package]] @@ -2660,6 +2789,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -2771,6 +2909,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -3805,6 +3963,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -4158,6 +4327,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.2.0" @@ -4424,6 +4599,12 @@ dependencies = [ "windows-core 0.57.0", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -4904,6 +5085,15 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 09b38712..03a43f0f 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -20,6 +20,9 @@ tauri-plugin-shell = "2.0.0-beta.5" tauri-plugin-dialog = "2.0.0-beta.9" tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } chrono = "0.4" +image = "0.24" +urlencoding = "2.1.0" +serde = { version = "1.0", features = ["derive"] } [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/frontend/src-tauri/src/commands/image.rs b/frontend/src-tauri/src/commands/image.rs new file mode 100644 index 00000000..0571d12f --- /dev/null +++ b/frontend/src-tauri/src/commands/image.rs @@ -0,0 +1,137 @@ +use image::{ImageFormat, imageops::FilterType}; +use std::path::Path; +use tauri::command; +use urlencoding::decode; + +#[derive(serde::Deserialize)] +pub struct CompressionOptions { + max_width: Option, + max_height: Option, + quality: Option, + maintain_aspect_ratio: Option, +} + +#[command] +pub async fn compress_image( + path: String, + options: Option +) -> Result { + println!("Starting compression for path: {}", path); + + // Use default options if none provided + let options = options.unwrap_or(CompressionOptions { + max_width: Some(1920), + max_height: Some(1080), + quality: Some(80), + maintain_aspect_ratio: Some(true), + }); + + // Decode the URL-encoded path and remove the asset:// protocol + let decoded_path = decode(&path) + .map_err(|e| format!("Failed to decode path: {}", e))? + .into_owned() + .replace("asset://localhost", ""); + + println!("Decoded path: {}", decoded_path); + + // Validate input path + let input_path = Path::new(&decoded_path); + if !input_path.exists() { + return Err(format!("File not found: {}", decoded_path)); + } + + // Get file components + let file_stem = input_path + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| "Invalid filename".to_string())?; + + let extension = input_path + .extension() + .and_then(|s| s.to_str()) + .map(|s| s.to_lowercase()) + .ok_or_else(|| "Invalid file extension".to_string())?; + + // Create output path in the same directory + let output_path = input_path.with_file_name(format!("{}_compressed.{}", file_stem, extension)); + println!("Output path: {}", output_path.display()); + + // Load image + let img = image::open(&decoded_path) + .map_err(|e| format!("Failed to open image: {}", e))?; + + println!("Original dimensions: {}x{}", img.width(), img.height()); + + // Calculate new dimensions + let (new_width, new_height) = if options.maintain_aspect_ratio.unwrap_or(true) { + let aspect_ratio = img.width() as f32 / img.height() as f32; + let max_width = options.max_width.unwrap_or(1920) as f32; + let max_height = options.max_height.unwrap_or(1080) as f32; + + if img.width() > options.max_width.unwrap_or(1920) || + img.height() > options.max_height.unwrap_or(1080) { + if aspect_ratio > max_width / max_height { + let new_width = max_width; + let new_height = new_width / aspect_ratio; + (new_width as u32, new_height as u32) + } else { + let new_height = max_height; + let new_width = new_height * aspect_ratio; + (new_width as u32, new_height as u32) + } + } else { + (img.width(), img.height()) + } + } else { + ( + options.max_width.unwrap_or(img.width()), + options.max_height.unwrap_or(img.height()) + ) + }; + + // Resize image if needed + let resized = if new_width != img.width() || new_height != img.height() { + println!("Resizing to: {}x{}", new_width, new_height); + img.resize_exact(new_width, new_height, FilterType::Lanczos3) + } else { + println!("No resizing needed"); + img + }; + + // Save with format-specific handling + let output_path_str = output_path + .to_str() + .ok_or_else(|| "Invalid output path".to_string())? + .to_string(); + + match extension.as_str() { + "jpg" | "jpeg" => { + let mut output = resized.into_rgb8(); + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality( + std::fs::File::create(&output_path) + .map_err(|e| format!("Failed to create output file: {}", e))?, + options.quality.unwrap_or(80) + ); + encoder.encode( + output.as_raw(), + new_width, + new_height, + image::ColorType::Rgb8 + ).map_err(|e| format!("Failed to encode JPEG: {}", e))?; + } + "png" => { + resized + .save_with_format(&output_path, ImageFormat::Png) + .map_err(|e| format!("Failed to save PNG: {}", e))?; + } + "webp" => { + resized + .save_with_format(&output_path, ImageFormat::WebP) + .map_err(|e| format!("Failed to save WebP: {}", e))?; + } + _ => return Err(format!("Unsupported format: {}", extension)), + } + + println!("Image successfully compressed and saved to: {}", output_path_str); + Ok(output_path_str) +} \ No newline at end of file diff --git a/frontend/src-tauri/src/commands/mod.rs b/frontend/src-tauri/src/commands/mod.rs new file mode 100644 index 00000000..b1b6b4f0 --- /dev/null +++ b/frontend/src-tauri/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod image; \ No newline at end of file diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index c07f3fa6..303adc42 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -7,8 +7,10 @@ mod repositories; mod services; mod models; mod utils; +mod commands; use crate::services::{FileService, CacheService}; +use crate::commands::image::compress_image; fn main() { tauri::Builder::default() @@ -28,7 +30,9 @@ fn main() { services::get_all_images_with_cache, services::get_all_videos_with_cache, services::delete_cache, + compress_image ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} \ No newline at end of file +} + diff --git a/frontend/src/components/Media/MediaView.tsx b/frontend/src/components/Media/MediaView.tsx index d53322ed..fb252beb 100644 --- a/frontend/src/components/Media/MediaView.tsx +++ b/frontend/src/components/Media/MediaView.tsx @@ -1,6 +1,7 @@ import { MediaViewProps } from '@/types/Media'; import React, { useEffect, useState } from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; +import { compressImage } from '@/utils/imageCompression'; const MediaView: React.FC = ({ initialIndex, @@ -17,6 +18,8 @@ const MediaView: React.FC = ({ const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [isCompressing, setIsCompressing] = useState(false); + const [compressionError, setCompressionError] = useState(null); useEffect(() => { setGlobalIndex((currentPage - 1) * itemsPerPage + initialIndex); @@ -37,6 +40,17 @@ const MediaView: React.FC = ({ return () => window.removeEventListener('keydown', handleKeyDown); }, [globalIndex, onClose]); + const handleCompression = async () => { + setIsCompressing(true); + try { + await compressImage(allMedia[globalIndex]); + setIsCompressing(false); + } catch (error) { + setCompressionError('Failed to compress image'); + setIsCompressing(false); + } + }; + const handelZoomOut = () => { setScale((s) => Math.max(0.1, s - 0.1)); }; @@ -152,6 +166,20 @@ const MediaView: React.FC = ({ > + + ) : ( @@ -163,6 +191,12 @@ const MediaView: React.FC = ({ /> )} + {compressionError && ( +
+ {compressionError} +
+ )} + {globalIndex > 0 && (