924 lines
26 KiB
JavaScript
924 lines
26 KiB
JavaScript
/**
|
||
* 获取位置的异步函数
|
||
*/
|
||
export const getLocation = async () => {
|
||
return new Promise((resolve, reject) => {
|
||
uni.getLocation({
|
||
isHighAccuracy: true,
|
||
altitude: true,
|
||
accuracy: 'best',
|
||
success: (res) => {
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
export const chooseImage = async () => {
|
||
return new Promise((resolve, reject) => {
|
||
uni.chooseImage({
|
||
sourceType:['camera'],
|
||
count: 1,
|
||
success: (res) => {
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
*
|
||
* @param {*} date
|
||
* @param {*} format
|
||
* @returns
|
||
*/
|
||
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
|
||
return format
|
||
.replace('YYYY', year)
|
||
.replace('MM', month)
|
||
.replace('DD', day)
|
||
.replace('HH', hours)
|
||
.replace('mm', minutes)
|
||
.replace('ss', seconds);
|
||
}
|
||
|
||
|
||
/**
|
||
* 缓存logo图片的Map
|
||
*/
|
||
const logoCache = new Map();
|
||
|
||
/**
|
||
* 获取缓存的logo图片路径
|
||
* @param {string} logoUrl - logo网络地址
|
||
* @returns {Promise<string>} - 返回本地缓存路径
|
||
*/
|
||
export const getCachedLogo = async (logoUrl) => {
|
||
// 检查缓存中是否已存在
|
||
if (logoCache.has(logoUrl)) {
|
||
console.log('使用缓存的logo:', logoUrl);
|
||
return logoCache.get(logoUrl);
|
||
}
|
||
|
||
// #ifdef H5
|
||
// H5 使用 fetch 获取 Blob
|
||
return new Promise((resolve, reject) => {
|
||
fetch(logoUrl)
|
||
.then(res => {
|
||
if (!res.ok) throw new Error("下载失败");
|
||
return res.blob();
|
||
})
|
||
.then(blob => {
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
logoCache.set(logoUrl, objectUrl);
|
||
console.log('H5 缓存 logo:', logoUrl);
|
||
resolve(objectUrl);
|
||
})
|
||
.catch(err => {
|
||
console.error('H5 下载logo失败:', err);
|
||
reject(err);
|
||
});
|
||
});
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// App/小程序使用 uni.downloadFile
|
||
return new Promise((resolve, reject) => {
|
||
uni.downloadFile({
|
||
url: logoUrl,
|
||
success: (downloadRes) => {
|
||
if (downloadRes.statusCode === 200) {
|
||
console.log('下载并缓存logo:', logoUrl);
|
||
logoCache.set(logoUrl, downloadRes.tempFilePath);
|
||
resolve(downloadRes.tempFilePath);
|
||
} else {
|
||
reject(new Error("下载失败: " + downloadRes.statusCode));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('下载logo失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
// #endif
|
||
};
|
||
|
||
/**
|
||
* 清除logo缓存
|
||
*/
|
||
export const clearLogoCache = () => {
|
||
logoCache.clear();
|
||
console.log('logo缓存已清除');
|
||
};
|
||
|
||
/**
|
||
* 获取缓存大小
|
||
*/
|
||
export const getLogoCacheSize = () => {
|
||
return logoCache.size;
|
||
};
|
||
|
||
|
||
/**
|
||
* 处理水印图片导出结果
|
||
* @param {string} tempFilePath - 临时文件路径
|
||
* @param {object} originalFileSize - 原图文件大小
|
||
* @param {number} width - 图片宽度
|
||
* @param {number} height - 图片高度
|
||
* @param {function} resolve - Promise resolve函数
|
||
*/
|
||
const handleWatermarkExport = async (tempFilePath, originalFileSize, width, height, resolve) => {
|
||
try {
|
||
const watermarkFileSize = await getFileSize(tempFilePath);
|
||
console.log('水印图片文件大小:', watermarkFileSize.sizeKB, 'KB');
|
||
resolve({
|
||
filePath: tempFilePath,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: watermarkFileSize
|
||
}
|
||
});
|
||
} catch (err) {
|
||
console.error('获取水印图片文件大小失败:', err);
|
||
// 即使获取文件大小失败,也返回其他信息
|
||
resolve({
|
||
filePath: tempFilePath,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: null
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 计算合适的压缩质量
|
||
* @param {number} originalSizeKB - 原图大小(KB)
|
||
* @returns {number} - 压缩质量(0-1)
|
||
*/
|
||
const calculateQuality = (originalSizeKB) => {
|
||
// 如果文件大小为0或无效,使用默认质量
|
||
if (!originalSizeKB || originalSizeKB <= 0) {
|
||
return 0.8; // 默认中等质量
|
||
}
|
||
|
||
if (originalSizeKB < 100) {
|
||
return 0.9; // 小图片使用高质量
|
||
} else if (originalSizeKB < 500) {
|
||
return 0.8; // 中等图片使用中等质量
|
||
} else {
|
||
return 0.7; // 大图片使用较低质量
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 计算合适的输出尺寸
|
||
* @param {number} width - 原图宽度
|
||
* @param {number} height - 原图高度
|
||
* @param {number} maxWidth - 最大宽度限制
|
||
* @returns {object} - 输出尺寸 {width, height}
|
||
*/
|
||
const calculateOutputSize = (width, height, maxWidth = 1920) => {
|
||
// 如果图片尺寸已经很小,不需要压缩
|
||
if (width <= maxWidth) {
|
||
return {
|
||
width,
|
||
height
|
||
};
|
||
}
|
||
|
||
// 按比例缩放
|
||
const ratio = maxWidth / width;
|
||
return {
|
||
width: Math.round(width * ratio),
|
||
height: Math.round(height * ratio)
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 获取文件大小
|
||
* @param {string|File} filePath - 本地文件路径(App/小程序) 或 File 对象(H5)
|
||
* @returns {Promise<{size:number,sizeKB:string,sizeMB:string}>}
|
||
*/
|
||
const getFileSize = async (filePath) => {
|
||
return new Promise((resolve, reject) => {
|
||
// #ifdef H5
|
||
try {
|
||
// H5 情况下,如果传的是 File 对象
|
||
if (filePath instanceof File) {
|
||
const size = filePath.size;
|
||
resolve({
|
||
size: size,
|
||
sizeKB: (size / 1024).toFixed(2),
|
||
sizeMB: (size / (1024 * 1024)).toFixed(2)
|
||
});
|
||
} else if (filePath.startsWith('blob:')) {
|
||
// 如果是blob URL,使用fetch获取blob对象
|
||
fetch(filePath)
|
||
.then(res => res.blob())
|
||
.then(blob => {
|
||
resolve({
|
||
size: blob.size,
|
||
sizeKB: (blob.size / 1024).toFixed(2),
|
||
sizeMB: (blob.size / (1024 * 1024)).toFixed(2)
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.error("H5 获取blob文件大小失败:", err);
|
||
// 如果获取失败,返回默认值
|
||
resolve({
|
||
size: 0,
|
||
sizeKB: "0.00",
|
||
sizeMB: "0.00"
|
||
});
|
||
});
|
||
} else {
|
||
// 如果传入的是普通URL,用 fetch 获取
|
||
fetch(filePath, { method: "HEAD" })
|
||
.then(res => {
|
||
const size = res.headers.get("content-length");
|
||
if (!size) throw new Error("无法获取文件大小");
|
||
resolve({
|
||
size: Number(size),
|
||
sizeKB: (size / 1024).toFixed(2),
|
||
sizeMB: (size / (1024 * 1024)).toFixed(2)
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.error("H5 获取文件大小失败:", err);
|
||
// 如果获取失败,返回默认值
|
||
resolve({
|
||
size: 0,
|
||
sizeKB: "0.00",
|
||
sizeMB: "0.00"
|
||
});
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error("H5 处理异常:", err);
|
||
// 如果处理异常,返回默认值
|
||
resolve({
|
||
size: 0,
|
||
sizeKB: "0.00",
|
||
sizeMB: "0.00"
|
||
});
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// App、小程序端
|
||
uni.getFileInfo({
|
||
filePath: filePath,
|
||
success: (res) => {
|
||
resolve({
|
||
size: res.size,
|
||
sizeKB: (res.size / 1024).toFixed(2),
|
||
sizeMB: (res.size / (1024 * 1024)).toFixed(2)
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取文件大小失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
// #endif
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* 添加水印到图片
|
||
* @param {string} imagePath - 原图片路径
|
||
* @param {object} watermarkInfo - 水印信息对象
|
||
* @param {string} logoUrl - logo图标网络地址(可选)
|
||
* @param {number} logoOpacity - logo透明度,范围0-1,默认1(不透明)
|
||
* @param {number} quality - 压缩质量,范围0-1,默认自动计算
|
||
* @param {number} maxWidth - 最大输出宽度,默认1920
|
||
* @returns {Promise<object>} - 返回包含水印图片路径和文件大小信息的对象
|
||
*/
|
||
export const addWatermark = async (imagePath, watermarkInfo, logoUrl = null, logoOpacity = 1, quality = null, maxWidth = 1920) => {
|
||
// #ifdef H5
|
||
// H5端水印实现
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
// 创建图片对象
|
||
const img = new Image();
|
||
img.crossOrigin = "anonymous";
|
||
|
||
img.onload = async () => {
|
||
try {
|
||
// 获取原图文件大小
|
||
let originalFileSize;
|
||
try {
|
||
originalFileSize = await getFileSize(imagePath);
|
||
console.log('原图文件大小:', originalFileSize.sizeKB, 'KB');
|
||
} catch (err) {
|
||
console.warn('获取原图文件大小失败,使用默认值:', err);
|
||
originalFileSize = {
|
||
size: 0,
|
||
sizeKB: "0.00",
|
||
sizeMB: "0.00"
|
||
};
|
||
}
|
||
|
||
// 计算压缩质量
|
||
const compressionQuality = quality !== null ? quality : calculateQuality(Number(originalFileSize.sizeKB));
|
||
console.log('使用压缩质量:', compressionQuality);
|
||
|
||
const { width, height } = img;
|
||
console.log('原图尺寸:', width, 'x', height);
|
||
|
||
// 计算输出尺寸
|
||
const outputSize = calculateOutputSize(width, height, maxWidth);
|
||
console.log('输出尺寸:', outputSize.width, 'x', outputSize.height);
|
||
|
||
// 创建canvas
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
canvas.width = outputSize.width;
|
||
canvas.height = outputSize.height;
|
||
|
||
// 绘制原图片到canvas
|
||
ctx.drawImage(img, 0, 0, outputSize.width, outputSize.height);
|
||
|
||
// 处理水印文本,超过25个字符换行,最多3行
|
||
const processText = (text, maxChars = 25, maxLines = 3) => {
|
||
if (text.length <= maxChars) {
|
||
return [text];
|
||
}
|
||
|
||
const lines = [];
|
||
let currentLine = '';
|
||
let lineCount = 0;
|
||
|
||
for (let i = 0; i < text.length && lineCount < maxLines; i++) {
|
||
currentLine += text[i];
|
||
|
||
if (currentLine.length >= maxChars || i === text.length - 1) {
|
||
lines.push(currentLine);
|
||
lineCount++;
|
||
currentLine = '';
|
||
|
||
// 如果还有剩余字符但已达到最大行数,添加省略号
|
||
if (lineCount === maxLines && i < text.length - 1) {
|
||
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -3) + '...';
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return lines;
|
||
};
|
||
|
||
// 水印内容
|
||
const watermarkText = [
|
||
`时间: ${watermarkInfo.time}`,
|
||
`经度: ${watermarkInfo.longitude}`,
|
||
`维度: ${watermarkInfo.latitude}`,
|
||
];
|
||
|
||
var location = processText("位置: " + watermarkInfo.location);
|
||
watermarkText.push(...location);
|
||
var department = processText("部门: " + watermarkInfo.department);
|
||
watermarkText.push(...department);
|
||
var status = processText("状态: " + watermarkInfo.status);
|
||
watermarkText.push(...status);
|
||
var workers = processText("人员: " + watermarkInfo.workers.join(', '));
|
||
watermarkText.push(...workers);
|
||
var remarks = processText("内容: " + watermarkInfo.remarks);
|
||
watermarkText.push(...remarks);
|
||
|
||
// 定义绘制文字水印的函数
|
||
const drawTextWatermark = async () => {
|
||
// 计算水印位置(左下角)
|
||
const padding = 30;
|
||
const fontSize = 30;
|
||
const lineHeight = 30;
|
||
const startX = padding;
|
||
const startY = outputSize.height - padding - (watermarkText.length * lineHeight);
|
||
|
||
// 设置文字样式
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||
ctx.font = `${fontSize}px Arial`;
|
||
ctx.textBaseline = 'top';
|
||
|
||
// 绘制文字
|
||
watermarkText.forEach((text, index) => {
|
||
ctx.fillText(text, startX, startY + (index * lineHeight));
|
||
});
|
||
|
||
// 如果有logo,在文字水印上方绘制logo
|
||
if (logoUrl) {
|
||
try {
|
||
// 使用缓存获取logo图片
|
||
const cachedLogoPath = await getCachedLogo(logoUrl);
|
||
|
||
// 创建logo图片对象
|
||
const logoImg = new Image();
|
||
logoImg.crossOrigin = "anonymous";
|
||
|
||
logoImg.onload = () => {
|
||
// 动态计算logo位置:在文字水印上方
|
||
const logoSize = 200; // logo大小
|
||
const logoPadding = 30;
|
||
const logoX = logoPadding;
|
||
// logo位置 = 文字水印起始位置 - logo高度 - 间距
|
||
const logoY = startY - logoSize - 10;
|
||
|
||
// 设置logo透明度
|
||
ctx.globalAlpha = logoOpacity;
|
||
|
||
// 绘制logo
|
||
ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize);
|
||
|
||
// 恢复透明度为1(不影响后续绘制)
|
||
ctx.globalAlpha = 1;
|
||
|
||
// 导出canvas为blob
|
||
canvas.toBlob((blob) => {
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
console.log('H5 水印图片导出成功:', objectUrl);
|
||
|
||
// 获取水印图片文件大小
|
||
const watermarkFileSize = {
|
||
size: blob.size,
|
||
sizeKB: (blob.size / 1024).toFixed(2),
|
||
sizeMB: (blob.size / (1024 * 1024)).toFixed(2)
|
||
};
|
||
|
||
resolve({
|
||
filePath: objectUrl,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileSize: watermarkFileSize
|
||
}
|
||
});
|
||
}, 'image/jpeg', compressionQuality);
|
||
};
|
||
|
||
logoImg.onerror = (err) => {
|
||
console.error('H5 logo加载失败:', err);
|
||
// logo加载失败时,只绘制文字水印
|
||
canvas.toBlob((blob) => {
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
console.log('H5 水印图片导出成功(logo加载失败):', objectUrl);
|
||
|
||
const watermarkFileSize = {
|
||
size: blob.size,
|
||
sizeKB: (blob.size / 1024).toFixed(2),
|
||
sizeMB: (blob.size / (1024 * 1024)).toFixed(2)
|
||
};
|
||
|
||
resolve({
|
||
filePath: objectUrl,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileSize: watermarkFileSize
|
||
}
|
||
});
|
||
}, 'image/jpeg', compressionQuality);
|
||
};
|
||
|
||
logoImg.src = cachedLogoPath;
|
||
} catch (err) {
|
||
console.error('H5 获取logo失败:', err);
|
||
// logo获取失败时,只绘制文字水印
|
||
canvas.toBlob((blob) => {
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
console.log('H5 水印图片导出成功(logo获取失败):', objectUrl);
|
||
|
||
const watermarkFileSize = {
|
||
size: blob.size,
|
||
sizeKB: (blob.size / 1024).toFixed(2),
|
||
sizeMB: (blob.size / (1024 * 1024)).toFixed(2)
|
||
};
|
||
|
||
resolve({
|
||
filePath: objectUrl,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileSize: watermarkFileSize
|
||
}
|
||
});
|
||
}, 'image/jpeg', compressionQuality);
|
||
}
|
||
} else {
|
||
// 没有logo时,只绘制文字水印
|
||
canvas.toBlob((blob) => {
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
console.log('H5 水印图片导出成功(无logo):', objectUrl);
|
||
|
||
const watermarkFileSize = {
|
||
size: blob.size,
|
||
sizeKB: (blob.size / 1024).toFixed(2),
|
||
sizeMB: (blob.size / (1024 * 1024)).toFixed(2)
|
||
};
|
||
|
||
resolve({
|
||
filePath: objectUrl,
|
||
originalSize: {
|
||
width: width,
|
||
height: height,
|
||
fileSize: originalFileSize
|
||
},
|
||
watermarkSize: {
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileSize: watermarkFileSize
|
||
}
|
||
});
|
||
}, 'image/jpeg', compressionQuality);
|
||
}
|
||
};
|
||
|
||
// 开始绘制文字水印
|
||
await drawTextWatermark();
|
||
} catch (error) {
|
||
console.error('H5 处理图片失败:', error);
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
img.onerror = (err) => {
|
||
console.error('H5 图片加载失败:', err);
|
||
reject(err);
|
||
};
|
||
|
||
img.src = imagePath;
|
||
} catch (error) {
|
||
console.error('H5 创建图片对象失败:', error);
|
||
reject(error);
|
||
}
|
||
});
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// App/小程序端水印实现(保持原有逻辑)
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
// 获取原图文件大小
|
||
const originalFileSize = await getFileSize(imagePath);
|
||
console.log('原图文件大小:', originalFileSize.sizeKB, 'KB');
|
||
|
||
// 计算压缩质量
|
||
const compressionQuality = quality !== null ? quality : calculateQuality(originalFileSize.sizeKB);
|
||
console.log('使用压缩质量:', compressionQuality);
|
||
|
||
// 获取图片信息
|
||
uni.getImageInfo({
|
||
src: imagePath,
|
||
success: (imageInfo) => {
|
||
const { width, height } = imageInfo;
|
||
console.log('原图尺寸:', width, 'x', height);
|
||
|
||
// 计算输出尺寸
|
||
const outputSize = calculateOutputSize(width, height, maxWidth);
|
||
console.log('输出尺寸:', outputSize.width, 'x', outputSize.height);
|
||
|
||
// 创建canvas上下文
|
||
const canvas = uni.createCanvasContext('watermarkCanvas');
|
||
|
||
// 清空canvas
|
||
canvas.clearRect(0, 0, outputSize.width, outputSize.height);
|
||
|
||
// 绘制原图片到canvas
|
||
canvas.drawImage(imagePath, 0, 0, outputSize.width, outputSize.height);
|
||
|
||
// 处理水印文本,超过25个字符换行,最多3行
|
||
const processText = (text, maxChars = 25, maxLines = 3) => {
|
||
if (text.length <= maxChars) {
|
||
return [text];
|
||
}
|
||
|
||
const lines = [];
|
||
let currentLine = '';
|
||
let lineCount = 0;
|
||
|
||
for (let i = 0; i < text.length && lineCount < maxLines; i++) {
|
||
currentLine += text[i];
|
||
|
||
if (currentLine.length >= maxChars || i === text.length - 1) {
|
||
lines.push(currentLine);
|
||
lineCount++;
|
||
currentLine = '';
|
||
|
||
// 如果还有剩余字符但已达到最大行数,添加省略号
|
||
if (lineCount === maxLines && i < text.length - 1) {
|
||
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -3) + '...';
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return lines;
|
||
};
|
||
|
||
// 水印内容
|
||
const watermarkText = [
|
||
`时间: ${watermarkInfo.time}`,
|
||
`经度: ${watermarkInfo.longitude}`,
|
||
`维度: ${watermarkInfo.latitude}`,
|
||
];
|
||
|
||
var location = processText("位置: " + watermarkInfo.location);
|
||
watermarkText.push(...location);
|
||
var department = processText("部门: " + watermarkInfo.department);
|
||
watermarkText.push(...department);
|
||
var status = processText("状态: " + watermarkInfo.status);
|
||
watermarkText.push(...status);
|
||
var workers = processText("人员: " + watermarkInfo.workers.join(', '));
|
||
watermarkText.push(...workers);
|
||
var remarks = processText("内容: " + watermarkInfo.remarks);
|
||
watermarkText.push(...remarks);
|
||
|
||
// 定义绘制文字水印的函数
|
||
const drawTextWatermark = () => {
|
||
// 计算水印位置(左下角)
|
||
const padding = 30;
|
||
const fontSize = 40;
|
||
const lineHeight = 50;
|
||
const startX = padding;
|
||
const startY = outputSize.height - padding - (watermarkText.length * lineHeight);
|
||
|
||
// 设置文字样式
|
||
canvas.setFillStyle('rgba(255, 255, 255, 1)');
|
||
canvas.setFontSize(fontSize);
|
||
canvas.setTextBaseline('top');
|
||
|
||
// 绘制文字
|
||
watermarkText.forEach((text, index) => {
|
||
canvas.fillText(text, startX, startY + (index * lineHeight));
|
||
});
|
||
|
||
// 如果有logo,在文字水印上方绘制logo
|
||
if (logoUrl) {
|
||
// 使用缓存获取logo图片
|
||
getCachedLogo(logoUrl).then((cachedLogoPath) => {
|
||
// 动态计算logo位置:在文字水印上方
|
||
const logoSize = 200; // logo大小
|
||
const logoPadding = 30;
|
||
const logoX = logoPadding;
|
||
// logo位置 = 文字水印起始位置 - logo高度 - 间距
|
||
const logoY = startY - logoSize - 10;
|
||
|
||
// 设置logo透明度
|
||
canvas.setGlobalAlpha(logoOpacity);
|
||
|
||
// 绘制logo
|
||
canvas.drawImage(cachedLogoPath, logoX, logoY, logoSize, logoSize);
|
||
|
||
// 恢复透明度为1(不影响后续绘制)
|
||
canvas.setGlobalAlpha(1);
|
||
|
||
// 绘制到canvas
|
||
canvas.draw(true, () => {
|
||
console.log('Canvas绘制完成(包含logo)');
|
||
// 延迟确保绘制完成
|
||
setTimeout(() => {
|
||
// 将canvas导出为图片
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'watermarkCanvas',
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileType: 'jpg', // 使用jpg格式,文件更小
|
||
quality: compressionQuality, // 使用动态计算的压缩质量
|
||
success: (res) => {
|
||
console.log('导出成功:', res.tempFilePath);
|
||
console.log('水印图片尺寸:', outputSize.width, 'x', outputSize.height);
|
||
handleWatermarkExport(res.tempFilePath, originalFileSize, outputSize.width, outputSize.height, resolve);
|
||
},
|
||
fail: (err) => {
|
||
console.error('导出图片失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
}, 100);
|
||
});
|
||
}).catch((err) => {
|
||
console.error('获取logo失败:', err);
|
||
// logo获取失败时,只绘制文字水印
|
||
canvas.draw(true, () => {
|
||
console.log('Canvas绘制完成(logo获取失败)');
|
||
// 延迟确保绘制完成
|
||
setTimeout(() => {
|
||
// 将canvas导出为图片
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'watermarkCanvas',
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileType: 'jpg', // 使用jpg格式,文件更小
|
||
quality: compressionQuality, // 使用动态计算的压缩质量
|
||
success: (res) => {
|
||
console.log('导出成功:', res.tempFilePath);
|
||
console.log('水印图片尺寸:', outputSize.width, 'x', outputSize.height);
|
||
handleWatermarkExport(res.tempFilePath, originalFileSize, outputSize.width, outputSize.height, resolve);
|
||
},
|
||
fail: (err) => {
|
||
console.error('导出图片失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
}, 100);
|
||
});
|
||
});
|
||
} else {
|
||
// 没有logo时,只绘制文字水印
|
||
canvas.draw(true, () => {
|
||
console.log('Canvas绘制完成(无logo)');
|
||
// 延迟确保绘制完成
|
||
setTimeout(() => {
|
||
// 将canvas导出为图片
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'watermarkCanvas',
|
||
width: outputSize.width,
|
||
height: outputSize.height,
|
||
fileType: 'jpg', // 使用jpg格式,文件更小
|
||
quality: compressionQuality, // 使用动态计算的压缩质量
|
||
success: (res) => {
|
||
console.log('导出成功:', res.tempFilePath);
|
||
console.log('水印图片尺寸:', outputSize.width, 'x', outputSize.height);
|
||
handleWatermarkExport(res.tempFilePath, originalFileSize, outputSize.width, outputSize.height, resolve);
|
||
},
|
||
fail: (err) => {
|
||
console.error('导出图片失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
}, 100);
|
||
});
|
||
}
|
||
};
|
||
|
||
// 开始绘制文字水印
|
||
drawTextWatermark();
|
||
},
|
||
fail: (err) => {
|
||
console.error('获取图片信息失败:', err);
|
||
reject(err);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('处理图片失败:', error);
|
||
reject(error);
|
||
}
|
||
});
|
||
// #endif
|
||
}
|
||
|
||
/**
|
||
* 保存图片到相册(兼容 H5 / App / 小程序)
|
||
* @param {string} image - 图片路径或 URL
|
||
* @returns {Promise<boolean>} 是否保存成功
|
||
*/
|
||
export const saveImageToPhotosAlbum = (image) => {
|
||
return new Promise((resolve, reject) => {
|
||
// #ifdef H5
|
||
try {
|
||
// 创建一个隐藏的 a 标签,触发下载
|
||
const link = document.createElement("a");
|
||
link.href = image;
|
||
// 如果 image 是 blob url 或 http url,都可以
|
||
link.download = "image_" + Date.now(); // 下载文件名
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
console.log("H5 触发浏览器下载成功");
|
||
resolve(true);
|
||
} catch (err) {
|
||
console.error("H5 保存图片失败:", err);
|
||
reject(err);
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
uni.saveImageToPhotosAlbum({
|
||
filePath: image,
|
||
success: () => {
|
||
console.log("图片保存到相册成功");
|
||
resolve(true);
|
||
},
|
||
fail: (err) => {
|
||
console.error("保存到相册失败:", err);
|
||
if (err.errMsg && err.errMsg.includes("auth deny")) {
|
||
uni.showModal({
|
||
title: "提示",
|
||
content: "需要您授权保存图片到相册,请在设置中开启相册权限",
|
||
showCancel: false,
|
||
confirmText: "知道了",
|
||
});
|
||
}
|
||
resolve(false);
|
||
},
|
||
});
|
||
// #endif
|
||
});
|
||
};
|
||
/**
|
||
* 图片转Base64(兼容小程序、App、H5)
|
||
* @param {string} filePath - 图片路径(小程序临时路径 / H5本地URL / App本地路径)
|
||
* @returns {Promise<string>} Base64字符串(带data:image/前缀)
|
||
*/
|
||
export const imageToBase64 = (filePath) => {
|
||
return new Promise((resolve, reject) => {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序环境
|
||
uni.getImageInfo({
|
||
src: filePath,
|
||
success: (info) => {
|
||
const type = info.type || 'jpeg';
|
||
uni.getFileSystemManager().readFile({
|
||
filePath,
|
||
encoding: 'base64',
|
||
success: (res) => resolve(`data:image/${type};base64,${res.data}`),
|
||
fail: reject
|
||
});
|
||
},
|
||
fail: reject
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
// App 环境
|
||
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
|
||
entry.file((file) => {
|
||
const reader = new plus.io.FileReader();
|
||
reader.onloadend = (e) => {
|
||
resolve(e.target.result); // 已经是 data:image/...;base64,...
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}, reject);
|
||
}, reject);
|
||
// #endif
|
||
|
||
// #ifdef H5
|
||
// H5 环境
|
||
const img = new Image();
|
||
img.crossOrigin = "anonymous";
|
||
|
||
// 先根据文件地址后缀判断类型
|
||
let mimeType = "image/jpeg"; // 默认
|
||
const match = filePath.match(/\.(png|jpe?g|gif|bmp|webp|svg)(\?.*)?$/i);
|
||
if (match) {
|
||
mimeType = `image/${match[1].toLowerCase()}`;
|
||
// jpg -> image/jpeg
|
||
if (mimeType === "image/jpg") mimeType = "image/jpeg";
|
||
}
|
||
|
||
img.src = filePath;
|
||
img.onload = () => {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
const ctx = canvas.getContext("2d");
|
||
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||
const dataURL = canvas.toDataURL(mimeType, 1.0);
|
||
resolve(dataURL);
|
||
};
|
||
img.onerror = reject;
|
||
// #endif
|
||
|
||
});
|
||
}; |