QRCode.js 11 KB

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