From 02aa82a3a37cfa6b7ed10e004ca3e62eb23fe80d Mon Sep 17 00:00:00 2001 From: Yet Another Account <33345431+yaaccount@users.noreply.github.com> Date: Wed, 21 Nov 2018 16:11:32 +0100 Subject: [PATCH 1/2] Wrap long lines during apostille certificate generation --- src/app/services/nty.service.js | 73 +++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/app/services/nty.service.js b/src/app/services/nty.service.js index e538308a..4c038a78 100644 --- a/src/app/services/nty.service.js +++ b/src/app/services/nty.service.js @@ -125,19 +125,71 @@ class Nty { let canvas = document.createElement('canvas'); let context = canvas.getContext('2d'); + const multilineMode = { + MATCH: 0, + SPLIT: 1 + } + + const fillTextMultiline = (text, x, y, mode, re, lineHeight, maxX = undefined) => { + let posY = y; + let maxWidth = (maxX===undefined ? -1 : maxX-x); + let tokens = (mode===multilineMode.MATCH ? text.match(re) : text.split(re)); + let line = ""; + tokens.forEach(token => { + if (maxWidth > 0) { + if (mode===multilineMode.MATCH) {//one token per line + if (context.measureText(token).width <= maxWidth) {//token fits in the line + context.fillText(token, x, posY); + posY += lineHeight; + } else {//token doesn't fit in the line - split it further - fallback to char-by-char, recurse + posY = fillTextMultiline(token, x, posY, multilineMode.SPLIT, new RegExp(''), lineHeight, maxX); + } + } else {//mode===multilineMode.SPLIT; possibly more tokens per line + if (context.measureText(line+token).width <= maxWidth) {//next token fits in the line + line+=token; + } else {//next token doesn't fit onto the line; output line and try it again + if (line.length > 0) { + context.fillText(line, x, posY); + posY += lineHeight; + line = ""; + } + if (context.measureText(token).width <= maxWidth || token.length <= 1) {//it fits now + line = token; + } else {//it still doesn't fit, need to break it further - fallback to char-by-char; recurse + posY = fillTextMultiline(token, x, posY, multilineMode.SPLIT, new RegExp(''), lineHeight, maxX); + } + } + } + } else {//one token per line + context.fillText(token, x, posY); + posY += lineHeight; + } + }); + if (line.length > 0) { + context.fillText(line, x, posY); + } + return posY; + } + let imageObj = new Image(); imageObj.onload = () => { context.canvas.width = imageObj.width; context.canvas.height = imageObj.height; context.drawImage(imageObj, 0, 0, imageObj.width, imageObj.height); context.font = "38px Roboto Arial sans-serif"; + + let qrX = 1687; + let qrY = 688; + // Top part - context.fillText(filename, 541, 756); + // Wrap filename - do not conflict with generated QR + fillTextMultiline(filename, 541, 756, multilineMode.SPLIT, new RegExp(''), 38+5, qrX-5); context.fillText(dateCreated, 607, 873); context.fillText(owner, 458, 989); - context.fillText(tags, 426, 1105); + // Wrap tags - do not conflict with generated QR + fillTextMultiline(tags, 426, 1105, multilineMode.SPLIT, new RegExp('([ ])'), 38+5, qrX-5); - // bottom part + // Bottom part context.font = "30px Roboto Arial sans-serif"; context.fillText(from, 345, 1550); context.fillText(to, 345, 1690); @@ -145,18 +197,7 @@ class Nty { context.fillText(txHash, 345, 1994); // Wrap file hash if too long - if (txHex.length > 70) { - let x = 345; - let y = 2137; - let lineHeight = 35; - let lines = txHex.match(/.{1,70}/g) - for (var i = 0; i < lines.length; ++i) { - context.fillText(lines[i], x, y); - y += lineHeight; - } - } else { - context.fillText(txHex, 345, 2137); - } + fillTextMultiline(txHex, 345, 2137, multilineMode.MATCH, new RegExp('.{1,70}', 'g'), 30+5); let qr = qrcode(10, 'H'); qr.addData(url); qr.make(); @@ -167,7 +208,7 @@ class Nty { context.fillStyle = qr.isDark(row, col) ? "#000000" : "#ffffff"; let w = (Math.ceil((col+1)*tileW) - Math.floor(col*tileW)); let h = (Math.ceil((row+1)*tileW) - Math.floor(row*tileW)); - context.fillRect(Math.round(col*tileW)+1687,Math.round(row*tileH)+688, w, h); + context.fillRect(Math.round(col*tileW)+qrX,Math.round(row*tileH)+qrY, w, h); } } return resolve(canvas.toDataURL()); From f78408c4f8de057212d8b20634c5e454927465b3 Mon Sep 17 00:00:00 2001 From: Yaaccount <33345431+yaaccount@users.noreply.github.com> Date: Fri, 30 Nov 2018 01:34:53 +0100 Subject: [PATCH 2/2] Wrap long lines during apostille certificate generation - make wrapping function testable + add test --- src/app/services/nty.service.js | 104 +++++++++++++++++--------------- tests/specs/ntyService.spec.js | 79 ++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 tests/specs/ntyService.spec.js diff --git a/src/app/services/nty.service.js b/src/app/services/nty.service.js index 4c038a78..f7e48b0f 100644 --- a/src/app/services/nty.service.js +++ b/src/app/services/nty.service.js @@ -10,7 +10,11 @@ class Nty { */ constructor($localStorage, Wallet) { 'ngInject'; - + + this.MULTILINE_MODE = { + MATCH: 0, + SPLIT: 1 + } /** * Service dependencies */ @@ -118,58 +122,58 @@ class Nty { } /** - * Draw an apostille certificate + * Utility function for drawCertificate */ - drawCertificate(filename, dateCreated, owner, tags, from, to, recipientPrivateKey, txHash, txHex, url) { - return new Promise((resolve, reject) => { - let canvas = document.createElement('canvas'); - let context = canvas.getContext('2d'); - - const multilineMode = { - MATCH: 0, - SPLIT: 1 - } - - const fillTextMultiline = (text, x, y, mode, re, lineHeight, maxX = undefined) => { - let posY = y; - let maxWidth = (maxX===undefined ? -1 : maxX-x); - let tokens = (mode===multilineMode.MATCH ? text.match(re) : text.split(re)); - let line = ""; - tokens.forEach(token => { - if (maxWidth > 0) { - if (mode===multilineMode.MATCH) {//one token per line - if (context.measureText(token).width <= maxWidth) {//token fits in the line - context.fillText(token, x, posY); - posY += lineHeight; - } else {//token doesn't fit in the line - split it further - fallback to char-by-char, recurse - posY = fillTextMultiline(token, x, posY, multilineMode.SPLIT, new RegExp(''), lineHeight, maxX); - } - } else {//mode===multilineMode.SPLIT; possibly more tokens per line - if (context.measureText(line+token).width <= maxWidth) {//next token fits in the line - line+=token; - } else {//next token doesn't fit onto the line; output line and try it again - if (line.length > 0) { - context.fillText(line, x, posY); - posY += lineHeight; - line = ""; - } - if (context.measureText(token).width <= maxWidth || token.length <= 1) {//it fits now - line = token; - } else {//it still doesn't fit, need to break it further - fallback to char-by-char; recurse - posY = fillTextMultiline(token, x, posY, multilineMode.SPLIT, new RegExp(''), lineHeight, maxX); - } - } - } - } else {//one token per line + _fillTextMultiline (context, text, x, y, mode, re, lineHeight, maxX = undefined) { + let self = this; + let posY = y; + let maxWidth = (maxX===undefined ? -1 : maxX-x); + let tokens = (mode===this.MULTILINE_MODE.MATCH ? text.match(re) : text.split(re)); + let line = ""; + tokens.forEach(token => { + if (maxWidth > 0) { + if (mode===this.MULTILINE_MODE.MATCH) {//one token per line + if (context.measureText(token).width <= maxWidth) {//token fits in the line context.fillText(token, x, posY); posY += lineHeight; + } else {//token doesn't fit in the line - split it further - fallback to char-by-char, recurse + posY = self._fillTextMultiline(context, token, x, posY, this.MULTILINE_MODE.SPLIT, new RegExp(''), lineHeight, maxX); + } + } else {//mode===this.MULTILINE_MODE.SPLIT; possibly more tokens per line + if (context.measureText(line+token).width <= maxWidth) {//next token fits in the line + line+=token; + } else {//next token doesn't fit onto the line; output line and try it again + if (line.length > 0) { + context.fillText(line, x, posY); + posY += lineHeight; + line = ""; + } + if (context.measureText(token).width <= maxWidth || token.length <= 1) {//it fits now + line = token; + } else {//it still doesn't fit, need to break it further - fallback to char-by-char; recurse + posY = self._fillTextMultiline(context, token, x, posY, this.MULTILINE_MODE.SPLIT, new RegExp(''), lineHeight, maxX); + } } - }); - if (line.length > 0) { - context.fillText(line, x, posY); } - return posY; + } else {//one token per line + context.fillText(token, x, posY); + posY += lineHeight; } + }); + if (line.length > 0) { + context.fillText(line, x, posY); + posY += lineHeight; + } + return posY; + } + + /** + * Draw an apostille certificate + */ + drawCertificate(filename, dateCreated, owner, tags, from, to, recipientPrivateKey, txHash, txHex, url) { + return new Promise((resolve, reject) => { + let canvas = document.createElement('canvas'); + let context = canvas.getContext('2d'); let imageObj = new Image(); imageObj.onload = () => { @@ -183,11 +187,11 @@ class Nty { // Top part // Wrap filename - do not conflict with generated QR - fillTextMultiline(filename, 541, 756, multilineMode.SPLIT, new RegExp(''), 38+5, qrX-5); + this._fillTextMultiline(context, filename, 541, 756, this.MULTILINE_MODE.SPLIT, new RegExp(''), 38+5, qrX-5); context.fillText(dateCreated, 607, 873); context.fillText(owner, 458, 989); // Wrap tags - do not conflict with generated QR - fillTextMultiline(tags, 426, 1105, multilineMode.SPLIT, new RegExp('([ ])'), 38+5, qrX-5); + this._fillTextMultiline(context, tags, 426, 1105, this.MULTILINE_MODE.SPLIT, new RegExp('([ ])'), 38+5, qrX-5); // Bottom part context.font = "30px Roboto Arial sans-serif"; @@ -197,7 +201,7 @@ class Nty { context.fillText(txHash, 345, 1994); // Wrap file hash if too long - fillTextMultiline(txHex, 345, 2137, multilineMode.MATCH, new RegExp('.{1,70}', 'g'), 30+5); + this._fillTextMultiline(context, txHex, 345, 2137, this.MULTILINE_MODE.MATCH, new RegExp('.{1,70}', 'g'), 30+5); let qr = qrcode(10, 'H'); qr.addData(url); qr.make(); diff --git a/tests/specs/ntyService.spec.js b/tests/specs/ntyService.spec.js new file mode 100644 index 00000000..615df62e --- /dev/null +++ b/tests/specs/ntyService.spec.js @@ -0,0 +1,79 @@ +describe('Nty service tests', function() { + let $controller, $rootscope, Wallet, Nty, $localStorage; + + beforeEach(angular.mock.module('app')); + + beforeEach(angular.mock.inject(function( _$controller_, _$rootScope_, _Wallet_, _Nty_, _$localStorage_) { + $controller = _$controller_; + $rootscope = _$rootScope_; + Wallet = _Wallet_; + Nty = _Nty_; + $localStorage = _$localStorage_; + $localStorage.$reset(); + })); + + function createDummyContext() { + let context = {}; + context.filledText = []; + context.fillText = function(text, x, y) { + context.filledText.push([text, x, y]); + } + context.measureText = function(text) { + return { + width: 10*text.length + }; + } + context.clear = function() { + context.filledText = []; + } + return context; + } + + //_fillTextMultiline = (context, text, x, y, mode, re, lineHeight, maxX = undefined) + + it("Checks _fillTextMultiline utility function", function() { + var context = createDummyContext(); + var y; + + context.clear(); + y = Nty._fillTextMultiline(context, "", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(''), 7, undefined); + expect(y).toBe(0); + expect(context.filledText).toEqual([ ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(''), 7, 30); + expect(y).toBe(0); + expect(context.filledText).toEqual([ ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hello world", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(' '), 7, 30); + expect(y).toBe(28); + expect(context.filledText).toEqual([ [ 'hel', 0, 0 ], [ 'lo' , 0, 7 ], [ 'wor', 0, 14 ], [ 'ld', 0, 21 ] ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hello", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(''), 7, 30); + expect(y).toBe(14); + expect(context.filledText).toEqual([ [ 'hel', 0, 0 ], [ 'lo', 0, 7 ] ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hello", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(''), 7, undefined); + expect(y).toBe(35); + expect(context.filledText).toEqual([ [ 'h', 0, 0 ], [ 'e', 0, 7 ], [ 'l', 0, 14 ], [ 'l', 0, 21 ], [ 'o', 0, 28 ] ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hello", 0, 0, Nty.MULTILINE_MODE.SPLIT, new RegExp(''), 7, 30); + expect(y).toBe(14); + expect(context.filledText).toEqual([ [ 'hel', 0, 0 ], [ 'lo', 0, 7 ] ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hellohellohelloh", 0, 0, Nty.MULTILINE_MODE.MATCH, new RegExp('.{1,6}', 'g'), 7, undefined); + expect(y).toBe(21); + expect(context.filledText).toEqual([ [ 'helloh', 0, 0 ], [ 'ellohe', 0, 7 ], [ 'lloh', 0, 14 ] ]); + + context.clear(); + y = Nty._fillTextMultiline(context, "hellohellohelloh", 0, 0, Nty.MULTILINE_MODE.MATCH, new RegExp('.{1,6}', 'g'), 7, 50); + expect(y).toBe(35); + expect(context.filledText).toEqual([ [ 'hello', 0, 0 ], [ 'h' , 0, 7 ], [ 'elloh', 0, 14 ], [ 'e', 0, 21 ], [ 'lloh' , 0, 28] ]); + }); + +}); \ No newline at end of file