UNPKG

imsc

Version:

Renders IMSC documents to HTML5 fragments

433 lines (297 loc) • 9.77 kB
/* * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * @module imscUtils */ /* * Parses a TTML color expression * */ const HEX_COLOR_RE = /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/; const DEC_COLOR_RE = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/; const DEC_COLORA_RE = /rgba\(\s*(\d+),\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/; const NAMED_COLOR = { transparent: [0, 0, 0, 0], black: [0, 0, 0, 255], silver: [192, 192, 192, 255], gray: [128, 128, 128, 255], white: [255, 255, 255, 255], maroon: [128, 0, 0, 255], red: [255, 0, 0, 255], purple: [128, 0, 128, 255], fuchsia: [255, 0, 255, 255], magenta: [255, 0, 255, 255], green: [0, 128, 0, 255], lime: [0, 255, 0, 255], olive: [128, 128, 0, 255], yellow: [255, 255, 0, 255], navy: [0, 0, 128, 255], blue: [0, 0, 255, 255], teal: [0, 128, 128, 255], aqua: [0, 255, 255, 255], cyan: [0, 255, 255, 255], }; /** * @param {string} str * @returns {[number, number, number, number]} */ export function parseColor(str) { let m; let r = null; const nc = NAMED_COLOR[str.toLowerCase()]; if (nc !== undefined) { r = nc; } else if ((m = HEX_COLOR_RE.exec(str)) !== null) { r = [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16), (m[4] !== undefined ? parseInt(m[4], 16) : 255)]; } else if ((m = DEC_COLOR_RE.exec(str)) !== null) { r = [parseInt(m[1]), parseInt(m[2]), parseInt(m[3]), 255]; } else if ((m = DEC_COLORA_RE.exec(str)) !== null) { r = [parseInt(m[1]), parseInt(m[2]), parseInt(m[3]), parseInt(m[4])]; } return r; }; const LENGTH_RE = /^((?:\+|-)?\d*(?:\.\d+)?)(px|em|c|%|rh|rw)$/; /** * @param {string} str * @returns {{ value: number, unit: string } | null} */ export function parseLength(str) { let m; let r = null; if ((m = LENGTH_RE.exec(str)) !== null) { r = { value: parseFloat(m[1]), unit: m[2] }; } return r; }; /** * @param {string} str * @returns {string | any[] | null} */ export function parseTextShadow(str) { const shadows = str.match(/([^(,)]|\([^)]+\))+/g); const r = []; for (let i = 0; i < shadows.length; i++) { const shadow = shadows[i].split(" "); if (shadow.length === 1 && shadow[0] === "none") { return "none"; } else if (shadow.length > 1 && shadow.length < 5) { const out_shadow = [null, null, null, null]; /* x offset */ let l = parseLength(shadow.shift()); if (l === null) return null; out_shadow[0] = l; /* y offset */ l = parseLength(shadow.shift()); if (l === null) return null; out_shadow[1] = l; /* is there a third component */ if (shadow.length === 0) { r.push(out_shadow); continue; } l = parseLength(shadow[0]); if (l !== null) { out_shadow[2] = l; shadow.shift(); } if (shadow.length === 0) { r.push(out_shadow); continue; } const c = parseColor(shadow[0]); if (c === null) return null; out_shadow[3] = c; r.push(out_shadow); } } return r; }; /** * @typedef {{ value: number, unit: string }} Offset * @typedef {{ edge: string, offset: Offset }} Position */ /** * @param {string} str * @returns {{h: Position, v: Position} | null} */ export function parsePosition(str) { /* see https://www.w3.org/TR/ttml2/#style-value-position */ const s = str.split(" "); const isKeyword = function (str) { return str === "center" || str === "left" || str === "top" || str === "bottom" || str === "right"; }; if (s.length > 4) { return null; } /* initial clean-up pass */ for (let j = 0; j < s.length; j++) { if (!isKeyword(s[j])) { const l = parseLength(s[j]); if (l === null) return null; s[j] = l; } } /* position default */ const pos = { h: { edge: "left", offset: { value: 50, unit: "%" } }, v: { edge: "top", offset: { value: 50, unit: "%" } }, }; /* update position */ for (let i = 0; i < s.length;) { /* extract the current component */ const comp = s[i++]; if (isKeyword(comp)) { /* we have a keyword */ let offset = { value: 0, unit: "%" }; /* peek at the next component */ if (s.length !== 2 && i < s.length && (!isKeyword(s[i]))) { /* followed by an offset */ offset = s[i++]; } /* skip if center */ if (comp === "right") { pos.h.edge = comp; pos.h.offset = offset; } else if (comp === "bottom") { pos.v.edge = comp; pos.v.offset = offset; } else if (comp === "left") { pos.h.offset = offset; } else if (comp === "top") { pos.v.offset = offset; } } else if (s.length === 1 || s.length === 2) { /* we have a bare value */ if (i === 1) { /* assign it to left edge if first bare value */ pos.h.offset = comp; } else { /* assign it to top edge if second bare value */ pos.v.offset = comp; } } else { /* error condition */ return null; } } return pos; }; export class ComputedLength { /** * @param {number} rw * @param {number} rh */ constructor(rw, rh) { this.rw = rw; this.rh = rh; } /** * @param {number} width * @param {number} height * @returns {number} */ toUsedLength(width, height) { return width * this.rw + height * this.rh; }; /** * @returns {boolean} */ isZero() { return this.rw === 0 && this.rh === 0; }; } /** * Computes a specified length to a root container relative length * * @param {number} lengthVal Length value to be computed * @param {string} lengthUnit Units of the length value * @param {number} emScale length of 1em, or null if em is not allowed * @param {number} percentScale length to which , or null if perecentage is not allowed * @param {number} cellScale length of 1c, or null if c is not allowed * @param {number} pxScale length of 1px, or null if px is not allowed * @param {number} direction 0 if the length is computed in the horizontal direction, 1 if the length is computed in the vertical direction * @return {number} Computed length */ export function toComputedLength(lengthVal, lengthUnit, emLength, percentLength, cellLength, pxLength) { if (lengthUnit === "%" && percentLength) { return new ComputedLength( percentLength.rw * lengthVal / 100, percentLength.rh * lengthVal / 100, ); } else if (lengthUnit === "em" && emLength) { return new ComputedLength( emLength.rw * lengthVal, emLength.rh * lengthVal, ); } else if (lengthUnit === "c" && cellLength) { return new ComputedLength( lengthVal * cellLength.rw, lengthVal * cellLength.rh, ); } else if (lengthUnit === "px" && pxLength) { return new ComputedLength( lengthVal * pxLength.rw, lengthVal * pxLength.rh, ); } else if (lengthUnit === "rh") { return new ComputedLength( 0, lengthVal / 100, ); } else if (lengthUnit === "rw") { return new ComputedLength( lengthVal / 100, 0, ); } else { return null; } } /** * @param {Object} obj * @param {string} prop * @returns {boolean} */ export function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }