diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0fd851c..dab387b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -106,6 +106,7 @@ struct ZcaResult { #[derive(serde::Deserialize)] struct ExportImageTask { path: String, + manual_position: Option, } #[derive(serde::Deserialize)] @@ -116,8 +117,9 @@ struct WatermarkSettings { color: String, // Hex code e.g. "#FFFFFF" opacity: f64, scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height) - manual_override: bool, - manual_position: ManualPosition, + // Global manual override is deprecated in favor of per-task control, but kept for struct compatibility if needed + _manual_override: bool, + _manual_position: ManualPosition, } #[derive(serde::Deserialize)] @@ -169,9 +171,10 @@ async fn export_batch(images: Vec, watermark: WatermarkSettings let final_scale = PxScale::from(scale_px); let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text); - // 4. Determine Position (Center based) - let (pos_x_pct, pos_y_pct) = if watermark.manual_override { - (watermark.manual_position.x, watermark.manual_position.y) + // 4. Determine Position (Task Specific > ZCA) + // If task has manual_position, use it. Otherwise calculate ZCA. + let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position { + (pos.x, pos.y) } else { match calculate_zca_internal(&dynamic_img) { Ok(res) => (res.x, res.y), @@ -231,7 +234,21 @@ async fn export_batch(images: Vec, watermark: WatermarkSettings // Save let file_name = input_path.file_name().unwrap_or_default(); let output_path = Path::new(&output_dir).join(file_name); - base_img.save(output_path).map_err(|e| e.to_string())?; + + // Handle format specific saving + // JPEG does not support Alpha channel. If we save Rgba8 to Jpeg, it might fail or look wrong. + let ext = output_path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase(); + if ext == "jpg" || ext == "jpeg" { + // Convert to RGB8 (dropping alpha) + // Note: This simply drops alpha. If background was transparent, it becomes black. + // For photos (JPEGs) this is usually fine as they don't have alpha. + let rgb_img = image::DynamicImage::ImageRgba8(base_img).to_rgb8(); + rgb_img.save(&output_path).map_err(|e| e.to_string())?; + } else { + // For PNG/WebP etc, keep RGBA + base_img.save(&output_path).map_err(|e| e.to_string())?; + } + Ok(()) } else { Err(format!("Failed to open {}", task.path)) diff --git a/src/App.vue b/src/App.vue index a94649b..15b1703 100644 --- a/src/App.vue +++ b/src/App.vue @@ -46,9 +46,24 @@ async function exportBatch() { if (outputDir && typeof outputDir === 'string') { isExporting.value = true; + + // Map images to include manual position + const exportTasks = store.images.map(img => ({ + path: img.path, + manual_position: img.manualPosition || null + })); + + // Pass dummy globals for rust struct compatibility + // The backend struct fields are named _manual_override and _manual_position + const rustWatermarkSettings = { + ...store.watermarkSettings, + _manual_override: false, + _manual_position: { x: 0.5, y: 0.5 } + }; + await invoke('export_batch', { - images: store.images, - watermark: store.watermarkSettings, + images: exportTasks, + watermark: rustWatermarkSettings, outputDir: outputDir }); alert("Batch export completed!"); diff --git a/src/components/HeroView.vue b/src/components/HeroView.vue index 32ff1e6..98cd75f 100644 --- a/src/components/HeroView.vue +++ b/src/components/HeroView.vue @@ -64,12 +64,13 @@ watch(() => store.selectedImage, () => { nextTick(() => calculateLayout()); }); -// Use either manual position (if override is true) or ZCA suggestion +// Use either manual position (if specific image has it) or ZCA suggestion const position = computed(() => { - if (store.watermarkSettings.manual_override) { - return store.watermarkSettings.manual_position; + if (store.selectedImage?.manualPosition) { + return store.selectedImage.manualPosition; } - return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.99 }; + // Default to bottom center if no ZCA + return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.97 }; }); const onMouseDown = (e: MouseEvent) => { @@ -80,9 +81,10 @@ const onMouseDown = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => { // Need exact dimensions for drag calculation + // Use imageRect for calculation as it matches the render size if (!isDragging.value || !store.selectedImage || imageRect.value.width === 0) return; - const rect = imageRect.value; // Use our calculated rect, not bounding client rect (though they should match) + const rect = imageRect.value; const deltaX = (e.clientX - dragStart.value.x) / rect.width; const deltaY = (e.clientY - dragStart.value.y) / rect.height; @@ -91,20 +93,18 @@ const onMouseMove = (e: MouseEvent) => { let newX = position.value.x + deltaX; let newY = position.value.y + deltaY; - // Clamp logic (0-1) - This is now strictly inside the image content area + // Clamp logic const padding = 0.005; newX = Math.max(padding, Math.min(1 - padding, newX)); newY = Math.max(padding, Math.min(1 - padding, newY)); - // Set store to manual mode immediately - store.updateWatermarkSettings({ - manual_override: true, - manual_position: { x: newX, y: newY } - }); + // Set ONLY for this image + store.setImageManualPosition(store.selectedIndex, newX, newY); dragStart.value = { x: e.clientX, y: e.clientY }; }; + const onMouseUp = () => { isDragging.value = false; }; diff --git a/src/components/SettingsPanel.vue b/src/components/SettingsPanel.vue index eba9d0f..ec38e0e 100644 --- a/src/components/SettingsPanel.vue +++ b/src/components/SettingsPanel.vue @@ -78,19 +78,21 @@ const store = useGalleryStore();
- +
- +
-

Smart Auto-Placement

-

Uses ZCA algorithm for each image

+

Auto (ZCA)

+

Manual Override

+

Using smart algorithm

+

Specific position set

- * Dragging the watermark in the preview will enable Manual Override for all images. + * Drag the watermark on the image to set a manual position for that specific image.

diff --git a/src/stores/gallery.ts b/src/stores/gallery.ts index 16fbee2..48cbddc 100644 --- a/src/stores/gallery.ts +++ b/src/stores/gallery.ts @@ -9,16 +9,18 @@ export interface ImageItem { width?: number; height?: number; zcaSuggestion?: { x: number; y: number; zone: string }; + manualPosition?: { x: number; y: number }; } export interface WatermarkSettings { - type: 'text'; // Fixed to text + type: 'text'; text: string; - color: string; // Hex - opacity: number; // 0-1 - scale: number; // 0.01 - 0.5 (relative to image height) - manual_override: boolean; - manual_position: { x: number, y: number }; + color: string; + opacity: number; + scale: number; + // Global override removed/deprecated in logic, but kept in type if needed for other things? + // Let's keep it clean: no global override flag anymore for logic. + // But backend struct expects it. We can just ignore it or send dummy. } export const useGalleryStore = defineStore("gallery", () => { @@ -28,10 +30,8 @@ export const useGalleryStore = defineStore("gallery", () => { type: 'text', text: 'Watermark', color: '#FFFFFF', - opacity: 0.8, - scale: 0.03, // 3% of height default - manual_override: false, - manual_position: { x: 0.5, y: 0.97 } // 3% from bottom + opacity: 1.0, + scale: 0.03, }); const selectedImage = computed(() => { @@ -50,6 +50,12 @@ export const useGalleryStore = defineStore("gallery", () => { watermarkSettings.value = { ...watermarkSettings.value, ...settings }; } + function setImageManualPosition(index: number, x: number, y: number) { + if (images.value[index]) { + images.value[index].manualPosition = { x, y }; + } + } + async function selectImage(index: number) { if (index < 0 || index >= images.value.length) return; selectedIndex.value = index; @@ -72,6 +78,7 @@ export const useGalleryStore = defineStore("gallery", () => { watermarkSettings, setImages, selectImage, - updateWatermarkSettings + updateWatermarkSettings, + setImageManualPosition }; });