Add support of fontSize in percentages and cell resolution unit for TTML captions (#2442)

Closes #2403
This commit is contained in:
Tanya Gelahova
2020-03-06 19:53:24 +03:00
committed by GitHub
parent b0238c3c00
commit d6de4e710d
8 changed files with 274 additions and 11 deletions
+1
View File
@@ -50,6 +50,7 @@ SameGoal Inc. <*@samegoal.com>
Sanborn Hilland <sanbornh@rogers.com>
Sander Saares <sander@saares.eu>
TalkTalk Plc <*@talktalkplc.com>
Tatsiana Gelahova <tatsiana.gelahova@gmail.com>
Tomas Tichy <mr.tichyt@gmail.com>
Tomohiro Matsuzawa <thmatuza75@hotmail.com>
Toshihiro Suzuki <t.suzuki326@gmail.com>
+1
View File
@@ -76,6 +76,7 @@ Sandra Lokshina <ismena@google.com>
Satheesh Velmurugan <satheesh@philo.com>
Semih Gokceoglu <semih.gkcoglu@gmail.com>
Seth Madison <seth@philo.com>
Tatsiana Gelahova <tatsiana.gelahova@gmail.com>
Theodore Abshire <theodab@google.com>
Thomas Stephens <thomas@ustudio.com>
Tim Plummer <objelisks@google.com>
+9
View File
@@ -246,6 +246,15 @@ shaka.extern.Cue = class {
*/
this.backgroundColor;
/**
* The number of horizontal and vertical cells into which
* the Root Container Region area is divided
*
* @type {{ columns: number, rows: number }}
* @exportDoc
*/
this.cellResolution;
/**
* Image background represented by any string that would be
* accepted in image HTML element.
+9
View File
@@ -200,6 +200,15 @@ shaka.text.Cue = class {
* @exportInterface
*/
this.spacer = false;
/**
* @override
* @exportInterface
*/
this.cellResolution = {
columns: 32,
rows: 15,
};
}
};
+55 -6
View File
@@ -76,6 +76,7 @@ shaka.text.TtmlTextParser = class {
let tickRate = null;
let spaceStyle = null;
let extent = null;
let cellResolution = null;
const tts = xml.getElementsByTagName('tt');
const tt = tts[0];
// TTML should always have tt element.
@@ -91,6 +92,7 @@ shaka.text.TtmlTextParser = class {
frameRateMultiplier =
XmlUtils.getAttributeNS(tt, ttpNs, 'frameRateMultiplier');
tickRate = XmlUtils.getAttributeNS(tt, ttpNs, 'tickRate');
cellResolution = XmlUtils.getAttributeNS(tt, ttpNs, 'cellResolution');
spaceStyle = tt.getAttribute('xml:space') || 'default';
extent = tt.getAttribute('tts:extent');
}
@@ -107,6 +109,9 @@ shaka.text.TtmlTextParser = class {
const rateInfo = new TtmlTextParser.RateInfo_(
frameRate, subFrameRate, frameRateMultiplier, tickRate);
const cellResolutionInfo =
TtmlTextParser.getCellResolution_(cellResolution);
const metadataElements = TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('metadata')[0]);
const styles = TtmlTextParser.getLeafNodes_(
@@ -127,7 +132,8 @@ shaka.text.TtmlTextParser = class {
for (const node of textNodes) {
const cue = TtmlTextParser.parseCue_(
node, time.periodStart, rateInfo, metadataElements, styles,
regionElements, cueRegions, whitespaceTrim, false);
regionElements, cueRegions, whitespaceTrim, false,
cellResolutionInfo);
if (cue) {
ret.push(cue);
}
@@ -239,12 +245,13 @@ shaka.text.TtmlTextParser = class {
* @param {!Array.<!shaka.text.CueRegion>} cueRegions
* @param {boolean} whitespaceTrim
* @param {boolean} isNested
* @param {?{columns: number, rows: number}} cellResolution
* @return {shaka.text.Cue}
* @private
*/
static parseCue_(
cueElement, offset, rateInfo, metadataElements, styles, regionElements,
cueRegions, whitespaceTrim, isNested) {
cueRegions, whitespaceTrim, isNested, cellResolution) {
if (isNested && cueElement.nodeName == 'br') {
const cue = new shaka.text.Cue(0, 0, '');
cue.spacer = true;
@@ -322,7 +329,8 @@ shaka.text.TtmlTextParser = class {
regionElements,
cueRegions,
whitespaceTrim,
/* isNested= */ true
/* isNested= */ true,
cellResolution,
);
if (nestedCue) {
@@ -334,6 +342,10 @@ shaka.text.TtmlTextParser = class {
const cue = new shaka.text.Cue(start, end, payload);
cue.nestedCues = nestedCues;
if (cellResolution) {
cue.cellResolution = cellResolution;
}
// Get other properties if available.
const regionElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
cueElement, 'region', regionElements, /* prefix= */ '')[0];
@@ -543,7 +555,12 @@ shaka.text.TtmlTextParser = class {
const fontSize = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'fontSize');
if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
const isValidFontSizeUnit = fontSize
&& (fontSize.match(TtmlTextParser.unitValues_)
|| fontSize.match(TtmlTextParser.percentValue_));
if (isValidFontSizeUnit) {
cue.fontSize = fontSize;
}
@@ -933,6 +950,31 @@ shaka.text.TtmlTextParser = class {
return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
}
/**
* If ttp:cellResolution provided returns cell resolution info
* with number of columns and rows into which the Root Container
* Region area is divided
*
* @param {?string} cellResolution
* @return {?{columns: number, rows: number}}
* @private
*/
static getCellResolution_(cellResolution) {
if (!cellResolution) {
return null;
}
const matches = /^(\d+) (\d+)$/.exec(cellResolution);
if (!matches) {
return null;
}
const columns = parseInt(matches[1], 10);
const rows = parseInt(matches[2], 10);
return {columns, rows};
}
};
/**
@@ -996,9 +1038,16 @@ shaka.text.TtmlTextParser.percentValues_ =
/**
* @const
* @private {!RegExp}
* @example 100px
* @example 0.6% 90%
*/
shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em)$/;
shaka.text.TtmlTextParser.percentValue_ = /^(\d{1,2}(?:\.\d+)?|100)%$/;
/**
* @const
* @private {!RegExp}
* @example 100px, 8em, 0.80c
*/
shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em|\d*\.?\d+c)$/;
/**
* @const
+63 -4
View File
@@ -792,8 +792,8 @@ describe('TtmlTextParser', () => {
verifyHelper(
[
{
startTime: 62.05,
endTime: 3723.2,
startTime: 1,
endTime: 2,
payload: 'Test',
color: 'red',
backgroundColor: 'blue',
@@ -803,6 +803,12 @@ describe('TtmlTextParser', () => {
lineHeight: '20px',
fontSize: '10em',
},
{
startTime: 2,
endTime: 4,
payload: 'Test 2',
fontSize: '0.80c',
},
],
'<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
'<styling>' +
@@ -813,12 +819,14 @@ describe('TtmlTextParser', () => {
'tts:fontStyle="italic" ' +
'tts:lineHeight="20px" ' +
'tts:fontSize="10em"/>' +
'<style xml:id="s2" tts:fontSize="0.80c" />' +
'</styling>' +
'<layout>' +
'<region xml:id="subtitleArea" />' +
'</layout>' +
'<body region="subtitleArea">' +
'<p begin="01:02.05" end="01:02:03.200" style="s1">Test</p>' +
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
'<p begin="00:02.00" end="00:04.00" style="s2">Test 2</p>' +
'</body>' +
'</tt>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
@@ -875,6 +883,58 @@ describe('TtmlTextParser', () => {
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});
it('cues should have default cellResolution', () => {
verifyHelper(
[
{
startTime: 1,
endTime: 2,
cellResolution: {
columns: 32,
rows: 15,
},
fontSize: '0.45c',
},
],
'<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
'<styling>' +
'<style xml:id="s1" tts:fontSize="0.45c"/>' +
'</styling>' +
'<body >' +
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
'</body>' +
'</tt>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});
it('parses cellResolution', () => {
verifyHelper(
[
{
startTime: 1,
endTime: 2,
payload: 'Test',
cellResolution: {
columns: 60,
rows: 20,
},
fontSize: '67%',
},
],
'<tt ' +
'xmlns:ttp="http://www.w3.org/ns/ttml#parameter" ' +
'xmlns:tts="http://www.w3.org/ns/ttml#styling" ' +
'ttp:cellResolution="60 20">' +
'<styling>' +
'<style xml:id="s1" tts:fontSize="67%"/>' +
'</styling>' +
'<body >' +
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
'</body>' +
'</tt>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});
it('chooses style on element over style on region', () => {
verifyHelper(
[
@@ -913,7 +973,6 @@ describe('TtmlTextParser', () => {
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});
/**
* @param {!Array} cues
* @param {string} text
+62
View File
@@ -10,6 +10,8 @@ describe('UITextDisplayer', () => {
let video;
/** @type {shaka.ui.TextDisplayer} */
let textDisplayer;
/** @type {number} */
const videoContainerHeight = 450;
/**
* Transform a cssText to an object.
@@ -42,12 +44,20 @@ describe('UITextDisplayer', () => {
beforeAll(() => {
videoContainer =
/** @type {!HTMLElement} */ (document.createElement('div'));
videoContainer.style.height = `${videoContainerHeight}px`;
document.body.appendChild(videoContainer);
video = shaka.test.UiUtils.createVideoElement();
videoContainer.appendChild(video);
});
beforeEach(() => {
textDisplayer = new shaka.ui.TextDisplayer(video, videoContainer);
});
afterEach(async () => {
await textDisplayer.destroy();
});
it('correctly displays styles for cues', async () => {
/** @type {!shaka.text.Cue} */
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
@@ -129,4 +139,56 @@ describe('UITextDisplayer', () => {
// 'writing-mode': 'horizontal-tb',
}));
});
it('correctly displays styles for cellResolution units', async () => {
/** @type {!shaka.text.Cue} */
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
cue.fontSize = '0.80c';
cue.cellResolution = {
columns: 60,
rows: 20,
};
textDisplayer.setTextVisibility(true);
textDisplayer.append([cue]);
// Wait until updateCaptions_() gets called.
await shaka.test.Util.delay(0.5);
// Expected value is calculated based on ttp:cellResolution="60 20"
// videoContainerHeight=450px and tts:fontSize="0.80c" on the default style.
const expectedFontSize = '18px';
const textContainer =
videoContainer.querySelector('.shaka-text-container');
const captions = textContainer.querySelector('span');
const cssObj = parseCssText(captions.style.cssText);
expect(cssObj).toEqual(
jasmine.objectContaining({'font-size': expectedFontSize}));
});
it('correctly displays styles for percentages units', async () => {
/** @type {!shaka.text.Cue} */
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
cue.fontSize = '90%';
cue.cellResolution = {
columns: 32,
rows: 15,
};
textDisplayer.setTextVisibility(true);
textDisplayer.append([cue]);
// Wait until updateCaptions_() gets called.
await shaka.test.Util.delay(0.5);
// Expected value is calculated based on ttp:cellResolution="32 15"
// videoContainerHeight=450px and tts:fontSize="90%" on the default style.
const expectedFontSize = '27px';
const textContainer =
videoContainer.querySelector('.shaka-text-container');
const captions = textContainer.querySelector('span');
const cssObj = parseCssText(captions.style.cssText);
expect(cssObj).toEqual(
jasmine.objectContaining({'font-size': expectedFontSize}));
});
});
+74 -1
View File
@@ -270,9 +270,11 @@ shaka.ui.TextDisplayer = class {
captionsStyle.fontFamily = cue.fontFamily;
captionsStyle.fontWeight = cue.fontWeight.toString();
captionsStyle.fontSize = cue.fontSize;
captionsStyle.fontStyle = cue.fontStyle;
captionsStyle.letterSpacing = cue.letterSpacing;
captionsStyle.fontSize = shaka.ui.TextDisplayer.convertLengthValue_(
cue.fontSize, cue, this.videoContainer_
);
// The line attribute defines the positioning of the text container inside
// the video container.
@@ -361,4 +363,75 @@ shaka.ui.TextDisplayer = class {
panelStyle.height = cue.size + '%';
}
}
/**
* Returns info about provided lengthValue
* @example 100px => { value: 100, unit: 'px' }
* @param {?string} lengthValue
*
* @return {?{ value: number, unit: string }}
* @private
*/
static getLengthValueInfo_(lengthValue) {
const matches = new RegExp(/(\d*\.?\d+)([a-z]+|%+)/).exec(lengthValue);
if (!matches) {
return null;
}
return {
value: Number(matches[1]),
unit: matches[2],
};
}
/**
* Converts length value to an absolute value in pixels.
* If lengthValue is already an absolute value it will not
* be modified. Relative lengthValue will be converted to an
* absolute value in pixels based on Computed Cell Size
*
* @param {string} lengthValue
* @param {!shaka.extern.Cue} cue
* @param {HTMLElement} videoContainer
* @return {string}
* @private
*/
static convertLengthValue_(lengthValue, cue, videoContainer) {
const lengthValueInfo =
shaka.ui.TextDisplayer.getLengthValueInfo_(lengthValue);
if (!lengthValueInfo) {
return lengthValue;
}
const {unit, value} = lengthValueInfo;
switch (unit) {
case '%':
return shaka.ui.TextDisplayer.getAbsoluteLengthInPixels_(
value / 100, cue, videoContainer);
case 'c':
return shaka.ui.TextDisplayer.getAbsoluteLengthInPixels_(
value, cue, videoContainer);
default:
return lengthValue;
}
}
/**
* Returns computed absolute length value in pixels based on cell
* and a video container size
* @param {number} value
* @param {!shaka.extern.Cue} cue
* @param {HTMLElement} videoContainer
* @return {string}
*
* @private
* */
static getAbsoluteLengthInPixels_(value, cue, videoContainer) {
const containerHeight = videoContainer.clientHeight;
return (containerHeight * value / cue.cellResolution.rows) + 'px';
}
};