seperate size
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user