import { store } from "@/store/store"; import { PictureSlice, Task } from "@/types"; // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因 // 通过回看的测试,打开回看,再关闭回看,稍等一会儿再打开回看,确实可以看到该缓存时缓存了,该丢弃时丢弃了 // 把store.currentTask当做 weakRef ,当它不存在时,就丢弃它所有的图片 // const weakedMapImages = new WeakMap>(); /** * 异步获取图片 * @param url 完整的图片路径 * @returns Promise */ export async function loadImage(url: string): Promise { return new Promise((resolve, reject) => { const image = new Image(); image.setAttribute("crossorigin", "anonymous"); image.src = url; image.onload = () => { resolve(image); }; image.onerror = () => { reject("图片载入错误"); }; }); } // 存放裁切图的ObjectUrls let objectUrlMap = new Map(); const OBJECT_URLS_MAP_MAX_SIZE = window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100; export async function getDataUrlForSliceConfig( image: HTMLImageElement, sliceConfig: PictureSlice, maxSliceWidth: number, urlForCache: string ) { const { i, x, y, w, h } = sliceConfig; const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`; if (objectUrlMap.get(key)) { console.log("cached slice objectUrl"); return objectUrlMap.get(key); } const canvas = document.createElement("canvas"); canvas.width = Math.max(sliceConfig.w, maxSliceWidth); canvas.height = sliceConfig.h; const ctx = canvas.getContext("2d"); if (!ctx) { console.log('canvas.getContext("2d") error'); throw new Error("canvas ctx error"); } // drawImage 画图软件透明色 ctx.drawImage( image, sliceConfig.x, sliceConfig.y, sliceConfig.w, sliceConfig.h, 0, 0, sliceConfig.w, sliceConfig.h ); // console.log(image, canvas.height, sliceConfig, ctx); // console.log(canvas.toDataURL()); // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放 // const dataurl = canvas.toDataURL(); const blob: Blob = await new Promise((res) => { canvas.toBlob((b) => res(b)); }); const dataurl = URL.createObjectURL(blob); cacheFIFO(); objectUrlMap.set(key, dataurl); return dataurl; } // 清理缓存的过时数据(清除头10张),First in first out function cacheFIFO() { if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) { const ary = [...objectUrlMap.entries()]; const toRelease = ary.splice(0, 10); // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix for (const u of toRelease) { // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中 if (document.querySelector(`img[src="${u[1]}"]`)) { ary.push(u); } else { // console.log("revoke ", u[1]); URL.revokeObjectURL(u[1]); } } objectUrlMap = new Map(ary); } } export async function getDataUrlForSplitConfig( image: HTMLImageElement, config: [number, number], maxSliceWidth: number, urlForCache: string ) { const [start, end] = config; const key = `${urlForCache}-${start}-${end}`; if (objectUrlMap.get(key)) { console.log("cached split objectUrl"); return objectUrlMap.get(key); } const width = image.naturalWidth * (end - start); const canvas = document.createElement("canvas"); canvas.width = Math.max(width, maxSliceWidth); canvas.height = image.naturalHeight; const ctx = canvas.getContext("2d"); if (!ctx) { console.log('canvas.getContext("2d") error'); throw new Error("canvas ctx error"); } // drawImage 画图软件透明色 ctx.drawImage( image, image.naturalWidth * start, 0, image.naturalWidth * end, image.naturalHeight, 0, 0, image.naturalWidth * end, image.naturalHeight ); // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放 // const dataurl = canvas.toDataURL(); const blob: Blob = await new Promise((res) => { canvas.toBlob((b) => res(b)); }); const dataurl = URL.createObjectURL(blob); cacheFIFO(); objectUrlMap.set(key, dataurl); return dataurl; } export async function getDataUrlForCoverConfig( image: HTMLImageElement, configs: PictureSlice[] ) { const key = `${image.src}`; if (objectUrlMap.get(key)) { return objectUrlMap.get(key); } const canvas = document.createElement("canvas"); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; const ctx = canvas.getContext("2d"); if (!ctx) { console.log('canvas.getContext("2d") error'); throw new Error("canvas ctx error"); } ctx.drawImage(image, 0, 0); ctx.fillStyle = "#ffffff"; configs.forEach((config) => { ctx.fillRect(config.x, config.y, config.w, config.h); }); const blob: Blob = await new Promise((res) => { canvas.toBlob((b) => res(b)); }); const dataurl = URL.createObjectURL(blob); cacheFIFO(); objectUrlMap.set(key, dataurl); return dataurl; } export async function preDrawImage(_currentTask: Task | undefined) { // console.log("preDrawImage=>curTask:", _currentTask); if (!_currentTask?.taskId) return; let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准 // const hasSliceConfig = store.currentTask?.sliceConfig?.length; const hasSliceConfig = _currentTask?.sliceConfig?.length; const images = []; if (hasSliceConfig) { // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度 const sliceNum = _currentTask.sliceUrls.length; if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) { console.warn("裁切图设置的数量小于该学生的总图片数量"); } _currentTask.sliceConfig = _currentTask.sliceConfig.filter( (v) => v.i <= sliceNum ); for (const sliceConfig of _currentTask.sliceConfig) { const url = _currentTask.sliceUrls[sliceConfig.i - 1]; const image = await loadImage(url); images[sliceConfig.i] = image; const { x, y, w, h } = sliceConfig; x < 0 && (sliceConfig.x = 0); y < 0 && (sliceConfig.y = 0); if (sliceConfig.w === 0 && sliceConfig.h === 0) { // 选择整图时,w/h 为0 sliceConfig.w = image.naturalWidth; sliceConfig.h = image.naturalHeight; } if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) { sliceConfig.x = image.naturalWidth * x; sliceConfig.y = image.naturalHeight * y; sliceConfig.w = image.naturalWidth * w; sliceConfig.h = image.naturalHeight * h; } } maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w)); // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围 for (const sliceConfig of _currentTask.sliceConfig) { const url = _currentTask.sliceUrls[sliceConfig.i - 1]; const image = images[sliceConfig.i]; try { await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url); } catch (error) { console.log("preDrawImage failed: ", error); } } } else { for (const url of _currentTask.sliceUrls) { const image = await loadImage(url); images.push(image); } const splitConfigPairs = store.setting.splitConfig.reduce< [number, number][] >((a, v, index) => { index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v); return a; }, []); const maxSplitConfig = Math.max(...store.setting.splitConfig); maxSliceWidth = Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig; for (const url of _currentTask.sliceUrls) { for (const config of splitConfigPairs) { const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1; const image = images[indexInSliceUrls - 1]; try { await getDataUrlForSplitConfig(image, config, maxSliceWidth, url); } catch (error) { console.log("preDrawImage failed: ", error); } } } } } export async function processSliceUrls(_currentTask: Task | undefined) { if (!_currentTask?.taskId) return; const getNum = (num) => Math.max(Math.min(1, num), 0); const sheetUrls = _currentTask.sheetUrls || []; const sheetConfig = store.setting.sheetConfig || []; const urls = []; for (let i = 0; i < sheetUrls.length; i++) { const url = sheetUrls[i]; const configs = sheetConfig.filter((item) => item.i === i + 1); if (!configs.length) { urls[i] = url; continue; } const image = await loadImage(url); configs.forEach((item) => { item.x = image.naturalWidth * getNum(item.x); item.y = image.naturalHeight * getNum(item.y); item.w = image.naturalWidth * getNum(item.w); item.h = image.naturalHeight * getNum(item.h); }); urls[i] = await getDataUrlForCoverConfig(image, configs); } return urls; } export async function preDrawImageHistory(_currentTask: Task | undefined) { console.log("preDrawImageHistory=>curTask:", _currentTask); if (!_currentTask?.taskId) return; let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准 // const hasSliceConfig = store.currentTask?.sliceConfig?.length; const hasSliceConfig = _currentTask?.sliceConfig?.length; _currentTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"]; _currentTask.sliceUrls = await processSliceUrls(_currentTask); const images = []; if (hasSliceConfig) { // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度 const sliceNum = _currentTask.sliceUrls.length; if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) { console.warn("裁切图设置的数量小于该学生的总图片数量"); } _currentTask.sliceConfig = _currentTask.sliceConfig.filter( (v) => v.i <= sliceNum ); for (const sliceConfig of _currentTask.sliceConfig) { const url = _currentTask.sliceUrls[sliceConfig.i - 1]; const image = await loadImage(url); images[sliceConfig.i] = image; const { x, y, w, h } = sliceConfig; x < 0 && (sliceConfig.x = 0); y < 0 && (sliceConfig.y = 0); if (sliceConfig.w === 0 && sliceConfig.h === 0) { // 选择整图时,w/h 为0 sliceConfig.w = image.naturalWidth; sliceConfig.h = image.naturalHeight; } if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) { sliceConfig.x = image.naturalWidth * x; sliceConfig.y = image.naturalHeight * y; sliceConfig.w = image.naturalWidth * w; sliceConfig.h = image.naturalHeight * h; } } maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w)); // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围 for (const sliceConfig of _currentTask.sliceConfig) { const url = _currentTask.sliceUrls[sliceConfig.i - 1]; const image = images[sliceConfig.i]; try { await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url); } catch (error) { console.log("preDrawImage failed: ", error); } } } else { for (const url of _currentTask.sliceUrls) { const image = await loadImage(url); images.push(image); } const splitConfigPairs = store.setting.splitConfig.reduce< [number, number][] >((a, v, index) => { index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v); return a; }, []); const maxSplitConfig = Math.max(...store.setting.splitConfig); maxSliceWidth = Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig; for (const url of _currentTask.sliceUrls) { for (const config of splitConfigPairs) { const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1; const image = images[indexInSliceUrls - 1]; try { await getDataUrlForSplitConfig(image, config, maxSliceWidth, url); } catch (error) { console.log("preDrawImage failed: ", error); } } } } } export function addFileServerPrefixToTask(rawTask: Task): Task { const newTask = JSON.parse(JSON.stringify(rawTask)) as Task; return newTask; } export function addHeaderTrackColorAttr(headerTrack: any): any { return headerTrack.map((item: any) => { item.color = "green"; return item; }); } /** * 获取随机code,默认获取16位 * @param {Number} len 推荐8的倍数 * */ export function randomCode(len = 16): string { if (len <= 0) return; const steps = Math.ceil(len / 8); const stepNums = []; for (let i = 0; i < steps; i++) { const ranNum = Math.random().toString(32).slice(-8); stepNums.push(ranNum); } return stepNums.join(""); } /** * 解析url获取指定参数值 * @param urlStr url * @param paramName 参数名 * @returns 参数值 */ export function parseHrefParam( urlStr: string, paramName: string = null ): string | object { if (!urlStr) return; const url = new URL(urlStr); const urlParams = new URLSearchParams(url.search); if (paramName) return urlParams.get(paramName); const params = {}; for (const kv of urlParams.entries()) { params[kv[0]] = kv[1]; } return params; } /** * 计算总数 * @param {Array} dataList 需要统计的数组 */ export function calcSum(dataList) { if (!dataList.length) return 0; return dataList.reduce(function (total, item) { return total + item; }, 0); }