seperate size

This commit is contained in:
Julian Freeman
2026-01-19 00:48:16 -04:00
parent 3e5d5aa848
commit fd90ac1df3
5 changed files with 103 additions and 26 deletions

View File

@@ -107,6 +107,8 @@ struct ZcaResult {
struct ExportImageTask { struct ExportImageTask {
path: String, path: String,
manual_position: Option<ManualPosition>, manual_position: Option<ManualPosition>,
scale: Option<f64>,
opacity: Option<f64>,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@@ -141,11 +143,7 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?; let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
let base_color = parse_hex_color(&watermark.color); let base_color = parse_hex_color(&watermark.color);
// Calculate final color with opacity // Note: opacity and final text color are now calculated per-task
// imageproc draws with the exact color given. To support opacity, we need to handle it.
// However, draw_text_mut blends. If we provide an Rgba with alpha < 255, it should blend.
let alpha = (watermark.opacity * 255.0) as u8;
let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]);
let results: Vec<Result<(), String>> = images.par_iter().map(|task| { let results: Vec<Result<(), String>> = images.par_iter().map(|task| {
let input_path = Path::new(&task.path); let input_path = Path::new(&task.path);
@@ -155,8 +153,16 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
let mut base_img = dynamic_img.to_rgba8(); let mut base_img = dynamic_img.to_rgba8();
let (width, height) = base_img.dimensions(); let (width, height) = base_img.dimensions();
// Determine effective settings (Task > Global)
let eff_scale = task.scale.unwrap_or(watermark.scale);
let eff_opacity = task.opacity.unwrap_or(watermark.opacity);
// Calculate final color with effective opacity
let alpha = (eff_opacity * 255.0) as u8;
let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]);
// 1. Calculate Font Scale based on Image Height // 1. Calculate Font Scale based on Image Height
let mut scale_px = height as f32 * watermark.scale as f32; let mut scale_px = height as f32 * eff_scale as f32;
// 2. Measure Text // 2. Measure Text
let scaled_font = PxScale::from(scale_px); let scaled_font = PxScale::from(scale_px);
@@ -207,6 +213,7 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
y = y.max(0); y = y.max(0);
// 6. Draw Stroke (Simple 4-direction offset for black outline) // 6. Draw Stroke (Simple 4-direction offset for black outline)
// Stroke alpha should match text alpha
let stroke_color = image::Rgba([0, 0, 0, text_color[3]]); let stroke_color = image::Rgba([0, 0, 0, text_color[3]]);
for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] { for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] {
draw_text_mut( draw_text_mut(

View File

@@ -47,10 +47,12 @@ async function exportBatch() {
if (outputDir && typeof outputDir === 'string') { if (outputDir && typeof outputDir === 'string') {
isExporting.value = true; isExporting.value = true;
// Map images to include manual position // Map images to include manual settings
const exportTasks = store.images.map(img => ({ const exportTasks = store.images.map(img => ({
path: img.path, path: img.path,
manual_position: img.manualPosition || null manual_position: img.manualPosition || null,
scale: img.scale || null,
opacity: img.opacity || null
})); }));
// Pass dummy globals for rust struct compatibility // Pass dummy globals for rust struct compatibility

View File

@@ -64,7 +64,7 @@ watch(() => store.selectedImage, () => {
nextTick(() => calculateLayout()); nextTick(() => calculateLayout());
}); });
// Use either manual position (if specific image has it) or ZCA suggestion // Use either manual position (if override is true) or ZCA suggestion
const position = computed(() => { const position = computed(() => {
if (store.selectedImage?.manualPosition) { if (store.selectedImage?.manualPosition) {
return store.selectedImage.manualPosition; return store.selectedImage.manualPosition;
@@ -73,6 +73,9 @@ const position = computed(() => {
return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.97 }; return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.97 };
}); });
const effectiveScale = computed(() => store.selectedImage?.scale ?? store.watermarkSettings.scale);
const effectiveOpacity = computed(() => store.selectedImage?.opacity ?? store.watermarkSettings.opacity);
const onMouseDown = (e: MouseEvent) => { const onMouseDown = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
isDragging.value = true; isDragging.value = true;
@@ -155,10 +158,10 @@ const onMouseLeave = () => {
left: (position.x * 100) + '%', left: (position.x * 100) + '%',
top: (position.y * 100) + '%', top: (position.y * 100) + '%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
opacity: store.watermarkSettings.opacity, opacity: effectiveOpacity,
color: store.watermarkSettings.color, color: store.watermarkSettings.color,
/* Scale based on HEIGHT of the IMAGE */ /* Scale based on HEIGHT of the IMAGE */
fontSize: (imageRect.height * store.watermarkSettings.scale) + 'px', fontSize: (imageRect.height * effectiveScale) + 'px',
height: '0px', height: '0px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@@ -1,15 +1,54 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGalleryStore } from "../stores/gallery"; import { useGalleryStore } from "../stores/gallery";
import { Settings, CheckSquare, Type, Palette } from 'lucide-vue-next'; import { Settings, CheckSquare, Type, Palette, Copy } from 'lucide-vue-next';
import { computed } from "vue";
const store = useGalleryStore(); const store = useGalleryStore();
// Computed properties to handle "Get from Image OR Global" and "Set to Image" logic
const currentScale = computed({
get: () => store.selectedImage?.scale ?? store.watermarkSettings.scale,
set: (val) => {
if (store.selectedIndex >= 0) {
store.setImageSetting(store.selectedIndex, 'scale', val);
} else {
store.updateWatermarkSettings({ scale: val });
}
}
});
const currentOpacity = computed({
get: () => store.selectedImage?.opacity ?? store.watermarkSettings.opacity,
set: (val) => {
if (store.selectedIndex >= 0) {
store.setImageSetting(store.selectedIndex, 'opacity', val);
} else {
store.updateWatermarkSettings({ opacity: val });
}
}
});
const applyAll = () => {
if (confirm("Apply current size and opacity settings to ALL images? This will reset individual adjustments.")) {
store.applySettingsToAll();
}
};
</script> </script>
<template> <template>
<div class="h-full bg-gray-800 text-white p-4 flex flex-col gap-6 overflow-y-auto border-l border-gray-700 w-80"> <div class="h-full bg-gray-800 text-white p-4 flex flex-col gap-6 overflow-y-auto border-l border-gray-700 w-80">
<h2 class="text-lg font-bold flex items-center gap-2"> <h2 class="text-lg font-bold flex items-center justify-between">
<Settings class="w-5 h-5" /> <div class="flex items-center gap-2">
Watermark Settings <Settings class="w-5 h-5" />
Settings
</div>
<button
@click="applyAll"
title="Apply Size & Opacity to All Images"
class="bg-gray-700 hover:bg-gray-600 p-1.5 rounded text-xs text-blue-300 transition-colors flex items-center gap-1"
>
<Copy class="w-3 h-3" /> All
</button>
</h2> </h2>
<!-- Text Input --> <!-- Text Input -->
@@ -45,15 +84,14 @@ const store = useGalleryStore();
<div> <div>
<div class="flex justify-between mb-1"> <div class="flex justify-between mb-1">
<label class="text-xs text-gray-400">Size (Scale)</label> <label class="text-xs text-gray-400">Size (Scale)</label>
<span class="text-xs text-gray-300">{{ (store.watermarkSettings.scale * 100).toFixed(1) }}%</span> <span class="text-xs text-gray-300">{{ (currentScale * 100).toFixed(1) }}%</span>
</div> </div>
<input <input
type="range" type="range"
min="0.01" min="0.01"
max="0.20" max="0.20"
step="0.001" step="0.001"
:value="store.watermarkSettings.scale" v-model.number="currentScale"
@input="e => store.updateWatermarkSettings({ scale: parseFloat((e.target as HTMLInputElement).value) })"
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500" class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
/> />
<p class="text-[10px] text-gray-500 mt-1">Relative to image height</p> <p class="text-[10px] text-gray-500 mt-1">Relative to image height</p>
@@ -62,15 +100,14 @@ const store = useGalleryStore();
<div> <div>
<div class="flex justify-between mb-1"> <div class="flex justify-between mb-1">
<label class="text-xs text-gray-400">Opacity</label> <label class="text-xs text-gray-400">Opacity</label>
<span class="text-xs text-gray-300">{{ (store.watermarkSettings.opacity * 100).toFixed(0) }}%</span> <span class="text-xs text-gray-300">{{ (currentOpacity * 100).toFixed(0) }}%</span>
</div> </div>
<input <input
type="range" type="range"
min="0.1" min="0.1"
max="1.0" max="1.0"
step="0.01" step="0.01"
:value="store.watermarkSettings.opacity" v-model.number="currentOpacity"
@input="e => store.updateWatermarkSettings({ opacity: parseFloat((e.target as HTMLInputElement).value) })"
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500" class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
/> />
</div> </div>

View File

@@ -4,12 +4,14 @@ import { invoke } from "@tauri-apps/api/core";
export interface ImageItem { export interface ImageItem {
path: string; path: string;
thumbnail: string; // Now mandatory from backend thumbnail: string;
name: string; name: string;
width?: number; width?: number;
height?: number; height?: number;
zcaSuggestion?: { x: number; y: number; zone: string }; zcaSuggestion?: { x: number; y: number; zone: string };
manualPosition?: { x: number; y: number }; manualPosition?: { x: number; y: number };
scale?: number;
opacity?: number;
} }
export interface WatermarkSettings { export interface WatermarkSettings {
@@ -18,9 +20,6 @@ export interface WatermarkSettings {
color: string; color: string;
opacity: number; opacity: number;
scale: 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", () => { export const useGalleryStore = defineStore("gallery", () => {
@@ -56,6 +55,33 @@ export const useGalleryStore = defineStore("gallery", () => {
} }
} }
function setImageSetting(index: number, setting: 'scale' | 'opacity', value: number) {
if (images.value[index]) {
images.value[index][setting] = value;
}
}
// Applies the settings from the CURRENT image (or global if not overridden) to ALL images
// Strategy: Update Global Settings to match current view, and clear individual overrides so everyone follows global.
function applySettingsToAll() {
const current = selectedImage.value;
if (!current) return;
const newScale = current.scale ?? watermarkSettings.value.scale;
const newOpacity = current.opacity ?? watermarkSettings.value.opacity;
// 1. Update Global
watermarkSettings.value.scale = newScale;
watermarkSettings.value.opacity = newOpacity;
// 2. Clear overrides for Scale and Opacity on ALL images
// (We keep manualPosition because position is usually unique per image content)
images.value.forEach(img => {
img.scale = undefined;
img.opacity = undefined;
});
}
async function selectImage(index: number) { async function selectImage(index: number) {
if (index < 0 || index >= images.value.length) return; if (index < 0 || index >= images.value.length) return;
selectedIndex.value = index; selectedIndex.value = index;
@@ -79,6 +105,8 @@ export const useGalleryStore = defineStore("gallery", () => {
setImages, setImages,
selectImage, selectImage,
updateWatermarkSettings, updateWatermarkSettings,
setImageManualPosition setImageManualPosition,
setImageSetting,
applySettingsToAll
}; };
}); });