seperate adjust
This commit is contained in:
@@ -106,6 +106,7 @@ struct ZcaResult {
|
|||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct ExportImageTask {
|
struct ExportImageTask {
|
||||||
path: String,
|
path: String,
|
||||||
|
manual_position: Option<ManualPosition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@@ -116,8 +117,9 @@ struct WatermarkSettings {
|
|||||||
color: String, // Hex code e.g. "#FFFFFF"
|
color: String, // Hex code e.g. "#FFFFFF"
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height)
|
scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height)
|
||||||
manual_override: bool,
|
// Global manual override is deprecated in favor of per-task control, but kept for struct compatibility if needed
|
||||||
manual_position: ManualPosition,
|
_manual_override: bool,
|
||||||
|
_manual_position: ManualPosition,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@@ -169,9 +171,10 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
|
|||||||
let final_scale = PxScale::from(scale_px);
|
let final_scale = PxScale::from(scale_px);
|
||||||
let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text);
|
let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text);
|
||||||
|
|
||||||
// 4. Determine Position (Center based)
|
// 4. Determine Position (Task Specific > ZCA)
|
||||||
let (pos_x_pct, pos_y_pct) = if watermark.manual_override {
|
// If task has manual_position, use it. Otherwise calculate ZCA.
|
||||||
(watermark.manual_position.x, watermark.manual_position.y)
|
let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position {
|
||||||
|
(pos.x, pos.y)
|
||||||
} else {
|
} else {
|
||||||
match calculate_zca_internal(&dynamic_img) {
|
match calculate_zca_internal(&dynamic_img) {
|
||||||
Ok(res) => (res.x, res.y),
|
Ok(res) => (res.x, res.y),
|
||||||
@@ -231,7 +234,21 @@ async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings
|
|||||||
// Save
|
// Save
|
||||||
let file_name = input_path.file_name().unwrap_or_default();
|
let file_name = input_path.file_name().unwrap_or_default();
|
||||||
let output_path = Path::new(&output_dir).join(file_name);
|
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(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Failed to open {}", task.path))
|
Err(format!("Failed to open {}", task.path))
|
||||||
|
|||||||
19
src/App.vue
19
src/App.vue
@@ -46,9 +46,24 @@ 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
|
||||||
|
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', {
|
await invoke('export_batch', {
|
||||||
images: store.images,
|
images: exportTasks,
|
||||||
watermark: store.watermarkSettings,
|
watermark: rustWatermarkSettings,
|
||||||
outputDir: outputDir
|
outputDir: outputDir
|
||||||
});
|
});
|
||||||
alert("Batch export completed!");
|
alert("Batch export completed!");
|
||||||
|
|||||||
@@ -64,12 +64,13 @@ watch(() => store.selectedImage, () => {
|
|||||||
nextTick(() => calculateLayout());
|
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(() => {
|
const position = computed(() => {
|
||||||
if (store.watermarkSettings.manual_override) {
|
if (store.selectedImage?.manualPosition) {
|
||||||
return store.watermarkSettings.manual_position;
|
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) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
@@ -80,9 +81,10 @@ const onMouseDown = (e: MouseEvent) => {
|
|||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
// Need exact dimensions for drag calculation
|
// 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;
|
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 deltaX = (e.clientX - dragStart.value.x) / rect.width;
|
||||||
const deltaY = (e.clientY - dragStart.value.y) / rect.height;
|
const deltaY = (e.clientY - dragStart.value.y) / rect.height;
|
||||||
@@ -91,20 +93,18 @@ const onMouseMove = (e: MouseEvent) => {
|
|||||||
let newX = position.value.x + deltaX;
|
let newX = position.value.x + deltaX;
|
||||||
let newY = position.value.y + deltaY;
|
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;
|
const padding = 0.005;
|
||||||
newX = Math.max(padding, Math.min(1 - padding, newX));
|
newX = Math.max(padding, Math.min(1 - padding, newX));
|
||||||
newY = Math.max(padding, Math.min(1 - padding, newY));
|
newY = Math.max(padding, Math.min(1 - padding, newY));
|
||||||
|
|
||||||
// Set store to manual mode immediately
|
// Set ONLY for this image
|
||||||
store.updateWatermarkSettings({
|
store.setImageManualPosition(store.selectedIndex, newX, newY);
|
||||||
manual_override: true,
|
|
||||||
manual_position: { x: newX, y: newY }
|
|
||||||
});
|
|
||||||
|
|
||||||
dragStart.value = { x: e.clientX, y: e.clientY };
|
dragStart.value = { x: e.clientX, y: e.clientY };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,19 +78,21 @@ const store = useGalleryStore();
|
|||||||
|
|
||||||
<!-- Placement Mode -->
|
<!-- Placement Mode -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">Placement</label>
|
<label class="text-sm text-gray-400 uppercase tracking-wider font-semibold">Placement Status</label>
|
||||||
<div class="flex items-center gap-2 p-2 rounded bg-gray-700/50 border border-gray-600">
|
<div class="flex items-center gap-2 p-2 rounded bg-gray-700/50 border border-gray-600">
|
||||||
<div class="p-1 rounded bg-green-500/20 text-green-400">
|
<div class="p-1 rounded bg-green-500/20 text-green-400">
|
||||||
<CheckSquare class="w-4 h-4" v-if="!store.watermarkSettings.manual_override" />
|
<CheckSquare class="w-4 h-4" v-if="!store.selectedImage?.manualPosition" />
|
||||||
<div class="w-4 h-4" v-else></div>
|
<div class="w-4 h-4" v-else></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-gray-200">Smart Auto-Placement</p>
|
<p class="text-sm font-medium text-gray-200" v-if="!store.selectedImage?.manualPosition">Auto (ZCA)</p>
|
||||||
<p class="text-xs text-gray-500">Uses ZCA algorithm for each image</p>
|
<p class="text-sm font-medium text-blue-300" v-else>Manual Override</p>
|
||||||
|
<p class="text-xs text-gray-500" v-if="!store.selectedImage?.manualPosition">Using smart algorithm</p>
|
||||||
|
<p class="text-xs text-gray-500" v-else>Specific position set</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1 italic">
|
<p class="text-xs text-gray-500 mt-1 italic">
|
||||||
* 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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ export interface ImageItem {
|
|||||||
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WatermarkSettings {
|
export interface WatermarkSettings {
|
||||||
type: 'text'; // Fixed to text
|
type: 'text';
|
||||||
text: string;
|
text: string;
|
||||||
color: string; // Hex
|
color: string;
|
||||||
opacity: number; // 0-1
|
opacity: number;
|
||||||
scale: number; // 0.01 - 0.5 (relative to image height)
|
scale: number;
|
||||||
manual_override: boolean;
|
// Global override removed/deprecated in logic, but kept in type if needed for other things?
|
||||||
manual_position: { x: number, y: number };
|
// 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", () => {
|
||||||
@@ -28,10 +30,8 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'Watermark',
|
text: 'Watermark',
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
opacity: 0.8,
|
opacity: 1.0,
|
||||||
scale: 0.03, // 3% of height default
|
scale: 0.03,
|
||||||
manual_override: false,
|
|
||||||
manual_position: { x: 0.5, y: 0.97 } // 3% from bottom
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedImage = computed(() => {
|
const selectedImage = computed(() => {
|
||||||
@@ -50,6 +50,12 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
watermarkSettings.value = { ...watermarkSettings.value, ...settings };
|
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) {
|
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;
|
||||||
@@ -72,6 +78,7 @@ export const useGalleryStore = defineStore("gallery", () => {
|
|||||||
watermarkSettings,
|
watermarkSettings,
|
||||||
setImages,
|
setImages,
|
||||||
selectImage,
|
selectImage,
|
||||||
updateWatermarkSettings
|
updateWatermarkSettings,
|
||||||
|
setImageManualPosition
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user