import { useMarkStore } from "@/store"; import { PictureSlice, Task } from "@/types"; import { loadImage } from "@/utils/utils"; // 存放裁切图的ObjectUrls let objectUrlMap = new Map(); const OBJECT_URLS_MAP_MAX_SIZE = window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100; // 清理缓存的过时数据(清除头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 { URL.revokeObjectURL(u[1]); } } objectUrlMap = new Map(ary); } } export default function useDraw() { const markStore = useMarkStore(); 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; } 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; } async function getDataUrlForCoverConfig( image: HTMLImageElement, configs: PictureSlice[] ) { const key = `${image.src}-slice`; 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; } async function preDrawImage(_currentTask: Task | undefined) { // console.log("preDrawImage=>curTask:", _currentTask); if (!_currentTask) return; let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准 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 = markStore.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(...markStore.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); } } } } } async function processSliceUrls(_currentTask: Task | undefined) { if (!_currentTask) return; const getNum = (num) => Math.max(Math.min(1, num), 0); const sheetUrls = _currentTask.sheetUrls || []; const sheetConfig = (markStore.setting.sheetConfig || []).map((item) => { return { ...item }; }); 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; } async function preDrawImageHistory(_currentTask: Task | undefined) { // console.log("preDrawImageHistory=>curTask:", _currentTask); if (!_currentTask) return; let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准 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 = markStore.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(...markStore.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); } } } } } return { getDataUrlForSliceConfig, getDataUrlForSplitConfig, getDataUrlForCoverConfig, preDrawImage, processSliceUrls, preDrawImageHistory, }; }