123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- import { store } from "@/store/store";
- import { PictureSlice, Task } from "@/types";
- // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因
- // 通过回看的测试,打开回看,再关闭回看,稍等一会儿再打开回看,确实可以看到该缓存时缓存了,该丢弃时丢弃了
- // 把store.currentTask当做 weakRef ,当它不存在时,就丢弃它所有的图片
- // const weakedMapImages = new WeakMap<Object, Map<string, HTMLImageElement>>();
- /**
- * 异步获取图片
- * @param url 完整的图片路径
- * @returns Promise<HTMLImageElement>
- */
- export async function loadImage(url: string): Promise<HTMLImageElement> {
- // 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<string, HTMLImageElement>();
- // weakedMapImages.set(store.currentTask, imagesCache);
- // }
- // imagesCache.set(url, image);
- // }
- resolve(image);
- };
- image.onerror = reject;
- });
- }
- // 存放裁切图的ObjectUrls
- let objectUrlMap = new Map<string, string>();
- 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(<Blob>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(<Blob>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;
- }
|