forked from zguiy/utils
工具完成
This commit is contained in:
773
src/components/tools/ImageWatermark.vue
Normal file
773
src/components/tools/ImageWatermark.vue
Normal file
@ -0,0 +1,773 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 工具栏 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="triggerFileInput"
|
||||
class="btn-primary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'upload']" class="mr-2" />
|
||||
上传图片
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="addTextWatermark"
|
||||
:disabled="!originalImage"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'font']" class="mr-2" />
|
||||
文字水印
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="triggerWatermarkInput"
|
||||
:disabled="!originalImage"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'image']" class="mr-2" />
|
||||
图片水印
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="downloadImage"
|
||||
:disabled="!processedImage"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
|
||||
下载图片
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="resetImage"
|
||||
:disabled="!originalImage"
|
||||
class="btn-secondary"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'undo']" class="mr-2" />
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
>
|
||||
|
||||
<input
|
||||
ref="watermarkInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleWatermarkUpload"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 设置区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- 文字水印设置 -->
|
||||
<div v-if="watermarkType === 'text'" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">文字水印设置</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 水印文字 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">水印文字</label>
|
||||
<input
|
||||
v-model="textWatermark.text"
|
||||
type="text"
|
||||
placeholder="请输入水印文字"
|
||||
class="input-field"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 字体大小 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
字体大小: {{ textWatermark.fontSize }}px
|
||||
</label>
|
||||
<input
|
||||
v-model.number="textWatermark.fontSize"
|
||||
type="range"
|
||||
min="12"
|
||||
max="120"
|
||||
step="2"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 字体颜色 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">字体颜色</label>
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="textWatermark.color"
|
||||
type="color"
|
||||
class="w-12 h-8 rounded border border-primary border-opacity-20"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
<input
|
||||
v-model="textWatermark.color"
|
||||
type="text"
|
||||
class="input-field flex-1"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 透明度 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
透明度: {{ Math.round(textWatermark.opacity * 100) }}%
|
||||
</label>
|
||||
<input
|
||||
v-model.number="textWatermark.opacity"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 字体样式 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">字体样式</label>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="toggleFontStyle('bold')"
|
||||
:class="['btn-sm', textWatermark.fontWeight === 'bold' ? 'btn-primary' : 'btn-secondary']"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'bold']" />
|
||||
</button>
|
||||
<button
|
||||
@click="toggleFontStyle('italic')"
|
||||
:class="['btn-sm', textWatermark.fontStyle === 'italic' ? 'btn-primary' : 'btn-secondary']"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'italic']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 旋转角度 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
旋转角度: {{ textWatermark.rotation }}°
|
||||
</label>
|
||||
<input
|
||||
v-model.number="textWatermark.rotation"
|
||||
type="range"
|
||||
min="-45"
|
||||
max="45"
|
||||
step="5"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片水印设置 -->
|
||||
<div v-if="watermarkType === 'image'" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">图片水印设置</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 水印图片预览 -->
|
||||
<div v-if="watermarkImage">
|
||||
<label class="block text-sm font-medium text-secondary mb-2">水印图片</label>
|
||||
<div class="bg-block rounded-lg p-4">
|
||||
<img :src="watermarkImage" alt="水印图片" class="max-w-full h-20 object-contain mx-auto">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缩放比例 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
缩放比例: {{ Math.round(imageWatermark.scale * 100) }}%
|
||||
</label>
|
||||
<input
|
||||
v-model.number="imageWatermark.scale"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="2"
|
||||
step="0.1"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 透明度 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
透明度: {{ Math.round(imageWatermark.opacity * 100) }}%
|
||||
</label>
|
||||
<input
|
||||
v-model.number="imageWatermark.opacity"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 旋转角度 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
旋转角度: {{ imageWatermark.rotation }}°
|
||||
</label>
|
||||
<input
|
||||
v-model.number="imageWatermark.rotation"
|
||||
type="range"
|
||||
min="-180"
|
||||
max="180"
|
||||
step="15"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 位置设置 -->
|
||||
<div v-if="originalImage" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">位置设置</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 预设位置 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">快速定位</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="(pos, key) in positions"
|
||||
:key="key"
|
||||
@click="setPosition(key)"
|
||||
:class="['btn-sm text-xs', currentPosition.key === key ? 'btn-primary' : 'btn-secondary']"
|
||||
>
|
||||
{{ pos.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义位置 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
X坐标: {{ currentPosition.x }}px
|
||||
</label>
|
||||
<input
|
||||
v-model.number="currentPosition.x"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="canvasWidth"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
Y坐标: {{ currentPosition.y }}px
|
||||
</label>
|
||||
<input
|
||||
v-model.number="currentPosition.y"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="canvasHeight"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量水印设置 -->
|
||||
<div v-if="originalImage" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">批量水印</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input
|
||||
v-model="batchSettings.enabled"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
@change="updateWatermark"
|
||||
>
|
||||
<span class="text-secondary">启用平铺水印</span>
|
||||
</label>
|
||||
|
||||
<div v-if="batchSettings.enabled" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
水平间距: {{ batchSettings.spacingX }}px
|
||||
</label>
|
||||
<input
|
||||
v-model.number="batchSettings.spacingX"
|
||||
type="range"
|
||||
min="50"
|
||||
max="300"
|
||||
step="10"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-secondary mb-2">
|
||||
垂直间距: {{ batchSettings.spacingY }}px
|
||||
</label>
|
||||
<input
|
||||
v-model.number="batchSettings.spacingY"
|
||||
type="range"
|
||||
min="50"
|
||||
max="300"
|
||||
step="10"
|
||||
class="w-full"
|
||||
@input="updateWatermark"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<!-- 原图预览 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">图片预览</h3>
|
||||
|
||||
<div v-if="!originalImage" class="bg-block rounded-lg p-8 text-center">
|
||||
<FontAwesomeIcon :icon="['fas', 'cloud-upload-alt']" class="text-4xl text-tertiary mb-4" />
|
||||
<p class="text-secondary mb-4">请上传图片或拖拽图片到此处</p>
|
||||
<button @click="triggerFileInput" class="btn-primary">
|
||||
选择图片
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="relative">
|
||||
<canvas
|
||||
ref="previewCanvas"
|
||||
class="max-w-full border border-primary border-opacity-20 rounded-lg cursor-crosshair"
|
||||
@click="handleCanvasClick"
|
||||
/>
|
||||
|
||||
<!-- 图片信息 -->
|
||||
<div class="mt-4 flex justify-between items-center text-sm text-secondary">
|
||||
<span>{{ imageInfo.width }} × {{ imageInfo.height }}</span>
|
||||
<span>{{ imageInfo.size }}</span>
|
||||
<span>{{ imageInfo.format }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<div v-if="processedImage" class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">处理结果</h3>
|
||||
|
||||
<div class="bg-block rounded-lg p-4">
|
||||
<img :src="processedImage" alt="处理后的图片" class="max-w-full mx-auto">
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between items-center text-sm text-secondary">
|
||||
<span>质量: {{ outputQuality }}%</span>
|
||||
<span>大小: {{ outputSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态消息 -->
|
||||
<div v-if="statusMessage" class="card p-4">
|
||||
<div :class="[
|
||||
'flex items-center space-x-2',
|
||||
statusType === 'success' ? 'text-success' : 'text-error'
|
||||
]">
|
||||
<FontAwesomeIcon
|
||||
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
|
||||
/>
|
||||
<span>{{ statusMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
|
||||
const { t } = useLanguage()
|
||||
|
||||
// DOM引用
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const watermarkInput = ref<HTMLInputElement>()
|
||||
const previewCanvas = ref<HTMLCanvasElement>()
|
||||
|
||||
// 响应式状态
|
||||
const originalImage = ref('')
|
||||
const processedImage = ref('')
|
||||
const watermarkImage = ref('')
|
||||
const watermarkType = ref<'text' | 'image' | null>(null)
|
||||
const statusMessage = ref('')
|
||||
const statusType = ref<'success' | 'error'>('success')
|
||||
|
||||
// 图片信息
|
||||
const imageInfo = reactive({
|
||||
width: 0,
|
||||
height: 0,
|
||||
size: '',
|
||||
format: ''
|
||||
})
|
||||
|
||||
// Canvas尺寸
|
||||
const canvasWidth = ref(0)
|
||||
const canvasHeight = ref(0)
|
||||
|
||||
// 文字水印设置
|
||||
const textWatermark = reactive({
|
||||
text: 'Sample Watermark',
|
||||
fontSize: 36,
|
||||
color: '#ffffff',
|
||||
opacity: 0.7,
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
rotation: 0
|
||||
})
|
||||
|
||||
// 图片水印设置
|
||||
const imageWatermark = reactive({
|
||||
scale: 0.3,
|
||||
opacity: 0.7,
|
||||
rotation: 0
|
||||
})
|
||||
|
||||
// 位置设置
|
||||
const currentPosition = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
key: 'center'
|
||||
})
|
||||
|
||||
// 批量水印设置
|
||||
const batchSettings = reactive({
|
||||
enabled: false,
|
||||
spacingX: 150,
|
||||
spacingY: 150
|
||||
})
|
||||
|
||||
// 预设位置
|
||||
const positions = {
|
||||
'top-left': { name: '左上', x: 0.1, y: 0.1 },
|
||||
'top-center': { name: '居上', x: 0.5, y: 0.1 },
|
||||
'top-right': { name: '右上', x: 0.9, y: 0.1 },
|
||||
'center-left': { name: '居左', x: 0.1, y: 0.5 },
|
||||
'center': { name: '居中', x: 0.5, y: 0.5 },
|
||||
'center-right': { name: '居右', x: 0.9, y: 0.5 },
|
||||
'bottom-left': { name: '左下', x: 0.1, y: 0.9 },
|
||||
'bottom-center': { name: '居下', x: 0.5, y: 0.9 },
|
||||
'bottom-right': { name: '右下', x: 0.9, y: 0.9 }
|
||||
}
|
||||
|
||||
// 输出设置
|
||||
const outputQuality = computed(() => 90)
|
||||
const outputSize = computed(() => {
|
||||
if (!processedImage.value) return ''
|
||||
return '估算大小'
|
||||
})
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const triggerWatermarkInput = () => {
|
||||
watermarkInput.value?.click()
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
const handleImageUpload = (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showStatus('请选择有效的图片文件', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
originalImage.value = e.target?.result as string
|
||||
loadImageInfo(file)
|
||||
nextTick(() => {
|
||||
setupCanvas()
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 处理水印图片上传
|
||||
const handleWatermarkUpload = (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showStatus('请选择有效的图片文件', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
watermarkImage.value = e.target?.result as string
|
||||
watermarkType.value = 'image'
|
||||
updateWatermark()
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 加载图片信息
|
||||
const loadImageInfo = (file: File) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageInfo.width = img.width
|
||||
imageInfo.height = img.height
|
||||
imageInfo.size = formatFileSize(file.size)
|
||||
imageInfo.format = file.type.split('/')[1].toUpperCase()
|
||||
}
|
||||
img.src = originalImage.value
|
||||
}
|
||||
|
||||
// 设置Canvas
|
||||
const setupCanvas = () => {
|
||||
if (!previewCanvas.value || !originalImage.value) return
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const canvas = previewCanvas.value!
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
// 设置画布尺寸
|
||||
const maxWidth = 600
|
||||
const maxHeight = 400
|
||||
let { width, height } = img
|
||||
|
||||
if (width > maxWidth) {
|
||||
height = (height * maxWidth) / width
|
||||
width = maxWidth
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
width = (width * maxHeight) / height
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvasWidth.value = width
|
||||
canvasHeight.value = height
|
||||
|
||||
// 绘制原图
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
// 设置默认水印位置
|
||||
setPosition('bottom-right')
|
||||
}
|
||||
img.src = originalImage.value
|
||||
}
|
||||
|
||||
// 添加文字水印
|
||||
const addTextWatermark = () => {
|
||||
watermarkType.value = 'text'
|
||||
updateWatermark()
|
||||
}
|
||||
|
||||
// 更新水印
|
||||
const updateWatermark = () => {
|
||||
if (!previewCanvas.value || !originalImage.value || !watermarkType.value) return
|
||||
|
||||
const canvas = previewCanvas.value
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
// 重新绘制原图
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 绘制水印
|
||||
if (watermarkType.value === 'text') {
|
||||
drawTextWatermark(ctx)
|
||||
} else if (watermarkType.value === 'image' && watermarkImage.value) {
|
||||
drawImageWatermark(ctx)
|
||||
}
|
||||
|
||||
// 生成处理后的图片
|
||||
processedImage.value = canvas.toDataURL('image/jpeg', outputQuality.value / 100)
|
||||
}
|
||||
img.src = originalImage.value
|
||||
}
|
||||
|
||||
// 绘制文字水印
|
||||
const drawTextWatermark = (ctx: CanvasRenderingContext2D) => {
|
||||
ctx.save()
|
||||
|
||||
// 设置字体
|
||||
const fontStyle = textWatermark.fontStyle === 'italic' ? 'italic ' : ''
|
||||
const fontWeight = textWatermark.fontWeight === 'bold' ? 'bold ' : ''
|
||||
ctx.font = `${fontStyle}${fontWeight}${textWatermark.fontSize}px Arial`
|
||||
|
||||
// 设置颜色和透明度
|
||||
ctx.fillStyle = textWatermark.color
|
||||
ctx.globalAlpha = textWatermark.opacity
|
||||
|
||||
if (batchSettings.enabled) {
|
||||
// 平铺水印
|
||||
for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
|
||||
for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
|
||||
drawSingleTextWatermark(ctx, x + 50, y + 50)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单个水印
|
||||
drawSingleTextWatermark(ctx, currentPosition.x, currentPosition.y)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 绘制单个文字水印
|
||||
const drawSingleTextWatermark = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
|
||||
ctx.save()
|
||||
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate((textWatermark.rotation * Math.PI) / 180)
|
||||
|
||||
// 添加阴影效果
|
||||
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
|
||||
ctx.shadowBlur = 3
|
||||
ctx.shadowOffsetX = 1
|
||||
ctx.shadowOffsetY = 1
|
||||
|
||||
ctx.fillText(textWatermark.text, 0, 0)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 绘制图片水印
|
||||
const drawImageWatermark = (ctx: CanvasRenderingContext2D) => {
|
||||
const watermarkImg = new Image()
|
||||
watermarkImg.onload = () => {
|
||||
ctx.save()
|
||||
ctx.globalAlpha = imageWatermark.opacity
|
||||
|
||||
const scaledWidth = watermarkImg.width * imageWatermark.scale
|
||||
const scaledHeight = watermarkImg.height * imageWatermark.scale
|
||||
|
||||
if (batchSettings.enabled) {
|
||||
// 平铺水印
|
||||
for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
|
||||
for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
|
||||
drawSingleImageWatermark(ctx, watermarkImg, x, y, scaledWidth, scaledHeight)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单个水印
|
||||
drawSingleImageWatermark(ctx, watermarkImg, currentPosition.x, currentPosition.y, scaledWidth, scaledHeight)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
watermarkImg.src = watermarkImage.value
|
||||
}
|
||||
|
||||
// 绘制单个图片水印
|
||||
const drawSingleImageWatermark = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, x: number, y: number, width: number, height: number) => {
|
||||
ctx.save()
|
||||
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate((imageWatermark.rotation * Math.PI) / 180)
|
||||
|
||||
ctx.drawImage(img, -width / 2, -height / 2, width, height)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 设置位置
|
||||
const setPosition = (key: string) => {
|
||||
const pos = positions[key as keyof typeof positions]
|
||||
if (pos) {
|
||||
currentPosition.x = canvasWidth.value * pos.x
|
||||
currentPosition.y = canvasHeight.value * pos.y
|
||||
currentPosition.key = key
|
||||
updateWatermark()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Canvas点击
|
||||
const handleCanvasClick = (event: MouseEvent) => {
|
||||
if (!previewCanvas.value) return
|
||||
|
||||
const rect = previewCanvas.value.getBoundingClientRect()
|
||||
const scaleX = canvasWidth.value / rect.width
|
||||
const scaleY = canvasHeight.value / rect.height
|
||||
|
||||
currentPosition.x = (event.clientX - rect.left) * scaleX
|
||||
currentPosition.y = (event.clientY - rect.top) * scaleY
|
||||
currentPosition.key = 'custom'
|
||||
|
||||
updateWatermark()
|
||||
}
|
||||
|
||||
// 切换字体样式
|
||||
const toggleFontStyle = (style: 'bold' | 'italic') => {
|
||||
if (style === 'bold') {
|
||||
textWatermark.fontWeight = textWatermark.fontWeight === 'bold' ? 'normal' : 'bold'
|
||||
} else {
|
||||
textWatermark.fontStyle = textWatermark.fontStyle === 'italic' ? 'normal' : 'italic'
|
||||
}
|
||||
updateWatermark()
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
const downloadImage = () => {
|
||||
if (!processedImage.value) return
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.download = `watermarked-image-${Date.now()}.jpg`
|
||||
link.href = processedImage.value
|
||||
link.click()
|
||||
|
||||
showStatus('图片下载完成', 'success')
|
||||
}
|
||||
|
||||
// 重置图片
|
||||
const resetImage = () => {
|
||||
originalImage.value = ''
|
||||
processedImage.value = ''
|
||||
watermarkImage.value = ''
|
||||
watermarkType.value = null
|
||||
statusMessage.value = ''
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 显示状态消息
|
||||
const showStatus = (message: string, type: 'success' | 'error') => {
|
||||
statusMessage.value = message
|
||||
statusType.value = type
|
||||
setTimeout(() => {
|
||||
statusMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user