utils.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import { store } from "@/store/store";
  2. import { PictureSlice, Task } from "@/types";
  3. // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因
  4. // 通过回看的测试,打开回看,再关闭回看,稍等一会儿再打开回看,确实可以看到该缓存时缓存了,该丢弃时丢弃了
  5. // 把store.currentTask当做 weakRef ,当它不存在时,就丢弃它所有的图片
  6. // const weakedMapImages = new WeakMap<Object, Map<string, HTMLImageElement>>();
  7. /**
  8. * 异步获取图片
  9. * @param url 完整的图片路径
  10. * @returns Promise<HTMLImageElement>
  11. */
  12. export async function loadImage(url: string): Promise<HTMLImageElement> {
  13. // if (store.currentTask && weakedMapImages.get(store.currentTask)) {
  14. // const imagesCache = weakedMapImages.get(store.currentTask);
  15. // if (imagesCache) {
  16. // // console.log("cached image", url);
  17. // const image = imagesCache.get(url);
  18. // if (image) return image;
  19. // }
  20. // }
  21. // else loading image
  22. return new Promise((resolve, reject) => {
  23. const image = new Image();
  24. image.setAttribute("crossorigin", "anonymous");
  25. image.src = url;
  26. image.onload = () => {
  27. // if (store.currentTask) {
  28. // let imagesCache = weakedMapImages.get(store.currentTask);
  29. // if (!imagesCache) {
  30. // imagesCache = new Map<string, HTMLImageElement>();
  31. // weakedMapImages.set(store.currentTask, imagesCache);
  32. // }
  33. // imagesCache.set(url, image);
  34. // }
  35. resolve(image);
  36. };
  37. image.onerror = reject;
  38. });
  39. }
  40. // 存放裁切图的ObjectUrls
  41. let objectUrlMap = new Map<string, string>();
  42. const OBJECT_URLS_MAP_MAX_SIZE =
  43. window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100;
  44. export async function getDataUrlForSliceConfig(
  45. image: HTMLImageElement,
  46. sliceConfig: PictureSlice,
  47. maxSliceWidth: number,
  48. urlForCache: string
  49. ) {
  50. const { i, x, y, w, h } = sliceConfig;
  51. const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`;
  52. if (objectUrlMap.get(key)) {
  53. console.log("cached slice objectUrl");
  54. return objectUrlMap.get(key);
  55. }
  56. const canvas = document.createElement("canvas");
  57. canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
  58. canvas.height = sliceConfig.h;
  59. const ctx = canvas.getContext("2d");
  60. if (!ctx) {
  61. console.log('canvas.getContext("2d") error');
  62. throw new Error("canvas ctx error");
  63. }
  64. // drawImage 画图软件透明色
  65. ctx.drawImage(
  66. image,
  67. sliceConfig.x,
  68. sliceConfig.y,
  69. sliceConfig.w,
  70. sliceConfig.h,
  71. 0,
  72. 0,
  73. sliceConfig.w,
  74. sliceConfig.h
  75. );
  76. // console.log(image, canvas.height, sliceConfig, ctx);
  77. // console.log(canvas.toDataURL());
  78. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  79. // const dataurl = canvas.toDataURL();
  80. const blob: Blob = await new Promise((res) => {
  81. canvas.toBlob((b) => res(<Blob>b));
  82. });
  83. const dataurl = URL.createObjectURL(blob);
  84. cacheFIFO();
  85. objectUrlMap.set(key, dataurl);
  86. return dataurl;
  87. }
  88. // 清理缓存的过时数据(清除头10张),First in first out
  89. function cacheFIFO() {
  90. if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) {
  91. const ary = [...objectUrlMap.entries()];
  92. const toRelease = ary.splice(0, 10);
  93. // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke
  94. // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix
  95. for (const u of toRelease) {
  96. // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中
  97. if (document.querySelector(`img[src="${u[1]}"]`)) {
  98. ary.push(u);
  99. } else {
  100. // console.log("revoke ", u[1]);
  101. URL.revokeObjectURL(u[1]);
  102. }
  103. }
  104. objectUrlMap = new Map(ary);
  105. }
  106. }
  107. export async function getDataUrlForSplitConfig(
  108. image: HTMLImageElement,
  109. config: [number, number],
  110. maxSliceWidth: number,
  111. urlForCache: string
  112. ) {
  113. const [start, end] = config;
  114. const key = `${urlForCache}-${start}-${end}`;
  115. if (objectUrlMap.get(key)) {
  116. console.log("cached split objectUrl");
  117. return objectUrlMap.get(key);
  118. }
  119. const width = image.naturalWidth * (end - start);
  120. const canvas = document.createElement("canvas");
  121. canvas.width = Math.max(width, maxSliceWidth);
  122. canvas.height = image.naturalHeight;
  123. const ctx = canvas.getContext("2d");
  124. if (!ctx) {
  125. console.log('canvas.getContext("2d") error');
  126. throw new Error("canvas ctx error");
  127. }
  128. // drawImage 画图软件透明色
  129. ctx.drawImage(
  130. image,
  131. image.naturalWidth * start,
  132. 0,
  133. image.naturalWidth * end,
  134. image.naturalHeight,
  135. 0,
  136. 0,
  137. image.naturalWidth * end,
  138. image.naturalHeight
  139. );
  140. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  141. // const dataurl = canvas.toDataURL();
  142. const blob: Blob = await new Promise((res) => {
  143. canvas.toBlob((b) => res(<Blob>b));
  144. });
  145. const dataurl = URL.createObjectURL(blob);
  146. cacheFIFO();
  147. objectUrlMap.set(key, dataurl);
  148. return dataurl;
  149. }
  150. export async function preDrawImage(_currentTask: Task) {
  151. if (!_currentTask?.libraryId) return;
  152. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  153. const hasSliceConfig = store.currentTask?.sliceConfig?.length;
  154. const images = [];
  155. if (hasSliceConfig) {
  156. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  157. const sliceNum = _currentTask.sliceUrls.length;
  158. if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  159. console.warn("裁切图设置的数量小于该学生的总图片数量");
  160. }
  161. _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
  162. (v) => v.i <= sliceNum
  163. );
  164. for (const sliceConfig of _currentTask.sliceConfig) {
  165. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  166. const image = await loadImage(url);
  167. images[sliceConfig.i] = image;
  168. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  169. // 选择整图时,w/h 为0
  170. sliceConfig.w = image.naturalWidth;
  171. sliceConfig.h = image.naturalHeight;
  172. }
  173. }
  174. maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
  175. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  176. for (const sliceConfig of _currentTask.sliceConfig) {
  177. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  178. const image = images[sliceConfig.i];
  179. try {
  180. await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url);
  181. } catch (error) {
  182. console.log("preDrawImage failed: ", error);
  183. }
  184. }
  185. } else {
  186. for (const url of _currentTask.sliceUrls) {
  187. const image = await loadImage(url);
  188. images.push(image);
  189. }
  190. const splitConfigPairs = store.setting.splitConfig
  191. .map<[number, number]>((v, index, ary) =>
  192. index % 2 === 0 ? [v, ary[index + 1]] : [0, 0]
  193. )
  194. .filter((v) => v[0] > 0 && v[1] > 0);
  195. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  196. maxSliceWidth =
  197. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  198. for (const url of _currentTask.sliceUrls) {
  199. for (const config of splitConfigPairs) {
  200. const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
  201. const image = images[indexInSliceUrls - 1];
  202. try {
  203. await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
  204. } catch (error) {
  205. console.log("preDrawImage failed: ", error);
  206. }
  207. }
  208. }
  209. }
  210. }
  211. export function addFileServerPrefixToTask(rawTask: Task): Task {
  212. const newTask = JSON.parse(JSON.stringify(rawTask)) as Task;
  213. const fileServer = store.setting.fileServer;
  214. newTask.sliceUrls = newTask.sliceUrls?.map((s) => fileServer + s);
  215. newTask.sheetUrls = newTask.sheetUrls?.map((s) => fileServer + s);
  216. newTask.jsonUrl = fileServer + newTask.jsonUrl;
  217. return newTask;
  218. }