aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/WebBoxRenderer.js
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2022-02-09 08:59:32 -0500
committerbobzel <zzzman@gmail.com>2022-02-09 08:59:32 -0500
commita6d904bcd18a2c9962abfd9b5b325340f6b18b0d (patch)
tree32ab8f94e8f742d9303ec06029247931f1ab3895 /src/client/views/nodes/WebBoxRenderer.js
parent66fb0e4bbf88a5e9bcb5869dd7c8b8a7714de024 (diff)
speeding up rendering using bitmaps for webpages and other heavyweight docs.
Diffstat (limited to 'src/client/views/nodes/WebBoxRenderer.js')
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js395
1 files changed, 395 insertions, 0 deletions
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
new file mode 100644
index 000000000..08a5746d1
--- /dev/null
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -0,0 +1,395 @@
+/**
+ *
+ * @param {StyleSheetList} styleSheets
+ */
+var ForeignHtmlRenderer = function (styleSheets) {
+
+ const self = this;
+
+ /**
+ *
+ * @param {String} binStr
+ */
+ const binaryStringToBase64 = function (binStr) {
+ return new Promise(function (resolve) {
+ const reader = new FileReader();
+ reader.readAsDataURL(binStr);
+ reader.onloadend = function () {
+ resolve(reader.result);
+ }
+ });
+ };
+
+ function prepend(extension) {
+ return window.location.origin + extension;
+ }
+ function CorsProxy(url) {
+ return prepend("/corsProxy/") + encodeURIComponent(url);
+ }
+ /**
+ *
+ * @param {String} url
+ * @returns {Promise}
+ */
+ const getResourceAsBase64 = function (webUrl, inurl) {
+ return new Promise(function (resolve, reject) {
+ const xhr = new XMLHttpRequest();
+ //const url = inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl;
+ //const url = CorsProxy(inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl);// inurl.startsWith("http") ? CorsProxy(inurl) : inurl;
+ var url = inurl;
+ if (inurl.startsWith("/static")) {
+ url = (new URL(webUrl).origin + inurl);
+ } else
+ if ((inurl.startsWith("/") && !inurl.startsWith("//"))) {
+ url = CorsProxy(new URL(webUrl).origin + inurl);
+ } else if (!inurl.startsWith("http") && !inurl.startsWith("//")) {
+ url = CorsProxy(webUrl + "/" + inurl);
+ }
+ xhr.open("GET", url);
+ xhr.responseType = 'blob';
+
+ xhr.onreadystatechange = async function () {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ const resBase64 = await binaryStringToBase64(xhr.response);
+
+ resolve(
+ {
+ "resourceUrl": inurl,
+ "resourceBase64": resBase64
+ }
+ );
+ } else if (xhr.readyState === 4) {
+ console.log("COULDN'T FIND: " + (inurl.startsWith("/") ? webUrl + inurl : inurl));
+ resolve(
+ {
+ "resourceUrl": "",
+ "resourceBase64": inurl
+ }
+ );
+ }
+ };
+
+ xhr.send(null);
+ });
+ };
+
+ /**
+ *
+ * @param {String[]} urls
+ * @returns {Promise}
+ */
+ const getMultipleResourcesAsBase64 = function (webUrl, urls) {
+ const promises = [];
+ for (let i = 0; i < urls.length; i++) {
+ promises.push(getResourceAsBase64(webUrl, urls[i]));
+ }
+ return Promise.all(promises);
+ };
+
+ /**
+ *
+ * @param {String} str
+ * @param {Number} startIndex
+ * @param {String} prefixToken
+ * @param {String[]} suffixTokens
+ *
+ * @returns {String|null}
+ */
+ const parseValue = function (str, startIndex, prefixToken, suffixTokens) {
+ const idx = str.indexOf(prefixToken, startIndex);
+ if (idx === -1) {
+ return null;
+ }
+
+ let val = '';
+ for (let i = idx + prefixToken.length; i < str.length; i++) {
+ if (suffixTokens.indexOf(str[i]) !== -1) {
+ break;
+ }
+
+ val += str[i];
+ }
+
+ return {
+ "foundAtIndex": idx,
+ "value": val
+ }
+ };
+
+ /**
+ *
+ * @param {String} cssRuleStr
+ * @returns {String[]}
+ */
+ const getUrlsFromCssString = function (cssRuleStr, selector = "url(", delimiters = [')'], mustEndWithQuote = false) {
+ const urlsFound = [];
+ let searchStartIndex = 0;
+
+ while (true) {
+ const url = parseValue(cssRuleStr, searchStartIndex, selector, delimiters);
+ if (url === null) {
+ break;
+ }
+ searchStartIndex = url.foundAtIndex + url.value.length;
+ if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue;
+ const unquoted = removeQuotes(url.value);
+ if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') {
+ continue;
+ }
+
+ unquoted && urlsFound.push(unquoted);
+ }
+
+ return urlsFound;
+ };
+
+ /**
+ *
+ * @param {String} html
+ * @returns {String[]}
+ */
+ const getImageUrlsFromFromHtml = function (html) {
+ return getUrlsFromCssString(html, "src=", [' ', '>', '\t'], true);
+ };
+ const getSourceUrlsFromFromHtml = function (html) {
+ return getUrlsFromCssString(html, "source=", [' ', '>', '\t'], true);
+ };
+
+ /**
+ *
+ * @param {String} str
+ * @returns {String}
+ */
+ const removeQuotes = function (str) {
+ return str.replace(/["']/g, "");
+ };
+
+ const escapeRegExp = function (string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+ };
+
+ /**
+ *
+ * @param {String} contentHtml
+ * @param {Number} width
+ * @param {Number} height
+ *
+ * @returns {Promise<String>}
+ */
+ const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll) {
+
+ return new Promise(async function (resolve, reject) {
+
+ /* !! The problems !!
+ * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with)
+ * 2. Platform won't wait for external assets to load (fonts, images, etc.)
+ */
+
+ // copy styles
+ let cssStyles = "";
+ let urlsFoundInCss = [];
+
+ for (let i = 0; i < styleSheets.length; i++) {
+ try {
+ const rules = styleSheets[i].cssRules
+ for (let j = 0; j < rules.length; j++) {
+ const cssRuleStr = rules[j].cssText;
+ urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr));
+ cssStyles += cssRuleStr;
+ }
+ } catch (e) {
+
+ }
+ }
+
+ // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss);
+ // for (let i = 0; i < fetchedResourcesFromStylesheets.length; i++) {
+ // const r = fetchedResourcesFromStylesheets[i];
+ // if (r.resourceUrl) {
+ // cssStyles = cssStyles.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64);
+ // }
+ // }
+
+ contentHtml = contentHtml.replace(/<source[^>]*>/g, "") // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture
+ .replace(/noscript/g, "div").replace(/<div class="mediaset"><\/div>/g, "") // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag
+ .replace(/<link[^>]*>/g, "") // don't need to keep any linked style sheets because we've already processed all style sheets above
+ .replace(/srcset="([^ "]*)[^"]*"/g, "src=\"$1\""); // instead of converting each item in the srcset to a data url, just convert the first one and use that
+ let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml);
+ const fetchedResources = await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml);
+ for (let i = 0; i < fetchedResources.length; i++) {
+ const r = fetchedResources[i];
+ if (r.resourceUrl) {
+ contentHtml = contentHtml.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64);
+ }
+ }
+
+ const styleElem = document.createElement("style");
+ styleElem.innerHTML = cssStyles.replace("&gt;", ">").replace("&lt;", "<");
+
+ const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/&gt;/g, ">").replace(/&lt;/g, "<");
+
+ // create DOM element string that encapsulates styles + content
+ const contentRootElem = document.createElement("body");
+ contentRootElem.style.zIndex = "1111";
+ // contentRootElem.style.transform = "scale(0.08)"
+ contentRootElem.innerHTML = styleElemString + contentHtml;
+ contentRootElem.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
+ //document.body.appendChild(contentRootElem);
+
+ const contentRootElemString = new XMLSerializer().serializeToString(contentRootElem);
+
+ // build SVG string
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'>
+ <foreignObject x='0' y='${-scroll}' width='${width}' height='${scroll + height}'>
+ ${contentRootElemString}
+ </foreignObject>
+ </svg>`;
+
+ // convert SVG to data-uri
+ const dataUri = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svg)))}`;
+
+ resolve(dataUri);
+ });
+ };
+
+ /**
+ * @param {String} html
+ * @param {Number} width
+ * @param {Number} height
+ *
+ * @return {Promise<Image>}
+ */
+ this.renderToImage = async function (webUrl, html, width, height, scroll) {
+ return new Promise(async function (resolve, reject) {
+ const img = new Image();
+ console.log("BUILDING SVG for:" + webUrl);
+ img.src = await buildSvgDataUri(webUrl, html, width, height, scroll);
+
+ img.onload = function () {
+ console.log("IMAGE SVG created:" + webUrl);
+ resolve(img);
+ };
+ });
+ };
+
+ /**
+ * @param {String} html
+ * @param {Number} width
+ * @param {Number} height
+ *
+ * @return {Promise<Image>}
+ */
+ this.renderToCanvas = async function (webUrl, html, width, height, scroll) {
+ return new Promise(async function (resolve, reject) {
+ const img = await self.renderToImage(webUrl, html, width, height, scroll);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ const canvasCtx = canvas.getContext('2d');
+ canvasCtx.drawImage(img, 0, 0, img.width, img.height);
+
+ resolve(canvas);
+ });
+ };
+
+ /**
+ * @param {String} html
+ * @param {Number} width
+ * @param {Number} height
+ *
+ * @return {Promise<String>}
+ */
+ this.renderToBase64Png = async function (webUrl, html, width, height, scroll) {
+ return new Promise(async function (resolve, reject) {
+ const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll);
+ resolve(canvas.toDataURL('image/png'));
+ });
+ };
+
+};
+
+
+export function CreateImage(webUrl, styleSheets, html, width, height, scroll) {
+ const val = (new ForeignHtmlRenderer(styleSheets)).renderToBase64Png(webUrl, html.replace(/\n/g, "").replace(/<script((?!\/script).)*<\/script>/g, ""), width, height, scroll);
+ return val;
+}
+
+
+
+var ClipboardUtils = new function () {
+ var permissions = {
+ 'image/bmp': true,
+ 'image/gif': true,
+ 'image/png': true,
+ 'image/jpeg': true,
+ 'image/tiff': true
+ };
+
+ function getType(types) {
+ for (var j = 0; j < types.length; ++j) {
+ var type = types[j];
+ if (permissions[type]) {
+ return type;
+ }
+ }
+ return null;
+ }
+ function getItem(items) {
+ for (var i = 0; i < items.length; ++i) {
+ var item = items[i];
+ if (item) {
+ var type = getType(item.types);
+ if (type) {
+ return item.getType(type);
+ }
+ }
+ }
+ return null;
+ }
+ function loadFile(file, callback) {
+ if (window.FileReader) {
+ var reader = new FileReader();
+ reader.onload = function () {
+ callback(reader.result, null);
+ };
+ reader.onerror = function () {
+ callback(null, 'Incorrect file.');
+ };
+ reader.readAsDataURL(file);
+ } else {
+ callback(null, 'File api is not supported.');
+ }
+ }
+ this.readImage = function (callback) {
+ if (navigator.clipboard) {
+ var promise = navigator.clipboard.read();
+ promise
+ .then(function (items) {
+ var promise = getItem(items);
+ if (promise == null) {
+ callback(null, null);
+ return;
+ }
+ promise
+ .then(function (result) {
+ loadFile(result, callback);
+ })
+ .catch(function (error) {
+ callback(null, 'Reading clipboard error.');
+ });
+ })
+ .catch(function (error) {
+ callback(null, 'Reading clipboard error.');
+ });
+ } else {
+ callback(null, 'Clipboard is not supported.');
+ }
+ };
+};
+
+
+export function pasteImageBitmap(callback) {
+ return ClipboardUtils.readImage(callback);
+} \ No newline at end of file