diff --git a/package-lock.json b/package-lock.json
index be702943..1a0f8455 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,8 @@
"version": "1.1.5",
"license": "BSD-2-Clause",
"dependencies": {
- "sax": "1.2.1"
+ "sax": "1.2.1",
+ "tinycolor2": "^1.6.0"
},
"devDependencies": {
"@eslint/js": "^9.1.1",
@@ -2800,6 +2801,11 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/tinycolor2": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
+ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
diff --git a/package.json b/package.json
index 591b4d61..42b56d4b 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
"test": "node --test ./src/test/js/*Test.js"
},
"dependencies": {
- "sax": "1.2.1"
+ "sax": "1.2.1",
+ "tinycolor2": "^1.6.0"
},
"devDependencies": {
"@eslint/js": "^9.1.1",
diff --git a/src/main/js/color customisation examples.txt b/src/main/js/color customisation examples.txt
new file mode 100644
index 00000000..5bb48f5b
--- /dev/null
+++ b/src/main/js/color customisation examples.txt
@@ -0,0 +1,7 @@
+[{"colorSelector": "*", "colorGenerator": {"exactColor":"yellow"}}]
+
+[{"colorSelector": "*", "colorGenerator": {"desaturate":"60"}}]
+
+[{"colorSelector": "yellow", "colorGenerator": {"spin":"45"}}]
+
+[{"colorSelector": "yellow", "colorGenerator": {"spin":"45"}},{"colorSelector": "*", "colorGenerator": {"desaturate":"60"}}]
diff --git a/src/main/js/html.js b/src/main/js/html.js
index bd0a557a..8cabd54d 100755
--- a/src/main/js/html.js
+++ b/src/main/js/html.js
@@ -26,11 +26,14 @@
import { reportError } from "./error.js";
import { byName } from "./styles.js";
+import { customizeColor, parseColor, parseLength } from "./utils.js";
/**
* @module imscHTML
*/
+const backgroundColorAdjustSuffix = "BackgroundColorAdjust";
+
const browserIsFirefox = /firefox/i.test(navigator.userAgent);
/**
@@ -56,6 +59,18 @@ const browserIsFirefox = /firefox/i.test(navigator.userAgent);
* is called for the next ISD, otherwise previousISDState should be set to
* null.
*
+ * The
optionsparameter can be used to configure adjustments + * that change the presentation away from the document defaults: + *
sizeAdjust: {number} scales the text size and line padding
+ * lineHeightAdjust: {number} scales the line height
+ * backgroundOpacityScale: {number} scales the backgroundColor opacity
+ * fontFamily: {string} comma-separated list of font family values to use, if present.
+ * colorAdjust: [{colorSelector: selector, ColorGenerator: generator}*] list of color replacement rules
+ * colorOpacityScale: {number} opacity override on text color (ignored if zero)
+ * regionOpacityScale: {number} scales the region opacity
+ * textOutline: {string} textOutline value to use, if present
+ * [span|p|div|body|region]BackgroundColorAdjust: {documentColor: replaceColor*} map of backgroundColors and the value with which to replace them for each element type
+ *
* @param {Object} isd ISD to be rendered
* @param {Object} element Element into which the ISD is rendered
* @param {?IMGResolver} imgResolver Resolve smpte:backgroundURIs into URLs. @@ -68,6 +83,7 @@ const browserIsFirefox = /firefox/i.test(navigator.userAgent); * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback * @param {Object} previousISDState State saved during processing of the previous ISD, or null if initial call * @param {?boolean} enableRollUp Enables roll-up animations (see CEA 708) + * @param {?Object} options Configuration options * @return {Object} ISD state to be provided when this funtion is called for the next ISD */ @@ -80,6 +96,8 @@ export function renderHTML(isd, errorHandler, previousISDState, enableRollUp, + options = Object.assign({}, options) || {}, /* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#deep_clone : */ + /* this isn't a get-out-of-jail for avoiding mutation of the incoming options if we ever put an object reference into options */ ) { /* maintain aspect ratio if specified */ @@ -135,6 +153,7 @@ export function renderHTML(isd, ruby: null, /* is ruby present in a
*/ textEmphasis: null, /* is textEmphasis present in a
*/ rubyReserve: null, /* is rubyReserve applicable to a
*/ + options: options, }; element.appendChild(rootcontainer); @@ -321,7 +340,7 @@ function processElement(context, dom_parent, isd_element, isd_parent) { if (lp && (!lp.isZero())) { - const plength = lp.toUsedLength(context.w, context.h); + const plength = lp.multiply(lp.toUsedLength(context.w, context.h), context.options.sizeAdjust); if (plength > 0) { @@ -1296,26 +1315,56 @@ const STYLING_MAP_DEFS = [ "http://www.w3.org/ns/ttml#styling backgroundColor", function (context, dom_element, isd_element, attr) { + const backgroundColorAdjustMap = + context.options[isd_element.kind + backgroundColorAdjustSuffix]; + + const map_attr = backgroundColorAdjustMap && + customizeColor(attr.toString(), backgroundColorAdjustMap); + if (map_attr) + attr = map_attr; + + let opacity = attr[3]; + /* skip if transparent */ - if (attr[3] === 0) + if (opacity === 0) return; + /* make sure that we allow a multiplier of 0 here*/ + if (context.options.backgroundOpacityScale != undefined) + opacity = opacity * context.options.backgroundOpacityScale; + + opacity = opacity / 255; + dom_element.style.backgroundColor = "rgba(" + attr[0].toString() + "," + attr[1].toString() + "," + attr[2].toString() + "," + - (attr[3] / 255).toString() + + opacity.toString() + ")"; }, ), new HTMLStylingMapDefinition( "http://www.w3.org/ns/ttml#styling color", function (context, dom_element, isd_element, attr) { + /* + *
colorAdjust: {documentColor: replaceColor*} map of document colors and the value with which to replace them
+ * colorOpacityScale: {number} opacity multiplier on text color (ignored if zero)
+ */
+ const opacityMultiplier = context.options.colorOpacityScale || 1;
+
+ const colorAdjustMap = context.options.colorAdjust;
+ if (colorAdjustMap != undefined) {
+ // var map_attr = colorAdjustMap[attr.toString()];
+ const map_attr = customizeColor(attr, colorAdjustMap);
+ if (map_attr)
+ attr = map_attr;
+ }
+
dom_element.style.color = "rgba(" +
attr[0].toString() + "," +
attr[1].toString() + "," +
attr[2].toString() + "," +
- (attr[3] / 255).toString() +
+ (opacityMultiplier * attr[3] / 255).toString() +
")";
},
),
@@ -1399,6 +1448,10 @@ const STYLING_MAP_DEFS = [
/* per IMSC1 */
+ if (context.options.fontFamily) {
+ attr = context.options.fontFamily.split(",");
+ }
+
for (let i = 0; i < attr.length; i++) {
attr[i] = attr[i].trim();
@@ -1495,7 +1548,7 @@ const STYLING_MAP_DEFS = [
new HTMLStylingMapDefinition(
"http://www.w3.org/ns/ttml#styling fontSize",
function (context, dom_element, isd_element, attr) {
- dom_element.style.fontSize = attr.toUsedLength(context.w, context.h) + "px";
+ dom_element.style.fontSize = attr.multiply(attr.toUsedLength(context.w, context.h), context.options.sizeAdjust) + "px";
},
),
@@ -1520,14 +1573,28 @@ const STYLING_MAP_DEFS = [
} else {
- dom_element.style.lineHeight = attr.toUsedLength(context.w, context.h) + "px";
+ dom_element.style.lineHeight =
+ attr.multiply(
+ attr.multiply(
+ attr.toUsedLength(context.w, context.h), context.options.sizeAdjust),
+ context.options.lineHeightAdjust) + "px";
}
},
),
new HTMLStylingMapDefinition(
"http://www.w3.org/ns/ttml#styling opacity",
function (context, dom_element, isd_element, attr) {
- dom_element.style.opacity = attr;
+ /*
+ * Customisable using regionOpacityScale: {number}
+ * which acts as a multiplier.
+ */
+ let opacity = attr;
+
+ if (context.options.regionOpacityScale != undefined) {
+ opacity = opacity * context.options.regionOpacityScale;
+ }
+
+ dom_element.style.opacity = opacity;
},
),
new HTMLStylingMapDefinition(
@@ -1540,7 +1607,13 @@ const STYLING_MAP_DEFS = [
new HTMLStylingMapDefinition(
"http://www.w3.org/ns/ttml#styling overflow",
function (context, dom_element, isd_element, attr) {
- dom_element.style.overflow = attr;
+
+ let overflow = attr;
+ if (context.options.overflow != undefined) {
+ overflow = context.options.overflow;
+ }
+
+ dom_element.style.overflow = overflow;
},
),
new HTMLStylingMapDefinition(
@@ -1661,7 +1734,39 @@ const STYLING_MAP_DEFS = [
"http://www.w3.org/ns/ttml#styling textShadow",
function (context, dom_element, isd_element, attr) {
- const txto = isd_element.styleAttrs[byName.textOutline.qname];
+ let txto = isd_element.styleAttrs[byName.textOutline.qname];
+ const otxto = context.options.textOutline;
+ if (otxto) {
+ if (otxto === "none") {
+
+ txto = otxto;
+
+ } else {
+ const r = {};
+ const os = otxto.split(" ");
+ if (os.length !== 0 && os.length <= 2)
+ {
+ const c = parseColor(os[0]);
+
+ r.color = c;
+
+ if (c !== null)
+ os.shift();
+
+ if (os.length === 1)
+ {
+ const l = parseLength(os[0]);
+
+ if (l)
+ {
+ r.thickness = l;
+
+ txto = r;
+ }
+ }
+ }
+ }
+ }
if (attr === "none" && txto === "none") {
diff --git a/src/main/js/utils.js b/src/main/js/utils.js
index 5a682f5a..321d8815 100644
--- a/src/main/js/utils.js
+++ b/src/main/js/utils.js
@@ -24,6 +24,8 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
+import tinycolor from "tinycolor2";
+
/**
* @module imscUtils
*/
@@ -113,6 +115,85 @@ export function parseColor(str) {
return r;
};
+export function toTinycolor(ic) {
+ return tinycolor(
+ {
+ r: ic[0],
+ g: ic[1],
+ b: ic[2],
+ a: ic[3] / 255,
+ },
+ );
+};
+
+export function fromTinycolor(tc) {
+ const rgb = tc.toRgb();
+ return [ rgb.r, rgb.g, rgb.b, rgb.a * 255 ];
+};
+
+export function customizeColor(inputColor, colorAdjustRules) {
+ let outputColor = inputColor;
+
+ for (let r = 0; r < colorAdjustRules.length; r++) {
+ const colorAdjustRule = colorAdjustRules[r];
+ const matchResult = colorMatchesSelector(inputColor, colorAdjustRule.colorSelector);
+ if (matchResult.matches) {
+ outputColor = generateAdjustedColor(matchResult, colorAdjustRule.colorGenerator);
+ break;
+ }
+ }
+
+ return outputColor;
+};
+
+export function arraysEqual(a1, a2) {
+ let rv = a1.length == a2.length;
+ if (rv) {
+ for (let i = 0; (i < a1.length) && rv; i++) {
+ rv = (a1[i] === a2[i]);
+ }
+ };
+ return rv;
+};
+
+export function colorMatchesSelector(inputColor, colorSelector) {
+ const rv = {
+ matches: false,
+ color: inputColor,
+ };
+
+ const parsedColorSelector = parseColor(colorSelector);
+ if (colorSelector === "*")
+ {
+ rv.matches = true;
+ } else if ( parsedColorSelector ) {
+ rv.matches = arraysEqual(inputColor, parsedColorSelector);
+ };
+
+ return rv;
+};
+
+export function generateAdjustedColor(matchResult, colorGenerator) {
+ let generatedColor = matchResult.color;
+
+ // TODO: refactor this to be a list of properties mapped to functions
+ // and iterate through that instead of this if ... else if ... else if
+ // pattern.
+ if (colorGenerator.exactColor) {
+ generatedColor = parseColor(colorGenerator.exactColor);
+ } else if (colorGenerator.desaturate) {
+ const desaturatedColor = toTinycolor(generatedColor).desaturate(colorGenerator.desaturate);
+ generatedColor = fromTinycolor(desaturatedColor);
+ } else if (colorGenerator.darken) {
+ const darkenedColor = toTinycolor(generatedColor).darken(colorGenerator.darken);
+ generatedColor = fromTinycolor(darkenedColor);
+ } else if (colorGenerator.spin) {
+ const spinnedColor = toTinycolor(generatedColor).spin(colorGenerator.spin);
+ generatedColor = fromTinycolor(spinnedColor);
+ };
+ return generatedColor;
+};
+
const LENGTH_RE = /^((?:\+|-)?\d*(?:\.\d+)?)(px|em|c|%|rh|rw)$/;
export function parseLength(str) {
@@ -336,6 +417,10 @@ export class ComputedLength {
return width * this.rw + height * this.rh;
};
+ multiply(value, factor) {
+ return factor ? value * factor: value;
+ };
+
isZero() {
return this.rw === 0 && this.rh === 0;
};