seperate adjust

This commit is contained in:
Julian Freeman
2026-01-19 00:41:28 -04:00
parent de0ed2bdc2
commit 3e5d5aa848
5 changed files with 76 additions and 35 deletions

View File

@@ -106,6 +106,7 @@ struct ZcaResult {
#[derive(serde::Deserialize)]
struct ExportImageTask {
path: String,
manual_position: Option<ManualPosition>,
}
#[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<ExportImageTask>, 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<ExportImageTask>, 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))

View File

@@ -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!");

View File

@@ -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;
};

View File

@@ -78,19 +78,21 @@ const store = useGalleryStore();
<!-- Placement Mode -->
<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="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>
<div class="flex-1">
<p class="text-sm font-medium text-gray-200">Smart Auto-Placement</p>
<p class="text-xs text-gray-500">Uses ZCA algorithm for each image</p>
<p class="text-sm font-medium text-gray-200" v-if="!store.selectedImage?.manualPosition">Auto (ZCA)</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>
<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>
</div>
</div>

View File

@@ -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
};
});