QRCode.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { Fragment as _Fragment, createVNode as _createVNode } from "vue";
  4. import { defineComponent, shallowRef, watch, computed, watchEffect } from 'vue';
  5. import { qrProps } from './interface';
  6. import qrcodegen from './qrcodegen';
  7. const ERROR_LEVEL_MAP = {
  8. L: qrcodegen.QrCode.Ecc.LOW,
  9. M: qrcodegen.QrCode.Ecc.MEDIUM,
  10. Q: qrcodegen.QrCode.Ecc.QUARTILE,
  11. H: qrcodegen.QrCode.Ecc.HIGH
  12. };
  13. const DEFAULT_SIZE = 128;
  14. const DEFAULT_LEVEL = 'L';
  15. const DEFAULT_BGCOLOR = '#FFFFFF';
  16. const DEFAULT_FGCOLOR = '#000000';
  17. const DEFAULT_INCLUDEMARGIN = false;
  18. const SPEC_MARGIN_SIZE = 4;
  19. const DEFAULT_MARGIN_SIZE = 0;
  20. // This is *very* rough estimate of max amount of QRCode allowed to be covered.
  21. // It is "wrong" in a lot of ways (area is a terrible way to estimate, it
  22. // really should be number of modules covered), but if for some reason we don't
  23. // get an explicit height or width, I'd rather default to something than throw.
  24. const DEFAULT_IMG_SCALE = 0.1;
  25. function generatePath(modules) {
  26. let margin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  27. const ops = [];
  28. modules.forEach(function (row, y) {
  29. let start = null;
  30. row.forEach(function (cell, x) {
  31. if (!cell && start !== null) {
  32. // M0 0h7v1H0z injects the space with the move and drops the comma,
  33. // saving a char per operation
  34. ops.push(`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`);
  35. start = null;
  36. return;
  37. }
  38. // end of row, clean up or skip
  39. if (x === row.length - 1) {
  40. if (!cell) {
  41. // We would have closed the op above already so this can only mean
  42. // 2+ light modules in a row.
  43. return;
  44. }
  45. if (start === null) {
  46. // Just a single dark module.
  47. ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
  48. } else {
  49. // Otherwise finish the current line.
  50. ops.push(`M${start + margin},${y + margin} h${x + 1 - start}v1H${start + margin}z`);
  51. }
  52. return;
  53. }
  54. if (cell && start === null) {
  55. start = x;
  56. }
  57. });
  58. });
  59. return ops.join('');
  60. }
  61. // We could just do this in generatePath, except that we want to support
  62. // non-Path2D canvas, so we need to keep it an explicit step.
  63. function excavateModules(modules, excavation) {
  64. return modules.slice().map((row, y) => {
  65. if (y < excavation.y || y >= excavation.y + excavation.h) {
  66. return row;
  67. }
  68. return row.map((cell, x) => {
  69. if (x < excavation.x || x >= excavation.x + excavation.w) {
  70. return cell;
  71. }
  72. return false;
  73. });
  74. });
  75. }
  76. function getImageSettings(cells, size, margin, imageSettings) {
  77. if (imageSettings == null) {
  78. return null;
  79. }
  80. const numCells = cells.length + margin * 2;
  81. const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
  82. const scale = numCells / size;
  83. const w = (imageSettings.width || defaultSize) * scale;
  84. const h = (imageSettings.height || defaultSize) * scale;
  85. const x = imageSettings.x == null ? cells.length / 2 - w / 2 : imageSettings.x * scale;
  86. const y = imageSettings.y == null ? cells.length / 2 - h / 2 : imageSettings.y * scale;
  87. let excavation = null;
  88. if (imageSettings.excavate) {
  89. const floorX = Math.floor(x);
  90. const floorY = Math.floor(y);
  91. const ceilW = Math.ceil(w + x - floorX);
  92. const ceilH = Math.ceil(h + y - floorY);
  93. excavation = {
  94. x: floorX,
  95. y: floorY,
  96. w: ceilW,
  97. h: ceilH
  98. };
  99. }
  100. return {
  101. x,
  102. y,
  103. h,
  104. w,
  105. excavation
  106. };
  107. }
  108. function getMarginSize(includeMargin, marginSize) {
  109. if (marginSize != null) {
  110. return Math.floor(marginSize);
  111. }
  112. return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
  113. }
  114. // For canvas we're going to switch our drawing mode based on whether or not
  115. // the environment supports Path2D. We only need the constructor to be
  116. // supported, but Edge doesn't actually support the path (string) type
  117. // argument. Luckily it also doesn't support the addPath() method. We can
  118. // treat that as the same thing.
  119. const SUPPORTS_PATH2D = function () {
  120. try {
  121. new Path2D().addPath(new Path2D());
  122. } catch (e) {
  123. return false;
  124. }
  125. return true;
  126. }();
  127. export const QRCodeCanvas = defineComponent({
  128. name: 'QRCodeCanvas',
  129. inheritAttrs: false,
  130. props: _extends(_extends({}, qrProps()), {
  131. level: String,
  132. bgColor: String,
  133. fgColor: String,
  134. marginSize: Number
  135. }),
  136. setup(props, _ref) {
  137. let {
  138. attrs,
  139. expose
  140. } = _ref;
  141. const imgSrc = computed(() => {
  142. var _a;
  143. return (_a = props.imageSettings) === null || _a === void 0 ? void 0 : _a.src;
  144. });
  145. const _canvas = shallowRef(null);
  146. const _image = shallowRef(null);
  147. const isImgLoaded = shallowRef(false);
  148. expose({
  149. toDataURL: (type, quality) => {
  150. var _a;
  151. return (_a = _canvas.value) === null || _a === void 0 ? void 0 : _a.toDataURL(type, quality);
  152. }
  153. });
  154. watchEffect(() => {
  155. const {
  156. value,
  157. size = DEFAULT_SIZE,
  158. level = DEFAULT_LEVEL,
  159. bgColor = DEFAULT_BGCOLOR,
  160. fgColor = DEFAULT_FGCOLOR,
  161. includeMargin = DEFAULT_INCLUDEMARGIN,
  162. marginSize,
  163. imageSettings
  164. } = props;
  165. if (_canvas.value != null) {
  166. const canvas = _canvas.value;
  167. const ctx = canvas.getContext('2d');
  168. if (!ctx) {
  169. return;
  170. }
  171. let cells = qrcodegen.QrCode.encodeText(value, ERROR_LEVEL_MAP[level]).getModules();
  172. const margin = getMarginSize(includeMargin, marginSize);
  173. const numCells = cells.length + margin * 2;
  174. const calculatedImageSettings = getImageSettings(cells, size, margin, imageSettings);
  175. const image = _image.value;
  176. const haveImageToRender = isImgLoaded.value && calculatedImageSettings != null && image !== null && image.complete && image.naturalHeight !== 0 && image.naturalWidth !== 0;
  177. if (haveImageToRender) {
  178. if (calculatedImageSettings.excavation != null) {
  179. cells = excavateModules(cells, calculatedImageSettings.excavation);
  180. }
  181. }
  182. // We're going to scale this so that the number of drawable units
  183. // matches the number of cells. This avoids rounding issues, but does
  184. // result in some potentially unwanted single pixel issues between
  185. // blocks, only in environments that don't support Path2D.
  186. const pixelRatio = window.devicePixelRatio || 1;
  187. canvas.height = canvas.width = size * pixelRatio;
  188. const scale = size / numCells * pixelRatio;
  189. ctx.scale(scale, scale);
  190. // Draw solid background, only paint dark modules.
  191. ctx.fillStyle = bgColor;
  192. ctx.fillRect(0, 0, numCells, numCells);
  193. ctx.fillStyle = fgColor;
  194. if (SUPPORTS_PATH2D) {
  195. // $FlowFixMe: Path2D c'tor doesn't support args yet.
  196. ctx.fill(new Path2D(generatePath(cells, margin)));
  197. } else {
  198. cells.forEach(function (row, rdx) {
  199. row.forEach(function (cell, cdx) {
  200. if (cell) {
  201. ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
  202. }
  203. });
  204. });
  205. }
  206. if (haveImageToRender) {
  207. ctx.drawImage(image, calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, calculatedImageSettings.h);
  208. }
  209. }
  210. }, {
  211. flush: 'post'
  212. });
  213. watch(imgSrc, () => {
  214. isImgLoaded.value = false;
  215. });
  216. return () => {
  217. var _a;
  218. const size = (_a = props.size) !== null && _a !== void 0 ? _a : DEFAULT_SIZE;
  219. const canvasStyle = {
  220. height: `${size}px`,
  221. width: `${size}px`
  222. };
  223. let img = null;
  224. if (imgSrc.value != null) {
  225. img = _createVNode("img", {
  226. "src": imgSrc.value,
  227. "key": imgSrc.value,
  228. "style": {
  229. display: 'none'
  230. },
  231. "onLoad": () => {
  232. isImgLoaded.value = true;
  233. },
  234. "ref": _image
  235. }, null);
  236. }
  237. return _createVNode(_Fragment, null, [_createVNode("canvas", _objectSpread(_objectSpread({}, attrs), {}, {
  238. "style": [canvasStyle, attrs.style],
  239. "ref": _canvas
  240. }), null), img]);
  241. };
  242. }
  243. });
  244. export const QRCodeSVG = defineComponent({
  245. name: 'QRCodeSVG',
  246. inheritAttrs: false,
  247. props: _extends(_extends({}, qrProps()), {
  248. color: String,
  249. level: String,
  250. bgColor: String,
  251. fgColor: String,
  252. marginSize: Number,
  253. title: String
  254. }),
  255. setup(props) {
  256. let cells = null;
  257. let margin = null;
  258. let numCells = null;
  259. let calculatedImageSettings = null;
  260. let fgPath = null;
  261. let image = null;
  262. watchEffect(() => {
  263. const {
  264. value,
  265. size = DEFAULT_SIZE,
  266. level = DEFAULT_LEVEL,
  267. includeMargin = DEFAULT_INCLUDEMARGIN,
  268. marginSize,
  269. imageSettings
  270. } = props;
  271. cells = qrcodegen.QrCode.encodeText(value, ERROR_LEVEL_MAP[level]).getModules();
  272. margin = getMarginSize(includeMargin, marginSize);
  273. numCells = cells.length + margin * 2;
  274. calculatedImageSettings = getImageSettings(cells, size, margin, imageSettings);
  275. if (imageSettings != null && calculatedImageSettings != null) {
  276. if (calculatedImageSettings.excavation != null) {
  277. cells = excavateModules(cells, calculatedImageSettings.excavation);
  278. }
  279. image = _createVNode("image", {
  280. "xlink:href": imageSettings.src,
  281. "height": calculatedImageSettings.h,
  282. "width": calculatedImageSettings.w,
  283. "x": calculatedImageSettings.x + margin,
  284. "y": calculatedImageSettings.y + margin,
  285. "preserveAspectRatio": "none"
  286. }, null);
  287. }
  288. // Drawing strategy: instead of a rect per module, we're going to create a
  289. // single path for the dark modules and layer that on top of a light rect,
  290. // for a total of 2 DOM nodes. We pay a bit more in string concat but that's
  291. // way faster than DOM ops.
  292. // For level 1, 441 nodes -> 2
  293. // For level 40, 31329 -> 2
  294. fgPath = generatePath(cells, margin);
  295. });
  296. return () => {
  297. const bgColor = props.bgColor && DEFAULT_BGCOLOR;
  298. const fgColor = props.fgColor && DEFAULT_FGCOLOR;
  299. return _createVNode("svg", {
  300. "height": props.size,
  301. "width": props.size,
  302. "viewBox": `0 0 ${numCells} ${numCells}`
  303. }, [!!props.title && _createVNode("title", null, [props.title]), _createVNode("path", {
  304. "fill": bgColor,
  305. "d": `M0,0 h${numCells}v${numCells}H0z`,
  306. "shape-rendering": "crispEdges"
  307. }, null), _createVNode("path", {
  308. "fill": fgColor,
  309. "d": fgPath,
  310. "shape-rendering": "crispEdges"
  311. }, null), image]);
  312. };
  313. }
  314. });