awesome-qr.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import { getRoundNum, loadImage } from "../util";
  2. import { QRCodeModel, QRErrorCorrectLevel, QRUtil } from "./qrcode";
  3. const defaultScale = 0.4;
  4. export class AwesomeQR {
  5. canvas;
  6. canvasContext;
  7. qrCode;
  8. options;
  9. static CorrectLevel = QRErrorCorrectLevel;
  10. static defaultComponentOptions = {
  11. data: {
  12. scale: 1,
  13. },
  14. timing: {
  15. scale: 1,
  16. protectors: false,
  17. },
  18. alignment: {
  19. scale: 1,
  20. protectors: false,
  21. },
  22. cornerAlignment: {
  23. scale: 1,
  24. protectors: true,
  25. },
  26. };
  27. static defaultOptions = {
  28. text: "",
  29. size: 400,
  30. margin: 20,
  31. colorDark: "#000000",
  32. colorLight: "#ffffff",
  33. correctLevel: QRErrorCorrectLevel.M,
  34. backgroundImage: undefined,
  35. backgroundDimming: "rgba(0,0,0,0)",
  36. logoImage: undefined,
  37. logoScale: 0.2,
  38. logoMargin: 4,
  39. logoCornerRadius: 8,
  40. whiteMargin: true,
  41. components: AwesomeQR.defaultComponentOptions,
  42. autoColor: true,
  43. };
  44. constructor(options) {
  45. this.setOptions(options);
  46. // this.canvas = new Canvas(options.size!, options.size!);
  47. }
  48. draw() {
  49. return new Promise((resolve) => this._draw().then(resolve));
  50. }
  51. setOptions(options) {
  52. const _options = Object.assign({}, options);
  53. Object.keys(AwesomeQR.defaultOptions).forEach((k) => {
  54. if (!(k in _options)) {
  55. Object.defineProperty(_options, k, { value: AwesomeQR.defaultOptions[k], enumerable: true, writable: true });
  56. }
  57. });
  58. if (!_options.components) {
  59. _options.components = AwesomeQR.defaultComponentOptions;
  60. }
  61. else if (typeof _options.components === "object") {
  62. Object.keys(AwesomeQR.defaultComponentOptions).forEach((k) => {
  63. if (!(k in _options.components)) {
  64. Object.defineProperty(_options.components, k, {
  65. value: AwesomeQR.defaultComponentOptions[k],
  66. enumerable: true,
  67. writable: true,
  68. });
  69. }
  70. else {
  71. Object.defineProperty(_options.components, k, {
  72. value: { ...AwesomeQR.defaultComponentOptions[k], ..._options.components[k] },
  73. enumerable: true,
  74. writable: true,
  75. });
  76. }
  77. });
  78. }
  79. if (_options.dotScale !== null && _options.dotScale !== undefined) {
  80. if (_options.dotScale <= 0 || _options.dotScale > 1) {
  81. throw new Error("dotScale should be in range (0, 1].");
  82. }
  83. _options.components.data.scale = _options.dotScale;
  84. _options.components.timing.scale = _options.dotScale;
  85. _options.components.alignment.scale = _options.dotScale;
  86. }
  87. this.options = _options;
  88. this.canvas = options.canvasContainer.qrMainContainer;
  89. this.canvasContext = this.canvas.getContext("2d");
  90. this.qrCode = new QRCodeModel(-1, this.options.correctLevel);
  91. if (Number.isInteger(this.options.maskPattern)) {
  92. this.qrCode.maskPattern = this.options.maskPattern;
  93. }
  94. if (Number.isInteger(this.options.version)) {
  95. this.qrCode.typeNumber = this.options.version;
  96. }
  97. this.qrCode.addData(this.options.text);
  98. this.qrCode.make();
  99. return _options;
  100. }
  101. _clear() {
  102. this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
  103. }
  104. static _prepareRoundedCornerClip(canvasContext, x, y, w, h, r) {
  105. canvasContext.beginPath();
  106. canvasContext.moveTo(x, y);
  107. canvasContext.arcTo(x + w, y, x + w, y + h, r);
  108. canvasContext.arcTo(x + w, y + h, x, y + h, r);
  109. canvasContext.arcTo(x, y + h, x, y, r);
  110. canvasContext.arcTo(x, y, x + w, y, r);
  111. canvasContext.closePath();
  112. }
  113. static _prepareRoundedCornerClipReverse(canvasContext, x, y, w, h, r, size) {
  114. canvasContext.beginPath();
  115. canvasContext.lineTo(0, 0);
  116. canvasContext.lineTo(0, size);
  117. canvasContext.lineTo(size, size);
  118. canvasContext.lineTo(size, 0);
  119. canvasContext.lineTo(0, 0);
  120. canvasContext.lineTo(x, y);
  121. canvasContext.arcTo(x + w, y, x + w, y + h, r);
  122. canvasContext.arcTo(x + w, y + h, x, y + h, r);
  123. canvasContext.arcTo(x, y + h, x, y, r);
  124. canvasContext.arcTo(x, y, x + w, y, r);
  125. canvasContext.closePath();
  126. }
  127. static _getAverageRGB(image, options) {
  128. const blockSize = 5;
  129. const defaultRGB = {
  130. r: 0,
  131. g: 0,
  132. b: 0,
  133. };
  134. let width, height;
  135. let i = -4;
  136. const rgb = {
  137. r: 0,
  138. g: 0,
  139. b: 0,
  140. };
  141. let count = 0;
  142. // @ts-ignore
  143. height = image.naturalHeight || image.height;
  144. // @ts-ignore
  145. width = image.naturalWidth || image.width;
  146. const canvas = options.canvasContainer.qrMainContainer;
  147. const context = canvas.getContext("2d");
  148. if (!context) {
  149. return defaultRGB;
  150. }
  151. let data;
  152. try {
  153. data = context.getImageData(0, 0, width, height);
  154. }
  155. catch (e) {
  156. return defaultRGB;
  157. }
  158. while ((i += blockSize * 4) < data.data.length) {
  159. if (data.data[i] > 200 || data.data[i + 1] > 200 || data.data[i + 2] > 200)
  160. continue;
  161. ++count;
  162. rgb.r += data.data[i];
  163. rgb.g += data.data[i + 1];
  164. rgb.b += data.data[i + 2];
  165. }
  166. rgb.r = ~~(rgb.r / count);
  167. rgb.g = ~~(rgb.g / count);
  168. rgb.b = ~~(rgb.b / count);
  169. return rgb;
  170. }
  171. static _drawDot(canvasContext, centerX, centerY, nSize, xyOffset = 0, dotScale = 1) {
  172. canvasContext.fillRect((centerX + xyOffset) * nSize, (centerY + xyOffset) * nSize, dotScale * nSize, dotScale * nSize);
  173. }
  174. static _drawAlignProtector(canvasContext, centerX, centerY, nSize) {
  175. canvasContext.clearRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
  176. canvasContext.fillRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
  177. }
  178. static _drawAlign(canvasContext, centerX, centerY, nSize, xyOffset = 0, dotScale = 1, colorDark, hasProtector) {
  179. const oldFillStyle = canvasContext.fillStyle;
  180. canvasContext.fillStyle = colorDark;
  181. new Array(4).fill(0).map((_, i) => {
  182. AwesomeQR._drawDot(canvasContext, centerX - 2 + i, centerY - 2, nSize, xyOffset, dotScale);
  183. AwesomeQR._drawDot(canvasContext, centerX + 2, centerY - 2 + i, nSize, xyOffset, dotScale);
  184. AwesomeQR._drawDot(canvasContext, centerX + 2 - i, centerY + 2, nSize, xyOffset, dotScale);
  185. AwesomeQR._drawDot(canvasContext, centerX - 2, centerY + 2 - i, nSize, xyOffset, dotScale);
  186. });
  187. AwesomeQR._drawDot(canvasContext, centerX, centerY, nSize, xyOffset, dotScale);
  188. if (!hasProtector) {
  189. canvasContext.fillStyle = "rgba(255, 255, 255, 0.6)";
  190. new Array(2).fill(0).map((_, i) => {
  191. AwesomeQR._drawDot(canvasContext, centerX - 1 + i, centerY - 1, nSize, xyOffset, dotScale);
  192. AwesomeQR._drawDot(canvasContext, centerX + 1, centerY - 1 + i, nSize, xyOffset, dotScale);
  193. AwesomeQR._drawDot(canvasContext, centerX + 1 - i, centerY + 1, nSize, xyOffset, dotScale);
  194. AwesomeQR._drawDot(canvasContext, centerX - 1, centerY + 1 - i, nSize, xyOffset, dotScale);
  195. });
  196. }
  197. canvasContext.fillStyle = oldFillStyle;
  198. }
  199. async _draw() {
  200. const nCount = this.qrCode?.moduleCount;
  201. const rawSize = this.options.size;
  202. let rawMargin = this.options.margin;
  203. if (rawMargin < 0 || rawMargin * 2 >= rawSize) {
  204. rawMargin = 0;
  205. }
  206. const margin = Math.ceil(rawMargin);
  207. const rawViewportSize = rawSize - 2 * rawMargin;
  208. const whiteMargin = this.options.whiteMargin;
  209. const backgroundDimming = this.options.backgroundDimming;
  210. const nSize = getRoundNum(rawViewportSize / nCount, 3);
  211. const viewportSize = nSize * nCount;
  212. let size = getRoundNum(viewportSize + 2 * margin, 3);
  213. // console.log({ rawViewportSize, size, correctLevel: this.options.correctLevel, nCount })
  214. // const mainCanvas = new Canvas(size, size);
  215. const mainCanvas = this.options.canvasContainer.qrMainContainer;
  216. const mainCanvasContext = mainCanvas.getContext("2d");
  217. this._clear();
  218. // Fill the margin
  219. if (whiteMargin) {
  220. mainCanvasContext.save();
  221. mainCanvasContext.fillStyle = "#FFFFFF";
  222. mainCanvasContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
  223. mainCanvasContext.restore();
  224. }
  225. // Translate to make the top and left margins off the viewport
  226. mainCanvasContext.save();
  227. mainCanvasContext.translate(margin, margin);
  228. // const backgroundCanvas = new Canvas(size, size);
  229. const backgroundCanvas = this.options.canvasContainer.qrMainContainer;
  230. const backgroundCanvasContext = backgroundCanvas.getContext("2d");
  231. let backgroundImage;
  232. if (!!this.options.backgroundImage) {
  233. backgroundImage = await loadImage(backgroundCanvas, this.options.backgroundImage);
  234. backgroundCanvasContext.drawImage(
  235. // @ts-ignore
  236. backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, 0, 0, rawViewportSize, rawViewportSize);
  237. if (this.options.autoColor) {
  238. const avgRGB = AwesomeQR._getAverageRGB(backgroundImage, this.options);
  239. this.options.colorDark = `rgb(${avgRGB.r},${avgRGB.g},${avgRGB.b})`;
  240. }
  241. backgroundCanvasContext.rect(-margin, -margin, size, size);
  242. backgroundCanvasContext.fillStyle = backgroundDimming;
  243. backgroundCanvasContext.fill();
  244. }
  245. else {
  246. backgroundCanvasContext.rect(-margin, -margin, size, size);
  247. backgroundCanvasContext.fillStyle = this.options.colorLight;
  248. backgroundCanvasContext.fill();
  249. }
  250. const alignmentPatternCenters = QRUtil.getPatternPosition(this.qrCode.typeNumber);
  251. const dataScale = this.options.components?.data?.scale || defaultScale;
  252. const dataXyOffset = (1 - dataScale) * 0.5;
  253. // 提前预备好logo margin的空
  254. if (!!this.options.logoImage && this.options.logoMargin) {
  255. let logoMargin = this.options.logoMargin;
  256. let logoScale = this.options.logoScale;
  257. let logoCornerRadius = this.options.logoCornerRadius;
  258. const logoSize = viewportSize * logoScale;
  259. const x = 0.5 * (size - logoSize);
  260. const y = x;
  261. // mainCanvasContext.save();
  262. AwesomeQR._prepareRoundedCornerClipReverse(mainCanvasContext, x - logoMargin - rawMargin, y - logoMargin - rawMargin, logoSize + 2 * logoMargin, logoSize + 2 * logoMargin, logoCornerRadius + logoMargin, size);
  263. mainCanvasContext.fill();
  264. // return
  265. // mainCanvasContext.globalCompositeOperation = "destination-over";
  266. mainCanvasContext.clip();
  267. }
  268. for (let row = 0; row < nCount; row++) {
  269. for (let col = 0; col < nCount; col++) {
  270. const bIsDark = this.qrCode.isDark(row, col);
  271. const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8);
  272. const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8);
  273. let isProtected = isBlkPosCtr || isTiming;
  274. for (let i = 1; i < alignmentPatternCenters.length - 1; i++) {
  275. isProtected =
  276. isProtected ||
  277. (row >= alignmentPatternCenters[i] - 2 &&
  278. row <= alignmentPatternCenters[i] + 2 &&
  279. col >= alignmentPatternCenters[i] - 2 &&
  280. col <= alignmentPatternCenters[i] + 2);
  281. }
  282. const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize);
  283. const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize);
  284. mainCanvasContext.strokeStyle = bIsDark ? this.options.colorDark : this.options.colorLight;
  285. mainCanvasContext.lineWidth = 0.5;
  286. mainCanvasContext.fillStyle = bIsDark ? this.options.colorDark : "rgba(255, 255, 255, 0.6)";
  287. if (alignmentPatternCenters.length === 0) {
  288. if (!isProtected) {
  289. mainCanvasContext.fillRect(nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize);
  290. }
  291. }
  292. else {
  293. const inAgnRange = col < nCount - 4 && col >= nCount - 4 - 5 && row < nCount - 4 && row >= nCount - 4 - 5;
  294. if (!isProtected && !inAgnRange) {
  295. // if align pattern list is empty, then it means that we don't need to leave room for the align patterns
  296. mainCanvasContext.fillRect(nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize);
  297. }
  298. }
  299. }
  300. }
  301. const cornerAlignmentCenter = alignmentPatternCenters[alignmentPatternCenters.length - 1];
  302. // - PROTECTORS
  303. const protectorStyle = "rgba(255, 255, 255, 0.6)";
  304. // - FINDER PROTECTORS
  305. mainCanvasContext.fillStyle = protectorStyle;
  306. mainCanvasContext.fillRect(0, 0, 8 * nSize, 8 * nSize);
  307. mainCanvasContext.fillRect(0, (nCount - 8) * nSize, 8 * nSize, 8 * nSize);
  308. mainCanvasContext.fillRect((nCount - 8) * nSize, 0, 8 * nSize, 8 * nSize);
  309. // - TIMING PROTECTORS
  310. if (this.options.components?.timing?.protectors) {
  311. mainCanvasContext.fillRect(8 * nSize, 6 * nSize, (nCount - 8 - 8) * nSize, nSize);
  312. mainCanvasContext.fillRect(6 * nSize, 8 * nSize, nSize, (nCount - 8 - 8) * nSize);
  313. }
  314. // - CORNER ALIGNMENT PROTECTORS
  315. if (this.options.components?.cornerAlignment?.protectors) {
  316. AwesomeQR._drawAlignProtector(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize);
  317. }
  318. // - ALIGNMENT PROTECTORS
  319. if (this.options.components?.alignment?.protectors) {
  320. for (let i = 0; i < alignmentPatternCenters.length; i++) {
  321. for (let j = 0; j < alignmentPatternCenters.length; j++) {
  322. const agnX = alignmentPatternCenters[j];
  323. const agnY = alignmentPatternCenters[i];
  324. if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
  325. continue;
  326. }
  327. else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
  328. continue;
  329. }
  330. else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
  331. continue;
  332. }
  333. else {
  334. AwesomeQR._drawAlignProtector(mainCanvasContext, agnX, agnY, nSize);
  335. }
  336. }
  337. }
  338. }
  339. // - FINDER
  340. mainCanvasContext.fillStyle = this.options.colorDark;
  341. mainCanvasContext.fillRect(0, 0, 7 * nSize, nSize);
  342. mainCanvasContext.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize);
  343. mainCanvasContext.fillRect(0, 6 * nSize, 7 * nSize, nSize);
  344. mainCanvasContext.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize);
  345. mainCanvasContext.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize);
  346. mainCanvasContext.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize);
  347. mainCanvasContext.fillRect(0, 0, nSize, 7 * nSize);
  348. mainCanvasContext.fillRect(6 * nSize, 0, nSize, 7 * nSize);
  349. mainCanvasContext.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize);
  350. mainCanvasContext.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize);
  351. mainCanvasContext.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize);
  352. mainCanvasContext.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize);
  353. mainCanvasContext.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
  354. mainCanvasContext.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
  355. mainCanvasContext.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize);
  356. // - TIMING
  357. const timingScale = this.options.components?.timing?.scale || defaultScale;
  358. const timingXyOffset = (1 - timingScale) * 0.5;
  359. for (let i = 0; i < nCount - 8; i += 2) {
  360. AwesomeQR._drawDot(mainCanvasContext, 8 + i, 6, nSize, timingXyOffset, timingScale);
  361. AwesomeQR._drawDot(mainCanvasContext, 6, 8 + i, nSize, timingXyOffset, timingScale);
  362. }
  363. // - CORNER ALIGNMENT PROTECTORS
  364. const cornerAlignmentScale = this.options.components?.cornerAlignment?.scale || defaultScale;
  365. const cornerAlignmentXyOffset = (1 - cornerAlignmentScale) * 0.5;
  366. AwesomeQR._drawAlign(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize, cornerAlignmentXyOffset, cornerAlignmentScale, this.options.colorDark, this.options.components?.cornerAlignment?.protectors || false);
  367. // - ALIGNEMNT
  368. const alignmentScale = this.options.components?.alignment?.scale || defaultScale;
  369. const alignmentXyOffset = (1 - alignmentScale) * 0.5;
  370. for (let i = 0; i < alignmentPatternCenters.length; i++) {
  371. for (let j = 0; j < alignmentPatternCenters.length; j++) {
  372. const agnX = alignmentPatternCenters[j];
  373. const agnY = alignmentPatternCenters[i];
  374. if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
  375. continue;
  376. }
  377. else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
  378. continue;
  379. }
  380. else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
  381. continue;
  382. }
  383. else {
  384. AwesomeQR._drawAlign(mainCanvasContext, agnX, agnY, nSize, alignmentXyOffset, alignmentScale, this.options.colorDark, this.options.components?.alignment?.protectors || false);
  385. }
  386. }
  387. }
  388. // mainCanvasContext.lineTo(1, 1, 10, 10)
  389. if (!!this.options.logoImage) {
  390. const logoImage = await loadImage(mainCanvas, this.options.logoImage);
  391. let logoScale = this.options.logoScale;
  392. let logoMargin = this.options.logoMargin;
  393. let logoCornerRadius = this.options.logoCornerRadius;
  394. if (logoScale <= 0 || logoScale >= 1.0) {
  395. logoScale = 0.2;
  396. }
  397. if (logoMargin < 0) {
  398. logoMargin = 0;
  399. }
  400. if (logoCornerRadius < 0) {
  401. logoCornerRadius = 0;
  402. }
  403. const logoSize = viewportSize * logoScale;
  404. const x = 0.5 * (size - logoSize);
  405. const y = x;
  406. mainCanvasContext.restore();
  407. // @ts-ignore
  408. mainCanvasContext.drawImage(logoImage, x, y, logoSize, logoSize);
  409. }
  410. this.qrCode = undefined;
  411. this.canvas = mainCanvas;
  412. return new Promise((reslove, reject) => {
  413. wx.canvasToTempFilePath({
  414. canvas: this.canvas,
  415. quality: 1,
  416. destWidth: this.canvas.width,
  417. destHeight: this.canvas.height
  418. }).then(rsp => {
  419. reslove(rsp.tempFilePath);
  420. }).catch(err => {
  421. console.error('canvasToTempFilePath 失败', err);
  422. reject(err);
  423. });
  424. });
  425. // Promise.resolve(this.canvas.toDataURL(format, 1));
  426. }
  427. getDataUrl(type = 'png', encoderOptions = 1) {
  428. return this.canvas.toDataURL(type, encoderOptions);
  429. }
  430. }