seperate size
This commit is contained in:
@@ -107,6 +107,8 @@ struct ZcaResult {
|
||||
struct ExportImageTask {
|
||||
path: String,
|
||||
manual_position: Option<ManualPosition>,
|
||||
scale: Option<f64>,
|
||||
opacity: Option<f64>,
|
||||
}
|
||||
|
||||
#[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 base_color = parse_hex_color(&watermark.color);
|
||||
|
||||
// Calculate final color with opacity
|
||||
// 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]);
|
||||
// Note: opacity and final text color are now calculated per-task
|
||||
|
||||
let results: Vec<Result<(), String>> = images.par_iter().map(|task| {
|
||||
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 (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
|
||||
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
|
||||
let scaled_font = PxScale::from(scale_px);
|
||||
@@ -207,6 +213,7 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
|
||||
y = y.max(0);
|
||||
|
||||
// 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]]);
|
||||
for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] {
|
||||
draw_text_mut(
|
||||
|
||||
@@ -47,10 +47,12 @@ async function exportBatch() {
|
||||
if (outputDir && typeof outputDir === 'string') {
|
||||
isExporting.value = true;
|
||||
|
||||
// Map images to include manual position
|
||||
// Map images to include manual settings
|
||||
const exportTasks = store.images.map(img => ({
|
||||
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
|
||||
|
||||
@@ -64,7 +64,7 @@ watch(() => store.selectedImage, () => {
|
||||
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(() => {
|
||||
if (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 };
|
||||
});
|
||||
|
||||
const effectiveScale = computed(() => store.selectedImage?.scale ?? store.watermarkSettings.scale);
|
||||
const effectiveOpacity = computed(() => store.selectedImage?.opacity ?? store.watermarkSettings.opacity);
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
isDragging.value = true;
|
||||
@@ -155,10 +158,10 @@ const onMouseLeave = () => {
|
||||
left: (position.x * 100) + '%',
|
||||
top: (position.y * 100) + '%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
opacity: store.watermarkSettings.opacity,
|
||||
opacity: effectiveOpacity,
|
||||
color: store.watermarkSettings.color,
|
||||
/* Scale based on HEIGHT of the IMAGE */
|
||||
fontSize: (imageRect.height * store.watermarkSettings.scale) + 'px',
|
||||
fontSize: (imageRect.height * effectiveScale) + 'px',
|
||||
height: '0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,15 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
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();
|
||||
|
||||
// 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>
|
||||
|
||||
<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">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2">
|
||||
<Settings class="w-5 h-5" />
|
||||
Watermark Settings
|
||||
<h2 class="text-lg font-bold flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
|
||||
<!-- Text Input -->
|
||||
@@ -45,15 +84,14 @@ const store = useGalleryStore();
|
||||
<div>
|
||||
<div class="flex justify-between mb-1">
|
||||
<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>
|
||||
<input
|
||||
type="range"
|
||||
min="0.01"
|
||||
max="0.20"
|
||||
step="0.001"
|
||||
:value="store.watermarkSettings.scale"
|
||||
@input="e => store.updateWatermarkSettings({ scale: parseFloat((e.target as HTMLInputElement).value) })"
|
||||
v-model.number="currentScale"
|
||||
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>
|
||||
@@ -62,15 +100,14 @@ const store = useGalleryStore();
|
||||
<div>
|
||||
<div class="flex justify-between mb-1">
|
||||
<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>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1.0"
|
||||
step="0.01"
|
||||
:value="store.watermarkSettings.opacity"
|
||||
@input="e => store.updateWatermarkSettings({ opacity: parseFloat((e.target as HTMLInputElement).value) })"
|
||||
v-model.number="currentOpacity"
|
||||
class="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,14 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface ImageItem {
|
||||
path: string;
|
||||
thumbnail: string; // Now mandatory from backend
|
||||
thumbnail: string;
|
||||
name: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
zcaSuggestion?: { x: number; y: number; zone: string };
|
||||
manualPosition?: { x: number; y: number };
|
||||
scale?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export interface WatermarkSettings {
|
||||
@@ -18,9 +20,6 @@ export interface WatermarkSettings {
|
||||
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", () => {
|
||||
@@ -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) {
|
||||
if (index < 0 || index >= images.value.length) return;
|
||||
selectedIndex.value = index;
|
||||
@@ -79,6 +105,8 @@ export const useGalleryStore = defineStore("gallery", () => {
|
||||
setImages,
|
||||
selectImage,
|
||||
updateWatermarkSettings,
|
||||
setImageManualPosition
|
||||
setImageManualPosition,
|
||||
setImageSetting,
|
||||
applySettingsToAll
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user