useDraw.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import { useMarkStore } from "@/store";
  2. import { PictureSlice, Task } from "@/types";
  3. import { loadImage } from "@/utils/utils";
  4. // 存放裁切图的ObjectUrls
  5. let objectUrlMap = new Map<string, string>();
  6. const OBJECT_URLS_MAP_MAX_SIZE =
  7. window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100;
  8. // 清理缓存的过时数据(清除头10张),First in first out
  9. function cacheFIFO() {
  10. if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) {
  11. const ary = [...objectUrlMap.entries()];
  12. const toRelease = ary.splice(0, 10);
  13. // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke
  14. // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix
  15. for (const u of toRelease) {
  16. // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中
  17. if (document.querySelector(`img[src="${u[1]}"]`)) {
  18. ary.push(u);
  19. } else {
  20. URL.revokeObjectURL(u[1]);
  21. }
  22. }
  23. objectUrlMap = new Map(ary);
  24. }
  25. }
  26. export default function useDraw() {
  27. const markStore = useMarkStore();
  28. async function getDataUrlForSliceConfig(
  29. image: HTMLImageElement,
  30. sliceConfig: PictureSlice,
  31. maxSliceWidth: number,
  32. urlForCache: string
  33. ) {
  34. const { i, x, y, w, h } = sliceConfig;
  35. const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`;
  36. if (objectUrlMap.get(key)) {
  37. // console.log("cached slice objectUrl");
  38. return objectUrlMap.get(key);
  39. }
  40. const canvas = document.createElement("canvas");
  41. canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
  42. canvas.height = sliceConfig.h;
  43. const ctx = canvas.getContext("2d");
  44. if (!ctx) {
  45. console.log('canvas.getContext("2d") error');
  46. throw new Error("canvas ctx error");
  47. }
  48. // drawImage 画图软件透明色
  49. ctx.drawImage(
  50. image,
  51. sliceConfig.x,
  52. sliceConfig.y,
  53. sliceConfig.w,
  54. sliceConfig.h,
  55. 0,
  56. 0,
  57. sliceConfig.w,
  58. sliceConfig.h
  59. );
  60. // console.log(image, canvas.height, sliceConfig, ctx);
  61. // console.log(canvas.toDataURL());
  62. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  63. // const dataurl = canvas.toDataURL();
  64. const blob: Blob = await new Promise((res) => {
  65. canvas.toBlob((b) => res(b));
  66. });
  67. const dataurl = URL.createObjectURL(blob);
  68. cacheFIFO();
  69. objectUrlMap.set(key, dataurl);
  70. return dataurl;
  71. }
  72. async function getDataUrlForSplitConfig(
  73. image: HTMLImageElement,
  74. config: [number, number],
  75. maxSliceWidth: number,
  76. urlForCache: string
  77. ) {
  78. const [start, end] = config;
  79. const key = `${urlForCache}-${start}-${end}`;
  80. if (objectUrlMap.get(key)) {
  81. console.log("cached split objectUrl");
  82. return objectUrlMap.get(key);
  83. }
  84. const width = image.naturalWidth * (end - start);
  85. const canvas = document.createElement("canvas");
  86. canvas.width = Math.max(width, maxSliceWidth);
  87. canvas.height = image.naturalHeight;
  88. const ctx = canvas.getContext("2d");
  89. if (!ctx) {
  90. console.log('canvas.getContext("2d") error');
  91. throw new Error("canvas ctx error");
  92. }
  93. // drawImage 画图软件透明色
  94. ctx.drawImage(
  95. image,
  96. image.naturalWidth * start,
  97. 0,
  98. image.naturalWidth * end,
  99. image.naturalHeight,
  100. 0,
  101. 0,
  102. image.naturalWidth * end,
  103. image.naturalHeight
  104. );
  105. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  106. // const dataurl = canvas.toDataURL();
  107. const blob: Blob = await new Promise((res) => {
  108. canvas.toBlob((b) => res(b));
  109. });
  110. const dataurl = URL.createObjectURL(blob);
  111. cacheFIFO();
  112. objectUrlMap.set(key, dataurl);
  113. return dataurl;
  114. }
  115. async function getDataUrlForCoverConfig(
  116. image: HTMLImageElement,
  117. configs: PictureSlice[]
  118. ) {
  119. const key = `${image.src}-slice`;
  120. if (objectUrlMap.get(key)) {
  121. return objectUrlMap.get(key);
  122. }
  123. const canvas = document.createElement("canvas");
  124. canvas.width = image.naturalWidth;
  125. canvas.height = image.naturalHeight;
  126. const ctx = canvas.getContext("2d");
  127. if (!ctx) {
  128. console.log('canvas.getContext("2d") error');
  129. throw new Error("canvas ctx error");
  130. }
  131. ctx.drawImage(image, 0, 0);
  132. ctx.fillStyle = "#ffffff";
  133. configs.forEach((config) => {
  134. ctx.fillRect(config.x, config.y, config.w, config.h);
  135. });
  136. const blob: Blob = await new Promise((res) => {
  137. canvas.toBlob((b) => res(b));
  138. });
  139. const dataurl = URL.createObjectURL(blob);
  140. cacheFIFO();
  141. objectUrlMap.set(key, dataurl);
  142. return dataurl;
  143. }
  144. async function preDrawImage(_currentTask: Task | undefined) {
  145. // console.log("preDrawImage=>curTask:", _currentTask);
  146. if (!_currentTask) return;
  147. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  148. const hasSliceConfig = _currentTask?.sliceConfig?.length;
  149. const images = [];
  150. if (hasSliceConfig) {
  151. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  152. const sliceNum = _currentTask.sliceUrls.length;
  153. if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  154. console.warn("裁切图设置的数量小于该学生的总图片数量");
  155. }
  156. _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
  157. (v) => v.i <= sliceNum
  158. );
  159. for (const sliceConfig of _currentTask.sliceConfig) {
  160. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  161. const image = await loadImage(url);
  162. images[sliceConfig.i] = image;
  163. const { x, y, w, h } = sliceConfig;
  164. x < 0 && (sliceConfig.x = 0);
  165. y < 0 && (sliceConfig.y = 0);
  166. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  167. // 选择整图时,w/h 为0
  168. sliceConfig.w = image.naturalWidth;
  169. sliceConfig.h = image.naturalHeight;
  170. }
  171. if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
  172. sliceConfig.x = image.naturalWidth * x;
  173. sliceConfig.y = image.naturalHeight * y;
  174. sliceConfig.w = image.naturalWidth * w;
  175. sliceConfig.h = image.naturalHeight * h;
  176. }
  177. }
  178. maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
  179. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  180. for (const sliceConfig of _currentTask.sliceConfig) {
  181. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  182. const image = images[sliceConfig.i];
  183. try {
  184. await getDataUrlForSliceConfig(
  185. image,
  186. sliceConfig,
  187. maxSliceWidth,
  188. url
  189. );
  190. } catch (error) {
  191. console.log("preDrawImage failed: ", error);
  192. }
  193. }
  194. } else {
  195. for (const url of _currentTask.sliceUrls) {
  196. const image = await loadImage(url);
  197. images.push(image);
  198. }
  199. const splitConfigPairs = markStore.setting.splitConfig.reduce<
  200. [number, number][]
  201. >((a, v, index) => {
  202. index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
  203. return a;
  204. }, []);
  205. const maxSplitConfig = Math.max(...markStore.setting.splitConfig);
  206. maxSliceWidth =
  207. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  208. for (const url of _currentTask.sliceUrls) {
  209. for (const config of splitConfigPairs) {
  210. const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
  211. const image = images[indexInSliceUrls - 1];
  212. try {
  213. await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
  214. } catch (error) {
  215. console.log("preDrawImage failed: ", error);
  216. }
  217. }
  218. }
  219. }
  220. }
  221. async function processSliceUrls(_currentTask: Task | undefined) {
  222. if (!_currentTask) return;
  223. const getNum = (num) => Math.max(Math.min(1, num), 0);
  224. const sheetUrls = _currentTask.sheetUrls || [];
  225. const sheetConfig = (markStore.setting.sheetConfig || []).map((item) => {
  226. return { ...item };
  227. });
  228. const urls = [];
  229. for (let i = 0; i < sheetUrls.length; i++) {
  230. const url = sheetUrls[i];
  231. const configs = sheetConfig.filter((item) => item.i === i + 1);
  232. if (!configs.length) {
  233. urls[i] = url;
  234. continue;
  235. }
  236. const image = await loadImage(url);
  237. configs.forEach((item) => {
  238. item.x = image.naturalWidth * getNum(item.x);
  239. item.y = image.naturalHeight * getNum(item.y);
  240. item.w = image.naturalWidth * getNum(item.w);
  241. item.h = image.naturalHeight * getNum(item.h);
  242. });
  243. urls[i] = await getDataUrlForCoverConfig(image, configs);
  244. }
  245. return urls;
  246. }
  247. async function preDrawImageHistory(_currentTask: Task | undefined) {
  248. // console.log("preDrawImageHistory=>curTask:", _currentTask);
  249. if (!_currentTask) return;
  250. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  251. const hasSliceConfig = _currentTask.sliceConfig?.length;
  252. // _currentTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
  253. _currentTask.sliceUrls = await processSliceUrls(_currentTask);
  254. const images = [];
  255. if (hasSliceConfig) {
  256. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  257. const sliceNum = _currentTask.sliceUrls.length;
  258. if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  259. console.warn("裁切图设置的数量小于该学生的总图片数量");
  260. }
  261. _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
  262. (v) => v.i <= sliceNum
  263. );
  264. for (const sliceConfig of _currentTask.sliceConfig) {
  265. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  266. const image = await loadImage(url);
  267. images[sliceConfig.i] = image;
  268. const { x, y, w, h } = sliceConfig;
  269. x < 0 && (sliceConfig.x = 0);
  270. y < 0 && (sliceConfig.y = 0);
  271. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  272. // 选择整图时,w/h 为0
  273. sliceConfig.w = image.naturalWidth;
  274. sliceConfig.h = image.naturalHeight;
  275. }
  276. if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
  277. sliceConfig.x = image.naturalWidth * x;
  278. sliceConfig.y = image.naturalHeight * y;
  279. sliceConfig.w = image.naturalWidth * w;
  280. sliceConfig.h = image.naturalHeight * h;
  281. }
  282. }
  283. maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
  284. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  285. for (const sliceConfig of _currentTask.sliceConfig) {
  286. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  287. const image = images[sliceConfig.i];
  288. try {
  289. await getDataUrlForSliceConfig(
  290. image,
  291. sliceConfig,
  292. maxSliceWidth,
  293. url
  294. );
  295. } catch (error) {
  296. console.log("preDrawImage failed: ", error);
  297. }
  298. }
  299. } else {
  300. for (const url of _currentTask.sliceUrls) {
  301. const image = await loadImage(url);
  302. images.push(image);
  303. }
  304. const splitConfigPairs = markStore.setting.splitConfig.reduce<
  305. [number, number][]
  306. >((a, v, index) => {
  307. index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
  308. return a;
  309. }, []);
  310. const maxSplitConfig = Math.max(...markStore.setting.splitConfig);
  311. maxSliceWidth =
  312. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  313. for (const url of _currentTask.sliceUrls) {
  314. for (const config of splitConfigPairs) {
  315. const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
  316. const image = images[indexInSliceUrls - 1];
  317. try {
  318. await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
  319. } catch (error) {
  320. console.log("preDrawImage failed: ", error);
  321. }
  322. }
  323. }
  324. }
  325. }
  326. return {
  327. getDataUrlForSliceConfig,
  328. getDataUrlForSplitConfig,
  329. getDataUrlForCoverConfig,
  330. preDrawImage,
  331. processSliceUrls,
  332. preDrawImageHistory,
  333. };
  334. }