feat: 添加水印和压缩功能,优化设置界面和验证逻辑

- 新增水印和压缩处理的客户端功能
- 增加设置验证逻辑,确保配置有效性
- 优化设置界面,增加选项卡和样式
- 更新类型定义,包含新配置项
- 添加文件类型和大小验证
- 实现文件上传前的水印和压缩处理
This commit is contained in:
fantasy-ke 2025-10-27 19:05:10 +08:00
parent 236a9d62d1
commit b834fdb5c3
8 changed files with 1052 additions and 10 deletions

22
copy-to-mobile.bat Normal file
View file

@ -0,0 +1,22 @@
@echo off
echo 正在复制文件到移动设备...
echo.
REM 请修改下面的路径为你的 Obsidian 库路径
set OBSIDIAN_PATH="../Doc/.obsidian\plugins\cf-imageBed"
REM 创建插件目录(如果不存在)
if not exist %OBSIDIAN_PATH% (
mkdir %OBSIDIAN_PATH%
)
REM 复制文件
copy main.js %OBSIDIAN_PATH%\
copy manifest.json %OBSIDIAN_PATH%\
copy styles.css %OBSIDIAN_PATH%\
echo.
echo 文件复制完成!
echo 请在 Obsidian 中重新加载插件或重启应用
echo.
pause

View file

@ -1,5 +1,6 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import { CFImageBedSettings } from '../types';
import { SettingsValidator } from './settingsValidator';
export class CFImageBedSettingTab extends PluginSettingTab {
plugin: any;
@ -13,11 +14,60 @@ export class CFImageBedSettingTab extends PluginSettingTab {
const {containerEl} = this;
containerEl.empty();
// 添加插件专用的CSS类名限制样式作用域
containerEl.addClass('cf-imagebed-settings');
containerEl.createEl('h2', { text: 'CF ImageBed 设置' });
// 创建选项卡容器
const tabContainer = containerEl.createDiv('cf-imagebed-tabs');
const tabContent = containerEl.createDiv('cf-imagebed-tab-content');
// 创建选项卡按钮
const basicTab = tabContainer.createEl('button', { text: '基础设置', cls: 'cf-tab-button active' });
const advancedTab = tabContainer.createEl('button', { text: '高级设置', cls: 'cf-tab-button' });
const userTab = tabContainer.createEl('button', { text: '用户体验', cls: 'cf-tab-button' });
const backupTab = tabContainer.createEl('button', { text: '备份设置', cls: 'cf-tab-button' });
// 创建选项卡内容区域
const basicContent = tabContent.createDiv('cf-tab-panel active');
const advancedContent = tabContent.createDiv('cf-tab-panel');
const userContent = tabContent.createDiv('cf-tab-panel');
const backupContent = tabContent.createDiv('cf-tab-panel');
// 选项卡切换逻辑
const switchTab = (activeTab: HTMLElement, activeContent: HTMLElement) => {
// 移除所有活动状态
tabContainer.querySelectorAll('.cf-tab-button').forEach(btn => btn.classList.remove('active'));
tabContent.querySelectorAll('.cf-tab-panel').forEach(panel => panel.classList.remove('active'));
// 添加活动状态
activeTab.classList.add('active');
activeContent.classList.add('active');
};
basicTab.addEventListener('click', () => switchTab(basicTab, basicContent));
advancedTab.addEventListener('click', () => switchTab(advancedTab, advancedContent));
userTab.addEventListener('click', () => switchTab(userTab, userContent));
backupTab.addEventListener('click', () => switchTab(backupTab, backupContent));
// 基础设置选项卡内容
this.createBasicSettings(basicContent);
// 高级设置选项卡内容
this.createAdvancedSettings(advancedContent);
// 用户体验选项卡内容
this.createUserExperienceSettings(userContent);
// 备份设置选项卡内容
this.createBackupSettings(backupContent);
}
private createBasicSettings(container: HTMLElement): void {
// API URL 设置
new Setting(containerEl)
new Setting(container)
.setName('API URL')
.setDesc('CloudFlare ImgBed 的 API 地址例如https://your.domain')
.addText(text => text
@ -29,7 +79,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 认证码设置
new Setting(containerEl)
new Setting(container)
.setName('认证码')
.setDesc('上传认证码')
.addText(text => text
@ -41,7 +91,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 上传渠道设置
new Setting(containerEl)
new Setting(container)
.setName('上传渠道')
.setDesc('选择上传渠道')
.addDropdown(dropdown => dropdown
@ -55,7 +105,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 文件命名方式设置
new Setting(containerEl)
new Setting(container)
.setName('文件命名方式')
.setDesc('选择文件命名方式')
.addDropdown(dropdown => dropdown
@ -70,7 +120,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 返回格式设置
new Setting(containerEl)
new Setting(container)
.setName('返回链接格式')
.setDesc('选择返回链接格式')
.addDropdown(dropdown => dropdown
@ -83,7 +133,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 上传目录设置
new Setting(containerEl)
new Setting(container)
.setName('上传目录')
.setDesc('上传目录用相对路径表示例如img/test')
.addText(text => text
@ -95,7 +145,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 服务端压缩设置
new Setting(containerEl)
new Setting(container)
.setName('服务端压缩')
.setDesc('启用服务端压缩(仅针对 Telegram 渠道的图片文件)')
.addToggle(toggle => toggle
@ -106,7 +156,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
}));
// 自动重试设置
new Setting(containerEl)
new Setting(container)
.setName('自动重试')
.setDesc('失败时自动切换渠道重试')
.addToggle(toggle => toggle
@ -116,4 +166,238 @@ export class CFImageBedSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
}
private createAdvancedSettings(container: HTMLElement): void {
// 文件大小限制
new Setting(container)
.setName('最大文件大小')
.setDesc('设置上传文件的最大大小MB')
.addSlider(slider => slider
.setLimits(1, 50, 1)
.setValue(this.plugin.settings.maxFileSize)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.maxFileSize = value;
await this.plugin.saveSettings();
}));
// 允许的文件类型
new Setting(container)
.setName('允许的文件类型')
.setDesc('设置允许上传的文件类型(用逗号分隔)')
.addText(text => text
.setPlaceholder('jpg,jpeg,png,gif,webp,bmp')
.setValue(this.plugin.settings.allowedFileTypes.join(','))
.onChange(async (value) => {
this.plugin.settings.allowedFileTypes = value.split(',').map(t => t.trim());
await this.plugin.saveSettings();
}));
// 水印设置
new Setting(container)
.setName('启用水印')
.setDesc('为上传的图片添加水印')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableWatermark)
.onChange(async (value) => {
this.plugin.settings.enableWatermark = value;
await this.plugin.saveSettings();
}));
// 水印文字
new Setting(container)
.setName('水印文字')
.setDesc('设置水印文字内容')
.addText(text => text
.setPlaceholder('水印文字')
.setValue(this.plugin.settings.watermarkText)
.setDisabled(!this.plugin.settings.enableWatermark)
.onChange(async (value) => {
this.plugin.settings.watermarkText = value;
await this.plugin.saveSettings();
}));
// 水印位置
new Setting(container)
.setName('水印位置')
.setDesc('设置水印在图片中的位置')
.addDropdown(dropdown => dropdown
.addOption('top-left', '左上角')
.addOption('top-right', '右上角')
.addOption('bottom-left', '左下角')
.addOption('bottom-right', '右下角')
.addOption('center', '居中')
.setValue(this.plugin.settings.watermarkPosition)
.setDisabled(!this.plugin.settings.enableWatermark)
.onChange(async (value) => {
this.plugin.settings.watermarkPosition = value;
await this.plugin.saveSettings();
}));
// 水印字体大小
new Setting(container)
.setName('水印字体大小')
.setDesc('设置水印文字的字体大小(像素)')
.addSlider(slider => slider
.setLimits(12, 72, 2)
.setValue(this.plugin.settings.watermarkSize)
.setDynamicTooltip()
.setDisabled(!this.plugin.settings.enableWatermark)
.onChange(async (value) => {
this.plugin.settings.watermarkSize = value;
await this.plugin.saveSettings();
}));
// 水印透明度
new Setting(container)
.setName('水印透明度')
.setDesc('设置水印的透明度0-1')
.addSlider(slider => slider
.setLimits(0.1, 1, 0.1)
.setValue(this.plugin.settings.watermarkOpacity)
.setDynamicTooltip()
.setDisabled(!this.plugin.settings.enableWatermark)
.onChange(async (value) => {
this.plugin.settings.watermarkOpacity = value;
await this.plugin.saveSettings();
}));
// 客户端压缩设置
new Setting(container)
.setName('启用客户端压缩')
.setDesc('在上传前自动压缩图片以减少文件大小')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableClientCompress)
.onChange(async (value) => {
this.plugin.settings.enableClientCompress = value;
await this.plugin.saveSettings();
}));
// 压缩阈值
new Setting(container)
.setName('压缩阈值')
.setDesc('设置图片大小阈值超过此值将自动压缩MB')
.addSlider(slider => slider
.setLimits(0.5, 10, 0.5)
.setValue(this.plugin.settings.compressThreshold)
.setDynamicTooltip()
.setDisabled(!this.plugin.settings.enableClientCompress)
.onChange(async (value) => {
this.plugin.settings.compressThreshold = value;
await this.plugin.saveSettings();
}));
// 期望大小
new Setting(container)
.setName('期望大小')
.setDesc('设置压缩后图片大小期望值MB')
.addSlider(slider => slider
.setLimits(0.1, 5, 0.1)
.setValue(this.plugin.settings.targetSize)
.setDynamicTooltip()
.setDisabled(!this.plugin.settings.enableClientCompress)
.onChange(async (value) => {
this.plugin.settings.targetSize = value;
await this.plugin.saveSettings();
}));
}
private createUserExperienceSettings(container: HTMLElement): void {
// 显示上传进度
new Setting(container)
.setName('显示上传进度')
.setDesc('在上传过程中显示进度条')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showUploadProgress)
.onChange(async (value) => {
this.plugin.settings.showUploadProgress = value;
await this.plugin.saveSettings();
}));
// 显示成功通知
new Setting(container)
.setName('显示成功通知')
.setDesc('上传成功后显示通知消息')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showSuccessNotification)
.onChange(async (value) => {
this.plugin.settings.showSuccessNotification = value;
await this.plugin.saveSettings();
}));
// 显示错误通知
new Setting(container)
.setName('显示错误通知')
.setDesc('上传失败时显示错误消息')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showErrorNotification)
.onChange(async (value) => {
this.plugin.settings.showErrorNotification = value;
await this.plugin.saveSettings();
}));
// 通知持续时间
new Setting(container)
.setName('通知持续时间')
.setDesc('设置通知消息显示的持续时间(秒)')
.addSlider(slider => slider
.setLimits(1, 10, 1)
.setValue(this.plugin.settings.notificationDuration)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.notificationDuration = value;
await this.plugin.saveSettings();
}));
// 快捷键设置
new Setting(container)
.setName('启用快捷键')
.setDesc('启用快捷键快速上传图片')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableHotkey)
.onChange(async (value) => {
this.plugin.settings.enableHotkey = value;
await this.plugin.saveSettings();
}));
// 快捷键
new Setting(container)
.setName('快捷键')
.setDesc('设置上传图片的快捷键')
.addText(text => text
.setPlaceholder('Ctrl+Shift+U')
.setValue(this.plugin.settings.hotkey)
.setDisabled(!this.plugin.settings.enableHotkey)
.onChange(async (value) => {
this.plugin.settings.hotkey = value;
await this.plugin.saveSettings();
}));
}
private createBackupSettings(container: HTMLElement): void {
// 启用本地备份
new Setting(container)
.setName('启用本地备份')
.setDesc('在上传到云端的同时,在本地保存一份备份')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableLocalBackup)
.onChange(async (value) => {
this.plugin.settings.enableLocalBackup = value;
await this.plugin.saveSettings();
}));
// 备份路径
new Setting(container)
.setName('备份路径')
.setDesc('设置本地备份的存储路径(相对于库根目录)')
.addText(text => text
.setPlaceholder('attachments/backup')
.setValue(this.plugin.settings.backupPath)
.setDisabled(!this.plugin.settings.enableLocalBackup)
.onChange(async (value) => {
this.plugin.settings.backupPath = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -0,0 +1,103 @@
import { Notice } from 'obsidian';
import { CFImageBedSettings } from '../types';
export class SettingsValidator {
static validateSettings(settings: CFImageBedSettings): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
// 验证基础配置
if (!settings.apiUrl) {
errors.push('API URL 不能为空');
} else if (!this.isValidUrl(settings.apiUrl)) {
errors.push('API URL 格式不正确');
}
if (!settings.authCode) {
errors.push('认证码不能为空');
}
// 验证文件大小
if (settings.maxFileSize < 1 || settings.maxFileSize > 100) {
errors.push('文件大小限制应在 1-100 MB 之间');
}
// 验证客户端压缩配置
if (settings.enableClientCompress) {
if (settings.compressThreshold < 0.1 || settings.compressThreshold > 20) {
errors.push('压缩阈值应在 0.1-20 MB 之间');
}
if (settings.targetSize < 0.1 || settings.targetSize > 10) {
errors.push('期望大小应在 0.1-10 MB 之间');
}
if (settings.targetSize >= settings.compressThreshold) {
errors.push('期望大小应小于压缩阈值');
}
}
// 验证通知持续时间
if (settings.notificationDuration < 1 || settings.notificationDuration > 30) {
errors.push('通知持续时间应在 1-30 秒之间');
}
// 验证文件类型
if (settings.allowedFileTypes.length === 0) {
errors.push('至少需要指定一种允许的文件类型');
}
// 验证水印设置
if (settings.enableWatermark) {
if (!settings.watermarkText.trim()) {
errors.push('启用水印时必须设置水印文字');
}
if (settings.watermarkSize < 8 || settings.watermarkSize > 100) {
errors.push('水印字体大小应在 8-100 像素之间');
}
if (settings.watermarkOpacity < 0.1 || settings.watermarkOpacity > 1) {
errors.push('水印透明度应在 0.1-1 之间');
}
}
// 验证备份路径
if (settings.enableLocalBackup && !settings.backupPath.trim()) {
errors.push('启用本地备份时必须设置备份路径');
}
return {
isValid: errors.length === 0,
errors
};
}
static isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
static validateFileType(fileName: string, allowedTypes: string[]): boolean {
const extension = fileName.split('.').pop()?.toLowerCase();
return extension ? allowedTypes.includes(extension) : false;
}
static validateFileSize(fileSize: number, maxSizeMB: number): boolean {
const maxSizeBytes = maxSizeMB * 1024 * 1024;
return fileSize <= maxSizeBytes;
}
static showValidationErrors(errors: string[]): void {
if (errors.length > 0) {
new Notice(`配置验证失败:\n${errors.join('\n')}`, 5000);
}
}
static showValidationSuccess(): void {
new Notice('配置验证通过', 2000);
}
}

View file

@ -1,4 +1,5 @@
export interface CFImageBedSettings {
// 基础配置
apiUrl: string;
authCode: string;
uploadChannel: string;
@ -7,9 +8,38 @@ export interface CFImageBedSettings {
uploadFolder: string;
serverCompress: boolean;
autoRetry: boolean;
// 高级配置
maxFileSize: number; // MB
allowedFileTypes: string[];
enableWatermark: boolean;
watermarkText: string;
watermarkPosition: string;
watermarkSize: number; // 水印字体大小
watermarkOpacity: number; // 水印透明度 0-1
// 客户端压缩配置
enableClientCompress: boolean;
compressThreshold: number; // MB - 压缩阈值
targetSize: number; // MB - 期望大小
// 用户体验配置
showUploadProgress: boolean;
showSuccessNotification: boolean;
showErrorNotification: boolean;
notificationDuration: number; // 秒
// 备份配置
enableLocalBackup: boolean;
backupPath: string;
// 快捷键配置
enableHotkey: boolean;
hotkey: string;
}
export const DEFAULT_SETTINGS: CFImageBedSettings = {
// 基础配置
apiUrl: '',
authCode: '',
uploadChannel: 'telegram',
@ -17,5 +47,33 @@ export const DEFAULT_SETTINGS: CFImageBedSettings = {
returnFormat: 'default',
uploadFolder: '',
serverCompress: true,
autoRetry: true
autoRetry: true,
// 高级配置
maxFileSize: 10, // 10MB
allowedFileTypes: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'],
enableWatermark: false,
watermarkText: '',
watermarkPosition: 'bottom-right',
watermarkSize: 24, // 字体大小
watermarkOpacity: 0.7, // 透明度
// 客户端压缩配置
enableClientCompress: false,
compressThreshold: 2, // 2MB
targetSize: 1, // 1MB
// 用户体验配置
showUploadProgress: false,
showSuccessNotification: true,
showErrorNotification: false,
notificationDuration: 5,
// 备份配置
enableLocalBackup: false,
backupPath: '',
// 快捷键配置
enableHotkey: false,
hotkey: 'Ctrl+Shift+U'
};

View file

@ -1,5 +1,7 @@
import { Notice } from 'obsidian';
import { CFImageBedSettings } from '../types';
import { ClientCompressor } from '../utils/clientCompressor';
import { ClientWatermark } from '../utils/clientWatermark';
export class UploadService {
constructor(private settings: CFImageBedSettings) {}
@ -11,8 +13,51 @@ export class UploadService {
}
try {
// 检查文件类型
if (!this.isAllowedFileType(file)) {
new Notice(`不支持的文件类型: ${file.type}`);
return null;
}
// 检查文件大小
if (!this.isFileSizeAllowed(file)) {
new Notice(`文件大小超过限制: ${ClientCompressor.formatFileSize(file.size)}`);
return null;
}
// 客户端处理(水印 + 压缩)
let processedFile = file;
// 1. 添加水印
if (this.settings.enableWatermark && ClientWatermark.isWatermarkable(file)) {
console.log('CF ImageBed: 开始添加水印');
processedFile = await ClientWatermark.addWatermark(
processedFile,
this.settings.watermarkText,
this.settings.watermarkPosition,
this.settings.watermarkSize,
this.settings.watermarkOpacity
);
}
// 2. 客户端压缩
if (this.settings.enableClientCompress && ClientCompressor.isCompressible(processedFile)) {
console.log('CF ImageBed: 开始客户端压缩处理');
processedFile = await ClientCompressor.compressImage(
processedFile,
this.settings.targetSize,
this.settings.compressThreshold
);
// 显示压缩结果
const originalSize = ClientCompressor.formatFileSize(file.size);
const processedSize = ClientCompressor.formatFileSize(processedFile.size);
console.log(`CF ImageBed: 处理完成 - 原始: ${originalSize}, 处理后: ${processedSize}`);
}
// 创建上传参数
const formData = new FormData();
formData.append('file', file);
formData.append('file', processedFile);
const params = new URLSearchParams({
authCode: this.settings.authCode,
@ -27,6 +72,7 @@ export class UploadService {
params.append('uploadFolder', this.settings.uploadFolder);
}
// 上传文件
const response = await fetch(`${this.settings.apiUrl}/upload?${params}`, {
method: 'POST',
body: formData
@ -56,4 +102,20 @@ export class UploadService {
return null;
}
}
/**
*
*/
private isAllowedFileType(file: File): boolean {
const extension = file.name.split('.').pop()?.toLowerCase();
return extension ? this.settings.allowedFileTypes.includes(extension) : false;
}
/**
*
*/
private isFileSizeAllowed(file: File): boolean {
const maxSizeBytes = this.settings.maxFileSize * 1024 * 1024;
return file.size <= maxSizeBytes;
}
}

View file

@ -0,0 +1,164 @@
import { Notice } from 'obsidian';
export class ClientCompressor {
/**
*
* @param file
* @param targetSizeMB MB
* @param thresholdMB MB
* @returns
*/
static async compressImage(
file: File,
targetSizeMB: number,
thresholdMB: number
): Promise<File> {
// 检查文件大小是否超过阈值
const fileSizeMB = file.size / (1024 * 1024);
if (fileSizeMB <= thresholdMB) {
console.log(`CF ImageBed: 文件大小 ${fileSizeMB.toFixed(2)}MB 未超过阈值 ${thresholdMB}MB跳过压缩`);
return file;
}
console.log(`CF ImageBed: 开始压缩图片,原始大小: ${fileSizeMB.toFixed(2)}MB目标大小: ${targetSizeMB}MB`);
try {
// 创建图片对象
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建画布上下文');
}
// 等待图片加载
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
// 计算压缩后的尺寸
const { width, height } = this.calculateCompressedDimensions(
img.width,
img.height,
targetSizeMB,
fileSizeMB
);
// 设置画布尺寸
canvas.width = width;
canvas.height = height;
// 绘制压缩后的图片
ctx.drawImage(img, 0, 0, width, height);
// 转换为 Blob
const compressedBlob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
throw new Error('压缩失败');
}
}, 'image/jpeg', 0.8); // 使用 JPEG 格式,质量 0.8
});
// 创建新的文件对象
const compressedFile = new File([compressedBlob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
const compressedSizeMB = compressedFile.size / (1024 * 1024);
console.log(`CF ImageBed: 压缩完成,压缩后大小: ${compressedSizeMB.toFixed(2)}MB`);
// 清理资源
URL.revokeObjectURL(img.src);
return compressedFile;
} catch (error) {
console.error('CF ImageBed: 图片压缩失败:', error);
new Notice('图片压缩失败,将上传原始文件', 3000);
return file;
}
}
/**
*
* @param originalWidth
* @param originalHeight
* @param targetSizeMB MB
* @param originalSizeMB MB
* @returns
*/
private static calculateCompressedDimensions(
originalWidth: number,
originalHeight: number,
targetSizeMB: number,
originalSizeMB: number
): { width: number; height: number } {
// 计算压缩比例(基于文件大小)
const sizeRatio = Math.sqrt(targetSizeMB / originalSizeMB);
// 计算新尺寸
let newWidth = Math.floor(originalWidth * sizeRatio);
let newHeight = Math.floor(originalHeight * sizeRatio);
// 确保尺寸不会太小(最小 100px
const minSize = 100;
if (newWidth < minSize || newHeight < minSize) {
const aspectRatio = originalWidth / originalHeight;
if (aspectRatio > 1) {
newWidth = Math.max(minSize, newWidth);
newHeight = Math.floor(newWidth / aspectRatio);
} else {
newHeight = Math.max(minSize, newHeight);
newWidth = Math.floor(newHeight * aspectRatio);
}
}
// 确保尺寸不会太大(最大 4000px
const maxSize = 4000;
if (newWidth > maxSize || newHeight > maxSize) {
const aspectRatio = originalWidth / originalHeight;
if (aspectRatio > 1) {
newWidth = Math.min(maxSize, newWidth);
newHeight = Math.floor(newWidth / aspectRatio);
} else {
newHeight = Math.min(maxSize, newHeight);
newWidth = Math.floor(newHeight * aspectRatio);
}
}
return { width: newWidth, height: newHeight };
}
/**
*
* @param file
* @returns
*/
static isCompressible(file: File): boolean {
const compressibleTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
return compressibleTypes.includes(file.type.toLowerCase());
}
/**
*
* @param bytes
* @returns
*/
static formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}

View file

@ -0,0 +1,178 @@
import { Notice } from 'obsidian';
export class ClientWatermark {
/**
*
* @param file
* @param watermarkText
* @param position
* @param fontSize
* @param opacity
* @returns
*/
static async addWatermark(
file: File,
watermarkText: string,
position: string,
fontSize: number,
opacity: number
): Promise<File> {
if (!watermarkText.trim()) {
console.log('CF ImageBed: 水印文字为空,跳过水印处理');
return file;
}
console.log(`CF ImageBed: 开始添加水印 - 文字: ${watermarkText}, 位置: ${position}`);
try {
// 创建图片对象
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建画布上下文');
}
// 等待图片加载
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
// 设置画布尺寸
canvas.width = img.width;
canvas.height = img.height;
// 绘制原始图片
ctx.drawImage(img, 0, 0);
// 设置水印样式
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.5})`;
ctx.lineWidth = 2;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 计算水印位置
const positionData = this.calculateWatermarkPosition(
img.width,
img.height,
position,
fontSize,
watermarkText
);
// 绘制水印文字(带描边效果)
ctx.strokeText(watermarkText, positionData.x, positionData.y);
ctx.fillText(watermarkText, positionData.x, positionData.y);
// 转换为 Blob
const watermarkedBlob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
throw new Error('水印添加失败');
}
}, file.type, 0.9);
});
// 创建新的文件对象
const watermarkedFile = new File([watermarkedBlob], file.name, {
type: file.type,
lastModified: Date.now()
});
console.log('CF ImageBed: 水印添加完成');
// 清理资源
URL.revokeObjectURL(img.src);
return watermarkedFile;
} catch (error) {
console.error('CF ImageBed: 水印添加失败:', error);
new Notice('水印添加失败,将上传原始文件', 3000);
return file;
}
}
/**
*
* @param imgWidth
* @param imgHeight
* @param position
* @param fontSize
* @param text
* @returns
*/
private static calculateWatermarkPosition(
imgWidth: number,
imgHeight: number,
position: string,
fontSize: number,
text: string
): { x: number; y: number } {
const padding = fontSize; // 边距
const textWidth = text.length * fontSize * 0.6; // 估算文字宽度
const textHeight = fontSize;
let x: number, y: number;
switch (position) {
case 'top-left':
x = padding + textWidth / 2;
y = padding + textHeight / 2;
break;
case 'top-right':
x = imgWidth - padding - textWidth / 2;
y = padding + textHeight / 2;
break;
case 'bottom-left':
x = padding + textWidth / 2;
y = imgHeight - padding - textHeight / 2;
break;
case 'bottom-right':
x = imgWidth - padding - textWidth / 2;
y = imgHeight - padding - textHeight / 2;
break;
case 'center':
x = imgWidth / 2;
y = imgHeight / 2;
break;
default:
// 默认右下角
x = imgWidth - padding - textWidth / 2;
y = imgHeight - padding - textHeight / 2;
}
return { x, y };
}
/**
*
* @param file
* @returns
*/
static isWatermarkable(file: File): boolean {
const watermarkableTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
return watermarkableTypes.includes(file.type.toLowerCase());
}
/**
*
* @returns
*/
static getPositionOptions(): Array<{ value: string; label: string }> {
return [
{ value: 'top-left', label: '左上角' },
{ value: 'top-right', label: '右上角' },
{ value: 'bottom-left', label: '左下角' },
{ value: 'bottom-right', label: '右下角' },
{ value: 'center', label: '居中' }
];
}
}

View file

@ -6,3 +6,174 @@ available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
/* CF ImageBed 插件专用样式 - 限制作用域避免影响全局 */
/* 设置界面选项卡样式 - 仅在插件设置页面生效 */
.cf-imagebed-settings .cf-imagebed-tabs {
display: flex;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 1rem;
gap: 0.5rem;
}
.cf-imagebed-settings .cf-tab-button {
background: none;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
color: var(--text-muted);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.cf-imagebed-settings .cf-tab-button:hover {
color: var(--text-normal);
background: var(--background-modifier-hover);
}
.cf-imagebed-settings .cf-tab-button.active {
color: var(--text-accent);
border-bottom-color: var(--text-accent);
background: var(--background-modifier-active);
}
.cf-imagebed-settings .cf-imagebed-tab-content {
position: relative;
}
.cf-imagebed-settings .cf-tab-panel {
display: none;
animation: cf-fadeIn 0.3s ease-in-out;
}
.cf-imagebed-settings .cf-tab-panel.active {
display: block;
}
@keyframes cf-fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 滑块样式优化 - 仅在插件设置页面生效 */
.cf-imagebed-settings .setting-item .slider-container {
width: 100%;
margin-top: 0.5rem;
}
.cf-imagebed-settings .setting-item .slider-container input[type="range"] {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--background-modifier-border);
outline: none;
-webkit-appearance: none;
}
.cf-imagebed-settings .setting-item .slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--text-accent);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.cf-imagebed-settings .setting-item .slider-container input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--text-accent);
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 按钮样式优化 - 仅在插件设置页面生效 */
.cf-imagebed-settings .setting-item .mod-button {
padding: 0.5rem 1rem;
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
background: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition: all 0.2s ease;
}
.cf-imagebed-settings .setting-item .mod-button:hover {
background: var(--background-modifier-hover);
border-color: var(--text-accent);
}
.cf-imagebed-settings .setting-item .mod-button.mod-warning {
background: var(--text-error);
color: var(--text-on-accent);
border-color: var(--text-error);
}
.cf-imagebed-settings .setting-item .mod-button.mod-warning:hover {
background: var(--text-error-hover);
}
/* 设置项分组样式 - 仅在插件设置页面生效 */
.cf-imagebed-settings .setting-item {
margin-bottom: 1rem;
padding: 0.75rem;
border-radius: 6px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
transition: all 0.2s ease;
}
.cf-imagebed-settings .setting-item:hover {
background: var(--background-modifier-hover);
border-color: var(--text-accent);
}
.cf-imagebed-settings .setting-item .setting-item-name {
font-weight: 600;
color: var(--text-normal);
margin-bottom: 0.25rem;
}
.cf-imagebed-settings .setting-item .setting-item-description {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.4;
}
/* 禁用状态样式 - 仅在插件设置页面生效 */
.cf-imagebed-settings .setting-item .setting-item-control input:disabled,
.cf-imagebed-settings .setting-item .setting-item-control select:disabled,
.cf-imagebed-settings .setting-item .setting-item-control button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 响应式设计 - 仅在插件设置页面生效 */
@media (max-width: 768px) {
.cf-imagebed-settings .cf-imagebed-tabs {
flex-wrap: wrap;
gap: 0.25rem;
}
.cf-imagebed-settings .cf-tab-button {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.cf-imagebed-settings .setting-item {
padding: 0.5rem;
margin-bottom: 0.75rem;
}
}