28 Commits
Author SHA1 Message Date
Arnavion b13173112d master 2016-05-04 00:18:56 -07:00
Arnavion 7293203dc2 Also capture window "error" events in page console.
Fixes #78
2016-05-04 00:12:26 -07:00
Arnavion 5f75c60eb0 Added accept attribute to file inputs. 2016-04-09 18:39:51 -07:00
Arnavion 8ec552464c Minor refactoring. 2016-03-20 17:16:44 -07:00
Arnavion 90a16c6350 Use libjass.configure 2016-03-19 23:17:55 -07:00
Arnavion dbe2dddc8b Ask user if their browser supports SVG filter effects, and set enableSvg accordingly.
Fixes #71
2016-03-19 23:16:25 -07:00
Arnavion 4cde2a0b5e Proper main page. 2016-03-10 22:13:28 -08:00
Arnavion 9dc9e3e53c Not just for HTML5 video. 2016-03-10 22:12:52 -08:00
Arnavion 36abcf4b66 v0.11.0 2016-01-24 02:33:06 -08:00
Arnavion 8ef48c7c95 Removed fullscreen handling. 2016-01-23 11:26:04 -08:00
Arnavion aa4e9eb018 Added inputs for arbitrary dummy video width and height. 2016-01-23 11:24:47 -08:00
Arnavion 7b3190956d Select sample video and sample ASS by default. 2016-01-18 00:36:06 -08:00
Arnavion 958da9931f Move inputs inside labels. 2016-01-18 00:34:22 -08:00
Arnavion 85542b38a4 Added a sample video that goes with the sample ASS script. 2016-01-02 00:04:26 -08:00
Arnavion b278de84c8 Preload a sample script. 2016-01-01 20:41:13 -08:00
Arnavion e761cd6444 Implemented dummy video functionality.
See #60
2015-12-31 16:22:32 -08:00
Arnavion 7bf9101b21 Fixed console.warn HTML logger. 2015-12-31 16:22:17 -08:00
Arnavion c97fba6ba8 Set enableSvg to false on IE. 2015-12-27 21:10:13 -08:00
Arnavion 6d2fe37eb2 Demo page improvements.
See #60
2015-12-27 16:34:29 -08:00
Arnavion 36f6600d6c v0.10.0 2015-05-05 21:50:51 -07:00
Arnavion 903e0dd0a7 v0.9.0 2014-11-27 00:22:36 -08:00
Arnavion d74f6e7b87 v0.8.0 2014-08-16 22:02:27 -07:00
Arnavion d7e97098f2 v0.7.0 2014-05-15 15:27:32 -07:00
Arnavion b71ec3c608 v0.6.0 2014-04-04 00:42:49 -07:00
Arnavion 02a70f667c v0.5.0 2014-01-26 11:44:56 -08:00
Arnavion 66969efa30 v0.4.0 2013-12-27 15:14:00 -08:00
Arnavion 19e24deed9 Added autogenerated API documentation. 2013-10-27 17:31:10 -07:00
Arnav Singh 063ab543d9 Create gh-pages branch via GitHub 2013-09-16 23:48:20 -07:00
31 changed files with 21710 additions and 3983 deletions
-18
View File
@@ -1,18 +0,0 @@
node_modules/
dialogue.js
iterators.js
libjass.js
parser.js
tags.js
utility.js
dialogue.min.js
iterators.min.js
libjass.min.js
parser.min.js
tags.min.js
utility.min.js
*.log
*.map
-40
View File
@@ -1,40 +0,0 @@
1. Install node.js from http://nodejs.org/ or via your package manager
1. Change to the directory where you cloned this repository.
1. Run the following command
npm install
This will install the dependencies - [Jake](https://github.com/mde/jake), [PEG.js](http://pegjs.majda.cz/), [TypeScript](http://www.typescriptlang.org/) and [UglifyJS2](https://github.com/mishoo/UglifyJS2). It will then run Jake to build libjass.js and use UglifyJS2 to minify it into libjass.min.js
1. Set the URLs of the video and the ASS file in index.xhtml
<video id="video" src=" <URL OF VIDEO HERE> " controls="">
<track src=" <URL OF SCRIPT HERE> " kind="metadata" data-format="ass" />
</video>
1. Set the URLs of any fonts you want to make available in fonts.css
@font-face {
font-family: " <NAME OF FONT HERE> ";
src: url(" <URL OF FONT HERE> ");
}
for each font. The name of the font is what it's called in the ASS file.
1. Start your web server and navigate to index.xhtml in a browser.
***
### Alternative ways to minify
* UglifyJS2 via command line
uglifyjs libjass.js --source-map libjass.min.js.map --in-source-map libjass.js.map --output libjass.min.js --mangle --compress
* Microsoft AJAX Minifer:
"C:\Program Files (x86)\Microsoft\Microsoft Ajax Minifier\ajaxmin.exe" libjass.js ass.pegjs.js -enc:in utf-8 -enc:out utf-8 -out libjass.min.js -comments:none -debug:false,console,libjass.debugMode,libjass.verboseMode -esc:true -inline:false -map:V3 libjass.min.js.map -strict:true
These commands will not preserve the license notice header in the minified file. Remember to prepend the license notice to libjass.min.js from libjass.js or any one of the TS files.
-133
View File
@@ -1,133 +0,0 @@
var allFiles = [];
var fileTask = function (filename, dependencies, callback, options) {
file(filename, dependencies, function () {
console.log("Building " + filename);
callback.call(this, arguments);
}, options);
allFiles.push(filename);
};
fileTask("libjass.js", ["dialogue.ts", "iterators.ts", "parser.ts", "tags.ts", "utility.ts", "ass.pegjs"], function () {
jake.exec(["tsc libjass.ts --out libjass.js --sourcemap --noImplicitAny --target ES5"], { printStdout: true, printStderr: true }, function () {
var fs = require("fs");
fs.readFile("ass.pegjs", { encoding: "utf8" }, function (error, data) {
if (error) {
throw error;
}
var PEG = require("pegjs");
var parser = PEG.buildParser(data);
fs.appendFile("libjass.js", "libjass.parser = " + parser.toSource() + ";\n", function (error) {
if (error) {
throw error;
}
complete();
});
});
});
}, { async: true });
fileTask("libjass.min.js", ["libjass.js"], function () {
var fs = require("fs");
fs.readFile("libjass.js.map", { encoding: "utf8"}, function (error, data) {
if (error) {
throw error;
}
var inputSourceMap = JSON.parse(data);
fs.readFile("libjass.js", { encoding: "utf8" }, function (error, data) {
if (error) {
throw error;
}
var UglifyJS = require("uglify-js");
// Parse
var ast = UglifyJS.parse(data, {
strict: true,
filename: "libjass.js"
});
ast.figure_out_scope();
ast.scope_warnings();
// Compress
var compressor = UglifyJS.Compressor();
ast = ast.transform(compressor);
ast.figure_out_scope();
// Mangle
ast.compute_char_frequency();
ast.mangle_names();
// Output and sourcemap
var sourceMap = UglifyJS.SourceMap({
file: "libjass.min.js",
orig: inputSourceMap,
root: inputSourceMap.sourceRoot
});
var firstCommentFound = false; // To detect and preserve the first license header
var output = UglifyJS.OutputStream({
beautify: false,
comments: function (node, comment) {
if (!firstCommentFound) {
firstCommentFound = !firstCommentFound;
return true;
}
return false;
},
source_map: sourceMap
});
ast.print(output);
// Write to files
fs.writeFile("libjass.min.js.map", sourceMap, function () {
if (error) {
throw error;
}
var minifiedCode = output.get() + "\n//# sourceMappingURL=libjass.min.js.map";
fs.writeFile("libjass.min.js", minifiedCode, function (error) {
if (error) {
throw error;
}
complete();
});
});
});
});
}, { async: true });
task("default", ["libjass.min.js"], function () {
});
task("clean", function () {
var fs = require("fs");
var t = function (i) {
if (i >= allFiles.length) {
complete();
return;
}
fs.unlink(allFiles[i], function (error, data) {
if (error && error.code !== "ENOENT") {
throw error;
}
t(i + 1);
});
};
t(0);
}, { async: true });
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-107
View File
@@ -1,107 +0,0 @@
libjass is a JavaScript library written in TypeScript to render ASS subs on HTML5 video in the browser.
### What's special about libjass?
* libjass requires no tweaks to the ASS file from the original video.
* It's easy to deploy. There is no server-side support required. A static hosting is all that's needed.
* One way to render subtitles on an HTML5 video is to draw them on an HTML5 &lt;canvas&gt;. However, libjass uses the browser's native CSS engine by converting the components of each line in the ASS script into a series of styled &lt;div&gt; and &lt;span&gt; elements. This allows all the layout and rendering to be handled by the browser instead of requiring complex and costly drawing and animation code. For example, libjass uses CSS3 animations to simulate tags such as \fad. While a canvas-drawing library would have to re-draw such a subtitle on the canvas for every frame of the video, libjass only renders it once and lets the browser render the fade effect.
As a result, libjass is able to render subtitles with very low CPU usage. The downside to libjass's aproach is that it is hard (and potentially impossible) to map all effects possible in ASS (using \t, ASS draw) etc. into DOM elements. As of now, the subset of tags supported by libjass has no such problems.
### What are all these files?
* The .ts files are the source of libjass. They are TypeScript files and must be compiled into JavaScript for the browser using the TypeScript compiler.
* The ass.pegjs file is the source of a parser for the ASS format.
* The rest of the files - index.xhtml, index.js, index.css and fonts.css - are a sample implementation of how to use libjass on a web page. They demonstrate the API to call, how to place &lt;div&gt; elements to render the subs, etc.
### I want to use libjass for my website. What do I need to do?
1. You need to build libjass.js using the instructions in BUILD.md
1. You need to load libjass.js on the page with your video.
1. You need to call the libjass API.
Only libjass.js is needed to use libjass on your website. The other files are only used during the build process and you don't need to deploy them to your website.
### Where's the API documentation? What API do I need to call to use libjass?
Formal documentation is coming soon. In the meantime, here's an overview:
* The constructor ASS() takes in the raw ASS string and returns an object representing the script information, the line styles and dialogue lines in it. The example index.js uses XHR to get this data using the URL specified in a track tag.
* ASS.dialogues is an array of Dialogue objects, each corresponding to a dialogue in the ASS file. These objects have a draw() method that returns a &lt;div&gt; containing the rendered subtitle line.
* index.js uses information from the ASS object to build up a series of div elements around the video tag. There is a wrapper (#subs) containing div's corresponding to the 9 alignment directions, 9 for each layer in the ASS script. index.css contains styles for these div's to render them at the correct location.
* It then listens for the video element's "timeupdate" event. In the event handler, it determines the set of dialogues to be shown, calls draw() on each of them, and appendChild's the result into the appropriate layer+alignment div. It only does this if the dialogue has not already been drawn by a previous timeupdate.
* index.js also contains code to change the size of the video based on user input, such as choosing a different resolution or clicking the browser's native fullscreen-video button. It demonstrates the API that should be called to tell the Dialogue objects to start drawing to the new size - ASS.scaleTo()
* Lastly, index.js contains an implementation of preloading all the fonts used in the ASS file. It matches the font names extracted from the script with URLs defined in fonts.css and XHR's the fonts.
### Can I contribute?
Yes! Feature requests, suggestions, bug reports and pull requests are welcome! I'm especially looking for details and edge-cases of the ASS syntax that libjass doesn't support.
You can also hop by the IRC channel below and ask any questions.
## Links
* [Website](https://github.com/Arnavion/libjass/)
* IRC channel - #libjass on irc.rizon.net
* [Aegisub's documentation on ASS](http://docs.aegisub.org/3.0/ASS_Tags/)
## Supported features
* Styles: Italic, Bold, Underline, StrikeOut, FontName, FontSize, ScaleX, ScaleY, Spacing, PrimaryColor, OutlineColor, Outline, Alignment, MarginL, MarginR, MarginV
* Tags: \i, \b, \u, \s, \bord, \xbord, \ybord, \blur, \fn, \fs, \fscx, \fscy, \fsp, \frx, \fry, \frz, \fax, \fay, \c, \1c, \3c, \alpha, \1a, \3a, \an, \r, \pos, \fad
* Custom fonts, using CSS web fonts.
## Known bugs
* \an4, \an5, \an6 aren't positioned correctly.
* Unsupported tags: Everything else, notably \t.
* Font sizes aren't pixel perfect.
* \blur uses an approximation instead of Gaussian blur.
* ASS draw is unsupported.
## Planned improvements
* Document browser compatibility. Currently libjass is tested on IE11 (Windows 7), Firefox Nightly and Google Chrome (Dev channel).
* Write API documentation. Add more explanatory comments to the code.
* Write more parser tests. Also figure out a way to test layout.
* Evaluate (document, benchmark) the benefits and drawbacks of DOM+CSS-based drawing over canvas.
# License
```
libjass
https://github.com/Arnavion/libjass
Copyright 2013 Arnav Singh
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
+10596
View File
File diff suppressed because it is too large Load Diff
-365
View File
@@ -1,365 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
dialogue
= parts:(enclosedTags / comment / newline / hardspace / text)* {
// Flatten parts
parts = parts.reduce(function (previous, current) {
return previous.concat(current);
}, []);
// Merge consecutive text and comment parts into one part
parts = parts.reduce(function (previous, current) {
if (current instanceof libjass.tags.Text && previous[previous.length - 1] instanceof libjass.tags.Text) {
previous[previous.length - 1] = new libjass.tags.Text(previous[previous.length - 1].value + current.value);
}
else if (current instanceof libjass.tags.Comment && previous[previous.length - 1] instanceof libjass.tags.Comment) {
previous[previous.length - 1] = new libjass.tags.Comment(previous[previous.length - 1].value + current.value);
}
else {
previous.push(current);
}
return previous;
}, []);
return parts;
}
enclosedTags
= "{" tagsWithSlashes:(
"\\" tag_alpha /
"\\" tag_xbord /
"\\" tag_ybord /
"\\" tag_bord /
"\\" tag_blur /
"\\" tag_fscx /
"\\" tag_fscy /
"\\" tag_fsp /
"\\" tag_frx /
"\\" tag_fry /
"\\" tag_frz /
"\\" tag_fax /
"\\" tag_fay /
"\\" tag_pos /
"\\" tag_fad /
"\\" tag_fn /
"\\" tag_fs /
"\\" tag_1c /
"\\" tag_3c /
"\\" tag_1a /
"\\" tag_3a /
"\\" tag_an /
"\\" tag_i /
"\\" tag_b /
"\\" tag_u /
"\\" tag_s /
"\\" tag_c /
"\\" tag_r
)+
"}" {
return tagsWithSlashes.map(function (tagWithSlash) { return tagWithSlash[1]; });
}
comment
= "{" value:[^}]* "}" {
return new libjass.tags.Comment(
value.join("")
);
}
newline
= "\\N" {
return new libjass.tags.NewLine();
}
hardspace
= "\\h" {
return new libjass.tags.HardSpace();
}
text
= value:. {
return new libjass.tags.Text(
value
);
}
tag_i
= "i" value:enableDisable? {
return new libjass.tags.Italic(
(value !== "") ? value : null
);
}
tag_b
= "b" value:(([1-9] "0" "0") / "1" / "0")? {
if (Array.isArray(value)) {
value = value.join("");
}
switch (value) {
case "1":
return new libjass.tags.Bold(true);
case "0":
return new libjass.tags.Bold(false);
case "":
return new libjass.tags.Bold(null);
default:
return new libjass.tags.Bold(parseInt(value));
}
}
tag_u
= "u" value:enableDisable? {
return new libjass.tags.Underline(
(value !== "") ? value : null
);
}
tag_s
= "s" value:enableDisable? {
return new libjass.tags.StrikeThrough(
(value !== "") ? value : null
);
}
tag_bord
= "bord" value:decimal? {
return new libjass.tags.Border(
(value !== "") ? value : null
);
}
tag_xbord
= "bord" value:decimal? {
return new libjass.tags.BorderX(
(value !== "") ? value : null
);
}
tag_ybord
= "bord" value:decimal? {
return new libjass.tags.BorderY(
(value !== "") ? value : null
);
}
tag_blur
= "blur" value:decimal? {
return new libjass.tags.Blur(
(value !== "") ? value : null
);
}
tag_fn
= "fn" value:[^\\}]* {
return new libjass.tags.FontName(
(value.length > 0) ? value.join("") : null
);
}
tag_fs
= "fs" value:decimal? {
return new libjass.tags.FontSize(
(value !== "") ? value : null
);
}
tag_fscx
= "fscx" value:decimal? {
return new libjass.tags.FontScaleX(
(value !== "") ? (value / 100) : null
);
}
tag_fscy
= "fscy" value:decimal? {
return new libjass.tags.FontScaleY(
(value !== "") ? (value / 100) : null
);
}
tag_fsp
= "fsp" value:decimal? {
return new libjass.tags.LetterSpacing(
(value !== "") ? value : null
);
}
tag_frx
= "frx" value:decimal? {
return new libjass.tags.RotateX(
(value !== "") ? value : null
);
}
tag_fry
= "fry" value:decimal? {
return new libjass.tags.RotateY(
(value !== "") ? value : null
);
}
tag_frz
= "frz" value:decimal? {
return new libjass.tags.RotateZ(
(value !== "") ? value : null
);
}
tag_fax
= "fax" value:decimal? {
return new libjass.tags.SkewX(
(value !== "") ? value : null
);
}
tag_fay
= "fay" value:decimal? {
return new libjass.tags.SkewY(
(value !== "") ? value : null
);
}
tag_1c
= "1c" value:color? {
return new libjass.tags.PrimaryColor(
(value !== "") ? value : null
);
}
tag_c
= "c" value:color? {
return new libjass.tags.PrimaryColor(
(value !== "") ? value : null
);
}
tag_3c
= "3c" value:color? {
return new libjass.tags.OutlineColor(
(value !== "") ? value : null
);
}
tag_alpha
= "alpha" value:alpha? {
return new libjass.tags.Alpha(
(value !== "") ? value : null
);
}
tag_1a
= "1a" value:alpha? {
return new libjass.tags.PrimaryAlpha(
(value !== "") ? value : null
);
}
tag_3a
= "3a" value:alpha? {
return new libjass.tags.OutlineAlpha(
(value !== "") ? value : null
);
}
tag_an
= "an" value:[1-9] {
return new libjass.tags.Alignment(
parseInt(value)
);
}
tag_r
= "r" value:[^\\}]* {
return new libjass.tags.Reset(
(value.length > 0) ? value.join("") : null
);
}
tag_pos
= "pos(" x:decimal "," y:decimal ")" {
return new libjass.tags.Pos(
x,
y
);
}
tag_fad
= "fad(" start:decimal "," end:decimal ")" {
return new libjass.tags.Fade(
parseFloat(start) / 1000,
parseFloat(end) / 1000
);
}
decimal
= sign:"-"? unsignedDecimal:unsignedDecimal {
return (sign === "") ? unsignedDecimal : -unsignedDecimal;
}
unsignedDecimal
= characteristic:[0-9]+ mantissa:("." [0-9]+)? {
return parseFloat(
characteristic.join("") +
(mantissa[0] || "") + (mantissa[1] && mantissa[1].join("") || "")
);
}
enableDisable
= value:("0" / "1") {
switch (value) {
case "0":
return false;
case "1":
return true;
}
}
hex
= [0-9a-fA-F]
color
= "&H" blue:(hex hex) green:(hex hex) red:(hex hex) "&" {
return new libjass.tags.Color(
parseInt(red.join(""), 16),
parseInt(green.join(""), 16),
parseInt(blue.join(""), 16)
);
}
alpha
= "&H" value:(hex hex) "&" {
return 1 - parseInt(value.join(""), 16) / 255;
}
colorWithAlpha
= "&H" alpha:(hex hex) blue:(hex hex) green:(hex hex) red:(hex hex) {
return new libjass.tags.Color(
parseInt(red.join(""), 16),
parseInt(green.join(""), 16),
parseInt(blue.join(""), 16),
1 - parseInt(alpha.join(""), 16) / 255
);
}
+204
View File
@@ -0,0 +1,204 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
/**
* Creates a video of the given color, dimensions and duration, and prepares the given video element to play it.
*/
function makeDummyVideo(video, width, height, color, duration) {
return new libjass.Promise(function (resolve, reject) {
video.width = width;
video.height = height;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.fillStyle = color;
context.fillRect(0, 0, width, height);
var stream = canvas.captureStream(0);
var recorder = new MediaRecorder(stream);
recorder.start(1); // Get as many events as possible to have a chance at getting the smallest possible chunk.
var blob = null;
recorder.addEventListener("dataavailable", function (event) {
if (recorder.state === "inactive") {
// Being called after recorder.stop(). Do nothing.
return;
}
if (event.data.size === 0) {
console.warn("No new data.");
return;
}
recorder.pause(); // Don't get flooded with new blobs while parsing the current blob.
if (blob === null) {
blob = event.data;
if (!MediaSource.isTypeSupported(blob.type)) {
/* MediaRecorder may record a format that MediaSource doesn't support. As of Nightly 46, this is true, since MediaRecorder
* records webm which MediaSource doesn't play unless media.mediasource.webm.enabled is true in about:config
*/
recorder.stop();
reject(new Error("MediaRecorder is recording video in " + blob.type + " but MediaSource doesn't support it. Make sure media.mediasource.webm.enabled is on in about:config"));
return;
}
}
else {
blob = new Blob([blob, event.data], { type: blob.type });
}
// Data is available but may not contain any frames. Test for that.
libjass.Promise.all([newMediaSourceAndBuffer(video, blob.type), blobToArrayBuffer(blob)]).then(function (results) {
var mediaSource = results[0][0];
var sourceBuffer = results[0][1];
var buffer = results[1];
return appendBuffer(sourceBuffer, buffer).then(function () {
console.log("Got enough data for " + getEndTime(sourceBuffer) + " seconds.");
return [mediaSource, sourceBuffer, buffer];
});
}).then(function (result) {
resolve(result);
recorder.stop();
}, function (reason) {
console.warn(reason);
console.warn("Waiting for more data...");
recorder.resume();
});
});
}).then(function (results) {
var mediaSource = results[0];
var sourceBuffer = results[1];
var buffer = results[2];
return appendBufferUntil(sourceBuffer, buffer, duration).then(function () {
return mediaSource.endOfStream();
});
});
}
/**
* Sets up the given `video` to use a new MediaSource, and appends a new SourceBuffer of the given `type`.
*/
function newMediaSourceAndBuffer(video, type) {
return new libjass.Promise(function (resolve, reject) {
var mediaSource = new MediaSource();
function onSourceOpen() {
mediaSource.removeEventListener("sourceopen", onSourceOpen, false);
try {
var sourceBuffer = mediaSource.addSourceBuffer(type);
resolve([mediaSource, sourceBuffer]);
}
catch (ex) {
reject(ex);
}
}
mediaSource.addEventListener("sourceopen", onSourceOpen, false);
video.src = URL.createObjectURL(mediaSource);
});
}
/**
* Converts a Blob to an ArrayBuffer
*/
function blobToArrayBuffer(blob) {
return new libjass.Promise(function (resolve, reject) {
var fileReader = new FileReader();
fileReader.addEventListener("load", function () {
resolve(fileReader.result);
}, false);
fileReader.addEventListener("error", function (event) {
reject(event);
});
fileReader.readAsArrayBuffer(blob);
});
}
/**
* Appends the given video data `buffer` to the given `sourceBuffer`.
*/
function appendBuffer(sourceBuffer, buffer) {
return new libjass.Promise(function (resolve, reject) {
var currentEndTime = getEndTime(sourceBuffer);
function onUpdateEnd() {
sourceBuffer.removeEventListener("updateend", onUpdateEnd, false);
if (sourceBuffer.buffered.length === 0) {
reject(new Error("buffer of length " + buffer.byteLength + " could not be appended to sourceBuffer. It's probably too small and doesn't contain any frames."));
return;
}
var newEndTime = getEndTime(sourceBuffer);
if (newEndTime === currentEndTime) {
reject(new Error("sourceBuffer is not increasing in size. Perhaps buffer is too small?"));
return;
}
resolve();
}
sourceBuffer.addEventListener("updateend", onUpdateEnd, false);
sourceBuffer.timestampOffset = currentEndTime;
sourceBuffer.appendBuffer(buffer);
});
}
/**
* Repeatedly appends the given video data `buffer` to the given `sourceBuffer` until it is of `duration` length.
*/
function appendBufferUntil(sourceBuffer, buffer, duration) {
var currentEndTime = getEndTime(sourceBuffer);
if (currentEndTime < duration) {
return appendBuffer(sourceBuffer, buffer).then(function () {
return appendBufferUntil(sourceBuffer, buffer, duration);
});
}
else {
return libjass.Promise.resolve();
}
}
/**
* Gets the end time of a SourceBuffer.
*/
function getEndTime(sourceBuffer) {
return (sourceBuffer.buffered.length === 0) ? 0 : sourceBuffer.buffered.end(0);
}
+23 -6
View File
@@ -18,12 +18,29 @@
* limitations under the License.
*/
sections.push(new Section("Color",
new Test("BBGGRR", "&H3F171F&", "color", new libjass.tags.Color(31, 23, 63, 1)),
body {
margin: 0;
padding: 10px;
box-sizing: border-box;
}
new Test("AABBGGRR", "&H00434441", "colorWithAlpha", new libjass.tags.Color(65, 68, 67, 1)),
.libjass-subs {
z-index: 2147483647;
}
new Test("AABBGGRR", "&HF0434441", "colorWithAlpha", new libjass.tags.Color(65, 68, 67, (1 - 240 / 255))),
#settings-form > fieldset {
display: inline-block;
}
new Test("AABBGGRR", "&HFF434441", "colorWithAlpha", new libjass.tags.Color(65, 68, 67, 0))
));
#console > .warning {
color: orange;
}
#console > .error {
color: red;
}
ul.choices-list {
list-style-type: none;
padding-left: 0;
}
+566
View File
@@ -0,0 +1,566 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* This page demonstrates how to use libjass. It allows you to choose a video that will be played in a <video> element, and an ASS script.
*
*
* Below you will find the basics of using libjass - the ASS.fromUrl() and ASS.fromString() functions, and the DefaultRenderer class - and some advanced concepts:
* - handling resizable video
* - autoplaying video
* - toggling subs on user input
*
* The advanced concepts are optional and marked with "(Advanced)"
*
*
* Some more advanced uses not demonstrated here include:
* - renderer settings, such as using custom fonts or controlling the pre-render time
* - using WebRenderer instead of DefaultRenderer for control over the placement of the subs <div>, enabling fullscreen video, etc.
* - using ASS.fromString() instead of ASS.fromUrl() when you have the ASS string already, such as in an online subs editor
* - using libjass.parser.StreamParser and its minimalAss promise directly, such as for dynamically generated scripts
* - working with SRT subs by specifying libjass.Format.SRT to the ASS.from*() functions
*
*
* API documentation is available at http://arnavion.github.io/libjass/api.xhtml
*/
"use strict";
/* This section sets up the default look of the page - a bunch of input controls for the video and ASS script, and the
* "Go" button that will create a <video> element and start rendering subs over it. If you already have URLs for a video and script on your website,
* you would just generate the <video> element and start using libjass directly.
*/
addEventListener("DOMContentLoaded", function () {
var htmlConsole = document.querySelector("#console");
htmlConsoleLog = htmlConsoleLog(htmlConsole);
htmlConsoleWarn = htmlConsoleWarn(htmlConsole);
htmlConsoleError = htmlConsoleError(htmlConsole);
var originalConsoleLog = console.log.bind(console);
var originalConsoleWarn = console.warn.bind(console);
var originalConsoleError = console.log.bind(console);
console.log = function () {
htmlConsoleLog([].slice.call(arguments, 0));
originalConsoleLog.apply(null, arguments);
};
console.warn = function () {
htmlConsoleWarn([].slice.call(arguments, 0));
originalConsoleWarn.apply(null, arguments);
};
console.error = function () {
htmlConsoleError([].slice.call(arguments, 0));
originalConsoleError.apply(null, arguments);
};
var content = document.querySelector("#content");
var videoChoiceLocalFileInput = document.querySelector("#video-choice-local-file");
var videoInputLocalFile = document.querySelector("#video-input-local-file");
var videoChoiceUrlInput = document.querySelector("#video-choice-url");
var videoInputUrl = document.querySelector("#video-input-url");
var videoChoiceSampleInput = document.querySelector("#video-choice-sample");
var videoChoiceDummyInput = document.querySelector("#video-choice-dummy");
var videoInputDummyResolution = document.querySelector("#video-input-dummy-resolution");
var videoInputDummyWidth = document.querySelector("#video-input-dummy-width");
var videoInputDummyHeight = document.querySelector("#video-input-dummy-height");
var videoInputDummyColor = document.querySelector("#video-input-dummy-color");
var videoInputDummyDuration = document.querySelector("#video-input-dummy-duration");
var assChoiceLocalFileInput = document.querySelector("#ass-choice-local-file");
var assInputLocalFile = document.querySelector("#ass-input-local-file");
var assChoiceUrlInput = document.querySelector("#ass-choice-url");
var assInputUrl = document.querySelector("#ass-input-url");
var assChoiceTextInput = document.querySelector("#ass-choice-text");
var assInputText = document.querySelector("#ass-input-text");
var enableSvgChoiceInputYes = document.querySelector("#enable-svg-yes");
var enableSvgChoiceInputNo = document.querySelector("#enable-svg-no");
// Local file input requires URL.createObjectURL, so disable those inputs if the function doesn't exist.
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
[
videoChoiceLocalFileInput,
assChoiceLocalFileInput
].forEach(function (input) {
input.disabled = true;
input.parentElement.appendChild(document.createTextNode(" (This browser doesn't support URL.createObjectURL)"));
});
[
videoInputLocalFile,
assInputLocalFile
].forEach(function (input) {
input.disabled = true;
});
}
if (
typeof HTMLCanvasElement.prototype.captureStream !== "function" ||
typeof MediaRecorder === "undefined" ||
typeof MediaSource === "undefined" ||
typeof MediaSource.isTypeSupported !== "function"/* ||
!MediaSource.isTypeSupported("video/webm")*/
) {
[
videoChoiceDummyInput,
videoInputDummyResolution,
videoInputDummyWidth,
videoInputDummyHeight,
videoInputDummyColor,
videoInputDummyDuration
].forEach(function (input) {
input.disabled = true;
});
videoChoiceDummyInput.parentElement.appendChild(document.createTextNode(
" (This browser doesn't support generating dummy video. Consider using Firefox 46 or newer and enabling media.mediasource.webm.enabled in about:config)"
));
}
// Update dummy video width and height inputs when the dropdown selection changes
function updateDummyWidthAndHeight() {
var resolution = videoInputDummyResolution.value.split("x");
videoInputDummyWidth.value = resolution[0];
videoInputDummyHeight.value = resolution[1];
}
videoInputDummyResolution.addEventListener("change", updateDummyWidthAndHeight, false);
updateDummyWidthAndHeight();
// Register event handlers to enable the Go button if all inputs are valid.
[
videoChoiceLocalFileInput,
videoChoiceUrlInput,
videoChoiceSampleInput,
videoChoiceDummyInput,
assChoiceLocalFileInput,
assChoiceUrlInput,
assChoiceTextInput
].forEach(function (input) {
input.addEventListener("change", updateGoButton, false);
});
[
videoInputLocalFile,
assInputLocalFile,
].forEach(function (input) {
input.addEventListener("change", updateGoButton, false);
});
[
videoInputUrl,
videoInputDummyDuration,
assInputUrl,
assInputText
].forEach(function (input) {
input.addEventListener("input", updateGoButton, false);
});
// libjass.debugMode and libjass.verboseMode are two properties that can be set to true to have libjass print some debug information.
var debugModeCheckbox = document.querySelector("#debug-mode");
debugModeCheckbox.addEventListener("change", function () {
console.log((debugModeCheckbox.checked ? "Enabling" : "Disabling") + " debug mode.");
libjass.configure({ debugMode: debugModeCheckbox.checked });
}, false);
var verboseModeCheckbox = document.querySelector("#verbose-mode");
verboseModeCheckbox.addEventListener("change", function () {
console.log((debugModeCheckbox.checked ? "Enabling" : "Disabling") + " verbose mode.");
libjass.configure({ verboseMode: verboseModeCheckbox.checked });
}, false);
var goButton = document.querySelector("#go-button");
function updateGoButton() {
var videoOk = false;
var assOk = false;
var videoChoice = document.querySelector('input[name="video-choice"]:checked');
switch (videoChoice) {
case videoChoiceLocalFileInput:
videoOk = videoInputLocalFile.files.length === 1;
break;
case videoChoiceUrlInput:
videoOk = document.querySelector("#video-input-url:invalid") === null && videoInputUrl.value.length > 0;
break;
case videoChoiceSampleInput:
videoOk = true;
case videoChoiceDummyInput:
videoOk =
parseInt(videoInputDummyWidth.value) > 0 &&
parseInt(videoInputDummyHeight.value) > 0 &&
videoInputDummyDuration.value.length > 0 &&
parseInt(videoInputDummyDuration.value) > 0;
break;
}
var assChoice = document.querySelector('input[name="ass-choice"]:checked');
switch (assChoice) {
case assChoiceLocalFileInput:
assOk = assInputLocalFile.files.length === 1;
break;
case assChoiceUrlInput:
assOk = document.querySelector("#ass-input-url:invalid") === null && assInputUrl.value.length > 0;
break;
case assChoiceTextInput:
assOk = document.querySelector("#ass-input-choice:invalid") === null && assInputText.value.length > 0;
break;
}
goButton.disabled = !videoOk || !assOk;
}
goButton.addEventListener("click", function () {
updateGoButton();
if (this.disabled) {
return;
}
var videoChoice = document.querySelector('input[name="video-choice"]:checked');
var assChoice = document.querySelector('input[name="ass-choice"]:checked');
var enableSvgChoice = document.querySelector('input[name="enable-svg"]:checked');
while (content.firstChild) {
content.removeChild(content.firstChild);
}
var template = document.querySelector("#template").cloneNode(true);
[].slice.call(template.querySelectorAll("[data-id]")).forEach(function (element) {
element.id = element.dataset.id;
});
[].slice.call(template.children).forEach(function (element) {
content.appendChild(element);
});
var videoPromise = null;
switch (videoChoice) {
case videoChoiceLocalFileInput:
// Video is a local file. Convert it into a blob URL.
videoPromise = prepareVideo(0 /* URL */, URL.createObjectURL(videoInputLocalFile.files[0]));
break;
case videoChoiceUrlInput:
videoPromise = prepareVideo(0 /* URL */, videoInputUrl.value);
break;
case videoChoiceSampleInput:
videoPromise = prepareVideo(1 /* sample */);
break;
case videoChoiceDummyInput:
var width = parseInt(videoInputDummyWidth.value);
var height = parseInt(videoInputDummyHeight.value);
var color = videoInputDummyColor.value;
var duration = parseInt(videoInputDummyDuration.value) * 60;
videoPromise = prepareVideo(2 /* dummy */, width, height, color, duration);
break;
}
var assPromise = null;
switch (assChoice) {
case assChoiceLocalFileInput:
assPromise = libjass.ASS.fromUrl(URL.createObjectURL(assInputLocalFile.files[0]));
break;
case assChoiceUrlInput:
assPromise = libjass.ASS.fromUrl(assInputUrl.value);
break;
case assChoiceTextInput:
assPromise = libjass.ASS.fromString(assInputText.value);
break;
}
var enableSvg = null;
switch (enableSvgChoice) {
case enableSvgChoiceInputYes:
enableSvg = true;
break;
case enableSvgChoiceInputNo:
enableSvg = false;
break;
}
go(videoPromise, assPromise, enableSvg);
});
updateGoButton();
}, false);
function prepareVideo(videoType /*, ...parameters */) {
var video = document.querySelector("#video");
var videoMetadataLoadedPromise = null;
if (videoType === 0 /* URL */ || videoType === 1 /* sample */) {
if (videoType === 0 /* URL */) {
var videoUrl = arguments[1];
/* Set the <video> element's src to the given URL
*/
video.src = videoUrl;
}
else {
/* Add <source> elements for the two sample videos.
*/
var webmSource = document.createElement("source");
video.appendChild(webmSource);
webmSource.type = "video/webm";
webmSource.src = "sample.webm";
var mp4Source = document.createElement("source");
video.appendChild(mp4Source);
mp4Source.type = "video/mp4";
mp4Source.src = "sample.mp4";
}
/* (Advanced)
*
* This demo lets you resize the video to its original resolution or the subs resolution. If you have control over the video, you
* might already know the resolution of the video, and any alternative resolutions you want to provide, so you don't need to do this.
*
* We'll get the original video resolution from the video metadata. We can see if the video has metadata already by comparing
* video.readyState to HTMLMediaElement.HAVE_METADATA. If not already loaded, we can wait for the loadedmetadata event.
*
* This code creates a promise that will be resolved when the video metadata is available.
*/
videoMetadataLoadedPromise = new libjass.Promise(function (resolve, reject) {
if (video.readyState < HTMLMediaElement.HAVE_METADATA) {
// Video metadata isn't available yet. Register an event handler for it.
video.addEventListener("loadedmetadata", resolve, false);
video.addEventListener("error", function (event) { reject(video.error); }, false);
}
else {
// Video metadata is already available.
resolve();
}
});
}
else if (videoType === 2 /* dummy */) {
var width = arguments[1];
var height = arguments[2];
var color = arguments[3];
var duration = arguments[4];
videoMetadataLoadedPromise = makeDummyVideo(video, width, height, color, duration);
}
return videoMetadataLoadedPromise.then(function () {
console.log("Video metadata loaded.");
// Prepare the "Video resolution" option label
document.querySelector("#video-resolution-label-width").appendChild(document.createTextNode(video.videoWidth));
document.querySelector("#video-resolution-label-height").appendChild(document.createTextNode(video.videoHeight));
}).catch(function (reason) {
var errorCode = (reason.code !== undefined) ? [null, "MEDIA_ERR_ABORTED", "MEDIA_ERR_NETWORK", "MEDIA_ERR_DECODE", "MEDIA_ERR_SRC_NOT_SUPPORTED"][reason.code] : "";
console.error("Video could not be loaded: %o %o", errorCode, reason);
throw reason;
});
}
/* This is a function that sets up libjass to render the subs.
*/
function go(videoPromise, assPromise, enableSvg) {
var video = document.querySelector("#video");
/* Now we need to fetch the ASS script at the given URL and convert it into a libjass.ASS object. We use the libjass.ASS.fromUrl()
* function for this. It fetches the ASS script asynchronously and returns a promise that will be resolved when the script is fully
* parsed.
*/
var assLoadedPromise = assPromise.then(function (ass) {
console.log("Script received.");
// Export the ASS object for debugging
window.ass = ass;
// Prepare the "Script resolution" option label
document.querySelector("#script-resolution-label-width").appendChild(document.createTextNode(ass.properties.resolutionX));
document.querySelector("#script-resolution-label-height").appendChild(document.createTextNode(ass.properties.resolutionY));
return ass;
}).catch(function (reason) {
console.error("ASS could not be loaded: %o", reason);
throw reason;
});
/* Next, we wait for both the video and the libjass.ASS object to be available, i.e., for their respective promises to be
* resolved. Once they have, we can create the libjass.renderers.DefaultRenderer object. This is the object that will display subs
* on the <video> element.
*/
libjass.Promise.all([videoPromise, assLoadedPromise]).then(function (results) {
var ass = results[1];
var rendererSettings = { };
if (enableSvg !== null) {
rendererSettings.enableSvg = enableSvg;
}
// else unset, which means libjass will try to auto-detect it.
// Create a DefaultRenderer using the video element and the ASS object
var renderer = new libjass.renderers.DefaultRenderer(video, ass, rendererSettings);
// Export the renderer for debugging
window.renderer = renderer;
/* (Advanced)
*
* The renderer sets some internal things up, and then fires a "ready" event when it's done. If you want to have autoplaying video,
* but want to wait for the renderer to set up first, you should only play the video when the "ready" event fires.
*/
renderer.addEventListener("ready", function () {
console.log("Beginning autoplay.");
video.play();
});
/* (Advanced)
*
* This demo page also has a checkbox that can be used to turn the subs off. We'll use the checkbox's value with renderer.setEnabled()
* to do this.
*/
document.querySelector("#enable-disable-subs").addEventListener("change", function (event) {
renderer.setEnabled(event.target.checked);
}, false);
/* (Advanced)
*
* As mentioned above, this demo page allows the user to resize the video with two presets - the original video resolution and the subs
* resolution. DefaultRenderer needs to be told of changes to the size of the <video> element because it needs the size information to
* calculate the positions and sizes of the subs.
*
* This next function is called whenever the user chooses a different preset size for the video.
*/
var applyVideoSizeSelection = function () {
// Find which option is selected
var id = videoSizeSelector.querySelector("input[name='video-size']:checked").id;
if (id === "video-size-video-radio") {
// Resize to video resolution
video.style.width = video.videoWidth + "px";
video.style.height = video.videoHeight + "px";
renderer.resize();
}
else if (id === "video-size-script-radio") {
// Resize to script resolution
video.style.width = ass.properties.resolutionX + "px";
video.style.height = ass.properties.resolutionY + "px";
renderer.resize();
}
};
var videoSizeSelector = document.querySelector("#video-size-selector");
videoSizeSelector.addEventListener("change", function (event) { applyVideoSizeSelection(); }, false);
// Set the resolution to whatever's selected by default
applyVideoSizeSelection();
});
}
var htmlConsoleLog = function (htmlConsole) {
return function (items) {
var message = document.createElement("div");
htmlConsole.appendChild(message);
message.className = "log";
var text = new Date().toString() + ": ";
items.forEach(function (item) {
switch (typeof item) {
case "boolean":
case "number":
case "string":
text += item + " ";
break;
default:
text += item + "[Check browser console for more details.] ";
break;
}
});
message.appendChild(document.createTextNode(text));
};
};
var htmlConsoleWarn = function (htmlConsole) {
return function (items) {
var message = document.createElement("div");
htmlConsole.appendChild(message);
message.className = "warning";
var text = new Date().toString() + ": ";
items.forEach(function (item) {
switch (typeof item) {
case "boolean":
case "number":
case "string":
text += item + " ";
break;
default:
text += item + "[Check browser console for more details.] ";
break;
}
});
message.appendChild(document.createTextNode(text));
};
};
var htmlConsoleError = function (htmlConsole) {
return function (items) {
var message = document.createElement("div");
htmlConsole.appendChild(message);
message.className = "error";
var text = new Date().toString() + ": ";
items.forEach(function (item) {
switch (typeof item) {
case "boolean":
case "number":
case "string":
text += item + " ";
break;
default:
text += item + "[Check browser console for more details.] ";
break;
}
});
message.appendChild(document.createTextNode(text));
};
};
addEventListener("error", function (event) {
console.error(event.message, event.error);
});
+227
View File
@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
libjass
https://github.com/Arnavion/libjass
Copyright 2013 Arnav Singh
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>libjass demo</title>
<link rel="stylesheet" href="libjass.css" />
<link rel="stylesheet" href="index.css" />
<script src="libjass.js" />
<script src="index.js" />
<script src="dummy-video.js" />
</head>
<body>
<p>This is a demo page for the libjass library - a library for displaying ASS subtitles in the browser. See the source of index.js for an explanation of how to use the library.</p>
<!-- Default look of the page - input controls to let the user choose a video and an ASS script. -->
<div id="content">
<fieldset>
<legend>Choose a video</legend>
<ul class="choices-list">
<li>
<label>
<input type="radio" id="video-choice-local-file" name="video-choice" />
Local file (The file won't be uploaded. It will be used directly within your browser.)
</label>
<input type="file" id="video-input-local-file" accept="video/*" />
</li>
<li>
<label>
<input type="radio" id="video-choice-url" name="video-choice" />
Direct video URL (webm / MP4)
</label>
<input type="url" id="video-input-url" />
</li>
<li>
<label>
<input type="radio" id="video-choice-sample" name="video-choice" checked="checked" />
Sample video (75s long 1280x720, meant to be used with the default "Text" ASS option below)
</label>
</li>
<li>
<label>
<input type="radio" id="video-choice-dummy" name="video-choice" />
Dummy video
</label>
<select id="video-input-dummy-resolution">
<option value="640x480">640 x 480 (SD fullscreen)</option>
<option value="704x480">704 x 480 (SD anamorphic)</option>
<option value="640x360">640 x 360 (SD widescreen)</option>
<option value="704x396">704 x 396 (SD widescreen)</option>
<option value="640x352">640 x 352 (SD widescreen MOD16)</option>
<option value="704x400">704 x 400 (SD widescreen MOD16)</option>
<option value="1280x720" selected="selected">1280 x 720 (HD 720p)</option>
<option value="1920x1080">1920 x 1080 (HD 1080p)</option>
<option value="1024x576">1024 x 576 (SuperPAL widescreen)</option>
</select>
<input type="number" id="video-input-dummy-width" />
<input type="number" id="video-input-dummy-height" />
<input type="color" id="video-input-dummy-color" value="#2fa3fe"></input>
<label><input type="number" id="video-input-dummy-duration" value="25"></input> mins</label>
</li>
</ul>
</fieldset>
<fieldset>
<legend>Choose an ASS script</legend>
<ul class="choices-list">
<li>
<label>
<input type="radio" id="ass-choice-local-file" name="ass-choice" />
Local file (The file won't be uploaded. It will be used directly within your browser.)
</label>
<input type="file" id="ass-input-local-file" accept=".ass" />
</li>
<li>
<label>
<input type="radio" id="ass-choice-url" name="ass-choice" />
Direct script URL (must be accessible via CORS)
</label>
<input type="url" id="ass-input-url" />
</li>
<li>
<label>
<input type="radio" id="ass-choice-text" name="ass-choice" checked="checked" />
Text
</label>
<textarea id="ass-input-text"><![CDATA[[Script Info]
; Script generated by Aegisub 2.1.8
; http://www.aegisub.org/
Title: Default Aegisub file
ScriptType: v4.00+
WrapStyle: 0
PlayResX: 1280
PlayResY: 720
ScaledBorderAndShadow: yes
Collisions: Normal
Video Aspect Ratio: 0
Video Zoom: 6
Video Position: 0
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,50,&H00F1F4F9,&H000000FF,&H000F1115,&H96000000,0,0,0,0,100,100,0,0,1,1.5,0,2,80,80,35,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:01:15.00,Default,,0000,0000,0000,,{\an8}This is a sample ASS file\Nthat showcases some of libjass's capabilities.
Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0000,0000,0000,,{\c&H0000FF&}Red text with \c
Dialogue: 0,0:00:10.00,0:00:15.00,Default,,0000,0000,0000,,{\c&H0000FF&\3c&H00FF00&}Red text with green outline with \c and \3c
Dialogue: 0,0:00:15.00,0:00:20.00,Default,,0000,0000,0000,,{\alpha&H80&}Translucent text with \alpha
Dialogue: 0,0:00:20.00,0:00:25.00,Default,,0000,0000,0000,,{\fad(2000,2000)}Fade-in and out text with \fad
Dialogue: 0,0:00:25.00,0:00:30.00,Default,,0000,0000,0000,,{\move(0,0,1280,720)}Moving text with \mov
Dialogue: 0,0:00:30.00,0:00:35.00,Default,,0000,0000,0000,,{\pos(640,360)}Positioned text with \pos
Dialogue: 0,0:00:35.00,0:00:40.00,Default,,0000,0000,0000,,{\i1}Italic,{\i} {\u1}underlined{\u} and {\s1}strikeout{\s} text with \i, \u and \s
Dialogue: 0,0:00:40.00,0:00:45.00,Default,,0000,0000,0000,,{\bord10}Borders with \bord
Dialogue: 0,0:00:45.00,0:00:50.00,Default,,0000,0000,0000,,{\shad10}Shadows with \shad
Dialogue: 0,0:00:50.00,0:00:55.00,Default,,0000,0000,0000,,{\fs20}Tiny text{\fs} and {\fs100}large text{\fs} with \fs
Dialogue: 0,0:00:55.00,0:01:00.00,Default,,0000,0000,0000,,{\fscx200}Wide text{\fscx} and {\fscy200}tall text{\fscy} with \fscx and \fscy
Dialogue: 0,0:01:00.00,0:01:05.00,Default,,0000,0000,0000,,{\pos(640,360)\frz60}Rotated text with \frz
Dialogue: 0,0:01:05.00,0:01:10.00,Default,,0000,0000,0000,,{\pos(640,360)\t(\frz720)}You spin me right round baby right round
Dialogue: 0,0:01:05.00,0:01:10.00,Default,,0000,0000,0000,,Spinning text with \t and \frz
Dialogue: 0,0:01:10.00,0:01:15.00,Default,,0000,0000,0000,,The End
]]></textarea>
</li>
</ul>
</fieldset>
<fieldset>
<legend>Other options</legend>
<ul>
<li>
Does the M at the end of this question appear red or black?
<span style="color: black; -webkit-filter: url('#redtext'); filter: url('#redtext');">M</span>
<label><input type="radio" id="enable-svg-yes" name="enable-svg" /> Red</label>
<label><input type="radio" id="enable-svg-no" name="enable-svg" /> Black</label>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="0" height="0">
<defs>
<filter id="redtext" x="-50%" y="-50%" width="200%" height="200%">
<feComponentTransfer in="SourceAlpha">
<feFuncR type="linear" slope="0" intercept="1" />
<feFuncG type="linear" slope="0" intercept="0" />
<feFuncB type="linear" slope="0" intercept="0" />
<feFuncA type="linear" slope="1" intercept="0" />
</feComponentTransfer>
</filter>
</defs>
</svg>
</li>
</ul>
</fieldset>
<button type="button" id="go-button" disabled="disabled">Go</button>
</div>
<fieldset id="console">
<legend>
Console output
<label>
<input type="checkbox" id="debug-mode" />
Enable debug mode
</label>
<label>
<input type="checkbox" id="verbose-mode" />
Enable verbose mode
</label>
</legend>
</fieldset>
<div>
<p>Found a bug? Please check if there's already a similar issue already reported at <a href="https://github.com/Arnavion/libjass/issues">https://github.com/Arnavion/libjass/issues</a> If there isn't, please open a new issue. You can also report it in the #libjass channel on the Rizon IRC network.</p>
<p>Please include the following information in your bug report:
<ul>
<li>Your OS and browser versions. Eg: "Chrome 49 on Windows 7"</li>
<li>A description of the bug. What did you expect to see? What happened instead? Eg: "All the subtitles are visible except the one at 00:00:05 'Was it you who broke the clock?'" or "The subtitle at 00:00:05 should be red but instead it's blue."</li>
<li>If possible, a URL to the video and script that I can access for testing.</li>
<li>Any text from the "Console output" section above.</li>
</ul>
</p>
</div>
<script type="text/template" id="template">
<!-- The video element that will be generated -->
<video data-id="video" controls="" />
<!-- These controls will be placed next to the video to allow changing its size, and for turning the subs off and on. -->
<form data-id="settings-form">
<fieldset>
<legend>Video size</legend>
<div data-id="video-size-selector">
<label>
<input type="radio" name="video-size" data-id="video-size-video-radio" checked="checked" />
Video resolution <span data-id="video-resolution-label-width" />x<span data-id="video-resolution-label-height" />
</label>
<label>
<input type="radio" name="video-size" data-id="video-size-script-radio" />
Script resolution <span data-id="script-resolution-label-width" />x<span data-id="script-resolution-label-height" />
</label>
</div>
</fieldset>
<fieldset>
<legend>Subtitles</legend>
<label>
<input type="checkbox" data-id="enable-disable-subs" checked="checked" />
Subtitles
</label>
</fieldset>
</form>
</script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.libjass-wrapper {
position: relative;
overflow: hidden;
}
.libjass-subs {
position: absolute;
overflow: hidden;
}
.libjass-subs, .libjass-subs * {
pointer-events: none;
-webkit-animation-fill-mode: both !important;
animation-fill-mode: both !important;
}
.libjass-subs.paused * {
-webkit-animation-play-state: paused !important;
animation-play-state: paused !important;
}
.libjass-subs .an {
position: absolute;
}
.libjass-subs .an1, .libjass-subs .an2, .libjass-subs .an3 {
bottom: 0;
}
.libjass-subs .an4, .libjass-subs .an5, .libjass-subs .an6 {
display: table;
width: 100%;
height: 100%;
}
.libjass-subs .an4 > *, .libjass-subs .an5 > *, .libjass-subs .an6 > * {
display: table-cell;
vertical-align: middle;
}
.libjass-subs .an7, .libjass-subs .an8, .libjass-subs .an9 {
top: 0;
}
.libjass-subs .an1, .libjass-subs .an4, .libjass-subs .an7 {
text-align: left;
}
.libjass-subs .an2, .libjass-subs .an5, .libjass-subs .an8 {
text-align: center;
}
.libjass-subs .an3, .libjass-subs .an6, .libjass-subs .an9 {
text-align: right;
}
.libjass-subs {
line-height: 0;
}
/* Filter wrapper span */
.libjass-subs div[data-dialogue-id] > span {
-webkit-perspective-origin: center;
-webkit-perspective: 400px;
perspective-origin: center;
perspective: 400px;
}
.libjass-font-measure {
position: absolute;
visibility: hidden;
border: 0;
margin: 0;
padding: 0;
line-height: normal;
}
.libjass-filters {
display: block;
}
.libjass-filters * {
color-interpolation-filters: sRGB;
}
+9980
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-664
View File
@@ -1,664 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
///<reference path="libjass.ts" />
"use strict";
module libjass {
export class Dialogue {
private static _lastDialogueId = -1;
private static _animationStyleElement: HTMLStyleElement = null;
private _id: number;
private _style: Style;
private _start: number;
private _end: number;
private _layer: number;
private _alignment: number;
private _transformOrigin: string;
private _parts: tags.Tag[];
private _sub: HTMLDivElement = null;
constructor(template: Object, private _ass: ASS) {
this._id = ++Dialogue._lastDialogueId;
this._style = this._ass.styles.filter(aStyle => aStyle.name === template["Style"])[0];
this._start = Dialogue._toTime(template["Start"]);
this._end = Dialogue._toTime(template["End"]);
this._layer = Math.max(parseInt(template["Layer"]), 0);
this._alignment = this._style.alignment;
this._setTransformOrigin();
this._parts = <tags.Tag[]>parser.parse(template["Text"]);
if (libjass.debugMode) {
if (this._parts.some(part => part instanceof tags.Comment && (<tags.Comment>part).value.indexOf("\\") !== -1)) {
console.warn("Possible incorrect parse: " + this.toString());
}
}
}
get id(): number {
return this._id;
}
get start(): number {
return this._start;
}
get end(): number {
return this._end;
}
get alignment(): number {
return this._alignment;
}
get layer(): number {
return this._layer;
}
get parts(): tags.Tag[] {
return this._parts;
}
/**
* The magic happens here. The subtitle div is rendered and stored. Call draw() to get a clone of the div to display.
*/
preRender(): void {
if (this._sub === null) {
this._preRender();
}
}
/**
* Discards the pre-rendered subtitle div created from an earlier call to preRender().
*/
unPreRender(): void {
this._sub = null;
}
/**
* Returns the subtitle div for display. The currentTime is used to shift the animations appropriately, so that at the time the
* div is inserted into the DOM and the animations begin, they are in sync with the video time.
*
* @param {number} currentTime
* @return {!HTMLDivElement}
*/
draw(currentTime: number): HTMLDivElement {
if (this._sub === null) {
if (libjass.debugMode) {
console.warn("This dialogue was not pre-rendered. Call preRender() before calling draw() so that draw() is faster.");
}
this._preRender();
}
var sub = <HTMLDivElement>this._sub.cloneNode(true);
var animationEndCallback: () => void = sub.remove.bind(sub);
sub.style.webkitAnimationDelay = (this._start - currentTime) + "s";
sub.addEventListener("webkitAnimationEnd", animationEndCallback, false);
sub.style.animationDelay = (this._start - currentTime) + "s";
sub.addEventListener("animationend", animationEndCallback, false);
return sub;
}
/**
* @return {string}
*/
toString(): string {
return "#" + this._id + " [" + this._start.toFixed(3) + "-" + this._end.toFixed(3) + "] " + this._parts.join(", ");
}
/**
* Converts this string into the number of seconds it represents. This string must be in the form of hh:mm:ss.MMM
*
* @param {string} string
* @return {number}
*/
private static _toTime(str: string): number {
return str.split(":").reduce((previousValue, currentValue) => previousValue * 60 + parseFloat(currentValue), 0);
}
private static _valueOrDefault = <T>(newValue: T, defaultValue: T): T => ((newValue !== null) ? newValue : defaultValue);
private _preRender(): void {
var sub = document.createElement("div");
// Create an animation if there is a part that requires it
var keyframes = new KeyframeCollection(this._id, this._start, this._end);
this._parts.forEach(part => {
if (part instanceof tags.Alignment) {
this._alignment = (<tags.Alignment>part).value;
this._setTransformOrigin();
}
else if (part instanceof tags.Fade) {
var fadePart = <tags.Fade>part;
if (fadePart.start !== 0) {
keyframes.add(this._start, "opacity", "0");
keyframes.add(this._start + fadePart.start, "opacity", "1");
}
if (fadePart.end !== 0) {
keyframes.add(this._end - fadePart.end, "opacity", "1");
keyframes.add(this._end, "opacity", "0");
}
}
});
if (Dialogue._animationStyleElement === null) {
Dialogue._animationStyleElement = <HTMLStyleElement>document.querySelector("#animation-styles");
}
Dialogue._animationStyleElement.appendChild(document.createTextNode(keyframes.toString()));
var scaleX = this._ass.scaleX;
var scaleY = this._ass.scaleY;
var dpi = this._ass.dpi;
sub.style.webkitAnimationName = "dialogue-" + this._id;
sub.style.webkitAnimationDuration = (this._end - this._start) + "s";
sub.style.animationName = "dialogue-" + this._id;
sub.style.animationDuration = (this._end - this._start) + "s";
sub.style.marginLeft = (scaleX * this._style.marginLeft) + "px";
sub.style.marginRight = (scaleX * this._style.marginRight) + "px";
sub.style.marginTop = sub.style.marginBottom = (scaleY * this._style.marginVertical) + "px";
var divTransformStyle = "";
var currentSpan: HTMLSpanElement = null;
var currentSpanStyles = new SpanStyles(this._style, this._transformOrigin, scaleX, scaleY, dpi);
var startNewSpan = (): void => {
if (currentSpan !== null) {
currentSpanStyles.setStylesOnSpan(currentSpan);
sub.appendChild(currentSpan);
}
currentSpan = document.createElement("span");
};
startNewSpan();
this._parts.forEach(part => {
if (part instanceof tags.Italic) {
currentSpanStyles.italic = (<tags.Italic>part).value;
}
else if (part instanceof tags.Bold) {
currentSpanStyles.bold = (<tags.Bold>part).value;
}
else if (part instanceof tags.Underline) {
currentSpanStyles.underline = (<tags.Underline>part).value;
}
else if (part instanceof tags.StrikeThrough) {
currentSpanStyles.strikeThrough = (<tags.StrikeThrough>part).value;
}
else if (part instanceof tags.Border) {
currentSpanStyles.outlineWidthX = (<tags.Border>part).value;
currentSpanStyles.outlineWidthY = (<tags.Border>part).value;
}
else if (part instanceof tags.BorderX) {
currentSpanStyles.outlineWidthX = (<tags.BorderX>part).value;
}
else if (part instanceof tags.BorderY) {
currentSpanStyles.outlineWidthY = (<tags.BorderY>part).value;
}
else if (part instanceof tags.Blur) {
currentSpanStyles.blur = (<tags.Blur>part).value;
}
else if (part instanceof tags.FontName) {
currentSpanStyles.fontName = (<tags.FontName>part).value;
}
else if (part instanceof tags.FontSize) {
currentSpanStyles.fontSize = (<tags.FontSize>part).value;
}
else if (part instanceof tags.FontScaleX) {
currentSpanStyles.fontScaleX = (<tags.FontScaleX>part).value;
}
else if (part instanceof tags.FontScaleY) {
currentSpanStyles.fontScaleY = (<tags.FontScaleY>part).value;
}
else if (part instanceof tags.LetterSpacing) {
currentSpanStyles.letterSpacing = (<tags.LetterSpacing>part).value;
}
else if (part instanceof tags.RotateX) {
divTransformStyle += " rotateX(" + (<tags.RotateX>part).value + "deg)";
}
else if (part instanceof tags.RotateY) {
divTransformStyle += " rotateY(" + (<tags.RotateY>part).value + "deg)";
}
else if (part instanceof tags.RotateZ) {
divTransformStyle += " rotateZ(" + (-1 * (<tags.RotateZ>part).value) + "deg)";
}
else if (part instanceof tags.SkewX) {
divTransformStyle += " skewX(" + (45 * (<tags.SkewX>part).value) + "deg)";
}
else if (part instanceof tags.SkewY) {
divTransformStyle += " skewY(" + (45 * (<tags.SkewY>part).value) + "deg)";
}
else if (part instanceof tags.PrimaryColor) {
currentSpanStyles.primaryColor = (<tags.PrimaryColor>part).value;
}
else if (part instanceof tags.OutlineColor) {
currentSpanStyles.outlineColor = (<tags.OutlineColor>part).value;
}
else if (part instanceof tags.Alpha) {
currentSpanStyles.primaryAlpha = (<tags.Alpha>part).value;
currentSpanStyles.outlineAlpha = (<tags.Alpha>part).value;
}
else if (part instanceof tags.PrimaryAlpha) {
currentSpanStyles.primaryAlpha = (<tags.PrimaryAlpha>part).value;
}
else if (part instanceof tags.OutlineAlpha) {
currentSpanStyles.outlineAlpha = (<tags.OutlineAlpha>part).value;
}
else if (part instanceof tags.Alignment) {
// Already handled at the beginning of draw()
}
else if (part instanceof tags.Reset) {
var newStyleName = (<tags.Reset>part).value;
var newStyle: Style = null;
if (newStyleName !== null) {
newStyle = this._ass.styles.filter(style => style.name === newStyleName)[0];
}
currentSpanStyles.reset(newStyle);
}
else if (part instanceof tags.Pos) {
// Will be handled at the end of draw()
}
else if (part instanceof tags.Fade) {
// Already handled at the beginning of draw()
}
else if (part instanceof tags.NewLine) {
sub.appendChild(document.createElement("br"));
}
else if (part instanceof tags.HardSpace) {
currentSpan.appendChild(document.createTextNode("\u00A0"));
startNewSpan();
}
else if (part instanceof tags.Text || (libjass.debugMode && part instanceof tags.Comment)) {
currentSpan.appendChild(document.createTextNode((<tags.Text>part).value));
startNewSpan();
}
});
if (divTransformStyle) {
sub.style.webkitTransform = divTransformStyle;
sub.style.webkitTransformOrigin = this._transformOrigin;
sub.style.transform = divTransformStyle;
sub.style.transformOrigin = this._transformOrigin;
}
this._parts.some(part => {
if (part instanceof tags.Pos) {
var posPart = <tags.Pos>part;
var absoluteWrapper = document.createElement("div");
absoluteWrapper.style.position = "absolute";
absoluteWrapper.style.left = (scaleX * posPart.x) + "px";
absoluteWrapper.style.top = (scaleY * posPart.y) + "px";
sub.style.position = "relative";
var relativeTop: number;
var relativeLeft: number;
switch (this._alignment) {
case 1: relativeLeft = 0; relativeTop = -100; break;
case 2: relativeLeft = -50; relativeTop = -100; break;
case 3: relativeLeft = -100; relativeTop = -100; break;
case 4: relativeLeft = 0; relativeTop = -50; break;
case 5: relativeLeft = -50; relativeTop = -50; break;
case 6: relativeLeft = -100; relativeTop = -50; break;
case 7: relativeLeft = 0; relativeTop = 0; break;
case 8: relativeLeft = -50; relativeTop = 0; break;
case 9: relativeLeft = -100; relativeTop = 0; break;
}
sub.style.left = relativeLeft + "%";
sub.style.top = relativeTop + "%";
absoluteWrapper.appendChild(sub);
sub = absoluteWrapper;
return true;
}
return false;
});
sub.setAttribute("data-dialogue-id", String(this._id));
this._sub = sub;
}
private _setTransformOrigin(): void {
var transformOriginX: number;
var transformOriginY: number;
switch (this._alignment) {
case 1: transformOriginX = 0; transformOriginY = 100; break;
case 2: transformOriginX = 50; transformOriginY = 100; break;
case 3: transformOriginX = 100; transformOriginY = 100; break;
case 4: transformOriginX = 0; transformOriginY = 50; break;
case 5: transformOriginX = 50; transformOriginY = 50; break;
case 6: transformOriginX = 100; transformOriginY = 50; break;
case 7: transformOriginX = 0; transformOriginY = 0; break;
case 8: transformOriginX = 50; transformOriginY = 0; break;
case 9: transformOriginX = 100; transformOriginY = 0; break;
}
this._transformOrigin = transformOriginX + "% " + transformOriginY + "%";
}
}
class KeyframeCollection {
/** @type {!Object.<string, !Object.<string, string>>} */
private _keyframes: Object = Object.create(null);
constructor(private _id: number, private _start: number, private _end: number) { }
/**
* @param {number} time
* @param {string} property
* @param {string} value
*/
add(time: number, property: string, value: string) {
var step = (100 * (time - this._start) / (this._end - this._start)) + "%";
this._keyframes[step] = this._keyframes[step] || {};
this._keyframes[step][property] = value;
}
/**
* Creates a CSS3 animations representation of this keyframe collection.
*
* @return {string}
*/
toString(): string {
var result = "";
var steps = Object.keys(this._keyframes);
if (steps.length > 0) {
var cssText = "";
steps.forEach(step => {
cssText += "\t" + step + " {\n";
var properties: Object = this._keyframes[step];
Object.keys(properties).forEach(property => {
cssText += "\t\t" + property + ": " + properties[property] + ";\n";
});
cssText += "\t}\n";
});
result =
"@-webkit-keyframes dialogue-" + this._id + " {\n" + cssText + "}\n\n" +
"@keyframes dialogue-" + this._id + " {\n" + cssText + "}\n\n";
}
return result;
}
}
class SpanStyles {
private _italic: boolean;
private _bold: Object;
private _underline: boolean;
private _strikeThrough: boolean;
private _outlineWidthX: number;
private _outlineWidthY: number;
private _fontName: string;
private _fontSize: number;
private _fontScaleX: number;
private _fontScaleY: number;
private _letterSpacing: number;
private _primaryColor: tags.Color;
private _outlineColor: tags.Color;
private _primaryAlpha: number;
private _outlineAlpha: number;
private _blur: number;
constructor(private _style: Style, private _transformOrigin: string, private _scaleX: number, private _scaleY: number, private _dpi: number) {
this.reset();
}
reset(newStyle: Style = this._style): void {
this.italic = newStyle.italic;
this.bold = newStyle.bold;
this.underline = newStyle.underline;
this.strikeThrough = newStyle.strikeThrough;
this.outlineWidthX = newStyle.outlineWidth;
this.outlineWidthY = newStyle.outlineWidth;
this.fontName = newStyle.fontName;
this.fontSize = newStyle.fontSize;
this.fontScaleX = newStyle.fontScaleX;
this.fontScaleY = newStyle.fontScaleY;
this.letterSpacing = newStyle.letterSpacing;
this.primaryColor = newStyle.primaryColor;
this.outlineColor = newStyle.outlineColor;
this.primaryAlpha = null;
this.outlineAlpha = null;
this.blur = null;
}
setStylesOnSpan(span: HTMLSpanElement): void {
if (this._italic) {
span.style.fontStyle = "italic";
}
if (this._bold === true) {
span.style.fontWeight = "bold";
}
else if (this._bold !== false) {
span.style.fontWeight = <string>this._bold;
}
var textDecoration = "";
if (this._underline) {
textDecoration = "underline";
}
if (this._strikeThrough) {
textDecoration += " line-through";
}
span.style.textDecoration = textDecoration.trim();
span.style.fontFamily = this._fontName;
span.style.fontSize = span.style.lineHeight = ((72 / this._dpi) * this._scaleY * this._fontSize) + "px";
span.style.webkitTransform = "scaleX(" + this._fontScaleX + ") scaleY(" + this._fontScaleY + ")";
span.style.webkitTransformOrigin = this._transformOrigin;
span.style.transform = "scaleX(" + this._fontScaleX + ") scaleY(" + this._fontScaleY + ")";
span.style.transformOrigin = this._transformOrigin;
span.style.letterSpacing = (this._scaleX * this._letterSpacing) + "px";
span.style.color = this._primaryColor.withAlpha(this._primaryAlpha).toString();
if (this._outlineWidthX > 0 || this._outlineWidthY > 0) {
var textShadowColor = this._outlineColor.withAlpha(this._outlineAlpha).toString();
var textShadowParts: number[][] = [];
/* Lay out text-shadows in an ellipse with horizontal radius = this._scaleX * this._outlineWidthX
* and vertical radius = this._scaleY * this._outlineWidthY
* Shadows are laid inside the region of the ellipse, separated by 0.5px
*
* The below loop is an unrolled version of the above algorithm that only roams over one quadrant and adds
* four shadows at a time.
*/
var a = this._scaleX * this._outlineWidthX;
var b = this._scaleY * this._outlineWidthY;
for (var x = 0; x < a; x += 0.5) {
for (var y = 0; y < b && ((x / a) * (x / a) + (y / b) * (y / b)) <= 1; y += 0.5) {
textShadowParts.push([x, y, this._scaleX * this._blur]);
if (x !== 0) {
textShadowParts.push([-x, y, this._scaleX * this._blur]);
}
if (x !== 0 && y !== 0) {
textShadowParts.push([-x, -y, this._scaleY * this._blur]);
}
if (y !== 0) {
textShadowParts.push([x, -y, this._scaleY * this._blur]);
}
}
}
// Make sure the four corner shadows exist
textShadowParts.push(
[a, 0, this._scaleX * this._blur],
[0, b, this._scaleX * this._blur],
[-a, 0, this._scaleY * this._blur],
[0, -b, this._scaleY * this._blur]
);
span.style.textShadow =
textShadowParts
.map(triple => triple[0] + "px " + triple[1] + "px " + triple[2] + "px " + textShadowColor)
.join(", ");
}
else if (this._blur > 0) {
// TODO: Blur text
}
}
set italic(value: boolean) {
this._italic = SpanStyles._valueOrDefault(value, this._style.italic);
}
set bold(value: Object) {
this._bold = SpanStyles._valueOrDefault(value, this._style.bold);
}
set underline(value: boolean) {
this._underline = SpanStyles._valueOrDefault(value, this._style.underline);
}
set strikeThrough(value: boolean) {
this._strikeThrough = SpanStyles._valueOrDefault(value, this._style.strikeThrough);
}
set outlineWidthX(value: number) {
this._outlineWidthX = SpanStyles._valueOrDefault(value, this._style.outlineWidth);
}
set outlineWidthY(value: number) {
this._outlineWidthY = SpanStyles._valueOrDefault(value, this._style.outlineWidth);
}
set blur(value: number) {
this._blur = SpanStyles._valueOrDefault(value, 0);
}
set fontName(value: string) {
this._fontName = SpanStyles._valueOrDefault(value, this._style.fontName);
}
set fontSize(value: number) {
this._fontSize = SpanStyles._valueOrDefault(value, this._style.fontSize);
}
set fontScaleX(value: number) {
this._fontScaleX = SpanStyles._valueOrDefault(value, this._style.fontScaleX);
}
set fontScaleY(value: number) {
this._fontScaleY = SpanStyles._valueOrDefault(value, this._style.fontScaleY);
}
set letterSpacing(value: number) {
this._letterSpacing = SpanStyles._valueOrDefault(value, this._style.letterSpacing);
}
set primaryColor(value: tags.Color) {
this._primaryColor = SpanStyles._valueOrDefault(value, this._style.primaryColor);
}
set outlineColor(value: tags.Color) {
this._outlineColor = SpanStyles._valueOrDefault(value, this._style.outlineColor);
}
set primaryAlpha(value: number) {
this._primaryAlpha = value;
}
set outlineAlpha(value: number) {
this._outlineAlpha = value;
}
private static _valueOrDefault = <T>(newValue: T, defaultValue: T): T => ((newValue !== null) ? newValue : defaultValue);
}
}
-136
View File
@@ -1,136 +0,0 @@
@font-face {
font-family: "Amienne";
src: url("/fonts/Amienne.ttf");
}
@font-face {
font-family: "ArtificeSSK";
src: url("/fonts/ArtificeSSK.ttf");
}
@font-face {
font-family: "Baar Sophia";
src: url("/fonts/BAARS.TTF");
}
@font-face {
font-family: "CAC Moose";
src: url("/fonts/CAC%20Moose.ttf");
}
@font-face {
font-family: "Charme";
src: url("/fonts/CHARME.TTF");
}
@font-face {
font-family: "Chinacat";
src: url("/fonts/chinrg.ttf");
}
@font-face {
font-family: "Complete in Him";
src: url("/fonts/Complete%20in%20Him.ttf");
}
@font-face {
font-family: "Consolas";
src: url("/fonts/consola.ttf");
}
@font-face {
font-family: "Consolas";
src: url("/fonts/consolab.ttf");
}
@font-face {
font-family: "Continuum Bold";
src: url("/fonts/contb.ttf");
}
@font-face {
font-family: "Elephant";
src: url("/fonts/ELEPHNT.TTF");
}
@font-face {
font-family: "EngraversGothic BT";
src: url("/fonts/Engrvgot.TTF");
}
@font-face {
font-family: "Essai";
src: url("/fonts/Essai.ttf");
}
@font-face {
font-family: "Forte";
src: url("/fonts/FORTE.TTF");
}
@font-face {
font-family: "HandelGotDBol";
src: url("/fonts/HANDGOTB.TTF");
}
@font-face {
font-family: "HandelGothic BT";
src: url("/fonts/HANDGOTN.TTF");
}
@font-face {
font-family: "HZHandwrite";
src: url("/fonts/HZHandwrite.ttf");
}
@font-face {
font-family: "LTFinnegan Medium";
src: url("/fonts/LT.ttf");
}
@font-face {
font-family: "LTFinnegan Medium";
src: url("/fonts/LT_3italic.ttf");
font-style: italic;
}
@font-face {
font-family: "Macron Finnetier Medium";
src: url("/fonts/macron%20finnetier%200.3.ttf");
}
@font-face {
font-family: "Old Block";
src: url("/fonts/OBGB.TTF");
}
@font-face {
font-family: "PaintyPaint";
src: url("/fonts/PAINP.TTF");
}
@font-face {
font-family: "PaintyPaint";
src: url("/fonts/PAINP_0.TTF");
}
@font-face {
font-family: "Profile";
src: url("/fonts/Profile-Medium.otf");
}
@font-face {
font-family: "Profile";
src: url("/fonts/Profile-Regular.otf");
}
@font-face {
font-family: "Prototype";
src: url("/fonts/Prototype.ttf");
}
@font-face {
font-family: "Swis721 Md BT";
src: url("/fonts/SWZ721M.TTF");
}
-100
View File
@@ -1,100 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
body {
margin: 0;
}
.wrapper {
position: relative;
}
.zoomed, .zoomed * {
display: inline-block;
font-size: 0;
}
#subs {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
#subs, #subs * {
pointer-events: none;
z-index: 2147483647;
}
#subs > * {
position: absolute;
width: 100%;
}
#subs.paused * {
-webkit-animation-play-state: paused;
animation-play-state: paused;
}
.an1, .an2, .an3 {
bottom: 0;
}
.an4, .an5, .an6 {
display: table;
}
.an4 > *, .an5 > *, .an6 > * {
display: table-cell;
top: 50%;
vertical-align: middle;
}
.an7, .an8, .an9 {
top: 0;
}
.an1, .an4, .an7 {
text-align: left;
}
.an2, .an5, .an8 {
text-align: center;
}
.an3, .an6, .an9 {
text-align: right;
}
#subs span {
white-space: pre-wrap;
}
#dpi-div {
position: absolute;
width: 1in;
height: 1in;
visibility: hidden;
}
.settings-form > fieldset {
display: inline-block;
}
-396
View File
@@ -1,396 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
libjass.debugMode = (location.search === "?debug") || (location.search === "?verbose");
libjass.verboseMode = (location.search === "?verbose");
var config = {
/**
* Subtitles will be pre-rendered for this amount of time (seconds)
*
* @const
*/
preRenderTime: 5
};
addEventListener("DOMContentLoaded", function () {
var ASS = libjass.ASS;
var debug = function () {
if (libjass.debugMode) {
console.log.apply(console, arguments);
}
}
var wrappers = Object.create(null);
var video = document.querySelector("#video");
var videoMetadataLoaded = false;
var ass = null;
var rawASS = null;
var testVideoAndASSLoaded = function () {
if (videoMetadataLoaded && ass) {
var subsWrapper = document.querySelector("#subs");
ass.dpi = parseFloat(getComputedStyle(document.querySelector("#dpi-div")).height.match(/(\d+)px/)[1]);
var dialogues = ass.dialogues.slice();
// Sort the dialogues array by start time and then by their original position in the script (id)
dialogues.sort(function (dialogue1, dialogue2) {
var result = dialogue1.start - dialogue2.start;
if (result === 0) {
result = dialogue1.id - dialogue2.id;
}
return result;
});
var layers = new Set();
dialogues.forEach(function (dialogue) {
layers.add(dialogue.layer);
});
var layersArray = [];
layers.forEach(function (layer) { layersArray.push(layer); });
layersArray.sort().forEach(function (layer) {
wrappers[layer] = new Array(9 + 1); // + 1 because alignments are 1-indexed (1 to 9)
for (var alignment = 1; alignment <= 9; alignment++) {
var wrapperDiv = document.createElement("div");
wrapperDiv.className = "an" + alignment + " layer" + layer;
subsWrapper.appendChild(wrapperDiv);
wrappers[layer][alignment] = wrapperDiv;
}
});
debug("Preloading fonts...");
var allFonts = new Set();
ass.styles.forEach(function (style) {
allFonts.add(style.fontName);
});
dialogues.forEach(function (dialogue) {
dialogue.parts.forEach(function (part) {
if (part instanceof libjass.tags.FontName) {
allFonts.add(part.value);
}
});
});
var numFonts = [].filter.call(document.styleSheets, function (stylesheet) {
return stylesheet.href && stylesheet.href.endsWith("/fonts.css");
}).reduce(function (previousValue, currentValue) {
return previousValue + [].filter.call(currentValue.cssRules, function (rule) {
return rule.type === CSSRule.FONT_FACE_RULE && allFonts.has(rule.style.getPropertyValue("font-family").match(/^['"]?(.*?)['"]?$/)[1]);
}).map(function (fontFaceRule) {
var xhr = new XMLHttpRequest();
var fontSrc = fontFaceRule.style.src;
if (fontSrc) {
fontSrc = fontSrc.match(/url\((.+)\)$/)[1];
}
else {
fontSrc = fontFaceRule.cssText.match(/url\("?(.+?)"?\)/)[1];
}
xhr.open("GET", fontSrc, true);
xhr.addEventListener("readystatechange", function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
debug("Preloaded " + fontSrc + ".");
--numFonts;
debug(numFonts + " fonts left to preload.");
if (numFonts === 0) {
debug("All fonts have been preloaded. Beginning autoplay.");
video.play();
}
}
}, false);
xhr.send(null);
return xhr;
})
.length;
}, 0);
debug(numFonts + " fonts left to preload.");
if (numFonts === 0) {
debug("All fonts have been preloaded. Beginning autoplay.");
video.play();
}
var currentTime;
// Array of subtitle div's that are being displayed right now
var currentSubs = [];
// Iterable of subtitle div's that are also to be displayed
var newSubs = dialogues.toIterable().map(function (entry) {
return entry[1];
}).skipWhile(function (dialogue) {
// Skip until dialogues which end at a time later than currentTime
return dialogue.end < currentTime;
}).takeWhile(function (dialogue) {
// Take until dialogue which starts later than currentTime + config.preRenderTime
return dialogue.start <= (currentTime + config.preRenderTime);
}).filter(function (dialogue) {
// Ignore dialogues which end at a time less than currentTime
if (dialogue.end < currentTime) {
return false;
}
// All these dialogues are visible at atleast one time in the range [currentTime, currentTime + config.preRenderTime]
// Ignore those dialogues which have already been displayed
if (currentSubs.some(function (sub) { return parseInt(sub.getAttribute("data-dialogue-id")) === dialogue.id; })) {
return false;
}
// If the dialogue is to be displayed, keep it to be drawn...
if (dialogue.start <= currentTime) {
return true;
}
// ... otherwise pre-render it and forget it
else {
dialogue.preRender();
return false;
}
}).map(function (dialogue) {
debug(dialogue.toString());
// Display the dialogue and return the drawn subtitle div
return wrappers[dialogue.layer][dialogue.alignment].appendChild(dialogue.draw(currentTime));
});
video.addEventListener("timeupdate", function () {
currentTime = video.currentTime;
currentSubs = currentSubs.filter(function (sub) {
var subDialogue = ass.dialogues[parseInt(sub.getAttribute("data-dialogue-id"))];
// If the sub should still be displayed at currentTime, keep it...
if (subDialogue.start <= currentTime && currentTime < subDialogue.end) {
return true;
}
// ... otherwise remove it from the DOM and from this array...
else {
sub.remove();
return false;
}
}).concat(Iterator(newSubs).toArray()); // ... and add the new subs that are to be displayed.
debug("video.timeupdate: video.currentTime = " + currentTime + ", video.paused = " + video.paused + ", video.seeking = " + video.seeking);
}, false);
video.addEventListener("seeking", function () {
currentSubs.forEach(function (sub) {
sub.remove();
});
currentSubs = [];
debug("video.seeking: video.currentTime = " + video.currentTime + ", video.paused = " + video.paused + ", video.seeking = " + video.seeking);
}, false);
video.addEventListener("pause", function () {
subsWrapper.className = "paused";
debug("video.pause: video.currentTime = " + video.currentTime + ", video.paused = " + video.paused + ", video.seeking = " + video.seeking);
}, false);
video.addEventListener("playing", function () {
subsWrapper.className = "";
debug("video.playing: video.currentTime = " + video.currentTime + ", video.paused = " + video.paused + ", video.seeking = " + video.seeking);
}, false);
var resizeVideo = function (width, height) {
currentSubs.forEach(function (sub) {
sub.remove();
});
currentSubs = [];
video.style.width = subsWrapper.style.width = width + "px";
video.style.height = subsWrapper.style.height = height + "px";
ass.scaleTo(width, height);
video.dispatchEvent(new Event("timeupdate"));
};
var videoIsFullScreen = false;
var onFullScreenChange = function () {
var fullScreenElement = document.fullscreenElement;
if (fullScreenElement === undefined) {
fullScreenElement = document.mozFullScreenElement;
}
if (fullScreenElement === undefined) {
fullScreenElement = document.msFullscreenElement;
}
if (fullScreenElement === undefined) {
fullScreenElement = document.webkitFullscreenElement;
}
if (fullScreenElement === video) {
resizeVideo(screen.width, screen.height);
videoIsFullScreen = true;
}
else if (fullScreenElement === null && videoIsFullScreen) {
changeVideoSizeSelection();
videoIsFullScreen = false;
}
};
document.addEventListener("webkitfullscreenchange", onFullScreenChange, false);
document.addEventListener("mozfullscreenchange", onFullScreenChange, false);
document.addEventListener("fullscreenchange", onFullScreenChange, false);
var changeVideoSizeSelection = function (id) {
if (typeof id === "undefined") {
[].some.call(videoSizeSelector.querySelectorAll("input[name='video-size']"), function (option) {
if (option.checked) {
id = option.id;
return true;
}
return false;
});
}
if (id === "video-size-video-radio") {
resizeVideo(video.videoWidth, video.videoHeight);
}
else if (id === "video-size-script-radio") {
resizeVideo(ass.resolutionX, ass.resolutionY);
}
};
var videoSizeSelector = document.querySelector("#video-size-selector");
videoSizeSelector.addEventListener("change", function (event) { changeVideoSizeSelection(event.target.id); }, false);
changeVideoSizeSelection();
};
}
if (video.readyState < HTMLMediaElement.HAVE_METADATA) {
video.addEventListener("loadedmetadata", function () {
debug("Video metadata loaded.");
videoMetadataLoaded = true;
document.querySelector("#video-resolution-label-width").appendChild(document.createTextNode(video.videoWidth));
document.querySelector("#video-resolution-label-height").appendChild(document.createTextNode(video.videoHeight));
testVideoAndASSLoaded();
}, false);
}
else {
debug("Video metadata loaded.");
videoMetadataLoaded = true;
testVideoAndASSLoaded();
}
var track = document.querySelector("#video > track[data-format='ass']");
var subsRequest = new XMLHttpRequest();
subsRequest.open("GET", track.src || track.getAttribute("src"), true);
subsRequest.addEventListener("readystatechange", function () {
if (subsRequest.readyState === XMLHttpRequest.DONE) {
debug("ASS script received.");
rawASS = subsRequest.responseText;
ass = new ASS(rawASS);
if (libjass.debugMode) {
window.ass = ass;
}
document.querySelector("#script-resolution-label-width").appendChild(document.createTextNode(ass.resolutionX));
document.querySelector("#script-resolution-label-height").appendChild(document.createTextNode(ass.resolutionY));
testVideoAndASSLoaded();
}
}, false);
subsRequest.send(null);
}, false);
String.prototype.endsWith = function (str) {
var index = this.indexOf(str);
return index !== -1 && index === this.length - str.length;
};
if (typeof window.Set !== "function" || typeof window.Set.prototype.forEach !== "function") {
/**
* Set implementation for browsers that don't support it. Only supports Number and String elements.
*
* Elements are stored as properties of an object, with derived names that won't clash with pre-defined properties.
*/
window.Set = function () {
var data = Object.create(null);
var toKey = function (value) {
if (typeof value === "number") {
return "#" + value;
}
else if (typeof value === "string") {
return "'" + value;
}
return null;
};
this.add = function (value) {
var key = toKey(value);
if (key === null) {
throw new Error("This Set implementation only supports string and number values.");
}
data[key] = value;
return this;
};
this.has = function (value) {
var key = toKey(value);
if (key === null) {
return false;
}
return key in data;
};
this.forEach = function (callbackfn, thisArg) {
if (typeof thisArg === "undefined") {
thisArg = window;
}
var that = this;
Object.keys(data).map(function (key) {
return data[key];
}).forEach(function (value, index) {
callbackfn.call(thisArg, value, value, that);
});
};
};
Set.prototype = new Set();
}
+9 -49
View File
@@ -1,56 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
libjass
https://github.com/Arnavion/libjass
Copyright 2013 Arnav Singh
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>&gt;2012 &gt;Streaman Animu</title>
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="fonts.css" />
<!-- Unminified file -->
<script src="libjass.js" />
<!-- Minified file -->
<!--
<script src="libjass.min.js" />
-->
<script src="index.js" />
<style id="animation-styles" type="text/css" />
<title>libjass</title>
</head>
<body>
<div class="wrapper">
<video id="video" src="/video/gc-01.webm" controls="">
<track src="/video/gc-01.ass" kind="metadata" data-format="ass" />
</video>
<div id="subs" />
</div>
<div id="dpi-div" />
<form class="settings-form">
<fieldset>
<legend>Video size</legend>
<div id="video-size-selector">
<input type="radio" name="video-size" id="video-size-video-radio" checked="checked" /><label for="video-size-video-radio">Video resolution <span id="video-resolution-label-width" />x<span id="video-resolution-label-height" /></label>
<input type="radio" name="video-size" id="video-size-script-radio" /><label for="video-size-script-radio">Script resolution <span id="script-resolution-label-width" />x<span id="script-resolution-label-height" /></label>
</div>
</fieldset>
</form>
<h1>libjass - Renders ASS subs in the browser</h1>
<ul>
<li><a href="https://github.com/Arnavion/libjass">GitHub</a></li>
<li>IRC channel - #libjass on irc.rizon.net</li>
<li><a href="https://arnavion.github.io/libjass/api.xhtml">API documentation</a></li>
<li><a href="https://arnavion.github.io/libjass/demo/">Demo</a></li>
</ul>
</body>
</html>
-475
View File
@@ -1,475 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
///<reference path="libjass.ts" />
"use strict";
interface Array<T> {
toIterable(): any
// TODO: Change the return type back to libjass.Iterable when https://typescript.codeplex.com/workitem/1454 is fixed.
}
module libjass {
export class Iterable {
/**
* The base class of all iterable objects. An iterable is a lazily evaluated sequence.
*
* @constructor
*/
constructor() { }
/**
* @param {function(*): *} transform A function (element) -> (transformedElement)
* @return {!Iterable} A new Iterable with the given transform applied
*/
map(transform: (element: any) => any): Iterable {
return new SelectIterable(this, transform);
}
/**
* @param {function(*): boolean} filter A function (element) -> (Boolean). Returns true if element should remain in the enumeration.
* @return {!Iterable} A new Iterable with the given filter applied
*/
filter(filter: (element: any) => boolean): Iterable {
return new WhereIterable(this, filter);
}
/**
* @param {function(*): boolean} filter A function (element) -> (Boolean). Returns false for an element if enumeration of this Iterable should stop at that element.
* @return {!Iterable} A new Iterable with the given filter applied
*/
takeWhile(filter: (element: any) => boolean): Iterable {
return new TakeWhileIterable(this, filter);
}
/**
* @param {number} count The number of elements to skip
* @return {!Iterable} A new Iterable that skips the given number of elements
*/
skip(count: number): Iterable {
return new SkipIterable(this, count);
}
/**
* @param {function(*): boolean} filter A function (element) -> (Boolean). Returns true for an element if enumeration of this Iterable should skip all elements upto that element.
* @return {!Iterable} A new Iterable with the given filter applied
*/
skipWhile(filter: (element: any) => boolean): Iterable {
return new SkipWhileIterable(this, filter);
}
}
// If this browser does not have an implementation of StopIteration, mock it
if (!window.StopIteration) {
window.StopIteration = Object.create(null);
}
class IteratorBase implements Iterator {
constructor() { }
next(): any {
throw new Error("Pure virtual method call.");
}
forEach(func: (element: any) => void ): void {
throw new Error("Pure virtual method call.");
}
toArray(): Array {
throw new Error("Pure virtual method call.");
}
}
/**
* A default Iterator for arrays in case Iterator(Array) is not defined by the browser.
*
* @constructor
* @param {!Array} array
*/
class ArrayIterator extends IteratorBase {
// The index of the element which will be returned in the next call to next()
private _currentIndex = 0;
constructor(private _array: Array) {
super();
}
/**
* @return {!Array} Returns a tuple [index, element]
*/
next(): any[] {
// Loop through the array looking for an element to return
while (this._currentIndex < this._array.length) {
// If the index is less than the array's length and an element is in the array at that index
if (this._currentIndex in this._array) {
var oldCurrentIndex = this._currentIndex;
this._currentIndex++;
// ... return it
return [oldCurrentIndex, this._array[oldCurrentIndex]];
}
// Else advance to the next index
else {
this._currentIndex++;
}
}
// If there are no more elements in the array, throw StopIteration
throw StopIteration;
}
}
var iteratorPrototype: Iterator;
if (!window.Iterator) {
/**
* A default function for creating iterators in case it is not defined by the browser.
*
* @param {!*} collection
* @param {boolean=} keysOnly
* @return {!{next: function(): *}}
*/
window.Iterator = (collection: any, keysOnly?: boolean): Iterator => {
if (keysOnly) {
throw new Error("This Iterator implementation doesn't support keysOnly = true.");
}
if (typeof collection.__iterator__ === "function") {
return <Iterator>collection.__iterator__();
}
else if (Array.isArray(collection)) {
return new ArrayIterator(<Array>collection);
}
else {
throw new Error("This Iterator implementation doesn't support iterating arbitrary objects.");
}
}
iteratorPrototype = IteratorBase.prototype;
}
else {
<any>IteratorBase.prototype = window.Iterator.prototype;
iteratorPrototype = window.Iterator.prototype;
}
/**
* Calls the provided function for each element in this Iterable.
*
* @this {{next: function(): *}}
* @param {function(*)} func A function (element)
*/
iteratorPrototype.forEach = function (func: (element: any) => void) {
var self: Iterator = this;
try {
for (; ;) {
var result = self.next();
func.call(self, result);
}
}
catch (ex) {
if (ex !== StopIteration) {
throw ex;
}
}
};
/**
* Evaluates this iterable.
*
* @this {{next: function(): *}}
* @return {!Array} An array of the elements of this Iterable
*/
iteratorPrototype.toArray = function (): Array {
var self: Iterator = this;
var result: Array = [];
self.forEach(element => {
result.push(element);
});
return result;
};
/**
* This class is an Iterable returned by Array.toIterable() and represents an Iterable backed by the
* elements of that array.
*
* @constructor
* @extends {Iterable}
* @param {!Array} array
*/
class ArrayIterable extends Iterable {
constructor(private _array: Array) {
super();
}
/**
* @return {{next: function(): !Array}}
*/
__iterator__(): Iterator {
return Iterator(this._array);
}
}
/**
* @return {!Iterable} An Iterable backed by this Array
*/
Array.prototype.toIterable = function (): Iterable {
return new ArrayIterable(this);
}
/**
* An Iterable returned from Iterable.map()
*
* @constructor
* @extends {Iterable}
* @param {!*} previous The underlying iterable
* @param {function(*): *} transform The transform function (element) -> (transformedElement)
*/
class SelectIterable extends Iterable {
constructor(private _previous: any, private _transform: (element: any) => any) {
super();
}
/**
* @return {!SelectIterator}
*/
__iterator__(): SelectIterator {
return new SelectIterator(Iterator(this._previous), this._transform);
}
}
/**
* @constructor
* @param {!{next: function(): *}} previous
* @param {function(*): *} transform
*/
class SelectIterator extends IteratorBase {
constructor(private _previous: Iterator, private _transform: (element: any) => any) {
super();
}
/**
* @return {*}
*/
next(): any {
// Apply the transform function and return the transformed value
return this._transform.call(this, this._previous.next());
}
}
/**
* An Iterable returned from Iterable.filter()
*
* @constructor
* @extends {Iterable}
* @param {!*} previous The underlying iterable
* @param {function(*): boolean} filter The filter function (element) -> (Boolean)
*/
class WhereIterable extends Iterable {
constructor(private _previous: any, private _filter: (element: any) => boolean) {
super();
}
/**
* @return {!WhereIterator}
*/
__iterator__(): WhereIterator {
return new WhereIterator(Iterator(this._previous), this._filter);
}
}
/**
* @constructor
* @param {!{next: function(): *}} previous
* @param {function(*): boolean} filter
*/
class WhereIterator extends IteratorBase {
constructor(private _previous: Iterator, private _filter: (element: any) => boolean) {
super();
}
/**
* @return {*}
*/
next(): any {
// Loop to find the next element from the underlying Iterable which passes the filter and return it
var result: any;
do {
result = this._previous.next();
} while (!this._filter.call(this, result));
return result;
}
}
/**
* An Iterable returned from Iterable.takeWhile()
*
* @constructor
* @extends {Iterable}
* @param {!*} previous The underlying iterable
* @param {function(*): boolean} predicate The predicate function (element) -> (Boolean)
*/
class TakeWhileIterable extends Iterable {
constructor(private _previous: any, private _predicate: (element: any) => boolean) {
super();
}
/**
* @return {!TakeWhileIterator}
*/
__iterator__(): TakeWhileIterator {
return new TakeWhileIterator(Iterator(this._previous), this._predicate);
}
}
/**
* @constructor
* @param {!{next: function(): *}} previous
* @param {function(*): boolean} predicate
*/
class TakeWhileIterator extends IteratorBase {
// Set to true when an element not matching the predicate is found
private _foundEnd = false;
constructor(private _previous: Iterator, private _predicate: (element: any) => boolean) {
super();
}
/**
* @return {*}
*/
next(): any {
var result: any; // Assigned null to silence closure compiler warning
// If we haven't already found the end in a previous call to next()
if (!this._foundEnd) {
// Get the next element from the underlying Iterable and see if we've found the end now
result = this._previous.next();
this._foundEnd = !this._predicate.call(this, result);
}
// If we haven't found the end, return the element
if (!this._foundEnd) {
return result;
}
// Else throw StopIteration
else {
throw StopIteration;
}
}
}
/**
* An Iterable returned from Iterable.skip()
*
* @constructor
* @extends {Iterable}
* @param {!*} previous The underlying iterable
* @param {number} count The number of elements to skip
*/
class SkipIterable extends Iterable {
constructor(private _previous: any, private _count: number) {
super();
}
/**
* @return {!SkipIterator}
*/
__iterator__(): SkipIterator {
return new SkipIterator(Iterator(this._previous), this._count);
}
}
/**
* @constructor
* @param {!{next: function(): *}} previous
* @param {number} count
*/
class SkipIterator extends IteratorBase {
private _skipped = 0;
constructor(private _previous: Iterator, private _count: number) {
super();
}
/**
* @return {*}
*/
next(): any {
for (; this._skipped < this._count; this._skipped++) {
this._previous.next();
}
return this._previous.next();
}
}
/**
* An Iterable returned from Iterable.skipWhile()
*
* @constructor
* @extends {Iterable}
* @param {!*} previous The underlying iterable
* @param {function(*): boolean} predicate The predicate function (element) -> (Boolean)
*/
class SkipWhileIterable extends Iterable {
constructor(private _previous: any, private _predicate: (element: any) => boolean) {
super();
}
/**
* @return {!SkipWhileIterator}
*/
__iterator__(): SkipWhileIterator {
return new SkipWhileIterator(Iterator(this._previous), this._predicate);
}
}
/**
* @constructor
* @param {!{next: function(): *}} previous
* @param {function(*): boolean} predicate
*/
class SkipWhileIterator extends IteratorBase {
// Set to true when an element not matching the filter is found
private _foundStart = false;
constructor(private _previous: Iterator, private _predicate: (element: any) => boolean) {
super();
}
/**
* @return {*}
*/
next(): any {
var result: any;
do {
// Get the next element
result = this._previous.next();
// and see if we've already found the start, or if we've found it now
this._foundStart = this._foundStart || !this._predicate.call(this, result);
} while (!this._foundStart); // Keep looping till we find the start
// We've found the start, so return the element
return result;
}
}
}
-5
View File
@@ -1,5 +0,0 @@
///<reference path="dialogue.ts" />
///<reference path="iterators.ts" />
///<reference path="parser.ts" />
///<reference path="tags.ts" />
///<reference path="utility.ts" />
-31
View File
@@ -1,31 +0,0 @@
{
"name": "libjass",
"version": "0.1.0",
"description": "A library to render ASS subtitles on HTML5 video in the browser.",
"keywords": ["browser", "html5", "subtitles"],
"homepage": "https://github.com/Arnavion/libjass",
"bugs": {
"url": "https://github.com/Arnavion/libjass/issues"
},
"licenses": [{
"type": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}],
"maintainers": [{
"name": "Arnav Singh",
"email": "arnavion@gmail.com"
}],
"repository": {
"type": "git",
"url": "https://github.com/Arnavion/libjass"
},
"scripts": {
"postinstall": "jake clean && jake"
},
"dependencies": {
"jake": "latest",
"pegjs": "0.7.0",
"typescript": "~0.9",
"uglify-js": "~2"
}
}
-373
View File
@@ -1,373 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
///<reference path="libjass.ts" />
"use strict";
module libjass {
export interface Parser {
parse(input: string, startRule?: string): any
}
export var parser: Parser;
export class ASS {
private _resolutionX: number;
private _resolutionY: number;
private _scaleX: number;
private _scaleY: number;
private _dpi: number;
private _styles: Style[] = [];
private _dialogues: Dialogue[] = [];
/**
* This class represents an ASS script. It contains a Info object with global information about the script,
* an array of Styles, and an array of Dialogues.
*
* @constructor
* @param {string} rawASS
*/
constructor(rawASS: string) {
// Make an iterable for all the lines in the script file.
var lines =
rawASS.replace(/\r$/gm, "").split("\n")
.toIterable()
.map((entry: any[]) => <string>entry[1])
.filter((line: string) => !line.startsWith(";")); // Skip comments
// Create the script info object
var infoTemplate = Object.create(null);
// Get script info key-value pairs from the script info section
Iterator(ASS._readSectionLines(lines, "Script Info")).forEach((keyValuePair: string[]) => {
infoTemplate[keyValuePair[0]] = keyValuePair[1];
});
if (libjass.verboseMode) {
console.log("Read script info: " + JSON.stringify(infoTemplate), infoTemplate);
}
// Parse the horizontal script resolution
this._resolutionX = parseInt(infoTemplate["PlayResX"]);
// Parse the vertical script resolution
this._resolutionY = parseInt(infoTemplate["PlayResY"]);
// Get styles from the styles section
Iterator(ASS._readSectionTemplates(lines, "V4+ Styles")).forEach((templateEntry: any[]) => {
var templateType: string = templateEntry[0];
if (templateType === "Style") {
var template: Object = templateEntry[1];
if (libjass.verboseMode) {
console.log("Read style: " + JSON.stringify(template), template);
}
// Create the style and add it into the styles array
this._styles.push(new Style(template));
}
});
// Get dialogues from the events section
Iterator(ASS._readSectionTemplates(lines, "Events")).forEach((templateEntry: any[]) => {
var templateType: string = templateEntry[0];
if (templateType === "Dialogue") {
var template: Object = templateEntry[1];
if (libjass.verboseMode) {
console.log("Read dialogue: " + JSON.stringify(template), template);
}
// Create the dialogue and add it to the dialogues array
this._dialogues.push(new Dialogue(template, this));
}
});
}
get resolutionX(): number {
return this._resolutionX;
}
get resolutionY(): number {
return this._resolutionY;
}
get scaleX(): number {
return this._scaleX;
}
get scaleY(): number {
return this._scaleY;
}
get dpi(): number {
return this._dpi;
}
set dpi(value: number) {
this._dpi = value;
}
get styles(): Style[] {
return this._styles;
}
get dialogues(): Dialogue[] {
return this._dialogues;
}
/**
* This method takes in the actual video height and width and prepares the scaleX and scaleY
* properties according to the script resolution.
*
* @param {number} videoWidth The width of the video, in pixels
* @param {number} videoHeight The height of the video, in pixels
*/
scaleTo(videoWidth: number, videoHeight: number): void {
this._scaleX = videoWidth / this._resolutionX;
this._scaleY = videoHeight / this._resolutionY;
// Any dialogues which have been rendered need to be re-rendered.
this._dialogues.forEach(dialogue => {
dialogue.unPreRender();
});
}
/**
* Returns an Iterable of the key-value pairs in the lines in the script from the given section.
*
* @param {Iterable} lines The lines of the script
* @param {string} sectionName The name of the section to parse
* @return {Iterable} An Iterable of key-value pairs. Each key-value pair is an array of two strings, the key and the value.
*/
private static _readSectionLines(lines: Iterable, sectionName: string): Iterable {
return lines
// Skip all lines till the script info section begins
.skipWhile((line: string) => line !== "[" + sectionName + "]")
// Skip the section header
.skip(1)
// Take all the lines till the section ends
.takeWhile((line: string) => !line.startsWith("["))
// Parse the line into a key-value pair
.map((line: string): string[] => {
var match = /^([^:]+):\s*(.+)/.exec(line);
if (match !== null) {
return [match[1], match[2]];
}
return null;
})
.filter((keyValuePair: string[]) => keyValuePair !== null);
}
/**
* Returns an Iterable of the templates in the lines in the script from the given section.
*
* @param {Iterable} lines The lines of the script
* @param {string} sectionName The name of the section to parse
* @return {Iterable} An Iterable of template entries. Each template is an array whose first element is the template type and the second element is the template object.
*/
private static _readSectionTemplates(lines: Iterable, sectionName: string): Iterable {
var formatParts: string[] = null;
return ASS._readSectionLines(lines, sectionName).map((keyValuePair: string[]) => {
var key = keyValuePair[0];
var value = keyValuePair[1];
// If this is a format line, parse its constituents...
if (key === "Format") {
formatParts = value.split(",").map((formatPart: string) => formatPart.trim());
return null;
}
// ... else parse this line according to the format constituents
if (formatParts === null) {
throw new Error("Format specification not found.");
}
var template: Object = Object.create(null);
var lineParts = value.split(",");
if (lineParts.length > formatParts.length) {
lineParts[formatParts.length - 1] = lineParts.slice(formatParts.length - 1).join(",");
}
formatParts.forEach((key, index) => {
template[key] = lineParts[index];
});
return [key, template];
}).filter((templateEntry: Array) => templateEntry !== null);
}
}
/**
* This class represents a single global style declaration in an ASS script. The styles can be obtained via the ASS.styles property.
*
* @constructor
* @param {string} name The name of the style
* @param {boolean} italic true if the style is italicized
* @param {(boolean|number)} bold true if the style is bolded, false if it isn't, or a numerical weight
* @param {boolean} underline true if the style is underlined
* @param {boolean} strikethrough true if the style is struck-through
* @param {number} outlineWidth The outline width, in pixels
* @param {string} fontName The name of the font
* @param {number} fontSize The size of the font, in pixels
* @param {string} primaryColor The primary color, as a CSS rgba string
* @param {string} outlineColor The outline color, as a CSS rgba string
* @param {number} alignment The alignment, as an integer
* @param {number} marginLeft The left margin
* @param {number} marginRight The right margin
* @param {number} marginVertical The vertical margin
*/
export class Style {
private _name: string;
private _italic: boolean;
private _bold: Object;
private _underline: boolean;
private _strikeThrough: boolean;
private _fontName: string;
private _fontSize: number;
private _fontScaleX: number;
private _fontScaleY: number;
private _letterSpacing: number;
private _primaryColor: tags.Color;
private _outlineColor: tags.Color;
private _outlineWidth: number;
private _alignment: number;
private _marginLeft: number;
private _marginRight: number;
private _marginVertical: number;
constructor(template: Object) {
this._name = template["Name"];
this._italic = template["Italic"] === "-1";
this._bold = template["Bold"] === "-1";
this._underline = template["Underline"] === "-1";
this._strikeThrough = template["StrikeOut"] === "-1";
this._fontName = template["Fontname"];
this._fontSize = parseFloat(template["Fontsize"]);
this._fontScaleX = parseFloat(template["ScaleX"]) / 100;
this._fontScaleY = parseFloat(template["ScaleY"]) / 100;
this._letterSpacing = parseFloat(template["Spacing"]);
this._primaryColor = <tags.Color>parser.parse(template["PrimaryColour"], "colorWithAlpha");
this._outlineColor = <tags.Color>parser.parse(template["OutlineColour"], "colorWithAlpha");
this._outlineWidth = parseFloat(template["Outline"]);
this._alignment = parseInt(template["Alignment"]);
this._marginLeft = parseFloat(template["MarginL"]);
this._marginRight = parseFloat(template["MarginR"]);
this._marginVertical = parseFloat(template["MarginV"]);
}
get name(): string {
return this._name;
}
get italic(): boolean {
return this._italic;
}
get bold(): Object {
return this._bold;
}
get underline(): boolean {
return this._underline;
}
get strikeThrough(): boolean {
return this._strikeThrough;
}
get fontName(): string {
return this._fontName;
}
get fontSize(): number {
return this._fontSize;
}
get fontScaleX(): number {
return this._fontScaleX;
}
get fontScaleY(): number {
return this._fontScaleY;
}
get letterSpacing(): number {
return this._letterSpacing;
}
get primaryColor(): tags.Color {
return this._primaryColor;
}
get outlineColor(): tags.Color {
return this._outlineColor;
}
get outlineWidth(): number {
return this._outlineWidth;
}
get alignment(): number {
return this._alignment;
}
get marginLeft(): number {
return this._marginLeft;
}
get marginRight(): number {
return this._marginRight;
}
get marginVertical(): number {
return this._marginVertical;
}
};
export var debugMode: boolean = false;
export var verboseMode: boolean = false;
}
-385
View File
@@ -1,385 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
///<reference path="libjass.ts" />
"use strict";
module libjass {
export module tags {
/**
* Represents a CSS color with red, green, blue and alpha components.
*
* Instances of this class are immutable.
*
* @constructor
*/
export class Color {
constructor(private _red: number, private _green: number, private _blue: number, private _alpha: number = 1) { }
get red(): number {
return this._red;
}
get green(): number {
return this._green;
}
get blue(): number {
return this._blue;
}
get alpha(): number {
return this._alpha;
}
/**
* @param {number} value The new alpha. If null, the existing alpha is used.
* @return {Color} Returns a new Color instance with the same color but the provided alpha.
*/
withAlpha(value: number): Color {
return new Color(this._red, this._green, this._blue, (value !== null) ? value : this._alpha);
}
/**
* @return {string} The CSS representation "rgba(...)" of this color.
*/
toString(): string {
return "rgba(" + this._red + ", " + this._green + ", " + this._blue + ", " + this._alpha + ")";
}
}
export interface Tag {
toString(): string;
}
export class TagBase implements Tag {
constructor(private _name: string, ... private _propertyNames: string[]) { }
/**
* @return {string}
*/
toString(): string {
return (
this._name + " { " +
this._propertyNames.map(name => name + ": " + this[name]).join(", ") +
((this._propertyNames.length > 0) ? " " : "") +
"}"
);
}
}
export class Comment extends TagBase {
constructor(private _value: string) {
super("Comment", "value");
}
get value(): string {
return this._value;
}
}
export class HardSpace extends TagBase {
constructor() {
super("HardSpace");
}
}
export class NewLine extends TagBase {
constructor() {
super("NewLine");
}
}
export class Text extends TagBase {
constructor(private _value: string) {
super("Text", "value");
}
get value(): string {
return this._value;
}
}
export class Italic extends TagBase {
constructor(private _value: boolean) {
super("Italic", "value");
}
get value(): boolean {
return this._value;
}
}
export class Bold extends TagBase {
constructor(private _value: Object) {
super("Bold", "value");
}
get value(): Object {
return this._value;
}
}
export class Underline extends TagBase {
constructor(private _value: boolean) {
super("Underline", "value");
}
get value(): boolean {
return this._value;
}
}
export class StrikeThrough extends TagBase {
constructor(private _value: boolean) {
super("StrikeThrough", "value");
}
get value(): boolean {
return this._value;
}
}
export class Border extends TagBase {
constructor(private _value: number) {
super("Border", "value");
}
get value(): number {
return this._value;
}
}
export class BorderX extends TagBase {
constructor(private _value: number) {
super("BorderX", "value");
}
get value(): number {
return this._value;
}
}
export class BorderY extends TagBase {
constructor(private _value: number) {
super("BorderY", "value");
}
get value(): number {
return this._value;
}
}
export class Blur extends TagBase {
constructor(private _value: number) {
super("Blur", "value");
}
get value(): number {
return this._value;
}
}
export class FontName extends TagBase {
constructor(private _value: string) {
super("FontName", "value");
}
get value(): string {
return this._value;
}
}
export class FontSize extends TagBase {
constructor(private _value: number) {
super("FontSize", "value");
}
get value(): number {
return this._value;
}
}
export class FontScaleX extends TagBase {
constructor(private _value: number) {
super("FontScaleX", "value");
}
get value(): number {
return this._value;
}
}
export class FontScaleY extends TagBase {
constructor(private _value: number) {
super("FontScaleX", "value");
}
get value(): number {
return this._value;
}
}
export class LetterSpacing extends TagBase {
constructor(private _value: number) {
super("LetterSpacing", "value");
}
get value(): number {
return this._value;
}
}
export class RotateX extends TagBase {
constructor(private _value: number) {
super("RotateX", "value");
}
get value(): number {
return this._value;
}
}
export class RotateY extends TagBase {
constructor(private _value: number) {
super("RotateY", "value");
}
get value(): number {
return this._value;
}
}
export class RotateZ extends TagBase {
constructor(private _value: number) {
super("RotateZ", "value");
}
get value(): number {
return this._value;
}
}
export class SkewX extends TagBase {
constructor(private _value: number) {
super("SkewX", "value");
}
get value() {
return this._value;
}
}
export class SkewY extends TagBase {
constructor(private _value: number) {
super("SkewY", "value");
}
get value(): number {
return this._value;
}
}
export class PrimaryColor extends TagBase {
constructor(private _value: Color) {
super("PrimaryColor", "value");
}
get value(): Color {
return this._value;
}
}
export class OutlineColor extends TagBase {
constructor(private _value: Color) {
super("OutlineColor", "value");
}
get value(): Color {
return this._value;
}
}
export class Alpha extends TagBase {
constructor(private _value: number) {
super("Alpha", "value");
}
get value(): number {
return this._value;
}
}
export class PrimaryAlpha extends TagBase {
constructor(private _value: number) {
super("PrimaryAlpha", "value");
}
get value(): number {
return this._value;
}
}
export class OutlineAlpha extends TagBase {
constructor(private _value: number) {
super("OutlineAlpha", "value");
}
get value(): number {
return this._value;
}
}
export class Alignment extends TagBase {
constructor(private _value: number) {
super("Alignment", "value");
}
get value(): number {
return this._value;
}
}
export class Reset extends TagBase {
constructor(private _value: string) {
super("Reset", "value");
}
get value(): string {
return this._value;
}
}
export class Pos extends TagBase {
constructor(private _x: number, private _y: number) {
super("Pos", "x", "y");
}
get x(): number {
return this._x;
}
get y(): number {
return this._y;
}
}
export class Fade extends TagBase {
constructor(private _start: number, private _end: number) {
super("Fade", "start", "end");
}
get start(): number {
return this._start;
}
get end(): number {
return this._end;
}
}
}
}
-257
View File
@@ -1,257 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Array.prototype.sum = function () {
return this.reduce(function (previous, current) {
return previous + current;
}, 0);
};
var Section = (function () {
var lastSectionNumber = 0;
return function (name/*, tests... */) {
var tests = [].slice.call(arguments, 1);
var number = ++lastSectionNumber;
Object.defineProperties(this, {
number: { value: number, enumerable: true },
name: { value: name, enumerable: true },
tests: { value: tests, enumerable: true },
total: { get: function () { return this.tests.length; }, enumerable: true },
passed: { get: function () { return this.tests.map(function (test) { return (test.passed === true) ? 1 : 0; }).sum(); }, enumerable: true },
failed: { get: function () { return this.tests.map(function (test) { return (test.passed === false) ? 1 : 0; }).sum(); }, enumerable: true }
});
};
})();
var Test = (function () {
var lastTestNumber = 0;
return function (name, input, rule, expected) {
var number = ++lastTestNumber;
var passed = null;
var exception = null;
Object.defineProperties(this, {
number: { value: number, enumerable: true },
name: { value: name, enumerable: true },
input: { value: input, enumerable: true },
rule: { value: rule, enumerable: true },
expected: { value: expected, enumerable: true },
passed: { get: function () { return passed; }, enumerable: true },
exception: { get: function () { return exception; }, enumerable: true }
});
this.execute = function () {
try {
passed = false;
var actual = null;
try {
actual = libjass.parser.parse(this.input, rule);
}
catch (parseException) {
if (expected === null) {
passed = true;
return;
}
else {
throw new Error("Expected parse to succeed but it threw an exception: " + parseException.message);
}
}
if (expected === null) {
throw new Error("Expected parse to fail.");
}
else if (actual === null) {
throw new Error("Parse failed without throwing an exception.");
}
Assert.AreEqual(expected, actual);
passed = true;
}
catch (testException) {
exception = testException;
}
};
};
})();
var Assert = new function () {
this.AreEqual = function (expected, actual) {
if (expected === undefined) {
throw new Error("Expected should not be undefined");
}
if (actual === undefined) {
throw new Error("Actual should not be undefined");
}
if (expected === null && actual === null) {
return;
}
if (expected === null) {
throw new Error("Expected null but got [" + actual + "]");
}
if (actual === null) {
throw new Error("Expected [" + expected + "] but got null")
}
if (expected.constructor !== actual.constructor) {
throw new Error("Parse result is of wrong type.");
}
switch(typeof expected) {
case "boolean":
case "number":
case "string":
if (expected !== actual) {
throw new Error("Expected [" + expected + "] but got [" + actual + "]");
}
break;
case "object":
Object.keys(expected).forEach(function (property) {
Assert.AreEqual(expected[property], actual[property]);
});
break;
default:
throw new Error("Unrecognized type: " + typeof expected);
}
};
};
var Logger = function (outputDiv) {
var testDiv = document.createElement("div");
testDiv.className = "test";
var sectionElement = document.createElement("fieldset");
sectionElement.className = "section";
sectionElement.appendChild(document.createElement("legend"));
var totalDiv = document.createElement("div");
var currentSectionElement = null;
this.beginSection = function (section) {
currentSectionElement = sectionElement.cloneNode(true);
outputDiv.appendChild(currentSectionElement);
var message = "Section " + section.number + " - \"" + section.name + "\"";
console.group(message);
currentSectionElement.querySelector("legend").appendChild(document.createTextNode(message));
};
this.endSection = function (section) {
var numTotal = section.total;
var numPassed = section.passed;
var numFailed = section.failed;
var currentSectionLegend = currentSectionElement.querySelector("legend");
var message = numPassed + " of " + numTotal + " tests passed.";
console.log(message);
if (numFailed > 0) {
var message = numFailed + " of " + numTotal + " tests failed.";
console.warn(message);
currentSectionLegend.appendChild(document.createTextNode(" - " + message));
currentSectionElement.className += " failed";
}
else {
currentSectionElement.className += " passed";
}
console.groupEnd();
};
this.writeTest = function (test, section) {
if (test.passed) {
var message = "Test " + section.number + "." + test.number + " - \"" + test.name + "\" - " + test.rule + " [ " + test.input + " ] ";
console.log(message);
append(testDiv, message).className += " passed";
}
else {
var message = "Test " + section.number + "." + test.number + " - \"" + test.name + "\" - " + test.rule + " [ " + test.input + " ] " + " : " + ((test.exception !== null) ? test.exception.message : "<No exception>");
console.warn(message);
append(testDiv, message).className += " failed";
}
};
this.writeTotal = function (sections) {
currentSectionElement = sectionElement.cloneNode(true);
outputDiv.appendChild(currentSectionElement);
currentSectionElement.className = "total";
console.group("Total");
currentSectionElement.querySelector("legend").appendChild(document.createTextNode("Total"));
var numTotal = sections.map(function (section) { return section.total; }).sum();
var numPassed = sections.map(function (section) { return section.passed; }).sum();
var numFailed = sections.map(function (section) { return section.failed; }).sum();
var message = numPassed + " of " + numTotal + " tests passed.";
console.log(message);
append(totalDiv, message).className = "passed";
if (numFailed > 0) {
var message = numFailed + " of " + numTotal + " tests failed.";
console.warn(message);
append(totalDiv, message).className = "failed";
}
};
var append = function (messageDivType, message) {
var messageDiv = messageDivType.cloneNode();
currentSectionElement.appendChild(messageDiv);
messageDiv.appendChild(document.createTextNode(message));
return messageDiv;
};
};
var Log = null;
addEventListener("DOMContentLoaded", function () {
Log = new Logger(document.querySelector("#output"));
sections.forEach(function (section) {
Log.beginSection(section);
section.tests.forEach(function (test) {
test.execute();
Log.writeTest(test, section);
});
Log.endSection(section);
});
Log.writeTotal(sections);
}, false);
var sections = [];
-46
View File
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
libjass
https://github.com/Arnavion/libjass
Copyright 2013 Arnav Singh
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Parser tests</title>
<style type="text/css">
<![CDATA[
.test.passed, .section.passed >legend, .total > .passed {
color: green;
}
.test.failed, .section.failed >legend, .total > .failed {
color: red;
}
]]>
</style>
<script src="../tags.js" />
<script src="../ass.pegjs.js" />
<script src="index.js" />
<script src="primitives.js" />
<script src="tags.js" />
<script src="miscellaneous.js" />
</head>
<body>
<div id="output" />
</body>
</html>
-30
View File
@@ -1,30 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
sections.push(new Section("Miscellaneous",
new Test("herkz", "{\\pos(311,4)\\blur0.8\\fs40\\bord0\\c&H3F171F&\\t(3820,3820,\\blur6}Chi{\\c&H422CB1&}tose {\\c&H3F171F&}Furu", "dialogue", [
new libjass.tags.Comment("\\pos(311,4)\\blur0.8\\fs40\\bord0\\c&H3F171F&\\t(3820,3820,\\blur6"),
new libjass.tags.Text("Chi"),
new libjass.tags.PrimaryColor(new libjass.tags.Color(177, 44, 66, 1)),
new libjass.tags.Text("tose "),
new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1)),
new libjass.tags.Text("Furu")
])
));
-67
View File
@@ -1,67 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
sections.push(new Section("Bold tag - \\b",
new Test("True", "b1", "tag_b", new libjass.tags.Bold(true)),
new Test("False", "b0", "tag_b", new libjass.tags.Bold(false)),
new Test("100", "b100", "tag_b", new libjass.tags.Bold(100)),
new Test("900", "b900", "tag_b", new libjass.tags.Bold(900)),
new Test("null", "b", "tag_b", new libjass.tags.Bold(null)),
new Test("2", "b2", "tag_b", null),
new Test("10", "b10", "tag_b", null),
new Test("150", "b150", "tag_b", null),
new Test("Enclosed tag", "{\\b1}", "enclosedTags", [new libjass.tags.Bold(true)]),
new Test("Enclosed tag", "{\\b0}", "enclosedTags", [new libjass.tags.Bold(false)]),
new Test("Enclosed tag", "{\\b100}", "enclosedTags", [new libjass.tags.Bold(100)]),
new Test("Enclosed tag", "{\\b900}", "enclosedTags", [new libjass.tags.Bold(900)]),
new Test("Enclosed tag", "{\\b}", "enclosedTags", [new libjass.tags.Bold(null)]),
new Test("Enclosed tag", "{\\b2}", "enclosedTags", [new libjass.tags.Bold(null), new libjass.tags.Comment("2")]),
new Test("Enclosed tag", "{\\b10}", "enclosedTags", [new libjass.tags.Bold(null), new libjass.tags.Comment("0")]),
new Test("Enclosed tag", "{\\b150}", "enclosedTags", [new libjass.tags.Bold(null), new libjass.tags.Comment("50")])
));
sections.push(new Section("Primary color tag - \\c or \\1c",
new Test("Just the tag", "1c&H3F171F&", "tag_1c", new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1))),
new Test("Just the tag", "c&H3F171F&", "tag_c", new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1))),
new Test("Enclosed tag", "{\\c&H3F171F&}", "enclosedTags", [new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1))]),
new Test("Dialogue", "{\\c&H3F171F&}", "dialogue", [new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1))])
));
sections.push(new Section("Alpha tag - \\alpha",
new Test("Just the tag", "alpha&H00&", "tag_alpha", new libjass.tags.Alpha(1))
));
-98
View File
@@ -1,98 +0,0 @@
/**
* libjass
*
* https://github.com/Arnavion/libjass
*
* Copyright 2013 Arnav Singh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
///<reference path="libjass.ts" />
"use strict";
interface Window {
Iterator(collection: any, keysOnly?: boolean): Iterator
StopIteration: any
}
declare function Iterator(collection: any, keysOnly?: boolean): Iterator
declare var StopIteration: any
interface Iterator {
next(): any
forEach(func: (element: any) => void): void
toArray(): Array
}
interface CSSStyleDeclaration {
webkitAnimationDelay: string
webkitAnimationDuration: string
webkitAnimationName: string
webkitTransform: string
webkitTransformOrigin: string
webkitPerspective: string
}
interface String {
startsWith(str: string): boolean
endsWith(str: string): boolean
}
interface HTMLDivElement {
remove(): void
}
module libjass {
/**
* @param {string} str
* @return {boolean} true if this string starts with str
*/
String.prototype.startsWith = function (str: string): boolean {
return (<string>this).indexOf(str) === 0;
};
if (parseInt("010") !== 10) {
// This browser doesn't parse strings with leading 0's as decimal. Replace its parseInt with an implementation that does.
var oldParseInt = parseInt;
/**
* An alternative parseInt that defaults to parsing input in base 10 if the second parameter is undefined.
*
* @param {string} s
* @param {number=} radix
* @return {number}
*/
(<any>window).parseInt = (s: string, radix?: number): number => {
// If str starts with 0x, then it is to be parsed as base 16 even if the second parameter is not given.
if (radix === undefined) {
if (s.startsWith("0x")) {
radix = 16;
}
else {
radix = 10;
}
}
return oldParseInt(s, radix);
}
}
HTMLDivElement.prototype.remove = function (): void {
if (this.parentElement !== null) {
this.parentElement.removeChild(this);
}
};
}