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..c944c9ce 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( @@ -1661,7 +1728,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; };