From 3f53f343fe6dd78723091d94c608914c16dffd97 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Wed, 30 Jun 2021 02:32:55 -0600 Subject: [PATCH] First commit: can open/save/sign/stamp PDFs --- css/main.css | 75 ++++++++++ img/signature-line.svg | 73 ++++++++++ index.html | 120 +++++++++++++++ js/drawtools.js | 164 +++++++++++++++++++++ js/filesystem.js | 46 ++++++ js/main.js | 124 ++++++++++++++++ js/pdf.js | 122 ++++++++++++++++ js/storage.js | 51 +++++++ js/svg-to-image.js | 103 +++++++++++++ js/util.js | 275 +++++++++++++++++++++++++++++++++++ nbproject/project.properties | 6 + nbproject/project.xml | 9 ++ package-lock.json | 228 +++++++++++++++++++++++++++++ package.json | 25 ++++ templates/stamps/mt.svg | 2 + test.pdf | Bin 0 -> 15174 bytes 16 files changed, 1423 insertions(+) create mode 100644 css/main.css create mode 100644 img/signature-line.svg create mode 100644 index.html create mode 100644 js/drawtools.js create mode 100644 js/filesystem.js create mode 100644 js/main.js create mode 100644 js/pdf.js create mode 100644 js/storage.js create mode 100644 js/svg-to-image.js create mode 100644 js/util.js create mode 100644 nbproject/project.properties create mode 100644 nbproject/project.xml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 templates/stamps/mt.svg create mode 100644 test.pdf diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..9aeb356 --- /dev/null +++ b/css/main.css @@ -0,0 +1,75 @@ +/* +Copyright 2021 Netsyms Technologies. +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ +/* + Created on : Jun 27, 2021, 7:34:10 PM + Author : Skylar Ittner +*/ + +html, body { + height: 100%; + width: 100%; +} + +#page-canvas-container { + height: 90vh; + max-height: 90vh; + max-width: 80vw; + overflow: scroll; +} + +#page-canvas-container .page-canvas { + margin: 0.5em; + box-shadow: 6px 6px 7px -1px rgba(0,0,0,0.5); + height: 100%; + border: 1px solid gray; +} + +#page-canvas-container .page-canvas.active { + margin: 0.5em; + box-shadow: 0px 0px 7px 0px rgba(0,255,0,0.8); + height: 100%; + border: 1px solid blue; +} + +#page-canvas-container #placementguidebox { + opacity: 0.5; + position: absolute; + float: left; + margin-top: -40px; + padding: 0; +} + +#signature_pad { + border: 1px solid black; +} + + +.signature-wrapper { + background-color: white; + border-radius: 5px; + position: relative; + width: 400px; + height: 200px; + margin: 0 auto; + border: 1px solid rgba(0,0,0,0.5); + /* fix bug on iOS where image sticks out right side and makes entire page scroll horiz. */ + overflow: hidden; +} + +.signature-wrapper img { + position: absolute; + bottom: 0; + left: 0; +} + +.signature-wrapper canvas { + position: absolute; + left: 0; + top: 0; + width: 400px; + height: 200px; +} \ No newline at end of file diff --git a/img/signature-line.svg b/img/signature-line.svg new file mode 100644 index 0000000..b757c38 --- /dev/null +++ b/img/signature-line.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..e6fddb0 --- /dev/null +++ b/index.html @@ -0,0 +1,120 @@ + + +IPENtool + + + + + + + + + + + + +
+
+
Fit Height
+
Fit Width
+
Zoom Out
+
Zoom In
+
Stamp/Seal
+
Sign (Client)
+
Sign (Notary)
+
+
+ +
+
+ Page count: +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/drawtools.js b/js/drawtools.js new file mode 100644 index 0000000..9e1d37e --- /dev/null +++ b/js/drawtools.js @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Netsyms Technologies. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +var activeDrawImage; +var signaturePadCallback = function () {}; +var clientSignatureSvg = ""; + +function getStampSvg(callback) { + $.get("templates/stamps/" + getStorage("notary_state") + ".svg", {}, function (data) { + data = data + ""; + data = data.replaceAll("[[[NAME]]]", getStorage("notary_name")); + data = data.replaceAll("[[[LOCATION]]]", getStorage("notary_location")); + data = data.replaceAll("[[[EXPIRES]]]", getStorage("notary_expires")); + data = data.replaceAll("[[[IDNUMBER]]]", getStorage("notary_idnumber")); + + callback(data); + }, "text"); +} + +function makeStampImage(callback) { + getStampSvg(function (data) { + svgToImage(data, function (err, image) { + if (err) { + callback(false); + return; + } + callback(image); + }); + }); +} + +function activateStampDrawTool() { + makeStampImage(function (image) { + activeDrawImage = image; + enableGuideBox(image); + }); +} + +function activateNotarySignatureTool() { + if (!inStorage("notary_signature")) { + alert("Please set a notary signature in the settings."); + return; + } + svgToImage(getStorage("notary_signature"), function (err, image) { + if (err) { + callback(false); + return; + } + activeDrawImage = image; + enableGuideBox(image); + }); +} + +function activateClientSignaturePad() { + initSignaturePad(); + signaturePadCallback = function () { + if (clientSignatureSvg != "" && signaturePad.isEmpty()) { + var signature = clientSignatureSvg; + } else { + var signature = signaturePad.toDataURL("image/svg+xml"); + signature = signature.replace("data:image/svg+xml;base64,", ""); + signature = atob(signature); + signature = trimAndShrinkSVG(signature); + clientSignatureSvg = signature; + } + + svgToImage(signature, function (err, image) { + if (err) { + callback(false); + return; + } + activeDrawImage = image; + enableGuideBox(image); + }); + }; +} + +function drawImageFromUrl(x, y, width, height, src, canvas) { + var ctx = canvas.getContext("2d"); + const image = new Image(); + image.src = src; + image.onload = () => { + ctx.drawImage(image, x, y, width, height); + } +} + +function drawImage(x, y, width, height, image, canvas) { + var ctx = canvas.getContext("2d"); + ctx.drawImage(image, x, y, width, height); +} + +$("#page-canvas-container").on("click", ".page-canvas", function (evt) { + $("#page-canvas-container .page-canvas").removeClass("active"); + $(this).addClass("active"); +}); + +$("#page-canvas-container").on("click", ".page-canvas.active", function (evt) { + if (typeof activeDrawImage == "undefined") { + return; + } + var canvas = $(this)[0]; + var coords = getMousePos(canvas, evt); + + var imageWidth = (activeDrawImage.width / 96) * pdfAssumedDPI * pdfPageScale; + var imageHeight = (activeDrawImage.height / 96) * pdfAssumedDPI * pdfPageScale; + drawImage(coords.x, coords.y, imageWidth, imageHeight, activeDrawImage, canvas); +}); + + +function enableGuideBox(image, scalecorrectionfactor) { + if (typeof scalecorrectionfactor == "undefined") { + scalecorrectionfactor = 1; + } + // disable first to clear contents + disableGuideBox(); + $("#placementguidebox").css("display", ""); + // calculate size of guide image + var pageWidthPx = $("#page-canvas-container .page-canvas")[0].getContext("2d").canvas.width; + var pageCanvasCurrentWidthPx = $("#page-canvas-container .page-canvas").css("width").replace("px", "") * 1; + var pageWidthInches = pageWidthPx / (pdfAssumedDPI * pdfPageScale); + var canvasCurrentDPI = pageCanvasCurrentWidthPx / pageWidthInches; + + var imageWidth = (image.width / (96 * scalecorrectionfactor)) * canvasCurrentDPI; + var imageHeight = (image.height / (96 * scalecorrectionfactor)) * canvasCurrentDPI; + + var canvas = $("#placementguidebox")[0]; + var ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, imageWidth, imageHeight); +} + +function disableGuideBox() { + $("#placementguidebox").css("display", "none"); + + var context = $("#placementguidebox")[0].getContext('2d'); + context.clearRect(0, 0, $("#placementguidebox")[0].width, $("#placementguidebox")[0].height); +} + +$("#page-canvas-container").on("mousemove", function (evt) { + $("#placementguidebox").css({ + left: evt.pageX, + top: evt.pageY + }); +}); + +/** + * https://stackoverflow.com/a/17130415 + * @param {type} canvas + * @param {type} evt + * @returns {getMousePos.pdfAnonym$1} + */ +function getMousePos(canvas, evt) { + var rect = canvas.getBoundingClientRect(), // abs. size of element + scaleX = canvas.width / rect.width, // relationship bitmap vs. element for X + scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y + + return { + x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have + y: (evt.clientY - rect.top) * scaleY // been adjusted to be relative to element + } +} \ No newline at end of file diff --git a/js/filesystem.js b/js/filesystem.js new file mode 100644 index 0000000..f271d95 --- /dev/null +++ b/js/filesystem.js @@ -0,0 +1,46 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +function openFileDialog(callback) { + $("#open-file-input").off("change"); + if (typeof callback != "undefined") { + $("#open-file-input").on("change", function () { + callback($("#open-file-input").val()); + }); + } + $("#open-file-input").click(); + +} + +function getFileAsString(path) { + const fs = require("fs"); + return fs.readFileSync(path, "utf8"); +} + +function getFileAsUint8Array(path) { + const fs = require("fs"); + return fs.readFileSync(path, null); +} + +function writeStringToFile(path, text) { + const fs = require("fs"); + fs.writeFileSync(path, text); +} + +function writeDataToFile(path, data) { + const fs = require("fs"); + fs.writeFileSync(path, data); +} + +function copyFile(source, dest) { + const fs = require("fs"); + fs.copyFileSync(source, dest); +} + +function getBasename(fullpath) { + var path = require("path"); + return path.basename(fullpath); +} \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..e111d7f --- /dev/null +++ b/js/main.js @@ -0,0 +1,124 @@ +/* + * Copyright 2021 Netsyms Technologies. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +var pdfjsLib = window['pdfjs-dist/build/pdf']; +pdfjsLib.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.min.js'; + +var signaturePad; + +function setupNotaryOptions(name, location, expires, idnumber, state) { + setStorage("notary_name", name); + setStorage("notary_location", location); + setStorage("notary_expires", expires); + setStorage("notary_idnumber", idnumber); + setStorage("notary_state", state); +} + +function openSettingsModal() { + $("#settingsModal #notary_name").val(getStorage("notary_name")); + $("#settingsModal #notary_location").val(getStorage("notary_location")); + $("#settingsModal #notary_expires").val(getStorage("notary_expires")); + $("#settingsModal #notary_idnumber").val(getStorage("notary_idnumber")); + $("#settingsModal #notary_state").val(getStorage("notary_state")); + + // show preview of stamp + if (inStorage("notary_state")) { + getStampSvg(function (svg) { + $("#settingsModal #stamp-preview").attr("src", "data:image/svg+xml;base64," + btoa(svg)); + }); + } + + // show signature + if (inStorage("notary_signature")) { + $("#settingsModal #signature-preview").attr("src", "data:image/svg+xml;base64," + btoa(getStorage("notary_signature"))); + } + + new bootstrap.Modal(document.getElementById('settingsModal')).show(); +} + +function saveSettingsModal() { + setupNotaryOptions( + $("#settingsModal #notary_name").val(), + $("#settingsModal #notary_location").val(), + $("#settingsModal #notary_expires").val(), + $("#settingsModal #notary_idnumber").val(), + $("#settingsModal #notary_state option:selected").val()); + // show preview of stamp + if (inStorage("notary_state")) { + getStampSvg(function (svg) { + $("#settingsModal #stamp-preview").attr("src", "data:image/svg+xml;base64," + btoa(svg)); + }); + } + // show signature + if (inStorage("notary_signature")) { + $("#settingsModal #signature-preview").attr("src", "data:image/svg+xml;base64," + btoa(getStorage("notary_signature"))); + } +} + +function initSignaturePad() { + var canvas = document.getElementById("signaturecanvas"); + signaturePad = new SignaturePad(canvas, { + backgroundColor: 'rgba(255, 255, 255, 0.5)', + onBegin: function () { + // stop page from jumping around if user starts drawing signature while a text box is focused + $("input").blur(); + } + }); + new bootstrap.Modal(document.getElementById('signatureModal')).show(); + + + $("#signatureModal").on("shown.bs.modal", resizeSignaturePadCanvas); +} + +function resizeSignaturePadCanvas() { + var canvas = document.getElementById("signaturecanvas"); + var ratio = Math.max(window.devicePixelRatio || 1, 1); + canvas.width = canvas.offsetWidth * ratio; + canvas.height = canvas.offsetHeight * ratio; + canvas.getContext("2d").scale(ratio, ratio); + if (signaturePad != null) { + signaturePad.clear(); // otherwise isEmpty() might return incorrect value + } +} + +function signaturePadUndo() { + var data = signaturePad.toData(); + + resizeSignaturePadCanvas(); + + if (data) { + data.pop(); // remove the last dot or line + signaturePad.fromData(data); + } +} + + +function activateNotarySignaturePad() { + initSignaturePad(); + signaturePadCallback = function () { + var signature = signaturePad.toDataURL("image/svg+xml"); + signature = signature.replace("data:image/svg+xml;base64,", ""); + signature = atob(signature); + setStorage("notary_signature", trimAndShrinkSVG(signature)); + $("#settingsModal #signature-preview").attr("src", "data:image/svg+xml;base64," + btoa(getStorage("notary_signature"))); + }; +} + +function trimAndShrinkSVG(svgstring) { + var div = document.getElementById('svgtrimbox'); + div.innerHTML = svgstring; + var svg = div.firstChild; + var bbox = svg.getBBox(); + var viewBox = [bbox.x, bbox.y, bbox.width, bbox.height].join(" "); + svg.setAttribute("viewBox", viewBox); + svg.setAttribute("width", 100); + svg.setAttribute("height", 50); + //console.log(svg.outerHTML); + div.innerHTML = ""; + return svg.outerHTML; +} \ No newline at end of file diff --git a/js/pdf.js b/js/pdf.js new file mode 100644 index 0000000..f1ab0c3 --- /dev/null +++ b/js/pdf.js @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Netsyms Technologies. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const {jsPDF} = window.jspdf; + +var pdfPageScale = 4; +var pdfAssumedDPI = 72; +var pdfDoc = null; +var pageNumber = 0; + +function addPDF() { + openFileDialog(function (path) { + var filedata = getFileAsUint8Array(path); + + /** + * Asynchronously downloads PDF. + */ + pdfjsLib.getDocument(filedata).promise.then(function (pdfDoc_) { + pdfDoc = pdfDoc_; + + renderAllPages(pdfDoc); + pdfZoom("fitwidth"); + // Initial/first page rendering + //renderPage(pageNum); + }); + }); +} + +function savePDF() { + var canvases = $("#page-canvas-container .page-canvas"); + const pdf = new jsPDF({ + unit: "in", + compress: true + }); + // creating a PDF creates a blank page that we don't want to use, + // as we haven't done the calculations yet + pdf.deletePage(1); + for (var i = 0; i < canvases.length; i++) { + var canvas = $("#page-canvas-container .page-canvas")[i]; + var widthpx = canvas.getContext("2d").canvas.width; + var heightpx = canvas.getContext("2d").canvas.height; + var pageWidthInches = widthpx / (pdfAssumedDPI * pdfPageScale); + var pageHeightInches = heightpx / (pdfAssumedDPI * pdfPageScale); + console.log(pageWidthInches + " x " + pageHeightInches); + var pageFormat = [pageWidthInches, pageHeightInches]; + var pageOrientation = (pageWidthInches > pageHeightInches ? "landscape" : "portrait"); + pdf.addPage(pageFormat, pageOrientation); + pdf.addImage($("#page-canvas-container .page-canvas")[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches); + } + pdf.save("signed.pdf"); +} + +function pdfZoom(str) { + disableGuideBox(); + var widthpx = $("#page-canvas-container .page-canvas").css("width").replace("px", "") * 1; + var zoomstep = 100; + console.log(widthpx); + switch (str) { + case "out": + $("#page-canvas-container .page-canvas").css("height", "auto"); + widthpx -= zoomstep; + $("#page-canvas-container .page-canvas").css("width", widthpx + "px"); + break; + case "in": + $("#page-canvas-container .page-canvas").css("height", "auto"); + widthpx += zoomstep; + $("#page-canvas-container .page-canvas").css("width", widthpx + "px"); + break; + case "fitwidth": + $("#page-canvas-container .page-canvas").css("width", "100%"); + $("#page-canvas-container .page-canvas").css("height", "auto"); + break; + case "fitheight": + $("#page-canvas-container .page-canvas").css("height", "100%"); + $("#page-canvas-container .page-canvas").css("width", "auto"); + break; + } +} + +function getNewCanvas(pagenumber) { + var canvas = document.createElement('canvas'); + canvas.id = "pdf-canvas-page-" + pagenumber; + canvas.className = "page-canvas"; + return canvas; +} + +function addPage() { + pageNumber++; + var canvas = getNewCanvas(pageNumber); + var prevPageCanvas = $("#page-canvas-container .page-canvas#pdf-canvas-page-" + (pageNumber - 1))[0]; + canvas.width = prevPageCanvas.getContext("2d").canvas.width; + canvas.height = prevPageCanvas.getContext("2d").canvas.height; + $("#page-canvas-container").append(canvas); +} + +function renderAllPages() { + var startingPageNumber = pageNumber; + var thisDocPageCount = pdfDoc.numPages; + for (var i = 1; i <= pdfDoc.numPages; i++) { + pdfDoc.getPage(i).then(function (page) { + var viewport = page.getViewport({scale: pdfPageScale}); + var canvas = getNewCanvas(page.pageNumber + startingPageNumber); + canvas.height = viewport.height; + canvas.width = viewport.width; + $("#page-canvas-container").append(canvas); + + // Render PDF page into canvas context + var renderContext = { + canvasContext: canvas.getContext("2d"), + viewport: viewport + }; + + page.render(renderContext); + }); + } + pageNumber = pageNumber + thisDocPageCount; + document.getElementById('page_count').textContent = pageNumber; +} \ No newline at end of file diff --git a/js/storage.js b/js/storage.js new file mode 100644 index 0000000..8fcc279 --- /dev/null +++ b/js/storage.js @@ -0,0 +1,51 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +/** + * Save something to persistent storage. + * @param {string} key + * @param {string} value non-string values are converted to strings. + * @returns {undefined} + */ +function setStorage(key, value) { + localStorage.setItem(key, value); +} + +/** + * Get an item from persistent storage. + * @param {type} key + * @returns {DOMString} + */ +function getStorage(key) { + return localStorage.getItem(key); +} + +/** + * Check if an item is in the persistent storage. + * @param {string} key + * @returns {Boolean} + */ +function inStorage(key) { + return localStorage.getItem(key) != null; +} + +/** + * Get all item from persistent storage. + * @returns {Array} [{key: "", value: ""},...] + */ +function getAllStorage() { + var all = []; + for (var key in localStorage) { + if (localStorage.hasOwnProperty(key)) { + all.push({ + key: key, + value: getStorage(key) + }); + } + } + return all; +} \ No newline at end of file diff --git a/js/svg-to-image.js b/js/svg-to-image.js new file mode 100644 index 0000000..d962c36 --- /dev/null +++ b/js/svg-to-image.js @@ -0,0 +1,103 @@ +/* + * https://github.com/Jam3/svg-to-image + * https://github.com/mattdesl/load-img + */ +function loadImage(src, opt, callback) { + if (typeof opt === 'function') { + callback = opt; + opt = null; + } + + var el = document.createElement('img'); + var locked; + + el.onload = function onLoaded() { + if (locked) + return; + locked = true; + + if (callback) + callback(undefined, el); + }; + + el.onerror = function onError() { + if (locked) + return; + locked = true; + + if (callback) + callback(new Error('Unable to load "' + src + '"'), el); + }; + + if (opt && opt.crossOrigin) { + el.crossOrigin = opt.crossOrigin; + } + + el.src = src; + + return el; +} + +function svgToImage(svg, opt, cb) { + + if (typeof opt === 'function') { + cb = opt + opt = {} + } + cb = cb || noop + opt = opt || {} + + if (typeof window === 'undefined') { + return bail('window global is undefined; not in a browser') + } + + var DOMURL = getURL() + if (!DOMURL || + typeof DOMURL.createObjectURL !== 'function' || + typeof DOMURL.revokeObjectURL !== 'function') { + return bail('browser does not support URL.createObjectURL') + } + + if (typeof window.Blob === 'undefined') { + return bail('browser does not support Blob constructor') + } + + if (!Array.isArray(svg)) { + svg = [svg] + } + + var blob + try { + blob = new window.Blob(svg, { + type: 'image/svg+xml;charset=utf-8' + }) + } catch (e) { + return bail(e) + } + + var url = DOMURL.createObjectURL(blob) + loadImage(url, opt, function (err, img) { + DOMURL.revokeObjectURL(url) + if (err) { + // try again for Safari 8.0, using simple encodeURIComponent + // this will fail with DOM content but at least it works with SVG + var url2 = 'data:image/svg+xml,' + encodeURIComponent(svg.join('')) + return loadImage(url2, opt, cb) + } + + cb(err, img) + }) + + function getURL() { + return window.URL || + window.webkitURL || + window.mozURL || + window.msURL + } + + function bail(msg) { + process.nextTick(function () { + cb(new Error(msg)) + }) + } +} \ No newline at end of file diff --git a/js/util.js b/js/util.js new file mode 100644 index 0000000..e0819ee --- /dev/null +++ b/js/util.js @@ -0,0 +1,275 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** + * Generate a UUID. + * From https://stackoverflow.com/a/2117523 + * @returns {String} + */ +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * Take a UNIX timestamp (seconds since Jan 1 1970) and format it. + * (Mostly) compatible with PHP's date() function. + * @param {String} date format string, see https://www.php.net/manual/en/function.date.php + * @param {Integer} timestamp UNIX timestamp + * @return {String} + */ +function formatTimestamp(format, timestamp) { + if (typeof timestamp == "undefined") { + timestamp = time(); + } + var date = new Date(timestamp * 1000); + + var out = ""; + + var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + for (var i = 0; i < format.length; i++) { + var c = format.charAt(i); + // Handle backslash-escaped characters + if (c == "\\" && i < format.length - 1) { + out += format.charAt(i + 1); + i++; + continue; + } + switch (c) { + case "d": + var d = date.getDate(); + if (d < 10) { + out += "0"; + } + out += d; + break; + case "D": + out += days[date.getDay()].substring(0, 3); + break; + case "j": + out += date.getDate(); + break; + case "l": + out += days[date.getDay()]; + break; + case "N": + // TODO + break; + case "S": + // TODO + break; + case "w": + out += date.getDay(); + break; + case "z": + // TODO + break; + case "W": + // TODO + break; + case "F": + out += months[date.getMonth()]; + break; + case "m": + var m = date.getMonth() + 1; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "M": + out += months[date.getMonth()].substring(0, 3); + break; + case "n": + out += date.getMonth() + 1; + break; + case "t": + // TODO + break; + case "L": + // TODO + break; + case "o": + // TODO + break; + case "Y": + out += date.getFullYear(); + break; + case "y": + var y = (date.getFullYear() + ""); + out += y.substring(y.length - 2); + break; + case "a": + if (date.getHours() < 12) { + out += "am"; + } else { + out += "pm"; + } + break; + case "A": + if (date.getHours() < 12) { + out += "AM"; + } else { + out += "PM"; + } + break; + case "B": + // TODO + break; + case "g": + var h = date.getHours() % 12; + if (h == 0) { + h = 12; + } + out += h; + break; + case "G": + out += date.getHours(); + break; + case "h": + var h = date.getHours() % 12; + if (h == 0) { + h = 12; + } + if (h < 10) { + out += "0"; + } + out += h; + break; + case "H": + var h = date.getHours(); + if (h < 10) { + out += "0"; + } + out += h; + break; + case "i": + var ii = date.getMinutes(); + if (ii < 10) { + out += "0"; + } + out += ii; + break; + case "s": + var s = date.getSeconds(); + if (s < 10) { + out += "0"; + } + out += s; + break; + case "u": + out += date.getMilliseconds() * 1000; + break; + case "v": + out += date.getMilliseconds(); + break; + case "e": + // TODO + break; + case "I": + // TODO + break; + case "O": + var off = date.getTimezoneOffset(); + var m = off % 60; + var h = (off - m) / 60; + if (off >= 0) { + out += "+"; + } else { + out += "-"; + } + if (h < 10) { + out += "0"; + } + out += h; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "P": + var off = date.getTimezoneOffset(); + var m = off % 60; + var h = (off - m) / 60; + if (off >= 0) { + out += "+"; + } else { + out += "-"; + } + if (h < 10) { + out += "0"; + } + out += h; + out += ":"; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "T": + // TODO + break; + case "Z": + out += date.getTimezoneOffset() * 60; + break; + case "c": + out += formatTimestamp(timestamp, "Y-m-d\\TH:i:sP"); + break; + case "r": + out += formatTimestamp(timestamp, "D, j M Y G:i:s O"); + break; + case "U": + out += Math.round(timestamp); + break; + default: + out += c; + } + } + + return out; +} + +function timestampToDateTimeString(timestamp) { + return timestampToDateString(timestamp) + " " + timestampToTimeString(timestamp); +} + +function timestampToDateString(timestamp) { + var date = new Date(timestamp * 1000); + + return date.toLocaleDateString(); +} + +function timestampToTimeString(timestamp) { + var date = new Date(timestamp * 1000); + + var pm = date.getHours() >= 12; + var hours = date.getHours() > 12 ? date.getHours() - 12 : date.getHours(); + hours = (hours == 0 ? 12 : hours); + var minutes = date.getMinutes(); + var time = hours + ":" + (minutes < 10 ? "0" + minutes : minutes) + " " + (pm ? "PM" : "AM"); + + return time; +} + +/** + * Get the current UNIX timestamp in seconds. + * @returns {Number} + */ +function time() { + return Date.now() / 1000; +} + +/** + * Get the number of seconds between now and the given timestamp. + * @param {Number} compareto + * @returns {Number} + */ +function timeDiff(compareto) { + return time() - compareto; +} \ No newline at end of file diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..ea44320 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,6 @@ +file.reference.IPENtool-public_html=public_html +file.reference.IPENtool-test=test +file.reference.Sources-IPENtool=. +files.encoding=UTF-8 +project.license=mpl +site.root.folder=${file.reference.Sources-IPENtool} diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..f48d529 --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.web.clientproject + + + IPENtool + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fece215 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,228 @@ +{ + "name": "IPENtool", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/runtime-corejs3": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.7.tgz", + "integrity": "sha512-Wvzcw4mBYbTagyBVZpAJWI06auSIj033T/yNE0Zn1xcup83MieCddZA7ls3kme17L4NOGBrQ09Q+nKB41RLWBA==", + "optional": true, + "requires": { + "core-js-pure": "^3.15.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@fortawesome/fontawesome-free": { + "version": "5.15.3", + "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-free/-/5.15.3/fontawesome-free-5.15.3.tgz", + "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" + }, + "@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "requires": { + "pako": "^1.0.6" + } + }, + "@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "requires": { + "pako": "^1.0.10" + } + }, + "@types/raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==", + "optional": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", + "optional": true + }, + "bootstrap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.2.tgz", + "integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==" + }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, + "canvg": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz", + "integrity": "sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA==", + "optional": true, + "requires": { + "@babel/runtime-corejs3": "^7.9.6", + "@types/raf": "^3.4.0", + "raf": "^3.4.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^5.0.5" + } + }, + "core-js": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz", + "integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==", + "optional": true + }, + "core-js-pure": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.2.tgz", + "integrity": "sha512-D42L7RYh1J2grW8ttxoY1+17Y4wXZeKe7uyplAI3FkNQyI5OgBIAjUfFiTPfL1rs0qLpxaabITNbjKl1Sp82tA==", + "optional": true + }, + "css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "optional": true, + "requires": { + "base64-arraybuffer": "^0.2.0" + } + }, + "dompurify": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.9.tgz", + "integrity": "sha512-+9MqacuigMIZ+1+EwoEltogyWGFTJZWU3258Rupxs+2CGs4H914G9er6pZbsme/bvb5L67o2rade9n21e4RW/w==", + "optional": true + }, + "fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, + "html2canvas": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", + "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", + "optional": true, + "requires": { + "css-line-break": "1.1.1" + } + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "jspdf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.3.1.tgz", + "integrity": "sha512-1vp0USP1mQi1h7NKpwxjFgQkJ5ncZvtH858aLpycUc/M+r/RpWJT8PixAU7Cw/3fPd4fpC8eB/Bj42LnsR21YQ==", + "requires": { + "atob": "^2.1.2", + "btoa": "^1.2.1", + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "fflate": "^0.4.8", + "html2canvas": "^1.0.0-rc.5" + } + }, + "konva": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/konva/-/konva-8.1.0.tgz", + "integrity": "sha512-HkS5jB4oZlj+koSBmKWWWBOoaivnKDsEAZUkk+6xlOT+ryj5HFXkTfBZZKBS+IA0WRz3vDpko0y3LFAzuc86kA==" + }, + "load-img": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-img/-/load-img-1.0.0.tgz", + "integrity": "sha1-CVN0SYk8MqhwkHRkVWbExfqprCY=" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "pdf-lib": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.16.0.tgz", + "integrity": "sha512-P/1SSmElOBKrPlbc+Sn7UxikRQbzVA64+4Dh6/uczPscvq/NatP9eryoOguyBTpTnzICNiG8EnMH4Ziqp2TnFA==", + "requires": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "pdfjs-dist": { + "version": "2.8.335", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.8.335.tgz", + "integrity": "sha512-2IKw7wP1RnzzWJcpkeZwF+cKROFiQext+/WburB6cgKwt9zc8rOyDH7a3FepdcciSGs8SDs/AuWe8qVx+iI6pw==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "requires": { + "performance-now": "^2.1.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "optional": true + }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=", + "optional": true + }, + "signature_pad": { + "version": "3.0.0-beta.4", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-3.0.0-beta.4.tgz", + "integrity": "sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw==" + }, + "stackblur-canvas": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz", + "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", + "optional": true + }, + "svg-pathdata": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz", + "integrity": "sha512-TAAvLNSE3fEhyl/Da19JWfMAdhSXTYeviXsLSoDT1UM76ADj5ndwAPX1FKQEgB/gFMPavOy6tOqfalXKUiXrow==", + "optional": true + }, + "svg-to-image": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/svg-to-image/-/svg-to-image-1.1.3.tgz", + "integrity": "sha1-1v9NiDyo9+P3krQrIyixXL4vsPM=", + "requires": { + "load-img": "^1.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b025a9 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "IPENtool", + "main": "index.html", + "version": "1.0.0", + "keywords": [ + "util", + "functional", + "server", + "client", + "browser" + ], + "author": "Skylar Ittner", + "contributors": [], + "dependencies": { + "@fortawesome/fontawesome-free": "^5.15.3", + "bootstrap": "^5.0.2", + "jquery": "^3.6.0", + "jspdf": "^2.3.1", + "konva": "^8.1.0", + "pdf-lib": "^1.16.0", + "pdfjs-dist": "^2.8.335", + "signature_pad": "^3.0.0-beta.4", + "svg-to-image": "^1.1.3" + } +} diff --git a/templates/stamps/mt.svg b/templates/stamps/mt.svg new file mode 100644 index 0000000..c781db5 --- /dev/null +++ b/templates/stamps/mt.svg @@ -0,0 +1,2 @@ + +[[[NAME]]]Notary Public for theState of MontanaResiding at[[[LOCATION]]]My Commission Expires[[[EXPIRES]]]SEALNotarialState of Montana[[[NAME]]] diff --git a/test.pdf b/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..598ee56c368b21e56a954fa588a6504676c58393 GIT binary patch literal 15174 zcmdVBcUV)|7B?J31VL0pnjnNGoe)asARPomiu59c&`BVnS1X7#>C#0+r1##W_ue}a znsks}-{3ei-kG`g`=0mSf8PDT$vXS2z1m)T?VSTZ#z)e!9AHi!d>$Yd5Mg44FCqfc zaJ7R0L63~hVfdIU5(akwf-z6k@Ih)Y6v7b+g`t4lzjY|XOf8Ki5iUSIE=&iXARi~U z08oGz%qbvffDe*Fz#TB7Q9#}w++t$*Fu3W3l8gJF%9LPma|a6`7$SgqjB-H2jBW9` zIJvoi&iGtFISk45+lL^=K+fMUNb}zw{HE!5y&OMpkuZESd`S&_kSrL;%LUXh!{>zn zHIy*&%<*~nE(G%bun?$WiqC9iXAZQqHD>{8SmDcP;M)Va1%Bwf@P`}xg9LN^_~7Na za0>{v1%ec~fYJz{>V-Q$P5$ZFMV$T?MHD_r(b5zJ)VqkW8t_M894~^3|7S$_{v0T2 zjDpExA_tO(**L)*ETP6fW(0K6hYym0LlLHyaC4BBC0qiIvi$uXAEfSR;((dCAI#uC zB}4v`WDZG$jp=_Sp8r3JS23}%w0Hc?rvF<91pcFj3kUueS_J<=ixei=En&$2Dn8(g zMEaQ$f6VMp!T){q!M`Nj-=zO9!UDcn=>N(5|A~@ciq9WjXjt09P#kIqTVwctHvzvC zo4*MEuah7APs{c=>h+;bbs4av{g*3U{FjM`&q0orPTJvKBHx6irED)wHF^#_AJdU94?CQ z#SUVFK&smrLopkYG|b5o3R9Dl#0N=QI-nlGkWvU+I|Te!hn37JRDFi%-V-QIKjMp5D1tZ$jt@j26172 zARJ&`AwGx@H$U)aqr+%JnwklzNz4A!g}D=@`{|UkvoohN4<`a?4gw1b3SP+I=H|d~ zaG+e_4#qAVa1{OT5~N`$DALl-!4d%nUPv@HK{z^y(qSz9%>`S#Kg9lHtbSosR{qbd zwzd}m_#qbMpa%QX-hUK~x=61-jwcv)`isGKLQ&9>qr%2nPhp0$~S~ zQilMUm5rg6@QV(XA0w2Ng%sc@OoBsU3euu<7#Wdu(s(=?XCYf>}UV=i`8vfXmny4RgJG({UU(f|LOIR645!UObSkz->K0WJ|?VH07U zHvrNwtNhX>Y%Hul3joG8yi3@Zv94k|v2cEU0>HuaU%|z~y9vO;ICBa6%4HmkOBX%@ zE@2a0#-Y7+1&k}9N^ERT$9)^`SxAy(o|*|gNiUBS^7#jbP$)S@^QB8zn2|V_@v(3& zbYlHB5PVDGiYgsuU2=r#($=$g{qRwTNnCOk|%jbafsrmYE zDX#|t5NRO4J?ax@x-nJ8V#5d50~z`cGL?hK0s}l?Bw>T5?J6%lziwypb&yu(6SU-nBon%Ev&DxW%ucj6lIgnH8 z#FKK54-@iESXm!3uLzRM<&hxR^xf~0cbA=bZ0D5}KZgkPJK}w)jp8A%{u~q~j=7cy zY}mPM(E9}!m02^mWH;aZ=y-SszS?SFsOf(!_VKfT050zU^-C5y1D@p{mi&6DLk4NR zoFy+qmQ_URjW_h&x5w(D|AG^pmJ06}VAFwdhbpEeoqjuh-}#*~E%yX2r}Jb?(K)xb z`!3t#(Ncu9lqNOGP@O)rru_q}7&@cv`4^9~SN6>MmRt5@ z>sGEh*yd2?%kJVge9I?p2ykc4FPoJ`ywOMBYtwBG!#GoKD)x9w6Gg+DGLrYsQ#xg& zu&kP|l6`FMU~Ote*r#ag-DF?2gW>8oPnNXW9m+W8o|ycS?R@KTv0Ve{k<~JS4C2?; zrpS=~`3Z(3v4&z_ft8qSPt`mK|IjF##rhhecLe z`qHHDdn>F{?i=0ToM05<%OQ#Okk4R8!faaJ=~c@;a=~Hqs`r)8%pa3gBzoSyII*Hc z5)h?+mui9ct?0azt<9;x;UwsG6+uXl8$C%RdQ^#Ht7*FSn?Z1N>StZsaeuf@u!ia9 z1D_|1P*3Y9TmLMP2PsyNqhNaf*Vs@Tav?xIdV7zB!>DAP(0GCxg?Qci))q=RNpU)s z=A{=MFXA084u+TaC#v-ye_X?*o+*B}nn3s*S9w$HUcauzhpPC_$vhS<>sFLWVR`c4 zp6}#UyF=g2j?9nRlPk8BOLtduLfwab)qR(N<=JT64iO%=C=v!qOs)tmCa_r;*$cm* z4ImS3evjocb%Rm?7Lzj-bfm8@W`j!=X3CLa8l9>>QxtzDai)F8nW;_TB$qGWI@Uk> zF7E~?Vj(1kQF)a35lcT`bbyz!Jo{q!mTy?KAR92g&9& zj}&H~@^almA5#YPrH^?Vc4RkKWts7Vl}+?Ih;mHb80^&^R`_D95H^w+Jv^EijFddWurkm66yZDWlwW6-!$B zV_(2jdzyG}$J2hnDOY~VAT{=_q9;K$SLq-6XCLb+G-d8>bAh7N4G0?$jDySN5vZ4H zK}z?6gCh!)AEIZjWDvU}Z_9x)L<96VrpTrh6L1SuF%B;1btxYY{DVh&xhjmwhr zS`8wn#ID$wzJL8HV!r!cb;Vp+nNl`GWt2boKCdZLcKD zZRJDB5yz?;*^lC*>$*;5VxLI61(|4CwdYski)Z^}D@m8+aUbQwb5?4HPE{YcxQeR? z8@Fd2<`6b5B)Pax6YFshKAaKJ>Y7MM9vTU!X^47R8L9BbpAGNQrRa@C-Y5beay{J0 zDT6n6Q;q!JKl1oA-5303y%QBPb;Px0Fq$U#kUp`fzzIrE8G9|l4)rW8=gshZj!n{h zkWZGVVv13M(9H_}1z#U&XV+e6T}p6aWA>|{sxIzU`$EAE)E&25csh>O&{mq)L zD+ZcY+6-#7qXegdcg_KHm7#4Sp>2%A0;%4?{wxI6#7`$YNa@?mF6So@kQJ773D8#5 zE+$)>lv-C&bia6$&f}}CluewqKvn<>8yXk&kv6tv>fuWRzd2uTzoL;9!8Jwm zE3p<*HH>^;F&kG;Q5c0dSuiTzVqd~e2$zs;ot4II_;GaTwm+u~>}J4Zwn^I(&%H(Y zFJ8D_ZSJ@-9BBm_$=kOT#MQK*Z%3cmRuJ-^3~cM}?VRL3VX83XX ziHggHSy>57ol0D@4l#^aF9(pKRfL1~hiSE?IdZKAQ8f1ZS}hK^4-8lzMQY5_$?XNPV$33SJ#^Cf{{i2wcuR>(U5@kD5T4} z`Rj4$meQ4(Ch{4U6wx-7-QBh#oQddjKv=rQXU=(^@d=y+t}>4!+t>y4j5=3bgh$4Z`LlIrlcxY}@a!@@kS5Y`*oN3dkc`J%$(ugm@tK=opY<1aFU@pJHyK%LkDkogON2NF~ zZ0J1+Axd@GdcRdJ?Ljw!lZJ&O?&Z_ro0wA>t9fw$L`i$d^`gETG~ghDi^%id77|DV)sO8}vl=G|b0jN5E?# zf$oI5Gk9WkCE*SZ+eoRNi*wg+ryUuY8`4+&_v_kLN=`eybV4-ScGj=cWep5C8fXQj zA@5LwUE-7|YOdb288XK^NeQMcdBX#yA9{V`E8=iCZ0bOKvgC( zh8DtGI8)s}U>_>@ifMPx+p0-ul7-{d9uJK zXYCYQG{>5+&H=`QcF~rL@GKDeSDE#n@!r>pG5iCS*ky&n=IUzs494z*OWBTLc3U|Nrx*dAXy}GZ5Wl6 z??Or7UV6W6RIq{xTMb7$0i;R#l!&psYMpK@gX zp2qXhVtkiHDU|(UrT)ydr$83(()Fo=cbkWZEOfTy7lb9YT5rx$@w6?HCUr#BJ5t6! zDcpkAN88ll)^i#P4wTBXJ0z1|gO>b7B?~+lpywqT1u_#pC6@ zeK&iPV+s{_lPE><2HFA=t_KY98je&JdfPuwGE8?_a%me~V|TSKe<7JeP3!%jvrI1Mj|Y- z{e|FE*Z9n*BLyDz>YD7KQFP2otAFLw*F;8*NS*cm>(TAULa~uK#e#&d<}&Pqo4i?1 z=TzSA?=uB7w}v!riWClITDl}0!@>w;i;_!(cQg~`@*=K2>CPEx2`*#WG zCyD+oTZ?1*kP5S)Cu>FDuUm+x7gQ9k`Y_T=S-TtU&I#U`u&7xu40~ZT7#d;IVeMy} ztnwhsxG^~#IlUVt*qkcZ>6XYTJfo(NB~2U;M4QcQ$2QhI6~WYx6AW!z!L5OE)LQt9pRCaM|F_ngntZs7L` z1qFKD{-`*l6ZgxP^1IGAEhiZs_ zmAwZ7`$1?$Dt7x>verp2Jx9(hHs`PT#YDxsxfS314oo*`A1azuy*`l10X?%ObQBpX z4r}ENixl8GTlV1F|C}JFs)aM0rq5tX2ROO0Pm-19xNO8`_&C5xm|+`tFXiB!=L+ks zRz-%YsF~u^FN>N2!u5(;H^1S&%aQSKX>+u$pnH?#vuQ+QU-cI4L4XU5s%sj7+3!r$ zD$9RL&oSH1j}LuSrMl1{1iLdx@@oFLiafeP#g4#ha-W~Lv9`iS;e<@b3oisMT2<4k zcw2MqpgQD4C%$1T&vZdqBoH+wRm@9RMD4hYIS`%$U=KzWth8R917bYS0hb-u+0F`H z+H+Kh)U;op_G9YYHk1o}PQaFN+-koW7@}Lx7E6r#054Yd%sTB|RU@m|=X%$`YFS&z589{CY=A?&rZ zc8WQbnMM^-k3GEoZY@p@EFO-xt_u1{>X?=Yn-%%3n$n&FT8p``6tk*_zut3gwm;Df zAnjDuwq{ky`I_)i%WvBIZP(Z=JRdURVX#_Q)?x_Xu^p3OY<|L2pPCrDzoWwT`5ds? zvlVs(*IuLA2VF~`Tw4r=D!V#1Gj&7`6i`8A4OH-XpB+@0RvB2xafgNATz8HcDXDuy zE9sT5b9xS#P}AzyYjxyVlbXpx`8^AbU%$3_oi2ivTUBA6(`Q#^=m9HnOnBd6Ua$E& zgT7uxT!}vv?lM}*|Bm3PC4zQ(sq}0q_>stXui|6($FsO}`~29jYPdjPRsT*1O@;fq znP;zXEX#DwmqC*zxWRFvSP>r+MW%uX-(CV!S5v46$^50+7v@qX4yh;{xtNR+WKR1; zr?!3Os|K?f$qgm^ffv0x`4DA~j3kp01Cv87skF|WDd?vxURK)1#$8~_Rf%1(XqpnJ z13{|_BX>bdh49s}igm@f7ku6X(E-JF?utG^^oiG)Vxj?bj#<#P`i7hWVEYgmNk%X=Jp64awyo<2Bu>n_=avm6{n z)TV}OO}3zAr^Xb>J!R{h!P9-kwib`!`Q@UgiXfI5Kf53;?}h@+=njVG!Ut54=a%Kw zIJRYdHL^xpL@zaAiMb!rGvo7KlERo?9V_}rqfYi6IPH7Bv!Shn{Z?(JUWiIiDp1Bz zeww{>XyiTNlK=SILVE1UbjNV>>T$@93dyRjW7<&#!SZCl!8w4_i)O0z={ewgB59+W zORQ2bLkH=J7MVm3S9!u?3+zqS>&{mDQ4yiXO0O;5pO2bwR#|wyLtqA8N@_I_J3De+ zU4u4{i8eH1E4V;C08Hs$;+DJAPJMbaGFm+O2-w$7rgAnJ;y{7Uqzgymf(>~*`mIo4 zBwIb4@u_GvLz^gfc0UFGJRam6VE?Su=&@sYaWOq!kxyl{`GF(1XV?0))0!!{m|jH8 zm@EC{PA-J`nYqdV?iL&C=xTMOII&UR@K=+{A~vlDV)^YWXF03Pp{tP(oFPq3deMf8 zar-eE2`Ow1TGF}`@loxIo@A*XL?cJoMy4JbY>x4IH&X1Th`&(Aoz;|#-KD=vWDpQQ z*VGU-GLEa#qaxn4QTrhf-5q;0GeZMH%mhwK}vMV67i>3R< zjKaz|>+WF4(n)#zM)9+)sw}ANsnpXX+|DPBr7B5>DDrO~*#$=VXm~mI zsFMuF9twnt8ShV%uh@_06nqn}={*NvSKFY-XGV8&-h86e7Ho8Xx87wN5xLLROu*0g zQXp7}&1uy*5wH4ga!hwx;)Fb^>6nn1S1B=;f$}!=u3I8 zkseOhVGV&;+cK0N6>DpW+lzi{Mbp>TCzdpg$kq~zH|t1X7h_3Cl_-mR{)DzIZL&YU z{@9%I(<;&)WxJ0=Fqm}{UHhazeXlx^)}D5fud%%&R31IUYURxbs|SJ7IFz$LAJj7E zXv)M&Z!?+J?oxoh#wcc)zmp=K`fS%Co$632fQKRG)C;fecj znTh-6`Fm7aLGAI4(za&-1w%m}i#pt2u%!t-S9ez<@{M!jbzM`jw;9hb%J#AkbvgJH z>3!+Nlbw68dPi`n$cfZp^KuWCA=G{C#qgF-VP5owzPuTJqI%bpidp( zV-^x5mq~6FEkvo$PWS0tOLMT;stt6`$jdF;5XG}()TwU`^bIwBzUIv`PX?B=Haqxg zPZBZ{csrrGa_)!@s;=2u6G6!~?p=~8eiuB4QYIs-^R<3uq1l?%{CPJDW!pcSd?UuQ zy@{Nz#4qyoAv ztw~4aKCxjd75MokbBl=uL#RW~^^fUF0Uz@_2G0SALoa@6WK?Mxw9wkQa!J4Guea&?;u?upbF2-hUR#x*u?Hgr(UZV0f?Bs%mW~#4SN~d>=Vk5FYx@ffpdEPG}L2!*1Uzej*umLboH#x zc{<_jtKih#IHT8k9pG*GQN+eN#Wx`diVP@}Rhcb~(FDH1Mvta$I0qD0Wtl}x8}3zx zHmI!x92HyVv66qgci;piX>y&mwje6ML9y}+$o+N{3DLC9u3oJ~3qE7##Q zhh|Il8i6Le8kMB+m#ldc7QcPafjMS;C_D$qxCuFeq_l%xvGs(FqHVyd*9uBZIcVAd z;xVlO^wcdKuk3?O-6CLL{(On?FBBo~aYwxnwMuhAAYyNQb*|XGCi^Ns)}iSAZPmjt zq2~PTPKef~>xuWKHN!QJAP$QOw_cfnC z=^K}Q6Q0;_aZ~wl4@SNf!Atd_Q*uw+tV(L=s}{-%TJF=4BSkx#o5nFs-ej};%9MIs z&VJ8k%XL+M8X=IWsa{TQ!M$$rJpWX{6H-1WjV2!+?;lG~%K4nHJD()p+u7Vtg%m0gwOj zhF-G?=4}_17tJBWQYmiW@5}wSS5QxZ%qkUPkkxKDZM*bj|5pSB-~HjfcjVJYk)PKN zT(uIPl^rWue}lVJ_iOW7k-er_c6EG2QuA&_FYg>+iSAG942X1@b{L6YdLdVNNw?4K zk{ie-h@qQzL-R3Jb(Rx%$%Oy1UE(Y!9cL*eMA-n6JD@e5a=S?Qx})*FoXA}1>lOH^ zym+Ldz_AgMStoxjc+*Av9ey;~lBZH&^#e53Ub{70QhfG}Hx{kdBMZw^eSDYM!~|U# z4AQ~&p3$^(qJ!%#Lb+ zCh0OfWbFJ-Q2PR1tyU%C8EiuwO&hrQsC%t-WAl`hrr%Iawxz^;hb_iCDp?pt`Ob1f zTft)l;(jmQ(A!LLe~pZFMGUQv_D`c$yvt@eV}v~lJmqHz5_QoE!D2rtUJKR0u2=R1-%{@cB3B2PF>ycaOxWEsG_>H!_wU$rgCo2W*OfHn{gj(^yB@Zx*5aYK`V047r1GO z52R;QDw59FSfxo6)6KXSPJ_+?HX6YsKI^soM{;6|uje@VuHDA(FUvpSOD66@aOy9^ z`L;D&^S85T82K|@P=16Tb`@qGMy8DHj7&b%-%!Q8Nrqa@K3@Jf&b0?jrjBo?FA~9J znV1LUqE&df-XeCPWX9!SUR2YN^`Nfh>rMs=k`7@|O7UA=ffF9VAfiN6nhdIO zruo(8_a;p*oaoWB>s_lT7dudf{ZOvZ_OV%zgMDNn7Y$*TosL_02BYst(Bh$1j!L*V z&ys(4g*QA7S^s<s5-MIF5>bN{Z!HN$)@+BU(NQW)#oP0k7CcBk(HtrY^J1$h^ zlsFQV7tJ$HiItuuyo_3Ru80$l`kHkhhg0dx#MrPJ%i>9N)?we6q1|_XLa~VVn+)RZ z#LHwfb;>!w;uvKTdseklPe7znW2EVJ*5#sMzkm|S!W5`w^bGlm>+8dHb2H zk*vL^LXORfLG#9Fj*XH3?#@Q<*?^2pMM@-;4Z?-J8T6D$B0Xp*ssrvZsq&34DEj1@ z=;X7vw&KEKa<0LQET$eI-IlRwU-#&>dgMnHspJVyGp>y6nVw~hJ`}$(tL)BLx9ZxA z%GF0P$!T{pgmw~r1C|8#t?mnyJT@${a$-0puPp9J8;nbyK33kEznfCm{sOjt50|q&$)u{b4M$EjlhvZSCp`lyE3)?Q!a7SkDxCwNp9)KAENpiN zr@9|C4m368mU1yghX#B=-@dS!5vXw5Qh$FeS_Al(F!SlE6ZqM|9tAsiMl*`JJ;P&^ipf8u9gDh z_<-;9f(6C!;qGpz$?y0oo z^U=E6c1z8_UiPW8`E!LjLD*yh??$~ceWKN(*0C+qX~7ybb>pX1NxqR;Ppq{wdn5_N zm}a9#>p4Idn264nN1c7plw$ME$Peeei{r+|GBY}_8)jL3+66hVcJ`HH$i896X+5#w zS1r}jYc6>fe-xA5O>(v<-`x22p$w%Be4gyqkeuEhrGF&@8egR33Z*U?G@X2jQZCh zpfJxS=NnO51<$H%KM4s(x;cN>0k4Ldg=ARm3tNcMrZ=7hMuMnY+GeYwQ&O$U7LW#N zQLt07zT}9kP=2|Sx@R)kKHo(Yc*tX%!4nE z`X4pV8djAa!@b!{-z-KMo|3#B9CCUoHb3JvEHapR!m4S0Cw(-^BRLi&wjU(o2pv8_ zo)H`?7ksTG_S?LsXLq7Pab2j#nl?aF$Ka$--fEG?RWFW~oSgw_8(-nXv}hU^P51a+ z(qAU>UvICZ?Po(uBE$$HaqHRQ2n;?0vu?zHdg!IP?nfx<*UKlsPXW@<*e}%vLdUHX z-bkE{7bWeH5T{lNf+)$>gk*y&?|7fQO8hQ&jMq3B`_+@t*gEK0bLwKR;xO-#>Etjt zCATso&vP4p1{xip=H*T5CA1&fu#+N)m?0Gg}-+k4kV{nzr#*@m6 zxt(h7Oe0A%E6~=XBUvl7KzZSo>RIBYx(4IJ$1B?7BerYZ8!_gjH_ia1pFINut)i4_ z$|y3XNG0ZL_>cY0`2C*Pua}gy>DMs#XZA{q;$dy~L?w1G3%N21gTKDo+ch=ZuPn*d z&PpF1DL-R;&PD2Uv-w@4uAJqQ%|3d+iu-$s;*G}J17*z}ee)h`QQ_lZ2h}OPV=Pr# zWZ0@|1*adTW0jgJOUcx^p8tpK6jNa@g|F}56l8zFO zsv@(Wn!Nn?#o!``uHZ(Y<3sB7n{Q?&`69|>k(j+vGir+ize?iT#8r4Zsn5X1+^;9w^w^>>z585P2>@Mert1rmgiL zq3ETXQA0`)wtWROdX_51nQxNs@SdK3JGlFed4XwY*^CSE<5ZQmc}LOIcq^ot7HI_3 zdcM&wyPo6W`!nyS%sLbmVYVqx z|K*F^AAdFc<3Mz9-wycZMG_u9Qw0^B?8DWF*sHI`-VSzDH8z$iE=y2EsPue5WO5oB z5LP*Bym84=(c})V)I!n8QL^8AFeaWhu}~FrB4L9?zc=|%7>Lp|- z&E|{Q6?WKE<-hBye&;KQ0Z*}(-xX7A*_sP!MFbQVdM%Pw20yis90TP%eC$!x&eBf* z+KxSyy+yWZyj=MHQ8b>=o6oj}LhYZTl91{Ol+KN%yCswj^}$c^i;`Wb867%p9hGjY z`Z7hD4XMii`u{-xROPAN+lAei?jn$14klP6v-C7sAhDx!F*qMhLv#ETgcUIQ*oQiec zRdNd7!1HOk(sU&P`*Gf5X*TUQ>?L8qoP>We{r{mp-1E`DL&g3C)?iR}NCZ?J10c`? zVc>W`kOs`f;U_Xe^5>`2&rbymdh7xS1{4wbfp5D2pkRP`vfRK6u*e0)1H1rrTu{8g z3;e|e#Rt5AH(gK=;15931<4QmfgZXb1%N+rJ{P1Q5c~tCh9NPW;NOsY7#a-x0d~1y z!SI5A*M?yR|E3QEumJspclgW1!3aiVq*D4F$PljJCX_i2do7Mk}}4CbP(njC`WS)rtv4p zQN<1hmw^7jIQ&HHVX!3*@<6zwjSVLH7raWwt{C705TpW!VGt^gw(wu{xFBI>7^opP z7@zBp288hO@$do7fWPyL6 zufX5?`1moC^_Q^_{=eG7#l`;*d;*Za^9l0(%|0*}A0|2eRRFU|wF{zxVO+|Dx9cX>4f&LtPx# literal 0 HcmV?d00001