@wiris/mathtype-html-integration-devkit
Version:
Allows to integrate MathType Web into any JavaScript HTML WYSIWYG rich text editor.
289 lines (269 loc) • 9.87 kB
JavaScript
import Configuration from "./configuration";
import Util from "./util";
/**
* @classdesc
* This class represents MathType Image class. Contains all the logic related
* to MathType images manipulation.
* All MathType images are generated using the appropriate MathType
* integration service: showimage or createimage.
*
* There are two available image formats:
* - svg (default)
* - png
*
* There are two formats for the image src attribute:
* - A data-uri scheme containing the URL-encoded SVG or a PNG's base64.
* - A link to the showimage service.
*/
export default class Image {
/**
* Removes data attributes from an image.
* @param {HTMLImageElement} img - Image where remove data attributes.
*/
static removeImgDataAttributes(img) {
const attributesToRemove = [];
const { attributes } = img;
Object.keys(attributes).forEach((key) => {
const attribute = attributes[key];
if (attribute !== undefined && attribute.name !== undefined && attribute.name.indexOf("data-") === 0) {
// Is preferred keep an array and remove after the search
// because when attribute is removed the array of attributes
// is modified.
attributesToRemove.push(attribute.name);
}
});
attributesToRemove.forEach((attribute) => {
img.removeAttribute(attribute);
});
}
/**
* @static
* Clones all MathType image attributes from a HTMLImageElement to another.
* @param {HTMLImageElement} originImg - The original image.
* @param {HTMLImageElement} destImg - The destination image.
*/
static clone(originImg, destImg) {
const customEditorAttributeName = Configuration.get("imageCustomEditorName");
if (!originImg.hasAttribute(customEditorAttributeName)) {
destImg.removeAttribute(customEditorAttributeName);
}
const mathmlAttributeName = Configuration.get("imageMathmlAttribute");
const imgAttributes = [
mathmlAttributeName,
customEditorAttributeName,
"alt",
"height",
"width",
"style",
"src",
"role",
];
imgAttributes.forEach((iterator) => {
const originAttribute = originImg.getAttribute(iterator);
if (originAttribute) {
destImg.setAttribute(iterator, originAttribute);
}
});
}
/**
* Determines whether an img src contains an SVG.
* @param {HTMLImageElement} img the img element to inspect
* @returns true if the img src contains an SVG, false otherwise
*/
static isSvg(img) {
return img.src.startsWith("data:image/svg+xml;");
}
/**
* Determines whether an img src is encoded in base64 or not.
* @param {HTMLImageElement} img the img element to inspect
* @returns true if the img src is encoded in base64, false otherwise
*/
static isBase64(img) {
return img.src.startsWith("data:image/svg+xml;base64,") || img.src.startsWith("data:image/png;base64,");
}
/**
* Calculates the metrics of a MathType image given the the service response and the image format.
* @param {HTMLImageElement} img - The HTMLImageElement.
* @param {String} uri - The URI generated by the image service: can be a data URI scheme or a URL.
* @param {Boolean} jsonResponse - True the response of the image service is a
* JSON object. False otherwise.
*/
static setImgSize(img, uri, jsonResponse) {
let ar;
let base64String;
let bytes;
let svgString;
if (jsonResponse) {
// Cleaning data:image/png;base64.
if (Image.isSvg(img)) {
// SVG format.
// If SVG is encoded in base64 we need to convert the base64 bytes into a SVG string.
if (!Image.isBase64(img)) {
ar = Image.getMetricsFromSvgString(uri);
} else {
base64String = img.src.substr(img.src.indexOf("base64,") + 7, img.src.length);
svgString = "";
bytes = Util.b64ToByteArray(base64String, base64String.length);
for (let i = 0; i < bytes.length; i += 1) {
svgString += String.fromCharCode(bytes[i]);
}
ar = Image.getMetricsFromSvgString(svgString);
}
// PNG format: we store all metrics information in the first 88 bytes.
} else {
base64String = img.src.substr(img.src.indexOf("base64,") + 7, img.src.length);
bytes = Util.b64ToByteArray(base64String, 88);
ar = Image.getMetricsFromBytes(bytes);
}
// Backwards compatibility: we store the metrics into createimage response.
} else {
ar = Util.urlToAssArray(uri);
}
let width = ar.cw;
if (!width) {
return;
}
let height = ar.ch;
let baseline = ar.cb;
const { dpi } = ar;
if (dpi) {
width = (width * 96) / dpi;
height = (height * 96) / dpi;
baseline = (baseline * 96) / dpi;
}
img.width = width;
img.height = height;
img.style.verticalAlign = `-${height - baseline}px`;
}
/**
* Calculates the metrics of an image which has been resized. Is used to restore the original
* metrics of a resized image.
* @param {HTMLImageElement } img - The resized HTMLImageElement.
*/
static fixAfterResize(img) {
img.removeAttribute("style");
img.removeAttribute("width");
img.removeAttribute("height");
// In order to avoid resize with max-width css property.
img.style.maxWidth = "none";
const processImg = (img) => {
if (img.src.indexOf("data:image") !== -1) {
if (img.src.indexOf("data:image/svg+xml") !== -1) {
// Image is in base64: decode it in order to calculate the size, and then bring it back to base64
// This is a bit of an ugly hack used to recycle the logic of Image.setImgSize instead of rewriting it
// (which would actually make more sense for readibility and efficiency).
if (img.src.indexOf("data:image/svg+xml;base64,") !== -1) {
// 'data:image/svg+xml;base64,'.length === 26
const base64String = img.getAttribute("src").substring(26);
const svgString = window.atob(base64String);
const encodedSvgString = encodeURIComponent(svgString);
img.setAttribute("src", `data:image/svg+xml;charset=utf8,${encodedSvgString}`);
// 'data:image/svg+xml;charset=utf8,'.length === 32.
const svg = decodeURIComponent(img.src.substring(32, img.src.length));
Image.setImgSize(img, svg, true);
// Return src to base64!
img.setAttribute("src", `data:image/svg+xml;base64,${base64String}`);
} else {
// 'data:image/svg+xml;charset=utf8,'.length === 32.
const svg = decodeURIComponent(img.src.substring(32, img.src.length));
Image.setImgSize(img, svg, true);
}
} else {
// 'data:image/png;base64,' === 22.
const base64 = img.src.substring(22, img.src.length);
Image.setImgSize(img, base64, true);
}
} else {
Image.setImgSize(img, img.src);
}
};
// If the image doesn't contain a blob, just process it normally
if (img.src.indexOf("blob:") === -1) {
processImg(img);
// if it does contain a blob, then read that, replace the src with the decoded content, and process it
} else {
const reader = new FileReader();
reader.onload = function () {
img.setAttribute("src", reader.result);
processImg(img);
};
fetch(img.src)
.then((r) => r.blob())
.then((blob) => {
reader.readAsDataURL(blob);
});
}
}
/**
* Returns the metrics (height, width and baseline) contained in a SVG image generated
* by the MathType image service. This image contains as an extra custom attribute:
* the baseline (wrs:baseline).
* @param {String} svgString - The SVG image.
* @return {Array} - The image metrics.
*/
static getMetricsFromSvgString(svgString) {
let first = svgString.indexOf('height="');
let last = svgString.indexOf('"', first + 8, svgString.length);
const height = svgString.substring(first + 8, last);
first = svgString.indexOf('width="');
last = svgString.indexOf('"', first + 7, svgString.length);
const width = svgString.substring(first + 7, last);
first = svgString.indexOf('wrs:baseline="');
last = svgString.indexOf('"', first + 14, svgString.length);
const baseline = svgString.substring(first + 14, last);
if (typeof width !== "undefined") {
const arr = [];
arr.cw = width;
arr.ch = height;
if (typeof baseline !== "undefined") {
arr.cb = baseline;
}
return arr;
}
return [];
}
/**
* Returns the metrics (width, height, baseline and dpi) contained in a PNG byte array.
* @param {Array.<Bytes>} bytes - png byte array.
* @return {Array} The png metrics.
*/
static getMetricsFromBytes(bytes) {
Util.readBytes(bytes, 0, 8);
let width;
let height;
let typ;
let baseline;
let dpi;
while (bytes.length >= 4) {
typ = Util.readInt32(bytes);
if (typ === 0x49484452) {
width = Util.readInt32(bytes);
height = Util.readInt32(bytes);
// Read 5 bytes.
Util.readInt32(bytes);
Util.readByte(bytes);
} else if (typ === 0x62615345) {
// Baseline: 'baSE'.
baseline = Util.readInt32(bytes);
} else if (typ === 0x70485973) {
// Dpis: 'pHYs'.
dpi = Util.readInt32(bytes);
dpi = Math.round(dpi / 39.37);
Util.readInt32(bytes);
Util.readByte(bytes);
}
Util.readInt32(bytes);
}
if (typeof width !== "undefined") {
const arr = [];
arr.cw = width;
arr.ch = height;
arr.dpi = dpi;
if (baseline) {
arr.cb = baseline;
}
return arr;
}
return [];
}
}