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 { // if (store.currentTask && weakedMapImages.get(store.currentTask)) { // const imagesCache = weakedMapImages.get(store.currentTask); // if (imagesCache) { // // console.log("cached image", url); // const image = imagesCache.get(url); // if (image) return image; // } // } // else loading image return new Promise((resolve, reject) => { const image = new Image(); image.setAttribute("crossorigin", "anonymous"); image.src = url; image.onload = () => { // if (store.currentTask) { // let imagesCache = weakedMapImages.get(store.currentTask); // if (!imagesCache) { // imagesCache = new Map(); // weakedMapImages.set(store.currentTask, imagesCache); // } // imagesCache.set(url, image); // } 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 preDrawImage(_currentTask: Task) { if (!_currentTask?.libraryId) return; let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准 const hasSliceConfig = store.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; if (sliceConfig.w === 0 && sliceConfig.h === 0) { // 选择整图时,w/h 为0 sliceConfig.w = image.naturalWidth; sliceConfig.h = image.naturalHeight; } } 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 .map<[number, number]>((v, index, ary) => index % 2 === 0 ? [v, ary[index + 1]] : [0, 0] ) .filter((v) => v[0] > 0 && v[1] > 0); 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; const fileServer = store.setting.fileServer; newTask.sliceUrls = newTask.sliceUrls?.map((s) => fileServer + s); newTask.sheetUrls = newTask.sheetUrls?.map((s) => fileServer + s); newTask.jsonUrl = fileServer + newTask.jsonUrl; return newTask; }