├── .dockerignore ├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── emscripten.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile_licence ├── README.md ├── assets ├── default.woff2 └── fonts.conf ├── build ├── WebIDL.py ├── license_defaults ├── license_extract.sh ├── license_fullnotice ├── license_lint.awk ├── patches │ ├── brotli │ │ ├── .gitkeep │ │ ├── 0001-fix-brotli-js-for-webworkers.patch │ │ └── 0002-upstream_Enable-install-with-emscripten.patch │ ├── expat │ │ └── .gitkeep │ ├── fontconfig │ │ ├── .gitkeep │ │ └── 0001-disable-pthreads.patch │ ├── freetype │ │ ├── .gitkeep │ │ └── 0002-disable-pthread.patch │ ├── fribidi │ │ └── .gitkeep │ ├── harfbuzz │ │ ├── .gitkeep │ │ └── 0001-force_disable_pthread.patch │ └── webidl_binder │ │ ├── 0001-WebIDL-Add-headers-and-make-it-work-under-SubtitlesO.patch │ │ ├── 0002-WebIDL-Implement-Integer-Pointer-type-IntPtr.patch │ │ ├── 0003-WebIDL-Implement-ByteString-type.patch │ │ └── 0004-WebIDL-Implement-Owner-Extended-Attribute-fix-77.patch ├── tempfiles.py └── webidl_binder.py ├── functions.mk ├── package-lock.json ├── package.json ├── run-buildah-build.sh ├── run-common.sh ├── run-docker-build.sh └── src ├── Makefile ├── SubtitleOctopus.cpp ├── SubtitleOctopus.idl ├── libass.cpp ├── polyfill.js ├── post-worker.js ├── pre-worker.js └── subtitles-octopus.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything for build image step, /code is passed via a volume 2 | * 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{yml,yaml}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{Makefile, *.mk}] 17 | indent_style = tab 18 | indent_size = 4 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>jellyfin/.github//renovate-presets/default", 5 | ":semanticCommitsDisabled", 6 | ":dependencyDashboard" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/emscripten.yml: -------------------------------------------------------------------------------- 1 | name: Emscripten 2 | 3 | on: 4 | push: 5 | branches: [master, ci] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Cancel previous runs 14 | uses: styfle/cancel-workflow-action@0.12.1 15 | with: 16 | access_token: ${{ github.token }} 17 | all_but_latest: true 18 | 19 | - name: Checkout Base Repo 20 | uses: actions/checkout@v4.1.1 21 | 22 | - name: Checkout Submodules 23 | run: | 24 | git submodule sync 25 | git submodule update --init --recursive --force 26 | 27 | - name: Build Docker Image 28 | run: | 29 | docker build -t libass/jso . 30 | 31 | - name: Build Binaries 32 | run: | 33 | docker run --rm --env LC_ALL=C.UTF-8 -v "${PWD}":/code libass/jso:latest 34 | 35 | - name: Upload Nightly Build 36 | uses: actions/upload-artifact@v4.3.1 37 | if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' 38 | with: 39 | name: js 40 | path: dist/js 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | name: Build & Publish to NPM 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4.1.1 16 | 17 | - name: Checkout submodules 18 | run: | 19 | git submodule sync 20 | git submodule update --init --recursive --force 21 | 22 | - name: Build Docker Image 23 | run: | 24 | docker build -t libass/jso . 25 | 26 | - name: Build Binaries 27 | run: | 28 | docker run --rm --env LC_ALL=C.UTF-8 -v "${PWD}":/code libass/jso:latest 29 | 30 | - name: Setup node environment for NPM 31 | uses: actions/setup-node@v4.0.2 32 | with: 33 | node-version: 20 34 | registry-url: 'https://registry.npmjs.org' 35 | check-latest: true 36 | 37 | - name: Publish to NPM 38 | run: SKIP_PREPARE=1 npm publish --access public 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | a.out.js 2 | a.out.wasm 3 | a.out.js.mem 4 | a.out 5 | a.wasm 6 | src/WebIDLGrammar.pkl 7 | src/parser.out 8 | lib/expat/xmlwf/xmlwf 9 | dist 10 | .libs/ 11 | *.bc 12 | *.lo 13 | *.la 14 | *.o 15 | *.pyc 16 | *.tgz 17 | venv/ 18 | node_modules/ 19 | build/lib 20 | src/SubOctpInterface.cpp 21 | src/SubOctpInterface.js 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/libass"] 2 | path = lib/libass 3 | url = https://github.com/libass/libass.git 4 | ignore = dirty 5 | [submodule "lib/fribidi"] 6 | path = lib/fribidi 7 | url = https://github.com/fribidi/fribidi.git 8 | ignore = dirty 9 | [submodule "lib/fontconfig"] 10 | path = lib/fontconfig 11 | url = https://gitlab.freedesktop.org/fontconfig/fontconfig.git 12 | ignore = dirty 13 | [submodule "lib/freetype"] 14 | path = lib/freetype 15 | url = https://gitlab.freedesktop.org/freetype/freetype.git 16 | ignore = dirty 17 | [submodule "lib/expat"] 18 | path = lib/expat 19 | url = https://github.com/libexpat/libexpat.git 20 | ignore = dirty 21 | [submodule "lib/harfbuzz"] 22 | path = lib/harfbuzz 23 | url = https://github.com/harfbuzz/harfbuzz.git 24 | ignore = dirty 25 | [submodule "lib/brotli"] 26 | path = lib/brotli 27 | url = https://github.com/google/brotli.git 28 | ignore = dirty 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/emscripten/emsdk:2.0.34 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends \ 4 | build-essential \ 5 | cmake \ 6 | git \ 7 | ragel \ 8 | patch \ 9 | libtool \ 10 | itstool \ 11 | pkg-config \ 12 | python3 \ 13 | python3-ply \ 14 | gettext \ 15 | autopoint \ 16 | automake \ 17 | autoconf \ 18 | m4 \ 19 | gperf \ 20 | licensecheck \ 21 | gawk \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | WORKDIR /code 25 | CMD ["make"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 JavascriptSubtitlesOctopus contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SubtitleOctopus.js - Makefile 2 | 3 | # make - Build Dependencies and the SubtitleOctopus.js 4 | BASE_DIR:=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 5 | DIST_DIR:=$(BASE_DIR)dist/libraries 6 | 7 | export LDFLAGS = -O3 -s EVAL_CTORS=1 -flto -s ENVIRONMENT=web,webview,worker -s NO_EXIT_RUNTIME=1 8 | export CFLAGS = -O3 -flto -s USE_PTHREADS=0 9 | export CXXFLAGS = $(CFLAGS) 10 | export PKG_CONFIG_PATH = $(DIST_DIR)/lib/pkgconfig 11 | export EM_PKG_CONFIG_PATH = $(PKG_CONFIG_PATH) 12 | 13 | all: subtitleoctopus 14 | subtitleoctopus: dist 15 | 16 | .PHONY: all subtitleoctopus dist 17 | 18 | include functions.mk 19 | 20 | # FriBidi 21 | build/lib/fribidi/configure: lib/fribidi $(wildcard $(BASE_DIR)build/patches/fribidi/*.patch) 22 | $(call PREPARE_SRC_PATCHED,fribidi) 23 | cd build/lib/fribidi && $(RECONF_AUTO) 24 | 25 | $(DIST_DIR)/lib/libfribidi.a: build/lib/fribidi/configure 26 | cd build/lib/fribidi && \ 27 | $(call CONFIGURE_AUTO) --disable-debug && \ 28 | $(JSO_MAKE) -C lib/ fribidi-unicode-version.h && \ 29 | $(JSO_MAKE) -C lib/ install && \ 30 | $(JSO_MAKE) install-pkgconfigDATA 31 | 32 | # Expat 33 | build/lib/expat/configured: lib/expat 34 | $(call PREPARE_SRC_VPATH,expat) 35 | touch build/lib/expat/configured 36 | 37 | $(DIST_DIR)/lib/libexpat.a: build/lib/expat/configured 38 | cd build/lib/expat && \ 39 | $(call CONFIGURE_CMAKE,$(BASE_DIR)lib/expat/expat) \ 40 | -DEXPAT_BUILD_DOCS=off \ 41 | -DEXPAT_SHARED_LIBS=off \ 42 | -DEXPAT_BUILD_EXAMPLES=off \ 43 | -DEXPAT_BUILD_FUZZERS=off \ 44 | -DEXPAT_BUILD_TESTS=off \ 45 | -DEXPAT_BUILD_TOOLS=off \ 46 | && \ 47 | $(JSO_MAKE) install 48 | 49 | # Brotli 50 | build/lib/brotli/js/decode.js: build/lib/brotli/configured 51 | build/lib/brotli/js/polyfill.js: build/lib/brotli/configured 52 | build/lib/brotli/configured: lib/brotli $(wildcard $(BASE_DIR)build/patches/brotli/*.patch) 53 | $(call PREPARE_SRC_PATCHED,brotli) 54 | touch build/lib/brotli/configured 55 | 56 | $(DIST_DIR)/lib/libbrotlidec.a: $(DIST_DIR)/lib/libbrotlicommon.a 57 | $(DIST_DIR)/lib/libbrotlicommon.a: build/lib/brotli/configured 58 | cd build/lib/brotli && \ 59 | $(call CONFIGURE_CMAKE) && \ 60 | $(JSO_MAKE) install 61 | # Normalise static lib names 62 | cd $(DIST_DIR)/lib/ && \ 63 | for lib in *-static.a ; do mv "$$lib" "$${lib%-static.a}.a" ; done 64 | 65 | 66 | # Freetype without Harfbuzz 67 | build/lib/freetype/configure: lib/freetype $(wildcard $(BASE_DIR)build/patches/freetype/*.patch) 68 | $(call PREPARE_SRC_PATCHED,freetype) 69 | cd build/lib/freetype && $(RECONF_AUTO) 70 | 71 | build/lib/freetype/build_hb/dist_hb/lib/libfreetype.a: $(DIST_DIR)/lib/libbrotlidec.a build/lib/freetype/configure 72 | cd build/lib/freetype && \ 73 | mkdir -p build_hb && \ 74 | cd build_hb && \ 75 | $(call CONFIGURE_AUTO,..) \ 76 | --prefix="$$(pwd)/dist_hb" \ 77 | --with-brotli=yes \ 78 | --without-harfbuzz \ 79 | && \ 80 | $(JSO_MAKE) install 81 | 82 | # Harfbuzz 83 | build/lib/harfbuzz/configure: lib/harfbuzz $(wildcard $(BASE_DIR)build/patches/harfbuzz/*.patch) 84 | $(call PREPARE_SRC_PATCHED,harfbuzz) 85 | cd build/lib/harfbuzz && $(RECONF_AUTO) 86 | 87 | $(DIST_DIR)/lib/libharfbuzz.a: build/lib/freetype/build_hb/dist_hb/lib/libfreetype.a build/lib/harfbuzz/configure 88 | cd build/lib/harfbuzz && \ 89 | EM_PKG_CONFIG_PATH=$(PKG_CONFIG_PATH):$(BASE_DIR)build/lib/freetype/build_hb/dist_hb/lib/pkgconfig \ 90 | CFLAGS="-DHB_NO_MT $(CFLAGS)" \ 91 | CXXFLAGS="-DHB_NO_MT $(CFLAGS)" \ 92 | $(call CONFIGURE_AUTO) \ 93 | --with-freetype \ 94 | && \ 95 | cd src && \ 96 | $(JSO_MAKE) install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfigDATA 97 | 98 | # Freetype with Harfbuzz 99 | $(DIST_DIR)/lib/libfreetype.a: $(DIST_DIR)/lib/libharfbuzz.a $(DIST_DIR)/lib/libbrotlidec.a 100 | cd build/lib/freetype && \ 101 | EM_PKG_CONFIG_PATH=$(PKG_CONFIG_PATH):$(BASE_DIR)build/lib/freetype/build_hb/dist_hb/lib/pkgconfig \ 102 | $(call CONFIGURE_AUTO) \ 103 | --with-brotli=yes \ 104 | --with-harfbuzz \ 105 | && \ 106 | $(JSO_MAKE) install 107 | 108 | # Fontconfig 109 | build/lib/fontconfig/configure: lib/fontconfig $(wildcard $(BASE_DIR)build/patches/fontconfig/*.patch) 110 | $(call PREPARE_SRC_PATCHED,fontconfig) 111 | cd build/lib/fontconfig && $(RECONF_AUTO) 112 | 113 | $(DIST_DIR)/lib/libfontconfig.a: $(DIST_DIR)/lib/libharfbuzz.a $(DIST_DIR)/lib/libexpat.a $(DIST_DIR)/lib/libfribidi.a $(DIST_DIR)/lib/libfreetype.a build/lib/fontconfig/configure 114 | cd build/lib/fontconfig && \ 115 | $(call CONFIGURE_AUTO) \ 116 | --disable-docs \ 117 | --with-default-fonts=/fonts \ 118 | && \ 119 | $(JSO_MAKE) -C src/ install && \ 120 | $(JSO_MAKE) -C fontconfig/ install && \ 121 | $(JSO_MAKE) install-pkgconfigDATA 122 | 123 | 124 | # libass 125 | build/lib/libass/configured: lib/libass 126 | cd lib/libass && $(RECONF_AUTO) 127 | $(call PREPARE_SRC_VPATH,libass) 128 | touch build/lib/libass/configured 129 | 130 | $(DIST_DIR)/lib/libass.a: $(DIST_DIR)/lib/libfontconfig.a $(DIST_DIR)/lib/libharfbuzz.a $(DIST_DIR)/lib/libexpat.a $(DIST_DIR)/lib/libfribidi.a $(DIST_DIR)/lib/libfreetype.a $(DIST_DIR)/lib/libbrotlidec.a build/lib/libass/configured 131 | cd build/lib/libass && \ 132 | $(call CONFIGURE_AUTO,../../../lib/libass) \ 133 | --disable-asm \ 134 | --enable-fontconfig \ 135 | && \ 136 | $(JSO_MAKE) install 137 | 138 | # SubtitleOctopus.js 139 | OCTP_DEPS = \ 140 | $(DIST_DIR)/lib/libfribidi.a \ 141 | $(DIST_DIR)/lib/libbrotlicommon.a \ 142 | $(DIST_DIR)/lib/libbrotlidec.a \ 143 | $(DIST_DIR)/lib/libfreetype.a \ 144 | $(DIST_DIR)/lib/libexpat.a \ 145 | $(DIST_DIR)/lib/libharfbuzz.a \ 146 | $(DIST_DIR)/lib/libfontconfig.a \ 147 | $(DIST_DIR)/lib/libass.a 148 | 149 | src/subtitles-octopus-worker.bc: $(OCTP_DEPS) all-src 150 | .PHONY: all-src 151 | all-src: 152 | $(MAKE) -C src all 153 | 154 | # Dist Files 155 | EMCC_COMMON_ARGS = \ 156 | $(LDFLAGS) \ 157 | -s AUTO_NATIVE_LIBRARIES=0 \ 158 | -s EXPORTED_FUNCTIONS="['_main', '_malloc']" \ 159 | -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE="['\$$Browser']" \ 160 | -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap', 'getValue', 'FS_createPreloadedFile', 'FS_createPath']" \ 161 | --embed-file assets/fonts.conf \ 162 | -s ALLOW_MEMORY_GROWTH=1 \ 163 | -s NO_FILESYSTEM=0 \ 164 | --memory-init-file=0 \ 165 | --no-heap-copy \ 166 | -o $@ 167 | 168 | dist: src/subtitles-octopus-worker.bc dist/js/subtitles-octopus-worker.js dist/js/subtitles-octopus-worker-legacy.js dist/js/subtitles-octopus.js dist/js/COPYRIGHT dist/js/default.woff2 169 | 170 | dist/js/subtitles-octopus-worker.js: src/subtitles-octopus-worker.bc src/pre-worker.js src/SubOctpInterface.js src/post-worker.js build/lib/brotli/js/decode.js 171 | mkdir -p dist/js 172 | emcc src/subtitles-octopus-worker.bc $(OCTP_DEPS) \ 173 | --pre-js src/pre-worker.js \ 174 | --pre-js build/lib/brotli/js/decode.js \ 175 | --post-js src/SubOctpInterface.js \ 176 | --post-js src/post-worker.js \ 177 | -s WASM=1 \ 178 | $(EMCC_COMMON_ARGS) 179 | 180 | dist/js/subtitles-octopus-worker-legacy.js: src/subtitles-octopus-worker.bc src/polyfill.js src/pre-worker.js src/SubOctpInterface.js src/post-worker.js build/lib/brotli/js/decode.js build/lib/brotli/js/polyfill.js 181 | mkdir -p dist/js 182 | emcc src/subtitles-octopus-worker.bc $(OCTP_DEPS) \ 183 | --pre-js src/polyfill.js \ 184 | --pre-js build/lib/brotli/js/polyfill.js \ 185 | --pre-js src/pre-worker.js \ 186 | --pre-js build/lib/brotli/js/decode.js \ 187 | --post-js src/SubOctpInterface.js \ 188 | --post-js src/post-worker.js \ 189 | -s WASM=0 \ 190 | -s LEGACY_VM_SUPPORT=1 \ 191 | -s MIN_CHROME_VERSION=27 \ 192 | -s MIN_SAFARI_VERSION=60005 \ 193 | $(EMCC_COMMON_ARGS) 194 | 195 | dist/js/subtitles-octopus.js: dist/license/all src/subtitles-octopus.js 196 | mkdir -p dist/js 197 | awk '1 {print "// "$$0}' dist/license/all | cat - src/subtitles-octopus.js > $@ 198 | 199 | dist/license/all: 200 | @#FIXME: allow -j in toplevel Makefile and reintegrate licence extraction into this file 201 | make -j "$$(nproc)" -f Makefile_licence all 202 | 203 | dist/js/COPYRIGHT: dist/license/all 204 | cp "$<" "$@" 205 | 206 | dist/js/default.woff2: 207 | cp assets/default.woff2 "$@" 208 | 209 | # Clean Tasks 210 | 211 | clean: clean-dist clean-libs clean-octopus 212 | 213 | clean-dist: 214 | rm -frv dist/libraries/* 215 | rm -frv dist/js/* 216 | rm -frv dist/license/* 217 | clean-libs: 218 | rm -frv dist/libraries build/lib 219 | clean-octopus: 220 | cd src && git clean -fdX 221 | 222 | git-checkout: 223 | git submodule sync --recursive && \ 224 | git submodule update --init --recursive 225 | 226 | SUBMODULES := brotli expat fontconfig freetype fribidi harfbuzz libass 227 | git-smreset: $(addprefix git-, $(SUBMODULES)) 228 | 229 | $(foreach subm, $(SUBMODULES), $(eval $(call TR_GIT_SM_RESET,$(subm)))) 230 | 231 | server: # Node http server npm i -g http-server 232 | http-server 233 | 234 | .PHONY: clean clean-dist clean-libs clean-octopus git-checkout git-smreset server 235 | -------------------------------------------------------------------------------- /Makefile_licence: -------------------------------------------------------------------------------- 1 | # FIXME: temporarily split Makefile to parallelise licence info extraction 2 | # once -j can be passed to the toplevel Makefile this should be moved 3 | # back into the main Makefile 4 | 5 | all: dist/license/all 6 | .PHONY: all 7 | 8 | LIB_LICENSES := brotli expat freetype fribidi fontconfig harfbuzz libass 9 | LIB_LICENSES_FINDOPT_brotli := -path ./research -prune -false -o ! -path ./js/decode.min.js 10 | LIB_LICENSES_FINDOPT_expat := -path ./expat/fuzz -prune -false -o 11 | LIB_LICENSES_FINDOPT_freetype := -path ./src/tools -prune -false -o 12 | LIB_LICENSES_FINDOPT_fribidi := -path ./bin -prune -false -o 13 | LIB_LICENSES_FINDOPT_harfbuzz := -path ./test -prune -false -o 14 | 15 | $(addprefix dist/license/, $(LIB_LICENSES)): dist/license/%: .git/modules/lib/%/HEAD build/license_extract.sh build/license_defaults 16 | @mkdir -p dist/license 17 | (cd "lib/$*" && FINDOPTS="$(LIB_LICENSES_FINDOPT_$*)" \ 18 | ../../build/license_extract.sh ../../build/license_defaults "$*" .) > $@ 19 | 20 | dist/license/subtitlesoctopus: .git/HEAD build/license_extract.sh 21 | @mkdir -p dist/license 22 | build/license_extract.sh build/license_defaults subtitlesoctopus src > dist/license/subtitlesoctopus 23 | 24 | dist/license/all: dist/license/subtitlesoctopus $(addprefix dist/license/, $(LIB_LICENSES)) build/license_fullnotice build/license_lint.awk 25 | @echo "# The following lists all copyright notices and licenses for the" > dist/license/all 26 | @echo "# work contained in JavascriptSubtitlesOctopus per project." >> dist/license/all 27 | @echo "" >> dist/license/all 28 | 29 | @echo "Concatenate extracted license info..." 30 | @$(foreach LIB_PROJ, subtitlesoctopus $(LIB_LICENSES), \ 31 | echo "# Project: $(LIB_PROJ)" >> dist/license/all && \ 32 | cat dist/license/$(LIB_PROJ) >> dist/license/all && \ 33 | ) : 34 | 35 | mv dist/license/all dist/license/all.tmp 36 | build/license_lint.awk dist/license/all.tmp build/license_fullnotice 37 | cat dist/license/all.tmp build/license_fullnotice > dist/license/all 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/jellyfin/JavascriptSubtitlesOctopus/actions/workflows/emscripten.yml/badge.svg)](https://github.com/jellyfin/JavascriptSubtitlesOctopus/actions/workflows/emscripten.yml?query=branch%3Amaster+event%3Apush) 2 | 3 | 4 | SubtitlesOctopus displays subtitles in .ass format and easily integrates with HTML5 videos. 5 | Since it uses [libass](https://github.com/libass/libass), SubtitlesOctopus supports most 6 | SSA/ASS features and enables you to get consistent results in authoring and web-playback, 7 | provided libass is also used locally. 8 | 9 | [ONLINE DEMO](https://jellyfin.github.io/JavascriptSubtitlesOctopus/videojs.html) 10 | / [other examples with demo](https://jellyfin.github.io/JavascriptSubtitlesOctopus/) 11 | 12 | ## Features 13 | 14 | - Supports most SSA/ASS features (everything libass supports) 15 | - Supports all OpenType- and TrueType-fonts (including woff2 fonts) 16 | - Works fast (because uses WebAssembly with fallback to asm.js if it's not available) 17 | - Uses Web Workers thus video and interface doesn't lag even on "heavy" subtitles (working in background) 18 | - Doesn't use DOM manipulations and render subtitles on single canvas 19 | - Fully compatible with [libass'](https://github.com/libass/libass) extensions 20 | (but beware of compatability to other ASS-renderers when using them) 21 | - Easy to use - just connect it to video element 22 | 23 | ## Included Libraries 24 | 25 | * libass 26 | * expat 27 | * fontconfig 28 | * freetype 29 | * fribidi 30 | * harfbuzz 31 | * brotli 32 | 33 | ## Usage 34 | 35 | To start using SubtitlesOctopus you only need to instantiate a new instance of 36 | `SubtitlesOctopus` and specify its [Options](#options). 37 | 38 | ```javascript 39 | var options = { 40 | video: document.getElementById('video'), // HTML5 video element 41 | subUrl: '/test/test.ass', // Link to subtitles 42 | fonts: ['/test/font-1.ttf', '/test/font-2.ttf'], // Links to fonts (not required, default font already included in build) 43 | workerUrl: '/libassjs-worker.js', // Link to WebAssembly-based file "libassjs-worker.js" 44 | legacyWorkerUrl: '/libassjs-worker-legacy.js' // Link to non-WebAssembly worker 45 | }; 46 | var instance = new SubtitlesOctopus(options); 47 | ``` 48 | 49 | After that SubtitlesOctopus automatically "connects" to your video and it starts 50 | to display subtitles. You can use it with any HTML5 player. 51 | 52 | [See other examples](https://github.com/jellyfin/JavascriptSubtitlesOctopus/tree/gh-pages/). 53 | 54 | ### Using only with canvas 55 | You're also able to use it without any video. However, that requires you to set 56 | the time the subtitles should render at yourself: 57 | 58 | ```javascript 59 | var options = { 60 | canvas: document.getElementById('canvas'), // canvas element 61 | subUrl: '/test/test.ass', // Link to subtitles 62 | fonts: ['/test/font-1.ttf', '/test/font-2.ttf'], // Links to fonts (not required, default font already included in build) 63 | workerUrl: '/libassjs-worker.js' // Link to file "libassjs-worker.js" 64 | }; 65 | var instance = new SubtitlesOctopus(options); 66 | // And then... 67 | instance.setCurrentTime(15); // Render subtitles at 00:15 on your canvas 68 | ``` 69 | 70 | ### Changing subtitles 71 | You're not limited to only display the subtitle file you referenced in your 72 | options. You're able to dynamically change subtitles on the fly. There's three 73 | methods that you can use for this specifically: 74 | 75 | - `setTrackByUrl(url)`: works the same as the `subUrl` option. It will set the 76 | subtitle to display by its URL. 77 | - `setTrack(content)`: works the same as the `subContent` option. It will set 78 | the subtitle to dispaly by its content. 79 | - `freeTrack()`: this simply removes the subtitles. You can use the two methods 80 | above to set a new subtitle file to be displayed. 81 | 82 | ```JavaScript 83 | var instance = new SubtitlesOctopus(options); 84 | 85 | // ... we want to change the subtitles to the Railgun OP 86 | instance.setTrackByUrl('/test/railgun_op.ass'); 87 | ``` 88 | 89 | ### Cleaning up the object 90 | After you're finished with rendering the subtitles. You need to call the 91 | `instance.dispose()` method to correctly dispose of the object. 92 | 93 | ```JavaScript 94 | var instance = new SubtitlesOctopus(options); 95 | 96 | // After you've finished using it... 97 | 98 | instance.dispose(); 99 | ``` 100 | 101 | 102 | ### Options 103 | When creating an instance of SubtitleOctopus, you can set the following options: 104 | 105 | - `video`: The video element to attach listeners to. (Optional) 106 | - `canvas`: The canvas to render the subtitles to. If none is given it will 107 | create a new canvas and insert it as a sibling of the video element (only if 108 | the video element exists). (Optional) 109 | - `subUrl`: The URL of the subtitle file to play. (Require either `subUrl` or 110 | `subContent` to be specified) 111 | - `subContent`: The content of the subtitle file to play. (Require either 112 | `subContent` or `subUrl` to be specified) 113 | - `workerUrl`: The URL of the worker. (Default: `libassjs-worker.js`) 114 | - `fonts`: An array of links to the fonts used in the subtitle. (Optional) 115 | - `availableFonts`: Object with all available fonts - Key is font name in lower 116 | case, value is link: `{"arial": "/font1.ttf"}` (Optional) 117 | - `fallbackFont`: URL to override fallback font, for example, with a CJK one. Default fallback font is Liberation Sans (Optional) 118 | - `lazyFileLoading`: A boolean, whether to load files in a lazy way via [FS.createLazyFile()](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.createLazyFile). [Requires](https://github.com/emscripten-core/emscripten/blob/c7b21c32fef92799da05d15ba1939b6394fe0373/src/library_fs.js#L1679-L1856) `Access-Control-Expose-Headers` for `Accept-Ranges, Content-Length, and Content-Encoding`. If encoding is compressed or length is not set, file will be fully fetched instead of just a HEAD request. 119 | - `timeOffset`: The amount of time the subtitles should be offset from the 120 | video. (Default: `0`) 121 | - `onReady`: Function that's called when SubtitlesOctopus is ready. (Optional) 122 | - `onError`: Function called in case of critical error meaning the subtitles 123 | wouldn't be shown and you should use an alternative method (for instance it 124 | occurs if browser doesn't support web workers). (Optional) 125 | - `debug`: Whether performance info is printed in the console. (Default: 126 | `false`) 127 | - `renderMode`: Rendering mode. 128 | (If not set, the deprecated option `lossyRender` is evaluated) 129 | - `js-blend` - JS Blending 130 | - `wasm-blend` - WASM Blending, currently the default 131 | - `lossy` - Lossy Render Mode (EXPERIMENTAL) 132 | - `targetFps`: Target FPS (Default: `24`) 133 | - `libassMemoryLimit`: libass bitmap cache memory limit in MiB (approximate) 134 | (Default: `0` - no limit) 135 | - `libassGlyphLimit`: libass glyph cache memory limit in MiB (approximate) 136 | (Default: `0` - no limit) 137 | - `prescaleFactor`: Scale down (`< 1.0`) the subtitles canvas to improve 138 | performance at the expense of quality, or scale it up (`> 1.0`). 139 | (Default: `1.0` - no scaling; must be a number > 0) 140 | - `prescaleHeightLimit`: The height beyond which the subtitles canvas won't be prescaled. 141 | (Default: `1080`) 142 | - `maxRenderHeight`: The maximum rendering height of the subtitles canvas. 143 | Beyond this subtitles will be upscaled by the browser. 144 | (Default: `0` - no limit) 145 | - `dropAllAnimations`: Remove all animation tags, such as karaoke, move, fade, etc. 146 | (Default: `false`) 147 | - `renderAhead`: How many MiB (approximate) of subtitles to render ahead and store. 148 | (Default: `0` - don't render ahead) 149 | - `resizeVariation`: The resize threshold at which the cache of pre-rendered events is cleared. 150 | (Default: `0.2`) 151 | 152 | ### Rendering Modes 153 | #### JS Blending 154 | To use this mode set `renderMode` to `js-blend` upon instance creation. 155 | This will do all the processing of the bitmaps produced by libass outside of WebAssembly. 156 | 157 | #### WASM Blending 158 | To use this mode set `renderMode` to `wasm-blend` upon instance creation. 159 | This will blend the bitmaps of the different events together in WebAssembly, 160 | so the JavaScript-part only needs to process a single image. 161 | If WebAssembly-support is available this will be faster than the default mode, 162 | especially for many and/or complex simultaneous subtitles. 163 | Without WebAssembly-support it will fallback to asm.js and 164 | should at least not be slower than the default mode. 165 | 166 | #### Lossy Render Mode (EXPERIMENTAL) 167 | To use this mode set `renderMode` to `lossy` upon instance creation. 168 | The Lossy Render mode has been created by @no1d as a suggestion for fix browser 169 | freezing when rendering heavy subtitles (#46), it uses 170 | [createImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap) 171 | to render the bitmap in the Worker, using Promises instead of direct render on 172 | canvas in the Main Thread. When the browser start to hang, it will not lock main 173 | thread, instead will run Async, so if the function createImageBitmap fail, it 174 | will not stop the rendering process at all and may cause some bitmap loss or 175 | simply will not draw anything in canvas, mostly on low end devices. 176 | 177 | **WARNING: Experimental, not stable and not working in some browsers** 178 | 179 | #### Render Ahead (WASM Blending with pre-rendering) (EXPERIMENTAL) 180 | Upon creating the SubtitleOctopus instance, set `renderAhead` in the options to a positive value to use this mode. 181 | In this mode, SubtitleOctopus renders events in advance (using WASM blending) so that they are ready in time. 182 | The amount of pre-rendered events is controlled by the `renderAhead` option. 183 | Each pre-rendered event is provided with information about its start time, end time, and end time of the gap after (if any). 184 | This mode will analyse the events to avoid rendering empty sections or rerendering non-animated events. 185 | Resizing the video player clears the cache of pre-rendered events (the threshold is set by `resizeVariation`). 186 | 187 | > The `renderMode` and `lossyRender` options are ignored. 188 | 189 | > **WARNING: Experimental, may stall on heavily animated subtitles** 190 | This mode tries to render every transition - at worst, every frame - in advance. 191 | If the rendering of many frames takes too long and the cache of prepared frames gets depleted 192 | (e.g. during a long section with heavy animations), the current subtitle-frame will continue to 193 | be displayed until the prerendering can catch up again. 194 | Adjusting `prescaleFactor`, `prescaleHeightLimit` and `maxRenderHeight` to lower the resolution of 195 | the rendering canvas can work around this at the expense of visual quality. 196 | 197 | 198 | ### Brotli Compressed Subtitles (DEPRECATED) 199 | Manual support for brotli-compressed subtitles is tentatively deprecated 200 | and may be removed with the next release. 201 | 202 | Instead use HTTP's `Content-Encoding:` header to transmit files compressed and 203 | let the browser handle decompression before it reaches JSO. This supports more 204 | compression algorithms and is likely faster. 205 | Do not use a `.br` file extension if you use `Content-Ecoding:` as this will 206 | conflict with the still existing manual support which tries to decompress any data 207 | with a `.br` extension. 208 | 209 | ## How to build? 210 | 211 | ### Dependencies 212 | * git 213 | * emscripten (Configure the enviroment) 214 | * make 215 | * python3 216 | * cmake 217 | * pkgconfig 218 | * patch 219 | * libtool 220 | * autotools (autoconf, automake, autopoint) 221 | * gettext 222 | * ragel - Required by Harfbuzz 223 | * itstool - Required by Fontconfig 224 | * python3-ply - Required by WebIDL 225 | * gperf - Required by Fontconfig 226 | * licensecheck 227 | 228 | ### Get the Source 229 | 230 | Run `git clone --recursive https://github.com/jellyfin/JavascriptSubtitlesOctopus.git` 231 | 232 | ### Build inside a Container 233 | #### Docker 234 | 1) Install Docker 235 | 2) `./run-docker-build.sh` 236 | 3) Artifacts are in /dist/js 237 | #### Buildah 238 | 1) Install Buildah and a suitable backend for `buildah run` like `crun` or `runc` 239 | 2) `./run-buildah-build.sh` 240 | 3) Artifacts are in /dist/js 241 | 242 | ### Build without Containers 243 | 1) Install the dependency packages listed above 244 | 2) `make` 245 | - If on macOS with libtool from brew, `LIBTOOLIZE=glibtoolize make` 246 | 3) Artifacts are in /dist/js 247 | 248 | ## Why "Octopus"? 249 | How am I an Octopus? [Ba da ba da ba!](https://www.youtube.com/watch?v=tOzOD-82mW0) 250 | -------------------------------------------------------------------------------- /assets/default.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/assets/default.woff2 -------------------------------------------------------------------------------- /assets/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /fonts 5 | 6 | 7 | mono 8 | 9 | 10 | monospace 11 | 12 | 13 | 14 | 15 | sans serif 16 | 17 | 18 | sans-serif 19 | 20 | 21 | 22 | 23 | sans 24 | 25 | 26 | sans-serif 27 | 28 | 29 | /fontconfig 30 | 31 | 32 | 30 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /build/license_defaults: -------------------------------------------------------------------------------- 1 | # Tab seperated list of default license 2 | # and if given copyrightholder per project 3 | subtitlesoctopus Expat 2017-2021 JavascriptSubtitlesOctopus contributors 4 | 5 | brotli Expat 2009, 2010, 2013-2016 by the Brotli Authors 6 | expat Expat 2000-2017 Expat development team / 1997-2000 Thai Open Source Software Center Ltd 7 | libass ISC 2006-2016 libass contributors 8 | fontconfig NTP~disclaimer 2000-2007 Keith Packard / 2005 Patrick Lam / 2009 Roozbeh Pournader / 2008,2009 Red Hat, Inc. / 2008 Danilo Šegan / 2012 Google, Inc. 9 | 10 | fribidi LGPL-2.1+ 11 | harfbuzz MIT~old 12 | freetype FTL 13 | -------------------------------------------------------------------------------- /build/license_extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2021 Oneric 4 | # SPDX-License-Identifier: ISC 5 | 6 | # Postprocesses the output of licensecheck to automatically 7 | # generate our distribution notice. 8 | # licensecheck is packaged in Debian and its derivatives 9 | # and can be obtained from CPAN everywhere. 10 | 11 | usage() { 12 | echo "$0 " >&2 13 | exit 1 14 | } 15 | 16 | set -eu 17 | 18 | FINDOPTS="${FINDOPTS:-}" 19 | tabulator="$(printf '\t')" 20 | 21 | if [ "$#" -ne 3 ] || [ ! -e "$3" ] \ 22 | || [ ! -e "$1" ] || [ -d "$1" ] ; then 23 | usage 24 | fi 25 | 26 | def_license="$(awk 'BEGIN{FS="\t+"} /^'"$2"'/ {print $2" | "$3}' "$1")" 27 | def_copy="${def_license#* | }" 28 | def_license="${def_license%% | *}" 29 | base_dir="$3" 30 | 31 | if [ -z "$def_license" ] || [ "$def_license" = "$def_copy" ] ; then 32 | echo "The map file appears to be borked or entry is missing!" >&2 33 | exit 1 34 | fi 35 | 36 | if command -v licensecheck >/dev/null ; then 37 | : 38 | else 39 | echo "Could not find licensecheck!" >&2 40 | exit 2 41 | fi 42 | 43 | # Do not ignore licensecheck errors 44 | set -o pipefail 45 | 46 | find "$base_dir" $FINDOPTS -type f -regextype egrep -regex '.*\.(c|h|cpp|hpp|js)$' -print0 \ 47 | | xargs -0 -P1 licensecheck --machine --copyright --deb-fmt --encoding UTF-8 \ 48 | | gawk -F"$tabulator" -v base_dir="$base_dir" \ 49 | -v def_license="$def_license" -v def_copy="$def_copy" ' 50 | BEGIN { 51 | split("", lcfiles) # Clear array with only pre-Issue 8 POSIX 52 | SUBSEP = sprintf("\n") # Seperator for pseudo-multidim arrays 53 | 54 | # Add default 55 | if(def_copy && def_license) 56 | ++lcfiles[def_license, def_copy] 57 | } 58 | 59 | 1 { 60 | if ($2 == "UNKNOWN") 61 | if (def_license) { 62 | $2 = def_license 63 | }else { 64 | print "ERROR: Unlicensed file "$1" matched!" | "cat>&2" 65 | print " If there is no default license, then" | "cat>&2" 66 | print " reporting this upstream might be a good idea." | "cat>&2" 67 | exit 1 68 | } 69 | if ($3 == "*No copyright*") { 70 | if (def_copy) { 71 | $3 = def_copy 72 | } else if (def_license != $2) { 73 | print "ERROR: Orphaned file "$1" and no default attribution!" | "cat>&2" 74 | exit 1 75 | } else { 76 | # Appears eg in freetype ; hope default copyright holder is correct 77 | next 78 | } 79 | } 80 | 81 | split($3, copyh, " / ") 82 | for(i in copyh) 83 | ++lcfiles[$2, copyh[i]]; 84 | } 85 | 86 | END { 87 | # Group copyright holders per license 88 | # The second pass in END is required to only add each (license, copy) pair once 89 | # using pure POSIX-only awk. 90 | split("", tmp) 91 | for(lc in lcfiles) { 92 | split(lc, lico, SUBSEP) 93 | if(lico[1] in tmp) 94 | tmp[lico[1]] = tmp[lico[1]]""SUBSEP""lico[2] 95 | else 96 | tmp[lico[1]] = lico[2] 97 | } 98 | 99 | asorti(tmp, tmp_sorted) 100 | 101 | for(i in tmp_sorted) { 102 | license = tmp_sorted[i] 103 | 104 | split(tmp[license], sorted_licenses, SUBSEP) 105 | asort(sorted_licenses); 106 | 107 | printf "License: %s\n", license 108 | 109 | printf "Copyright: " 110 | 111 | for (j in sorted_licenses) { 112 | if (j > 1) 113 | printf " " 114 | 115 | printf "%s\n", sorted_licenses[j] 116 | } 117 | 118 | printf "\n" 119 | } 120 | } 121 | ' 122 | -------------------------------------------------------------------------------- /build/license_fullnotice: -------------------------------------------------------------------------------- 1 | # The following section contains the full license text or 2 | # where permitted a shorter notice for all works contained 3 | # in JavascriptSubtitlesOctopus listed above 4 | # 5 | # Note that the shortnames used here are identical to those used by Debian 6 | # and will in some cases differ from SPDX identifiers. See below. 7 | # 8 | # Debian-name | SPDX-name 9 | # ---------------+------------------- 10 | # MIT~old | MIT-Modern-Variant 11 | # Expat | MIT 12 | # NTP~disclaimer | (None; it is NTP with a more verbose warranty disclaimer) 13 | 14 | License: BSD-2-clause 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are met: 17 | . 18 | 1. Redistributions of source code must retain the above copyright notice, 19 | this list of conditions and the following disclaimer. 20 | . 21 | 2. Redistributions in binary form must reproduce the above copyright notice, 22 | this list of conditions and the following disclaimer in the documentation 23 | and/or other materials provided with the distribution. 24 | . 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” 26 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 29 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | POSSIBILITY OF SUCH DAMAGE. 36 | 37 | License: Expat 38 | Permission is hereby granted, free of charge, to any person obtaining 39 | a copy of this software and associated documentation files (the 40 | "Software"), to deal in the Software without restriction, including 41 | without limitation the rights to use, copy, modify, merge, publish, 42 | distribute, sublicense, and/or sell copies of the Software, and to permit 43 | persons to whom the Software is furnished to do so, subject to the 44 | following conditions: 45 | . 46 | The above copyright notice and this permission notice shall be included 47 | in all copies or substantial portions of the Software. 48 | . 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 50 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 51 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 52 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 53 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 54 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 55 | USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | 57 | License: FTL 58 | Portions of this software are copyright © 2021 The FreeType 59 | Project (www.freetype.org). All rights reserved. 60 | A copy of the FreeType License can be obtained at 61 | https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/docs/FTL.TXT 62 | 63 | License: ISC 64 | Permission to use, copy, modify, and/or distribute this software for any 65 | purpose with or without fee is hereby granted, provided that the above 66 | copyright notice and this permission notice appear in all copies. 67 | . 68 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 69 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 70 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 71 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 72 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 73 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 74 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 75 | 76 | License: MIT~old 77 | Permission is hereby granted, without written agreement and without 78 | license or royalty fees, to use, copy, modify, and distribute this 79 | software and its documentation for any purpose, provided that the 80 | above copyright notice and the following two paragraphs appear in 81 | all copies of this software. 82 | . 83 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR 84 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 85 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 86 | IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 87 | DAMAGE. 88 | . 89 | THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 90 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 91 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 92 | ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO 93 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS 94 | 95 | License: NTP 96 | Permission to use, copy, modify, and distribute this 97 | software and its documentation for any purpose and without 98 | fee is hereby granted, provided that the above copyright 99 | notice appear in all copies. The University of California 100 | makes no representations about the suitability of this 101 | software for any purpose. It is provided "as is" without 102 | express or implied warranty. 103 | 104 | License: NTP~disclaimer 105 | Permission to use, copy, modify, distribute, and sell this software and its 106 | documentation for any purpose is hereby granted without fee, provided that 107 | the above copyright notice appear in all copies and that both that 108 | copyright notice and this permission notice appear in supporting 109 | documentation, and that the name of the author(s) not be used in 110 | advertising or publicity pertaining to distribution of the software without 111 | specific, written prior permission. The authors make no 112 | representations about the suitability of this software for any purpose. It 113 | is provided "as is" without express or implied warranty. 114 | . 115 | THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, 116 | INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO 117 | EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR 118 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 119 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 120 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 121 | PERFORMANCE OF THIS SOFTWARE. 122 | 123 | License: Zlib 124 | This software is provided 'as-is', without any express or implied 125 | warranty. In no event will the authors be held liable for any damages 126 | arising from the use of this software. 127 | . 128 | Permission is granted to anyone to use this software for any purpose, 129 | including commercial applications, and to alter it and redistribute it 130 | freely, subject to the following restrictions: 131 | . 132 | 1. The origin of this software must not be misrepresented; you must not 133 | claim that you wrote the original software. If you use this software 134 | in a product, an acknowledgment in the product documentation would be 135 | appreciated but is not required. 136 | 2. Altered source versions must be plainly marked as such, and must not be 137 | misrepresented as being the original software. 138 | 3. This notice may not be removed or altered from any source distribution. 139 | 140 | License: BSL-1 141 | Boost Software License - Version 1.0 - August 17th, 2003 142 | . 143 | Permission is hereby granted, free of charge, to any person or organization 144 | obtaining a copy of the software and accompanying documentation covered by 145 | this license (the "Software") to use, reproduce, display, distribute, 146 | execute, and transmit the Software, and to prepare derivative works of the 147 | Software, and to permit third-parties to whom the Software is furnished to 148 | do so, all subject to the following: 149 | . 150 | The copyright notices in the Software and this entire statement, including 151 | the above license grant, this restriction and the following disclaimer, 152 | must be included in all copies of the Software, in whole or in part, and 153 | all derivative works of the Software, unless such copies or derivative 154 | works are solely in the form of machine-executable object code generated by 155 | a source language processor. 156 | . 157 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 158 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 159 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 160 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 161 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 162 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 163 | DEALINGS IN THE SOFTWARE. 164 | 165 | License: LGPL-2.1 166 | GNU LESSER GENERAL PUBLIC LICENSE 167 | Version 2.1, February 1999 168 | . 169 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 170 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 171 | Everyone is permitted to copy and distribute verbatim copies 172 | of this license document, but changing it is not allowed. 173 | . 174 | [This is the first released version of the Lesser GPL. It also counts 175 | as the successor of the GNU Library Public License, version 2, hence 176 | the version number 2.1.] 177 | . 178 | Preamble 179 | . 180 | The licenses for most software are designed to take away your 181 | freedom to share and change it. By contrast, the GNU General Public 182 | Licenses are intended to guarantee your freedom to share and change 183 | free software--to make sure the software is free for all its users. 184 | . 185 | This license, the Lesser General Public License, applies to some 186 | specially designated software packages--typically libraries--of the 187 | Free Software Foundation and other authors who decide to use it. You 188 | can use it too, but we suggest you first think carefully about whether 189 | this license or the ordinary General Public License is the better 190 | strategy to use in any particular case, based on the explanations below. 191 | . 192 | When we speak of free software, we are referring to freedom of use, 193 | not price. Our General Public Licenses are designed to make sure that 194 | you have the freedom to distribute copies of free software (and charge 195 | for this service if you wish); that you receive source code or can get 196 | it if you want it; that you can change the software and use pieces of 197 | it in new free programs; and that you are informed that you can do 198 | these things. 199 | . 200 | To protect your rights, we need to make restrictions that forbid 201 | distributors to deny you these rights or to ask you to surrender these 202 | rights. These restrictions translate to certain responsibilities for 203 | you if you distribute copies of the library or if you modify it. 204 | . 205 | For example, if you distribute copies of the library, whether gratis 206 | or for a fee, you must give the recipients all the rights that we gave 207 | you. You must make sure that they, too, receive or can get the source 208 | code. If you link other code with the library, you must provide 209 | complete object files to the recipients, so that they can relink them 210 | with the library after making changes to the library and recompiling 211 | it. And you must show them these terms so they know their rights. 212 | . 213 | We protect your rights with a two-step method: (1) we copyright the 214 | library, and (2) we offer you this license, which gives you legal 215 | permission to copy, distribute and/or modify the library. 216 | . 217 | To protect each distributor, we want to make it very clear that 218 | there is no warranty for the free library. Also, if the library is 219 | modified by someone else and passed on, the recipients should know 220 | that what they have is not the original version, so that the original 221 | author's reputation will not be affected by problems that might be 222 | introduced by others. 223 | . 224 | Finally, software patents pose a constant threat to the existence of 225 | any free program. We wish to make sure that a company cannot 226 | effectively restrict the users of a free program by obtaining a 227 | restrictive license from a patent holder. Therefore, we insist that 228 | any patent license obtained for a version of the library must be 229 | consistent with the full freedom of use specified in this license. 230 | . 231 | Most GNU software, including some libraries, is covered by the 232 | ordinary GNU General Public License. This license, the GNU Lesser 233 | General Public License, applies to certain designated libraries, and 234 | is quite different from the ordinary General Public License. We use 235 | this license for certain libraries in order to permit linking those 236 | libraries into non-free programs. 237 | . 238 | When a program is linked with a library, whether statically or using 239 | a shared library, the combination of the two is legally speaking a 240 | combined work, a derivative of the original library. The ordinary 241 | General Public License therefore permits such linking only if the 242 | entire combination fits its criteria of freedom. The Lesser General 243 | Public License permits more lax criteria for linking other code with 244 | the library. 245 | . 246 | We call this license the "Lesser" General Public License because it 247 | does Less to protect the user's freedom than the ordinary General 248 | Public License. It also provides other free software developers Less 249 | of an advantage over competing non-free programs. These disadvantages 250 | are the reason we use the ordinary General Public License for many 251 | libraries. However, the Lesser license provides advantages in certain 252 | special circumstances. 253 | . 254 | For example, on rare occasions, there may be a special need to 255 | encourage the widest possible use of a certain library, so that it becomes 256 | a de-facto standard. To achieve this, non-free programs must be 257 | allowed to use the library. A more frequent case is that a free 258 | library does the same job as widely used non-free libraries. In this 259 | case, there is little to gain by limiting the free library to free 260 | software only, so we use the Lesser General Public License. 261 | . 262 | In other cases, permission to use a particular library in non-free 263 | programs enables a greater number of people to use a large body of 264 | free software. For example, permission to use the GNU C Library in 265 | non-free programs enables many more people to use the whole GNU 266 | operating system, as well as its variant, the GNU/Linux operating 267 | system. 268 | . 269 | Although the Lesser General Public License is Less protective of the 270 | users' freedom, it does ensure that the user of a program that is 271 | linked with the Library has the freedom and the wherewithal to run 272 | that program using a modified version of the Library. 273 | . 274 | The precise terms and conditions for copying, distribution and 275 | modification follow. Pay close attention to the difference between a 276 | "work based on the library" and a "work that uses the library". The 277 | former contains code derived from the library, whereas the latter must 278 | be combined with the library in order to run. 279 | . 280 | GNU LESSER GENERAL PUBLIC LICENSE 281 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 282 | . 283 | 0. This License Agreement applies to any software library or other 284 | program which contains a notice placed by the copyright holder or 285 | other authorized party saying it may be distributed under the terms of 286 | this Lesser General Public License (also called "this License"). 287 | Each licensee is addressed as "you". 288 | . 289 | A "library" means a collection of software functions and/or data 290 | prepared so as to be conveniently linked with application programs 291 | (which use some of those functions and data) to form executables. 292 | . 293 | The "Library", below, refers to any such software library or work 294 | which has been distributed under these terms. A "work based on the 295 | Library" means either the Library or any derivative work under 296 | copyright law: that is to say, a work containing the Library or a 297 | portion of it, either verbatim or with modifications and/or translated 298 | straightforwardly into another language. (Hereinafter, translation is 299 | included without limitation in the term "modification".) 300 | . 301 | "Source code" for a work means the preferred form of the work for 302 | making modifications to it. For a library, complete source code means 303 | all the source code for all modules it contains, plus any associated 304 | interface definition files, plus the scripts used to control compilation 305 | and installation of the library. 306 | . 307 | Activities other than copying, distribution and modification are not 308 | covered by this License; they are outside its scope. The act of 309 | running a program using the Library is not restricted, and output from 310 | such a program is covered only if its contents constitute a work based 311 | on the Library (independent of the use of the Library in a tool for 312 | writing it). Whether that is true depends on what the Library does 313 | and what the program that uses the Library does. 314 | 315 | 1. You may copy and distribute verbatim copies of the Library's 316 | complete source code as you receive it, in any medium, provided that 317 | you conspicuously and appropriately publish on each copy an 318 | appropriate copyright notice and disclaimer of warranty; keep intact 319 | all the notices that refer to this License and to the absence of any 320 | warranty; and distribute a copy of this License along with the 321 | Library. 322 | . 323 | You may charge a fee for the physical act of transferring a copy, 324 | and you may at your option offer warranty protection in exchange for a 325 | fee. 326 | . 327 | 2. You may modify your copy or copies of the Library or any portion 328 | of it, thus forming a work based on the Library, and copy and 329 | distribute such modifications or work under the terms of Section 1 330 | above, provided that you also meet all of these conditions: 331 | . 332 | a) The modified work must itself be a software library. 333 | . 334 | b) You must cause the files modified to carry prominent notices 335 | stating that you changed the files and the date of any change. 336 | . 337 | c) You must cause the whole of the work to be licensed at no 338 | charge to all third parties under the terms of this License. 339 | . 340 | d) If a facility in the modified Library refers to a function or a 341 | table of data to be supplied by an application program that uses 342 | the facility, other than as an argument passed when the facility 343 | is invoked, then you must make a good faith effort to ensure that, 344 | in the event an application does not supply such function or 345 | table, the facility still operates, and performs whatever part of 346 | its purpose remains meaningful. 347 | . 348 | (For example, a function in a library to compute square roots has 349 | a purpose that is entirely well-defined independent of the 350 | application. Therefore, Subsection 2d requires that any 351 | application-supplied function or table used by this function must 352 | be optional: if the application does not supply it, the square 353 | root function must still compute square roots.) 354 | . 355 | These requirements apply to the modified work as a whole. If 356 | identifiable sections of that work are not derived from the Library, 357 | and can be reasonably considered independent and separate works in 358 | themselves, then this License, and its terms, do not apply to those 359 | sections when you distribute them as separate works. But when you 360 | distribute the same sections as part of a whole which is a work based 361 | on the Library, the distribution of the whole must be on the terms of 362 | this License, whose permissions for other licensees extend to the 363 | entire whole, and thus to each and every part regardless of who wrote 364 | it. 365 | . 366 | Thus, it is not the intent of this section to claim rights or contest 367 | your rights to work written entirely by you; rather, the intent is to 368 | exercise the right to control the distribution of derivative or 369 | collective works based on the Library. 370 | . 371 | In addition, mere aggregation of another work not based on the Library 372 | with the Library (or with a work based on the Library) on a volume of 373 | a storage or distribution medium does not bring the other work under 374 | the scope of this License. 375 | . 376 | 3. You may opt to apply the terms of the ordinary GNU General Public 377 | License instead of this License to a given copy of the Library. To do 378 | this, you must alter all the notices that refer to this License, so 379 | that they refer to the ordinary GNU General Public License, version 2, 380 | instead of to this License. (If a newer version than version 2 of the 381 | ordinary GNU General Public License has appeared, then you can specify 382 | that version instead if you wish.) Do not make any other change in 383 | these notices. 384 | . 385 | Once this change is made in a given copy, it is irreversible for 386 | that copy, so the ordinary GNU General Public License applies to all 387 | subsequent copies and derivative works made from that copy. 388 | . 389 | This option is useful when you wish to copy part of the code of 390 | the Library into a program that is not a library. 391 | . 392 | 4. You may copy and distribute the Library (or a portion or 393 | derivative of it, under Section 2) in object code or executable form 394 | under the terms of Sections 1 and 2 above provided that you accompany 395 | it with the complete corresponding machine-readable source code, which 396 | must be distributed under the terms of Sections 1 and 2 above on a 397 | medium customarily used for software interchange. 398 | . 399 | If distribution of object code is made by offering access to copy 400 | from a designated place, then offering equivalent access to copy the 401 | source code from the same place satisfies the requirement to 402 | distribute the source code, even though third parties are not 403 | compelled to copy the source along with the object code. 404 | . 405 | 5. A program that contains no derivative of any portion of the 406 | Library, but is designed to work with the Library by being compiled or 407 | linked with it, is called a "work that uses the Library". Such a 408 | work, in isolation, is not a derivative work of the Library, and 409 | therefore falls outside the scope of this License. 410 | . 411 | However, linking a "work that uses the Library" with the Library 412 | creates an executable that is a derivative of the Library (because it 413 | contains portions of the Library), rather than a "work that uses the 414 | library". The executable is therefore covered by this License. 415 | Section 6 states terms for distribution of such executables. 416 | . 417 | When a "work that uses the Library" uses material from a header file 418 | that is part of the Library, the object code for the work may be a 419 | derivative work of the Library even though the source code is not. 420 | Whether this is true is especially significant if the work can be 421 | linked without the Library, or if the work is itself a library. The 422 | threshold for this to be true is not precisely defined by law. 423 | . 424 | If such an object file uses only numerical parameters, data 425 | structure layouts and accessors, and small macros and small inline 426 | functions (ten lines or less in length), then the use of the object 427 | file is unrestricted, regardless of whether it is legally a derivative 428 | work. (Executables containing this object code plus portions of the 429 | Library will still fall under Section 6.) 430 | . 431 | Otherwise, if the work is a derivative of the Library, you may 432 | distribute the object code for the work under the terms of Section 6. 433 | Any executables containing that work also fall under Section 6, 434 | whether or not they are linked directly with the Library itself. 435 | . 436 | 6. As an exception to the Sections above, you may also combine or 437 | link a "work that uses the Library" with the Library to produce a 438 | work containing portions of the Library, and distribute that work 439 | under terms of your choice, provided that the terms permit 440 | modification of the work for the customer's own use and reverse 441 | engineering for debugging such modifications. 442 | . 443 | You must give prominent notice with each copy of the work that the 444 | Library is used in it and that the Library and its use are covered by 445 | this License. You must supply a copy of this License. If the work 446 | during execution displays copyright notices, you must include the 447 | copyright notice for the Library among them, as well as a reference 448 | directing the user to the copy of this License. Also, you must do one 449 | of these things: 450 | . 451 | a) Accompany the work with the complete corresponding 452 | machine-readable source code for the Library including whatever 453 | changes were used in the work (which must be distributed under 454 | Sections 1 and 2 above); and, if the work is an executable linked 455 | with the Library, with the complete machine-readable "work that 456 | uses the Library", as object code and/or source code, so that the 457 | user can modify the Library and then relink to produce a modified 458 | executable containing the modified Library. (It is understood 459 | that the user who changes the contents of definitions files in the 460 | Library will not necessarily be able to recompile the application 461 | to use the modified definitions.) 462 | . 463 | b) Use a suitable shared library mechanism for linking with the 464 | Library. A suitable mechanism is one that (1) uses at run time a 465 | copy of the library already present on the user's computer system, 466 | rather than copying library functions into the executable, and (2) 467 | will operate properly with a modified version of the library, if 468 | the user installs one, as long as the modified version is 469 | interface-compatible with the version that the work was made with. 470 | . 471 | c) Accompany the work with a written offer, valid for at 472 | least three years, to give the same user the materials 473 | specified in Subsection 6a, above, for a charge no more 474 | than the cost of performing this distribution. 475 | . 476 | d) If distribution of the work is made by offering access to copy 477 | from a designated place, offer equivalent access to copy the above 478 | specified materials from the same place. 479 | . 480 | e) Verify that the user has already received a copy of these 481 | materials or that you have already sent this user a copy. 482 | . 483 | For an executable, the required form of the "work that uses the 484 | Library" must include any data and utility programs needed for 485 | reproducing the executable from it. However, as a special exception, 486 | the materials to be distributed need not include anything that is 487 | normally distributed (in either source or binary form) with the major 488 | components (compiler, kernel, and so on) of the operating system on 489 | which the executable runs, unless that component itself accompanies 490 | the executable. 491 | . 492 | It may happen that this requirement contradicts the license 493 | restrictions of other proprietary libraries that do not normally 494 | accompany the operating system. Such a contradiction means you cannot 495 | use both them and the Library together in an executable that you 496 | distribute. 497 | . 498 | 7. You may place library facilities that are a work based on the 499 | Library side-by-side in a single library together with other library 500 | facilities not covered by this License, and distribute such a combined 501 | library, provided that the separate distribution of the work based on 502 | the Library and of the other library facilities is otherwise 503 | permitted, and provided that you do these two things: 504 | . 505 | a) Accompany the combined library with a copy of the same work 506 | based on the Library, uncombined with any other library 507 | facilities. This must be distributed under the terms of the 508 | Sections above. 509 | . 510 | b) Give prominent notice with the combined library of the fact 511 | that part of it is a work based on the Library, and explaining 512 | where to find the accompanying uncombined form of the same work. 513 | . 514 | 8. You may not copy, modify, sublicense, link with, or distribute 515 | the Library except as expressly provided under this License. Any 516 | attempt otherwise to copy, modify, sublicense, link with, or 517 | distribute the Library is void, and will automatically terminate your 518 | rights under this License. However, parties who have received copies, 519 | or rights, from you under this License will not have their licenses 520 | terminated so long as such parties remain in full compliance. 521 | . 522 | 9. You are not required to accept this License, since you have not 523 | signed it. However, nothing else grants you permission to modify or 524 | distribute the Library or its derivative works. These actions are 525 | prohibited by law if you do not accept this License. Therefore, by 526 | modifying or distributing the Library (or any work based on the 527 | Library), you indicate your acceptance of this License to do so, and 528 | all its terms and conditions for copying, distributing or modifying 529 | the Library or works based on it. 530 | . 531 | 10. Each time you redistribute the Library (or any work based on the 532 | Library), the recipient automatically receives a license from the 533 | original licensor to copy, distribute, link with or modify the Library 534 | subject to these terms and conditions. You may not impose any further 535 | restrictions on the recipients' exercise of the rights granted herein. 536 | You are not responsible for enforcing compliance by third parties with 537 | this License. 538 | . 539 | 11. If, as a consequence of a court judgment or allegation of patent 540 | infringement or for any other reason (not limited to patent issues), 541 | conditions are imposed on you (whether by court order, agreement or 542 | otherwise) that contradict the conditions of this License, they do not 543 | excuse you from the conditions of this License. If you cannot 544 | distribute so as to satisfy simultaneously your obligations under this 545 | License and any other pertinent obligations, then as a consequence you 546 | may not distribute the Library at all. For example, if a patent 547 | license would not permit royalty-free redistribution of the Library by 548 | all those who receive copies directly or indirectly through you, then 549 | the only way you could satisfy both it and this License would be to 550 | refrain entirely from distribution of the Library. 551 | . 552 | If any portion of this section is held invalid or unenforceable under any 553 | particular circumstance, the balance of the section is intended to apply, 554 | and the section as a whole is intended to apply in other circumstances. 555 | . 556 | It is not the purpose of this section to induce you to infringe any 557 | patents or other property right claims or to contest validity of any 558 | such claims; this section has the sole purpose of protecting the 559 | integrity of the free software distribution system which is 560 | implemented by public license practices. Many people have made 561 | generous contributions to the wide range of software distributed 562 | through that system in reliance on consistent application of that 563 | system; it is up to the author/donor to decide if he or she is willing 564 | to distribute software through any other system and a licensee cannot 565 | impose that choice. 566 | . 567 | This section is intended to make thoroughly clear what is believed to 568 | be a consequence of the rest of this License. 569 | . 570 | 12. If the distribution and/or use of the Library is restricted in 571 | certain countries either by patents or by copyrighted interfaces, the 572 | original copyright holder who places the Library under this License may add 573 | an explicit geographical distribution limitation excluding those countries, 574 | so that distribution is permitted only in or among countries not thus 575 | excluded. In such case, this License incorporates the limitation as if 576 | written in the body of this License. 577 | . 578 | 13. The Free Software Foundation may publish revised and/or new 579 | versions of the Lesser General Public License from time to time. 580 | Such new versions will be similar in spirit to the present version, 581 | but may differ in detail to address new problems or concerns. 582 | . 583 | Each version is given a distinguishing version number. If the Library 584 | specifies a version number of this License which applies to it and 585 | "any later version", you have the option of following the terms and 586 | conditions either of that version or of any later version published by 587 | the Free Software Foundation. If the Library does not specify a 588 | license version number, you may choose any version ever published by 589 | the Free Software Foundation. 590 | . 591 | 14. If you wish to incorporate parts of the Library into other free 592 | programs whose distribution conditions are incompatible with these, 593 | write to the author to ask for permission. For software which is 594 | copyrighted by the Free Software Foundation, write to the Free 595 | Software Foundation; we sometimes make exceptions for this. Our 596 | decision will be guided by the two goals of preserving the free status 597 | of all derivatives of our free software and of promoting the sharing 598 | and reuse of software generally. 599 | . 600 | NO WARRANTY 601 | . 602 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 603 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 604 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 605 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 606 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 607 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 608 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 609 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 610 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 611 | . 612 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 613 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 614 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 615 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 616 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 617 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 618 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 619 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 620 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 621 | DAMAGES. 622 | . 623 | END OF TERMS AND CONDITIONS 624 | . 625 | How to Apply These Terms to Your New Libraries 626 | . 627 | If you develop a new library, and you want it to be of the greatest 628 | possible use to the public, we recommend making it free software that 629 | everyone can redistribute and change. You can do so by permitting 630 | redistribution under these terms (or, alternatively, under the terms of the 631 | ordinary General Public License). 632 | . 633 | To apply these terms, attach the following notices to the library. It is 634 | safest to attach them to the start of each source file to most effectively 635 | convey the exclusion of warranty; and each file should have at least the 636 | "copyright" line and a pointer to where the full notice is found. 637 | . 638 | 639 | Copyright (C) 640 | . 641 | This library is free software; you can redistribute it and/or 642 | modify it under the terms of the GNU Lesser General Public 643 | License as published by the Free Software Foundation; either 644 | version 2.1 of the License, or (at your option) any later version. 645 | . 646 | This library is distributed in the hope that it will be useful, 647 | but WITHOUT ANY WARRANTY; without even the implied warranty of 648 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 649 | Lesser General Public License for more details. 650 | . 651 | You should have received a copy of the GNU Lesser General Public 652 | License along with this library; if not, write to the Free Software 653 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 654 | . 655 | Also add information on how to contact you by electronic and paper mail. 656 | . 657 | You should also get your employer (if you work as a programmer) or your 658 | school, if any, to sign a "copyright disclaimer" for the library, if 659 | necessary. Here is a sample; alter the names: 660 | . 661 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 662 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 663 | . 664 | , 1 April 1990 665 | Ty Coon, President of Vice 666 | . 667 | That's all there is to it! 668 | -------------------------------------------------------------------------------- /build/license_lint.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | 3 | # Copyright 2021 Oneric 4 | # SPDX-License-Identifier: ISC 5 | 6 | # Check that we have the full text of all licenses 7 | # being used in the JSO and subprojects 8 | # 9 | # Usage: license_lint.awk 10 | 11 | function canonicalise_name(l) 12 | { 13 | sub(/\++$/, "", l) 14 | sub(/(.0)+$/, "", l) 15 | return l 16 | } 17 | 18 | BEGIN { 19 | FS = ": | and/or " 20 | split("", licenses) 21 | fulltext = 0 22 | errors = 0 23 | } 24 | 25 | FNR == 1 && FILENAME == ARGV[ARGC-1] { 26 | fulltext = 1 27 | } 28 | 29 | 30 | !fulltext && /^License: / { 31 | for(i=2; i<=NF; ++i) { 32 | l = canonicalise_name($2) 33 | ++licenses[l] 34 | } 35 | } 36 | 37 | fulltext && /^License: / { 38 | for(i=2; i<=NF; ++i) { 39 | l = canonicalise_name($2) 40 | if(!(l in licenses)) { 41 | print "Superfluous full text for '"l"'!" | "cat>&2" 42 | ++errors 43 | } else { 44 | delete licenses[l] 45 | } 46 | } 47 | } 48 | 49 | END { 50 | for (l in licenses) { 51 | if(l == "public-domain" || l == "Unlicense") 52 | continue # No notice required 53 | print "Missing full text for '"l"'!" | "cat>&2" 54 | ++errors 55 | } 56 | 57 | if (errors) { 58 | exit 1 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /build/patches/brotli/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/brotli/.gitkeep -------------------------------------------------------------------------------- /build/patches/brotli/0001-fix-brotli-js-for-webworkers.patch: -------------------------------------------------------------------------------- 1 | diff --git a/js/decode.js b/js/decode.js 2 | index 7896a50..a282bd1 100644 3 | --- a/js/decode.js 4 | +++ b/js/decode.js 5 | @@ -2026,4 +2026,4 @@ function BrotliDecodeClosure() { 6 | /** @export */ 7 | var BrotliDecode = BrotliDecodeClosure(); 8 | 9 | -window["BrotliDecode"] = BrotliDecode; 10 | +Module["BrotliDecode"] = BrotliDecode; 11 | -------------------------------------------------------------------------------- /build/patches/brotli/0002-upstream_Enable-install-with-emscripten.patch: -------------------------------------------------------------------------------- 1 | Backported from: https://github.com/google/brotli/commit/ce222e317e36aa362e83fc50c7a6226d238e03fd 2 | From: Dirk Lemstra 3 | Date: Wed, 23 Jun 2021 10:12:21 +0200 4 | Subject: [PATCH] Enabled install when building with emscripten. (#906) 5 | 6 | * Enabled install when building with emscripten. 7 | 8 | * Also install the pkg-config files. 9 | --- 10 | CMakeLists.txt | 18 ++++++++---------- 11 | 1 file changed, 8 insertions(+), 10 deletions(-) 12 | 13 | diff --git a/CMakeLists.txt b/CMakeLists.txt 14 | index 4ff3401..6999292 100644 15 | --- a/CMakeLists.txt 16 | +++ b/CMakeLists.txt 17 | @@ -219,19 +219,20 @@ add_executable(brotli ${BROTLI_CLI_C}) 18 | target_link_libraries(brotli ${BROTLI_LIBRARIES_STATIC}) 19 | 20 | # Installation 21 | -if(NOT BROTLI_EMSCRIPTEN) 22 | if(NOT BROTLI_BUNDLED_MODE) 23 | install( 24 | TARGETS brotli 25 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 26 | ) 27 | 28 | - install( 29 | - TARGETS ${BROTLI_LIBRARIES_CORE} 30 | - ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 31 | - LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" 32 | - RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 33 | - ) 34 | + if(NOT BROTLI_EMSCRIPTEN) 35 | + install( 36 | + TARGETS ${BROTLI_LIBRARIES_CORE} 37 | + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 38 | + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" 39 | + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 40 | + ) 41 | + endif() # BROTLI_EMSCRIPTEN 42 | 43 | install( 44 | TARGETS ${BROTLI_LIBRARIES_CORE_STATIC} 45 | @@ -245,7 +246,6 @@ if(NOT BROTLI_BUNDLED_MODE) 46 | DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" 47 | ) 48 | endif() # BROTLI_BUNDLED_MODE 49 | -endif() # BROTLI_EMSCRIPTEN 50 | 51 | # Tests 52 | 53 | @@ -405,7 +405,6 @@ transform_pc_file("scripts/libbrotlidec.pc.in" "${CMAKE_CURRENT_BINARY_DIR}/libb 54 | 55 | transform_pc_file("scripts/libbrotlienc.pc.in" "${CMAKE_CURRENT_BINARY_DIR}/libbrotlienc.pc" "${BROTLI_VERSION}") 56 | 57 | -if(NOT BROTLI_EMSCRIPTEN) 58 | if(NOT BROTLI_BUNDLED_MODE) 59 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libbrotlicommon.pc" 60 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") 61 | @@ -414,7 +413,6 @@ if(NOT BROTLI_BUNDLED_MODE) 62 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libbrotlienc.pc" 63 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") 64 | endif() # BROTLI_BUNDLED_MODE 65 | -endif() # BROTLI_EMSCRIPTEN 66 | 67 | if (ENABLE_COVERAGE STREQUAL "yes") 68 | SETUP_TARGET_FOR_COVERAGE(coverage test coverage) 69 | -- 70 | 2.30.2 71 | 72 | -------------------------------------------------------------------------------- /build/patches/expat/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/expat/.gitkeep -------------------------------------------------------------------------------- /build/patches/fontconfig/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/fontconfig/.gitkeep -------------------------------------------------------------------------------- /build/patches/fontconfig/0001-disable-pthreads.patch: -------------------------------------------------------------------------------- 1 | diff --git a/configure.ac b/configure.ac 2 | index 26b974a..b73d132 100644 3 | --- a/configure.ac 4 | +++ b/configure.ac 5 | @@ -712,22 +712,8 @@ if $fc_cv_have_solaris_atomic_ops; then 6 | AC_DEFINE(HAVE_SOLARIS_ATOMIC_OPS, 1, [Have Solaris __machine_*_barrier and atomic_* operations]) 7 | fi 8 | 9 | -if test "$os_win32" = no && ! $have_pthread; then 10 | - AC_CHECK_HEADERS(sched.h) 11 | - AC_SEARCH_LIBS(sched_yield,rt,AC_DEFINE(HAVE_SCHED_YIELD, 1, [Have sched_yield])) 12 | -fi 13 | - 14 | -have_pthread=false 15 | -if test "$os_win32" = no; then 16 | - AX_PTHREAD([have_pthread=true]) 17 | -fi 18 | -if $have_pthread; then 19 | - LIBS="$PTHREAD_LIBS $LIBS" 20 | - CFLAGS="$CFLAGS $PTHREAD_CFLAGS" 21 | - CC="$PTHREAD_CC" 22 | - AC_DEFINE(HAVE_PTHREAD, 1, [Have POSIX threads]) 23 | -fi 24 | -AM_CONDITIONAL(HAVE_PTHREAD, $have_pthread) 25 | +AC_DEFINE(HAVE_PTHREAD, 0, [Have POSIX threads]) 26 | +AM_CONDITIONAL(HAVE_PTHREAD, false) 27 | 28 | 29 | dnl =========================================================================== 30 | -------------------------------------------------------------------------------- /build/patches/freetype/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/freetype/.gitkeep -------------------------------------------------------------------------------- /build/patches/freetype/0002-disable-pthread.patch: -------------------------------------------------------------------------------- 1 | diff --git a/builds/unix/configure.raw b/builds/unix/configure.raw 2 | index 56e0a8e2c..0639c10db 100644 3 | --- a/builds/unix/configure.raw 4 | +++ b/builds/unix/configure.raw 5 | @@ -1007,7 +1007,7 @@ esac 6 | 7 | # Check for pthreads 8 | 9 | -AX_PTHREAD([have_pthread=yes], [have_pthread=no]) 10 | +have_pthread=no 11 | 12 | # Check for Python and docwriter 13 | 14 | -------------------------------------------------------------------------------- /build/patches/fribidi/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/fribidi/.gitkeep -------------------------------------------------------------------------------- /build/patches/harfbuzz/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jellyfin/JavascriptSubtitlesOctopus/950dbb4b5bcb713e4bf0d33dec63957e5806bbfd/build/patches/harfbuzz/.gitkeep -------------------------------------------------------------------------------- /build/patches/harfbuzz/0001-force_disable_pthread.patch: -------------------------------------------------------------------------------- 1 | diff --git a/configure.ac b/configure.ac 2 | index c88cffea..a8f98078 100644 3 | --- a/configure.ac 4 | +++ b/configure.ac 5 | @@ -128,9 +128,9 @@ AC_MSG_RESULT([$hb_os_win32]) 6 | AM_CONDITIONAL(OS_WIN32, test "$hb_os_win32" = "yes") 7 | 8 | have_pthread=false 9 | -AX_PTHREAD([have_pthread=true]) 10 | +AX_PTHREAD([have_pthread=false]) 11 | if $have_pthread; then 12 | - AC_DEFINE(HAVE_PTHREAD, 1, [Have POSIX threads]) 13 | + AC_DEFINE(HAVE_PTHREAD, 0, [Have POSIX threads]) 14 | fi 15 | AM_CONDITIONAL(HAVE_PTHREAD, $have_pthread) 16 | 17 | -------------------------------------------------------------------------------- /build/patches/webidl_binder/0001-WebIDL-Add-headers-and-make-it-work-under-SubtitlesO.patch: -------------------------------------------------------------------------------- 1 | From d9b7e2738213d631810ace5e5f47cc8bc210fa8c Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Thiago=20Fran=C3=A7a=20da=20Silva?= 3 | 4 | Date: Fri, 18 Feb 2022 21:18:04 -0300 5 | Subject: [PATCH 1/4] WebIDL: Add headers and make it work under 6 | SubtitlesOctopus project 7 | 8 | --- 9 | build/WebIDL.py | 3 +++ 10 | build/tempfiles.py | 3 +++ 11 | build/webidl_binder.py | 20 ++++++++++---------- 12 | 3 files changed, 16 insertions(+), 10 deletions(-) 13 | 14 | diff --git a/build/WebIDL.py b/build/WebIDL.py 15 | index 14689cb..8892616 100644 16 | --- a/build/WebIDL.py 17 | +++ b/build/WebIDL.py 18 | @@ -1,3 +1,6 @@ 19 | +## JavascriptSubtitlesOctopus 20 | +## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/third_party/WebIDL.py 21 | + 22 | # from https://hg.mozilla.org/mozilla-central/file/tip/dom/bindings/parser/WebIDL.py 23 | # rev 501baeb3a034 24 | 25 | diff --git a/build/tempfiles.py b/build/tempfiles.py 26 | index e1c9dcc..6487516 100644 27 | --- a/build/tempfiles.py 28 | +++ b/build/tempfiles.py 29 | @@ -1,3 +1,6 @@ 30 | +## JavascriptSubtitlesOctopus 31 | +## From https://github.com/emscripten-core/emscripten/blob/c834ef7d69ccb4100239eeba0b0f6573fed063bc/tools/tempfiles.py 32 | + 33 | # Copyright 2013 The Emscripten Authors. All rights reserved. 34 | # Emscripten is available under two separate licenses, the MIT license and the 35 | # University of Illinois/NCSA Open Source License. Both these licenses can be 36 | diff --git a/build/webidl_binder.py b/build/webidl_binder.py 37 | index da864f8..687a5ba 100644 38 | --- a/build/webidl_binder.py 39 | +++ b/build/webidl_binder.py 40 | @@ -1,3 +1,6 @@ 41 | +## JavascriptSubtitlesOctopus 42 | +## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/tools/webidl_binder.py 43 | + 44 | # Copyright 2014 The Emscripten Authors. All rights reserved. 45 | # Emscripten is available under two separate licenses, the MIT license and the 46 | # University of Illinois/NCSA Open Source License. Both these licenses can be 47 | @@ -10,15 +13,12 @@ https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder. 48 | 49 | import os 50 | import sys 51 | +from tempfiles import try_delete 52 | 53 | -__scriptdir__ = os.path.dirname(os.path.abspath(__file__)) 54 | -__rootdir__ = os.path.dirname(__scriptdir__) 55 | -sys.path.append(__rootdir__) 56 | - 57 | -from tools import shared, utils 58 | +def path_from_root(*pathelems): 59 | + return os.path.join(os.path.join('/', 'emsdk', 'upstream', 'emscripten'), *pathelems) 60 | 61 | -sys.path.append(utils.path_from_root('third_party')) 62 | -sys.path.append(utils.path_from_root('third_party/ply')) 63 | +sys.path.append(path_from_root('third_party', 'ply')) 64 | 65 | import WebIDL 66 | 67 | @@ -50,14 +50,14 @@ class Dummy: 68 | input_file = sys.argv[1] 69 | output_base = sys.argv[2] 70 | 71 | -shared.try_delete(output_base + '.cpp') 72 | -shared.try_delete(output_base + '.js') 73 | +try_delete(output_base + '.cpp') 74 | +try_delete(output_base + '.js') 75 | 76 | p = WebIDL.Parser() 77 | p.parse(r''' 78 | interface VoidPtr { 79 | }; 80 | -''' + utils.read_file(input_file)) 81 | +''' + open(input_file).read()) 82 | data = p.finish() 83 | 84 | interfaces = {} 85 | -- 86 | 2.35.1 87 | 88 | -------------------------------------------------------------------------------- /build/patches/webidl_binder/0002-WebIDL-Implement-Integer-Pointer-type-IntPtr.patch: -------------------------------------------------------------------------------- 1 | From 2e7bb7592195c94853c4e3b6718d47677e1555bf Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Thiago=20Fran=C3=A7a=20da=20Silva?= 3 | 4 | Date: Fri, 18 Feb 2022 21:19:20 -0300 5 | Subject: [PATCH 2/4] WebIDL: Implement Integer Pointer type (IntPtr) 6 | 7 | This allows compatibility with `changed` parameter of the `renderImage` function 8 | --- 9 | build/WebIDL.py | 15 +++++++++++++++ 10 | build/webidl_binder.py | 4 ++++ 11 | 2 files changed, 19 insertions(+) 12 | 13 | diff --git a/build/WebIDL.py b/build/WebIDL.py 14 | index 8892616..d140f8f 100644 15 | --- a/build/WebIDL.py 16 | +++ b/build/WebIDL.py 17 | @@ -1,4 +1,6 @@ 18 | ## JavascriptSubtitlesOctopus 19 | +## Patched to: 20 | +## - add integer pointers (IntPtr) 21 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/third_party/WebIDL.py 22 | 23 | # from https://hg.mozilla.org/mozilla-central/file/tip/dom/bindings/parser/WebIDL.py 24 | @@ -2165,6 +2167,7 @@ class IDLBuiltinType(IDLType): 25 | 'date', 26 | 'void', 27 | # Funny stuff 28 | + 'IntPtr', 29 | 'ArrayBuffer', 30 | 'ArrayBufferView', 31 | 'Int8Array', 32 | @@ -2184,6 +2187,7 @@ class IDLBuiltinType(IDLType): 33 | Types.short: IDLType.Tags.int16, 34 | Types.unsigned_short: IDLType.Tags.uint16, 35 | Types.long: IDLType.Tags.int32, 36 | + Types.IntPtr: IDLType.Tags.int32, 37 | Types.unsigned_long: IDLType.Tags.uint32, 38 | Types.long_long: IDLType.Tags.int64, 39 | Types.unsigned_long_long: IDLType.Tags.uint64, 40 | @@ -2380,6 +2384,9 @@ BuiltinTypes = { 41 | IDLBuiltinType.Types.any: 42 | IDLBuiltinType(BuiltinLocation(""), "Any", 43 | IDLBuiltinType.Types.any), 44 | + IDLBuiltinType.Types.IntPtr: 45 | + IDLBuiltinType(BuiltinLocation(""), "IntPtr", 46 | + IDLBuiltinType.Types.IntPtr), 47 | IDLBuiltinType.Types.domstring: 48 | IDLBuiltinType(BuiltinLocation(""), "String", 49 | IDLBuiltinType.Types.domstring), 50 | @@ -3622,6 +3629,7 @@ class Tokenizer(object): 51 | "...": "ELLIPSIS", 52 | "::": "SCOPE", 53 | "Date": "DATE", 54 | + "IntPtr": "INTPTR", 55 | "DOMString": "DOMSTRING", 56 | "ByteString": "BYTESTRING", 57 | "any": "ANY", 58 | @@ -4507,6 +4515,7 @@ class Parser(Tokenizer): 59 | | DATE 60 | | DOMSTRING 61 | | BYTESTRING 62 | + | INTPTR 63 | | ANY 64 | | ATTRIBUTE 65 | | BOOLEAN 66 | @@ -4573,6 +4582,12 @@ class Parser(Tokenizer): 67 | """ 68 | p[0] = self.handleModifiers(BuiltinTypes[IDLBuiltinType.Types.any], p[2]) 69 | 70 | + def p_SingleTypeIntPtrType(self, p): 71 | + """ 72 | + SingleType : INTPTR TypeSuffixStartingWithArray 73 | + """ 74 | + p[0] = self.handleModifiers(BuiltinTypes[IDLBuiltinType.Types.IntPtr], p[2]) 75 | + 76 | def p_UnionType(self, p): 77 | """ 78 | UnionType : LPAREN UnionMemberType OR UnionMemberType UnionMemberTypes RPAREN 79 | diff --git a/build/webidl_binder.py b/build/webidl_binder.py 80 | index 687a5ba..e9a56e5 100644 81 | --- a/build/webidl_binder.py 82 | +++ b/build/webidl_binder.py 83 | @@ -1,4 +1,6 @@ 84 | ## JavascriptSubtitlesOctopus 85 | +## Patched to: 86 | +## - add integer pointers (IntPtr) 87 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/tools/webidl_binder.py 88 | 89 | # Copyright 2014 The Emscripten Authors. All rights reserved. 90 | @@ -337,6 +339,8 @@ def type_to_c(t, non_pointing=False): 91 | ret = 'bool' 92 | elif t == 'Any' or t == 'VoidPtr': 93 | ret = 'void*' 94 | + elif t == 'IntPtr': 95 | + ret = 'int*' 96 | elif t in interfaces: 97 | ret = (interfaces[t].getExtendedAttribute('Prefix') or [''])[0] + t + ('' if non_pointing else '*') 98 | else: 99 | -- 100 | 2.35.1 101 | 102 | -------------------------------------------------------------------------------- /build/patches/webidl_binder/0003-WebIDL-Implement-ByteString-type.patch: -------------------------------------------------------------------------------- 1 | From ea9c45b10d807966510711da723ea1ae558efd45 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Thiago=20Fran=C3=A7a=20da=20Silva?= 3 | 4 | Date: Mon, 14 Feb 2022 22:27:57 -0300 5 | Subject: [PATCH 3/4] WebIDL: Implement ByteString type 6 | 7 | This allows compatibility with `bitmap` attribute of the ASS_Image struct 8 | --- 9 | build/webidl_binder.py | 3 +++ 10 | 1 file changed, 3 insertions(+) 11 | 12 | diff --git a/build/webidl_binder.py b/build/webidl_binder.py 13 | index e9a56e5..faedf10 100644 14 | --- a/build/webidl_binder.py 15 | +++ b/build/webidl_binder.py 16 | @@ -1,6 +1,7 @@ 17 | ## JavascriptSubtitlesOctopus 18 | ## Patched to: 19 | ## - add integer pointers (IntPtr) 20 | +## - implement ByteString type 21 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/tools/webidl_binder.py 22 | 23 | # Copyright 2014 The Emscripten Authors. All rights reserved. 24 | @@ -327,6 +328,8 @@ def type_to_c(t, non_pointing=False): 25 | ret = 'char' 26 | elif t == 'Octet': 27 | ret = 'unsigned char' 28 | + elif t == 'ByteString': 29 | + ret = 'unsigned char*' 30 | elif t == 'Void': 31 | ret = 'void' 32 | elif t == 'String': 33 | -- 34 | 2.35.1 35 | 36 | -------------------------------------------------------------------------------- /build/patches/webidl_binder/0004-WebIDL-Implement-Owner-Extended-Attribute-fix-77.patch: -------------------------------------------------------------------------------- 1 | From af1dc9ac94cc057e8f63641fcb7e0dbe1c320c54 Mon Sep 17 00:00:00 2001 2 | From: =?UTF-8?q?Thiago=20Fran=C3=A7a=20da=20Silva?= 3 | 4 | Date: Fri, 11 Mar 2022 14:56:15 -0300 5 | Subject: [PATCH] WebIDL: Implement Owner Extended Attribute, fix #77 6 | 7 | Currently the ensureCache create a temporary pointer that will 8 | transfer the value to the WASM/C part and recycle it with to use in the 9 | next value, but since the libass struct pointers are owned by the 10 | library, the pointers can't be recycled or freed or can lead into 11 | an undefined behavior. 12 | 13 | This configure the binder to tranfer the pointer ownership instead of 14 | recycle it. 15 | 16 | To avoid creating complex code, I decided to fix it in the webidl 17 | binder. 18 | --- 19 | build/WebIDL.py | 4 ++- 20 | build/webidl_binder.py | 78 +++++++++++++++++++++++++++-------------- 21 | 3 files changed, 64 insertions(+), 36 deletions(-) 22 | 23 | diff --git a/build/WebIDL.py b/build/WebIDL.py 24 | index d140f8f..4b967f0 100644 25 | --- a/build/WebIDL.py 26 | +++ b/build/WebIDL.py 27 | @@ -1,6 +1,7 @@ 28 | ## JavascriptSubtitlesOctopus 29 | ## Patched to: 30 | ## - add integer pointers (IntPtr) 31 | +## - add [Owner] Extended attribute 32 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/third_party/WebIDL.py 33 | 34 | # from https://hg.mozilla.org/mozilla-central/file/tip/dom/bindings/parser/WebIDL.py 35 | @@ -2863,6 +2864,7 @@ class IDLAttribute(IDLInterfaceMember): 36 | identifier == "AvailableIn" or 37 | identifier == "Const" or 38 | identifier == "Value" or 39 | + identifier == "Owner" or 40 | identifier == "BoundsChecked" or 41 | identifier == "NewObject"): 42 | # Known attributes that we don't need to do anything with here 43 | @@ -2938,7 +2940,7 @@ class IDLArgument(IDLObjectWithIdentifier): 44 | self.enforceRange = True 45 | elif identifier == "TreatNonCallableAsNull": 46 | self._allowTreatNonCallableAsNull = True 47 | - elif identifier in ['Ref', 'Const']: 48 | + elif identifier in ['Ref', 'Const', 'Owner']: 49 | # ok in emscripten 50 | self._extraAttributes[identifier] = True 51 | else: 52 | diff --git a/build/webidl_binder.py b/build/webidl_binder.py 53 | index faedf10..1627057 100644 54 | --- a/build/webidl_binder.py 55 | +++ b/build/webidl_binder.py 56 | @@ -2,6 +2,7 @@ 57 | ## Patched to: 58 | ## - add integer pointers (IntPtr) 59 | ## - implement ByteString type 60 | +## - add [Owner] Extended attribute for Pointer Ownership transfer 61 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/tools/webidl_binder.py 62 | 63 | # Copyright 2014 The Emscripten Authors. All rights reserved. 64 | @@ -174,6 +175,7 @@ var ensureCache = { 65 | size: 0, // the size of buffer 66 | pos: 0, // the next free offset in buffer 67 | temps: [], // extra allocations 68 | + owned: [], // Owned allocations 69 | needed: 0, // the total size we need next time 70 | 71 | prepare: function() { 72 | @@ -197,22 +199,29 @@ var ensureCache = { 73 | } 74 | ensureCache.pos = 0; 75 | }, 76 | - alloc: function(array, view) { 77 | + alloc: function(array, view, owner) { 78 | assert(ensureCache.buffer); 79 | var bytes = view.BYTES_PER_ELEMENT; 80 | var len = array.length * bytes; 81 | len = (len + 7) & -8; // keep things aligned to 8 byte boundaries 82 | var ret; 83 | - if (ensureCache.pos + len >= ensureCache.size) { 84 | - // we failed to allocate in the buffer, ensureCache time around :( 85 | + if (owner) { 86 | assert(len > 0); // null terminator, at least 87 | ensureCache.needed += len; 88 | ret = Module['_malloc'](len); 89 | - ensureCache.temps.push(ret); 90 | + ensureCache.owned.push(ret); 91 | } else { 92 | - // we can allocate in the buffer 93 | - ret = ensureCache.buffer + ensureCache.pos; 94 | - ensureCache.pos += len; 95 | + if (ensureCache.pos + len >= ensureCache.size) { 96 | + // we failed to allocate in the buffer, ensureCache time around :( 97 | + assert(len > 0); // null terminator, at least 98 | + ensureCache.needed += len; 99 | + ret = Module['_malloc'](len); 100 | + ensureCache.temps.push(ret); 101 | + } else { 102 | + // we can allocate in the buffer 103 | + ret = ensureCache.buffer + ensureCache.pos; 104 | + ensureCache.pos += len; 105 | + } 106 | } 107 | return ret; 108 | }, 109 | @@ -228,58 +237,73 @@ var ensureCache = { 110 | view[offset + i] = array[i]; 111 | } 112 | }, 113 | + clear: function(clearOwned) { 114 | + for (var i = 0; i < ensureCache.temps.length; i++) { 115 | + Module['_free'](ensureCache.temps[i]); 116 | + } 117 | + if (clearOwned) { 118 | + for (var i = 0; i < ensureCache.owned.length; i++) { 119 | + Module['_free'](ensureCache.owned[i]); 120 | + } 121 | + } 122 | + ensureCache.temps.length = 0; 123 | + Module['_free'](ensureCache.buffer); 124 | + ensureCache.buffer = 0; 125 | + ensureCache.size = 0; 126 | + ensureCache.needed = 0; 127 | + } 128 | }; 129 | 130 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 131 | -function ensureString(value) { 132 | +function ensureString(value, owner) { 133 | if (typeof value === 'string') { 134 | var intArray = intArrayFromString(value); 135 | - var offset = ensureCache.alloc(intArray, HEAP8); 136 | + var offset = ensureCache.alloc(intArray, HEAP8, owner); 137 | ensureCache.copy(intArray, HEAP8, offset); 138 | return offset; 139 | } 140 | return value; 141 | } 142 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 143 | -function ensureInt8(value) { 144 | +function ensureInt8(value, owner) { 145 | if (typeof value === 'object') { 146 | - var offset = ensureCache.alloc(value, HEAP8); 147 | + var offset = ensureCache.alloc(value, HEAP8, owner); 148 | ensureCache.copy(value, HEAP8, offset); 149 | return offset; 150 | } 151 | return value; 152 | } 153 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 154 | -function ensureInt16(value) { 155 | +function ensureInt16(value, owner) { 156 | if (typeof value === 'object') { 157 | - var offset = ensureCache.alloc(value, HEAP16); 158 | + var offset = ensureCache.alloc(value, HEAP16, owner); 159 | ensureCache.copy(value, HEAP16, offset); 160 | return offset; 161 | } 162 | return value; 163 | } 164 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 165 | -function ensureInt32(value) { 166 | +function ensureInt32(value, owner) { 167 | if (typeof value === 'object') { 168 | - var offset = ensureCache.alloc(value, HEAP32); 169 | + var offset = ensureCache.alloc(value, HEAP32, owner); 170 | ensureCache.copy(value, HEAP32, offset); 171 | return offset; 172 | } 173 | return value; 174 | } 175 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 176 | -function ensureFloat32(value) { 177 | +function ensureFloat32(value, owner) { 178 | if (typeof value === 'object') { 179 | - var offset = ensureCache.alloc(value, HEAPF32); 180 | + var offset = ensureCache.alloc(value, HEAPF32, owner); 181 | ensureCache.copy(value, HEAPF32, offset); 182 | return offset; 183 | } 184 | return value; 185 | } 186 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 187 | -function ensureFloat64(value) { 188 | +function ensureFloat64(value, owner) { 189 | if (typeof value === 'object') { 190 | - var offset = ensureCache.alloc(value, HEAPF64); 191 | + var offset = ensureCache.alloc(value, HEAPF64, owner); 192 | ensureCache.copy(value, HEAPF64, offset); 193 | return offset; 194 | } 195 | @@ -390,7 +414,7 @@ def type_to_cdec(raw): 196 | 197 | def render_function(class_name, func_name, sigs, return_type, non_pointer, 198 | copy, operator, constructor, func_scope, 199 | - call_content=None, const=False, array_attribute=False): 200 | + call_content=None, const=False, owned=False, array_attribute=False): 201 | legacy_mode = CHECKS not in ['ALL', 'FAST'] 202 | all_checks = CHECKS == 'ALL' 203 | 204 | @@ -505,20 +529,20 @@ def render_function(class_name, func_name, sigs, return_type, non_pointer, 205 | if not (arg.type.isArray() and not array_attribute): 206 | body += " if ({0} && typeof {0} === 'object') {0} = {0}.ptr;\n".format(js_arg) 207 | if arg.type.isString(): 208 | - body += " else {0} = ensureString({0});\n".format(js_arg) 209 | + body += " else {0} = ensureString({0}, {1});\n".format(js_arg, "true" if (owned) else "false") 210 | else: 211 | # an array can be received here 212 | arg_type = arg.type.name 213 | if arg_type in ['Byte', 'Octet']: 214 | - body += " if (typeof {0} == 'object') {{ {0} = ensureInt8({0}); }}\n".format(js_arg) 215 | + body += " if (typeof {0} == 'object') {{ {0} = ensureInt8({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 216 | elif arg_type in ['Short', 'UnsignedShort']: 217 | - body += " if (typeof {0} == 'object') {{ {0} = ensureInt16({0}); }}\n".format(js_arg) 218 | + body += " if (typeof {0} == 'object') {{ {0} = ensureInt16({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 219 | elif arg_type in ['Long', 'UnsignedLong']: 220 | - body += " if (typeof {0} == 'object') {{ {0} = ensureInt32({0}); }}\n".format(js_arg) 221 | + body += " if (typeof {0} == 'object') {{ {0} = ensureInt32({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 222 | elif arg_type == 'Float': 223 | - body += " if (typeof {0} == 'object') {{ {0} = ensureFloat32({0}); }}\n".format(js_arg) 224 | + body += " if (typeof {0} == 'object') {{ {0} = ensureFloat32({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 225 | elif arg_type == 'Double': 226 | - body += " if (typeof {0} == 'object') {{ {0} = ensureFloat64({0}); }}\n".format(js_arg) 227 | + body += " if (typeof {0} == 'object') {{ {0} = ensureFloat64({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 228 | 229 | c_names = {} 230 | for i in range(min_args, max_args): 231 | @@ -737,6 +761,7 @@ for name in names: 232 | func_scope=interface, 233 | call_content=get_call_content, 234 | const=m.getExtendedAttribute('Const'), 235 | + owned=m.getExtendedAttribute('Owner'), 236 | array_attribute=m.type.isArray()) 237 | 238 | if m.readonly: 239 | @@ -755,6 +780,7 @@ for name in names: 240 | func_scope=interface, 241 | call_content=set_call_content, 242 | const=m.getExtendedAttribute('Const'), 243 | + owned=m.getExtendedAttribute('Owner'), 244 | array_attribute=m.type.isArray()) 245 | mid_js += [r''' 246 | Object.defineProperty(%s.prototype, '%s', { get: %s.prototype.%s, set: %s.prototype.%s });''' % (name, attr, name, get_name, name, set_name)] 247 | -- 248 | 2.35.1 249 | 250 | -------------------------------------------------------------------------------- /build/tempfiles.py: -------------------------------------------------------------------------------- 1 | ## JavascriptSubtitlesOctopus 2 | ## From https://github.com/emscripten-core/emscripten/blob/c834ef7d69ccb4100239eeba0b0f6573fed063bc/tools/tempfiles.py 3 | 4 | # Copyright 2013 The Emscripten Authors. All rights reserved. 5 | # Emscripten is available under two separate licenses, the MIT license and the 6 | # University of Illinois/NCSA Open Source License. Both these licenses can be 7 | # found in the LICENSE file. 8 | 9 | import os 10 | import shutil 11 | import tempfile 12 | import atexit 13 | import stat 14 | import sys 15 | 16 | 17 | # Attempts to delete given possibly nonexisting or read-only directory tree or filename. 18 | # If any failures occur, the function silently returns without throwing an error. 19 | def try_delete(pathname): 20 | try: 21 | os.unlink(pathname) 22 | except OSError: 23 | pass 24 | if not os.path.exists(pathname): 25 | return 26 | try: 27 | shutil.rmtree(pathname, ignore_errors=True) 28 | except IOError: 29 | pass 30 | if not os.path.exists(pathname): 31 | return 32 | 33 | # Ensure all files are readable and writable by the current user. 34 | permission_bits = stat.S_IWRITE | stat.S_IREAD 35 | 36 | def is_writable(path): 37 | return (os.stat(path).st_mode & permission_bits) != permission_bits 38 | 39 | def make_writable(path): 40 | os.chmod(path, os.stat(path).st_mode | permission_bits) 41 | 42 | # Some tests make files and subdirectories read-only, so rmtree/unlink will not delete 43 | # them. Force-make everything writable in the subdirectory to make it 44 | # removable and re-attempt. 45 | if not is_writable(pathname): 46 | make_writable(pathname) 47 | 48 | if os.path.isdir(pathname): 49 | for directory, subdirs, files in os.walk(pathname): 50 | for item in files + subdirs: 51 | i = os.path.join(directory, item) 52 | make_writable(i) 53 | 54 | try: 55 | shutil.rmtree(pathname, ignore_errors=True) 56 | except IOError: 57 | pass 58 | 59 | 60 | class TempFiles: 61 | def __init__(self, tmpdir, save_debug_files): 62 | self.tmpdir = tmpdir 63 | self.save_debug_files = save_debug_files 64 | self.to_clean = [] 65 | 66 | atexit.register(self.clean) 67 | 68 | def note(self, filename): 69 | self.to_clean.append(filename) 70 | 71 | def get(self, suffix): 72 | """Returns a named temp file with the given prefix.""" 73 | named_file = tempfile.NamedTemporaryFile(dir=self.tmpdir, suffix=suffix, delete=False) 74 | self.note(named_file.name) 75 | return named_file 76 | 77 | def get_file(self, suffix): 78 | """Returns an object representing a RAII-like access to a temp file 79 | that has convenient pythonesque semantics for being used via a construct 80 | 'with TempFiles.get_file(..) as filename:'. 81 | The file will be deleted immediately once the 'with' block is exited. 82 | """ 83 | class TempFileObject: 84 | def __enter__(self_): 85 | self_.file = tempfile.NamedTemporaryFile(dir=self.tmpdir, suffix=suffix, delete=False) 86 | self_.file.close() # NamedTemporaryFile passes out open file handles, but callers prefer filenames (and open their own handles manually if needed) 87 | return self_.file.name 88 | 89 | def __exit__(self_, type, value, traceback): 90 | if not self.save_debug_files: 91 | try_delete(self_.file.name) 92 | return TempFileObject() 93 | 94 | def get_dir(self): 95 | """Returns a named temp directory with the given prefix.""" 96 | directory = tempfile.mkdtemp(dir=self.tmpdir) 97 | self.note(directory) 98 | return directory 99 | 100 | def clean(self): 101 | if self.save_debug_files: 102 | print(f'not cleaning up temp files since in debug-save mode, see them in {self.tmpdir}', file=sys.stderr) 103 | return 104 | for filename in self.to_clean: 105 | try_delete(filename) 106 | self.to_clean = [] 107 | -------------------------------------------------------------------------------- /build/webidl_binder.py: -------------------------------------------------------------------------------- 1 | ## JavascriptSubtitlesOctopus 2 | ## Patched to: 3 | ## - add integer pointers (IntPtr) 4 | ## - implement ByteString type 5 | ## - add [Owner] Extended attribute for Pointer Ownership transfer 6 | ## From https://github.com/emscripten-core/emscripten/blob/f36f9fcaf83db93e6a6d0f0cdc47ab6379ade139/tools/webidl_binder.py 7 | 8 | # Copyright 2014 The Emscripten Authors. All rights reserved. 9 | # Emscripten is available under two separate licenses, the MIT license and the 10 | # University of Illinois/NCSA Open Source License. Both these licenses can be 11 | # found in the LICENSE file. 12 | 13 | """WebIDL binder 14 | 15 | https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html 16 | """ 17 | 18 | import os 19 | import sys 20 | from tempfiles import try_delete 21 | 22 | def path_from_root(*pathelems): 23 | return os.path.join(os.path.join('/', 'emsdk', 'upstream', 'emscripten'), *pathelems) 24 | 25 | sys.path.append(path_from_root('third_party', 'ply')) 26 | 27 | import WebIDL 28 | 29 | # CHECKS='FAST' will skip most argument type checks in the wrapper methods for 30 | # performance (~3x faster than default). 31 | # CHECKS='ALL' will do extensive argument type checking (~5x slower than default). 32 | # This will catch invalid numbers, invalid pointers, invalid strings, etc. 33 | # Anything else defaults to legacy mode for backward compatibility. 34 | CHECKS = os.environ.get('IDL_CHECKS', 'DEFAULT') 35 | # DEBUG=1 will print debug info in render_function 36 | DEBUG = os.environ.get('IDL_VERBOSE') == '1' 37 | 38 | if DEBUG: 39 | print("Debug print ON, CHECKS=%s" % CHECKS) 40 | 41 | # We need to avoid some closure errors on the constructors we define here. 42 | CONSTRUCTOR_CLOSURE_SUPPRESSIONS = '/** @suppress {undefinedVars, duplicate} @this{Object} */' 43 | 44 | 45 | class Dummy: 46 | def __init__(self, init): 47 | for k, v in init.items(): 48 | self.__dict__[k] = v 49 | 50 | def getExtendedAttribute(self, name): # noqa: U100 51 | return None 52 | 53 | 54 | input_file = sys.argv[1] 55 | output_base = sys.argv[2] 56 | 57 | try_delete(output_base + '.cpp') 58 | try_delete(output_base + '.js') 59 | 60 | p = WebIDL.Parser() 61 | p.parse(r''' 62 | interface VoidPtr { 63 | }; 64 | ''' + open(input_file).read()) 65 | data = p.finish() 66 | 67 | interfaces = {} 68 | implements = {} 69 | enums = {} 70 | 71 | for thing in data: 72 | if isinstance(thing, WebIDL.IDLInterface): 73 | interfaces[thing.identifier.name] = thing 74 | elif isinstance(thing, WebIDL.IDLImplementsStatement): 75 | implements.setdefault(thing.implementor.identifier.name, []).append(thing.implementee.identifier.name) 76 | elif isinstance(thing, WebIDL.IDLEnum): 77 | enums[thing.identifier.name] = thing 78 | 79 | # print interfaces 80 | # print implements 81 | 82 | pre_c = [] 83 | mid_c = [] 84 | mid_js = [] 85 | 86 | pre_c += [r''' 87 | #include 88 | '''] 89 | 90 | mid_c += [r''' 91 | extern "C" { 92 | '''] 93 | 94 | 95 | def build_constructor(name): 96 | implementing_name = implements[name][0] if implements.get(name) else 'WrapperObject' 97 | return [r'''{name}.prototype = Object.create({implementing}.prototype); 98 | {name}.prototype.constructor = {name}; 99 | {name}.prototype.__class__ = {name}; 100 | {name}.__cache__ = {{}}; 101 | Module['{name}'] = {name}; 102 | '''.format(name=name, implementing=implementing_name)] 103 | 104 | 105 | mid_js += [''' 106 | // Bindings utilities 107 | 108 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 109 | function WrapperObject() { 110 | } 111 | '''] 112 | 113 | mid_js += build_constructor('WrapperObject') 114 | 115 | mid_js += [''' 116 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) 117 | @param {*=} __class__ */ 118 | function getCache(__class__) { 119 | return (__class__ || WrapperObject).__cache__; 120 | } 121 | Module['getCache'] = getCache; 122 | 123 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) 124 | @param {*=} __class__ */ 125 | function wrapPointer(ptr, __class__) { 126 | var cache = getCache(__class__); 127 | var ret = cache[ptr]; 128 | if (ret) return ret; 129 | ret = Object.create((__class__ || WrapperObject).prototype); 130 | ret.ptr = ptr; 131 | return cache[ptr] = ret; 132 | } 133 | Module['wrapPointer'] = wrapPointer; 134 | 135 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 136 | function castObject(obj, __class__) { 137 | return wrapPointer(obj.ptr, __class__); 138 | } 139 | Module['castObject'] = castObject; 140 | 141 | Module['NULL'] = wrapPointer(0); 142 | 143 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 144 | function destroy(obj) { 145 | if (!obj['__destroy__']) throw 'Error: Cannot destroy object. (Did you create it yourself?)'; 146 | obj['__destroy__'](); 147 | // Remove from cache, so the object can be GC'd and refs added onto it released 148 | delete getCache(obj.__class__)[obj.ptr]; 149 | } 150 | Module['destroy'] = destroy; 151 | 152 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 153 | function compare(obj1, obj2) { 154 | return obj1.ptr === obj2.ptr; 155 | } 156 | Module['compare'] = compare; 157 | 158 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 159 | function getPointer(obj) { 160 | return obj.ptr; 161 | } 162 | Module['getPointer'] = getPointer; 163 | 164 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 165 | function getClass(obj) { 166 | return obj.__class__; 167 | } 168 | Module['getClass'] = getClass; 169 | 170 | // Converts big (string or array) values into a C-style storage, in temporary space 171 | 172 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 173 | var ensureCache = { 174 | buffer: 0, // the main buffer of temporary storage 175 | size: 0, // the size of buffer 176 | pos: 0, // the next free offset in buffer 177 | temps: [], // extra allocations 178 | owned: [], // Owned allocations 179 | needed: 0, // the total size we need next time 180 | 181 | prepare: function() { 182 | if (ensureCache.needed) { 183 | // clear the temps 184 | for (var i = 0; i < ensureCache.temps.length; i++) { 185 | Module['_free'](ensureCache.temps[i]); 186 | } 187 | ensureCache.temps.length = 0; 188 | // prepare to allocate a bigger buffer 189 | Module['_free'](ensureCache.buffer); 190 | ensureCache.buffer = 0; 191 | ensureCache.size += ensureCache.needed; 192 | // clean up 193 | ensureCache.needed = 0; 194 | } 195 | if (!ensureCache.buffer) { // happens first time, or when we need to grow 196 | ensureCache.size += 128; // heuristic, avoid many small grow events 197 | ensureCache.buffer = Module['_malloc'](ensureCache.size); 198 | assert(ensureCache.buffer); 199 | } 200 | ensureCache.pos = 0; 201 | }, 202 | alloc: function(array, view, owner) { 203 | assert(ensureCache.buffer); 204 | var bytes = view.BYTES_PER_ELEMENT; 205 | var len = array.length * bytes; 206 | len = (len + 7) & -8; // keep things aligned to 8 byte boundaries 207 | var ret; 208 | if (owner) { 209 | assert(len > 0); // null terminator, at least 210 | ensureCache.needed += len; 211 | ret = Module['_malloc'](len); 212 | ensureCache.owned.push(ret); 213 | } else { 214 | if (ensureCache.pos + len >= ensureCache.size) { 215 | // we failed to allocate in the buffer, ensureCache time around :( 216 | assert(len > 0); // null terminator, at least 217 | ensureCache.needed += len; 218 | ret = Module['_malloc'](len); 219 | ensureCache.temps.push(ret); 220 | } else { 221 | // we can allocate in the buffer 222 | ret = ensureCache.buffer + ensureCache.pos; 223 | ensureCache.pos += len; 224 | } 225 | } 226 | return ret; 227 | }, 228 | copy: function(array, view, offset) { 229 | offset >>>= 0; 230 | var bytes = view.BYTES_PER_ELEMENT; 231 | switch (bytes) { 232 | case 2: offset >>>= 1; break; 233 | case 4: offset >>>= 2; break; 234 | case 8: offset >>>= 3; break; 235 | } 236 | for (var i = 0; i < array.length; i++) { 237 | view[offset + i] = array[i]; 238 | } 239 | }, 240 | clear: function(clearOwned) { 241 | for (var i = 0; i < ensureCache.temps.length; i++) { 242 | Module['_free'](ensureCache.temps[i]); 243 | } 244 | if (clearOwned) { 245 | for (var i = 0; i < ensureCache.owned.length; i++) { 246 | Module['_free'](ensureCache.owned[i]); 247 | } 248 | } 249 | ensureCache.temps.length = 0; 250 | Module['_free'](ensureCache.buffer); 251 | ensureCache.buffer = 0; 252 | ensureCache.size = 0; 253 | ensureCache.needed = 0; 254 | } 255 | }; 256 | 257 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 258 | function ensureString(value, owner) { 259 | if (typeof value === 'string') { 260 | var intArray = intArrayFromString(value); 261 | var offset = ensureCache.alloc(intArray, HEAP8, owner); 262 | ensureCache.copy(intArray, HEAP8, offset); 263 | return offset; 264 | } 265 | return value; 266 | } 267 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 268 | function ensureInt8(value, owner) { 269 | if (typeof value === 'object') { 270 | var offset = ensureCache.alloc(value, HEAP8, owner); 271 | ensureCache.copy(value, HEAP8, offset); 272 | return offset; 273 | } 274 | return value; 275 | } 276 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 277 | function ensureInt16(value, owner) { 278 | if (typeof value === 'object') { 279 | var offset = ensureCache.alloc(value, HEAP16, owner); 280 | ensureCache.copy(value, HEAP16, offset); 281 | return offset; 282 | } 283 | return value; 284 | } 285 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 286 | function ensureInt32(value, owner) { 287 | if (typeof value === 'object') { 288 | var offset = ensureCache.alloc(value, HEAP32, owner); 289 | ensureCache.copy(value, HEAP32, offset); 290 | return offset; 291 | } 292 | return value; 293 | } 294 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 295 | function ensureFloat32(value, owner) { 296 | if (typeof value === 'object') { 297 | var offset = ensureCache.alloc(value, HEAPF32, owner); 298 | ensureCache.copy(value, HEAPF32, offset); 299 | return offset; 300 | } 301 | return value; 302 | } 303 | /** @suppress {duplicate} (TODO: avoid emitting this multiple times, it is redundant) */ 304 | function ensureFloat64(value, owner) { 305 | if (typeof value === 'object') { 306 | var offset = ensureCache.alloc(value, HEAPF64, owner); 307 | ensureCache.copy(value, HEAPF64, offset); 308 | return offset; 309 | } 310 | return value; 311 | } 312 | 313 | '''] 314 | 315 | mid_c += [''' 316 | // Not using size_t for array indices as the values used by the javascript code are signed. 317 | 318 | EM_JS(void, array_bounds_check_error, (size_t idx, size_t size), { 319 | throw 'Array index ' + idx + ' out of bounds: [0,' + size + ')'; 320 | }); 321 | 322 | void array_bounds_check(const int array_size, const int array_idx) { 323 | if (array_idx < 0 || array_idx >= array_size) { 324 | array_bounds_check_error(array_idx, array_size); 325 | } 326 | } 327 | '''] 328 | 329 | C_FLOATS = ['float', 'double'] 330 | 331 | 332 | def full_typename(arg): 333 | return ('const ' if arg.getExtendedAttribute('Const') else '') + arg.type.name + ('[]' if arg.type.isArray() else '') 334 | 335 | 336 | def type_to_c(t, non_pointing=False): 337 | # print 'to c ', t 338 | def base_type_to_c(t): 339 | if t == 'Long': 340 | ret = 'int' 341 | elif t == 'UnsignedLong': 342 | ret = 'unsigned int' 343 | elif t == 'LongLong': 344 | ret = 'long long' 345 | elif t == 'UnsignedLongLong': 346 | ret = 'unsigned long long' 347 | elif t == 'Short': 348 | ret = 'short' 349 | elif t == 'UnsignedShort': 350 | ret = 'unsigned short' 351 | elif t == 'Byte': 352 | ret = 'char' 353 | elif t == 'Octet': 354 | ret = 'unsigned char' 355 | elif t == 'ByteString': 356 | ret = 'unsigned char*' 357 | elif t == 'Void': 358 | ret = 'void' 359 | elif t == 'String': 360 | ret = 'char*' 361 | elif t == 'Float': 362 | ret = 'float' 363 | elif t == 'Double': 364 | ret = 'double' 365 | elif t == 'Boolean': 366 | ret = 'bool' 367 | elif t == 'Any' or t == 'VoidPtr': 368 | ret = 'void*' 369 | elif t == 'IntPtr': 370 | ret = 'int*' 371 | elif t in interfaces: 372 | ret = (interfaces[t].getExtendedAttribute('Prefix') or [''])[0] + t + ('' if non_pointing else '*') 373 | else: 374 | ret = t 375 | return ret 376 | 377 | t = t.replace(' (Wrapper)', '') 378 | 379 | prefix = '' 380 | suffix = '' 381 | if '[]' in t: 382 | t = t.replace('[]', '') 383 | suffix = '*' 384 | if 'const ' in t: 385 | t = t.replace('const ', '') 386 | prefix = 'const ' 387 | return prefix + base_type_to_c(t) + suffix 388 | 389 | 390 | def take_addr_if_nonpointer(m): 391 | if m.getExtendedAttribute('Ref') or m.getExtendedAttribute('Value'): 392 | return '&' 393 | return '' 394 | 395 | 396 | def deref_if_nonpointer(m): 397 | if m.getExtendedAttribute('Ref') or m.getExtendedAttribute('Value'): 398 | return '*' 399 | return '' 400 | 401 | 402 | def type_to_cdec(raw): 403 | ret = type_to_c(raw.type.name, non_pointing=True) 404 | if raw.getExtendedAttribute('Const'): 405 | ret = 'const ' + ret 406 | if raw.type.name not in interfaces: 407 | return ret 408 | if raw.getExtendedAttribute('Ref'): 409 | return ret + '&' 410 | if raw.getExtendedAttribute('Value'): 411 | return ret 412 | return ret + '*' 413 | 414 | 415 | def render_function(class_name, func_name, sigs, return_type, non_pointer, 416 | copy, operator, constructor, func_scope, 417 | call_content=None, const=False, owned=False, array_attribute=False): 418 | legacy_mode = CHECKS not in ['ALL', 'FAST'] 419 | all_checks = CHECKS == 'ALL' 420 | 421 | bindings_name = class_name + '_' + func_name 422 | min_args = min(sigs.keys()) 423 | max_args = max(sigs.keys()) 424 | 425 | all_args = sigs.get(max_args) 426 | 427 | if DEBUG: 428 | print('renderfunc', class_name, func_name, list(sigs.keys()), return_type, constructor) 429 | for i in range(max_args): 430 | a = all_args[i] 431 | if isinstance(a, WebIDL.IDLArgument): 432 | print(' ', a.identifier.name, a.identifier, a.type, a.optional) 433 | else: 434 | print(" arg%d" % i) 435 | 436 | # JS 437 | 438 | cache = ('getCache(%s)[this.ptr] = this;' % class_name) if constructor else '' 439 | call_prefix = '' if not constructor else 'this.ptr = ' 440 | call_postfix = '' 441 | if return_type != 'Void' and not constructor: 442 | call_prefix = 'return ' 443 | if not constructor: 444 | if return_type in interfaces: 445 | call_prefix += 'wrapPointer(' 446 | call_postfix += ', ' + return_type + ')' 447 | elif return_type == 'String': 448 | call_prefix += 'UTF8ToString(' 449 | call_postfix += ')' 450 | elif return_type == 'Boolean': 451 | call_prefix += '!!(' 452 | call_postfix += ')' 453 | 454 | args = [(all_args[i].identifier.name if isinstance(all_args[i], WebIDL.IDLArgument) else ('arg%d' % i)) for i in range(max_args)] 455 | if not constructor: 456 | body = ' var self = this.ptr;\n' 457 | pre_arg = ['self'] 458 | else: 459 | body = '' 460 | pre_arg = [] 461 | 462 | if any(arg.type.isString() or arg.type.isArray() for arg in all_args): 463 | body += ' ensureCache.prepare();\n' 464 | 465 | full_name = "%s::%s" % (class_name, func_name) 466 | 467 | for i, (js_arg, arg) in enumerate(zip(args, all_args)): 468 | if i >= min_args: 469 | optional = True 470 | else: 471 | optional = False 472 | do_default = False 473 | # Filter out arguments we don't know how to parse. Fast casing only common cases. 474 | compatible_arg = isinstance(arg, Dummy) or (isinstance(arg, WebIDL.IDLArgument) and arg.optional is False) 475 | # note: null has typeof object, but is ok to leave as is, since we are calling into asm code where null|0 = 0 476 | if not legacy_mode and compatible_arg: 477 | if isinstance(arg, WebIDL.IDLArgument): 478 | arg_name = arg.identifier.name 479 | else: 480 | arg_name = '' 481 | # Format assert fail message 482 | check_msg = "[CHECK FAILED] %s(%s:%s): " % (full_name, js_arg, arg_name) 483 | if isinstance(arg.type, WebIDL.IDLWrapperType): 484 | inner = arg.type.inner 485 | else: 486 | inner = "" 487 | 488 | # Print type info in comments. 489 | body += " /* %s <%s> [%s] */\n" % (js_arg, arg.type.name, inner) 490 | 491 | # Wrap asserts with existence check when argument is optional. 492 | if all_checks and optional: 493 | body += "if(typeof {0} !== 'undefined' && {0} !== null) {{\n".format(js_arg) 494 | # Special case argument types. 495 | if arg.type.isNumeric(): 496 | if arg.type.isInteger(): 497 | if all_checks: 498 | body += " assert(typeof {0} === 'number' && !isNaN({0}), '{1}Expecting ');\n".format(js_arg, check_msg) 499 | else: 500 | if all_checks: 501 | body += " assert(typeof {0} === 'number', '{1}Expecting ');\n".format(js_arg, check_msg) 502 | # No transform needed for numbers 503 | elif arg.type.isBoolean(): 504 | if all_checks: 505 | body += " assert(typeof {0} === 'boolean' || (typeof {0} === 'number' && !isNaN({0})), '{1}Expecting ');\n".format(js_arg, check_msg) 506 | # No transform needed for booleans 507 | elif arg.type.isString(): 508 | # Strings can be DOM strings or pointers. 509 | if all_checks: 510 | body += " assert(typeof {0} === 'string' || ({0} && typeof {0} === 'object' && typeof {0}.ptr === 'number'), '{1}Expecting ');\n".format(js_arg, check_msg) 511 | do_default = True # legacy path is fast enough for strings. 512 | elif arg.type.isInterface(): 513 | if all_checks: 514 | body += " assert(typeof {0} === 'object' && typeof {0}.ptr === 'number', '{1}Expecting ');\n".format(js_arg, check_msg) 515 | if optional: 516 | body += " if(typeof {0} !== 'undefined' && {0} !== null) {{ {0} = {0}.ptr }};\n".format(js_arg) 517 | else: 518 | # No checks in fast mode when the arg is required 519 | body += " {0} = {0}.ptr;\n".format(js_arg) 520 | else: 521 | do_default = True 522 | 523 | if all_checks and optional: 524 | body += "}\n" 525 | else: 526 | do_default = True 527 | 528 | if do_default: 529 | if not (arg.type.isArray() and not array_attribute): 530 | body += " if ({0} && typeof {0} === 'object') {0} = {0}.ptr;\n".format(js_arg) 531 | if arg.type.isString(): 532 | body += " else {0} = ensureString({0}, {1});\n".format(js_arg, "true" if (owned) else "false") 533 | else: 534 | # an array can be received here 535 | arg_type = arg.type.name 536 | if arg_type in ['Byte', 'Octet']: 537 | body += " if (typeof {0} == 'object') {{ {0} = ensureInt8({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 538 | elif arg_type in ['Short', 'UnsignedShort']: 539 | body += " if (typeof {0} == 'object') {{ {0} = ensureInt16({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 540 | elif arg_type in ['Long', 'UnsignedLong']: 541 | body += " if (typeof {0} == 'object') {{ {0} = ensureInt32({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 542 | elif arg_type == 'Float': 543 | body += " if (typeof {0} == 'object') {{ {0} = ensureFloat32({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 544 | elif arg_type == 'Double': 545 | body += " if (typeof {0} == 'object') {{ {0} = ensureFloat64({0}, {1}); }}\n".format(js_arg, "true" if (owned) else "false") 546 | 547 | c_names = {} 548 | for i in range(min_args, max_args): 549 | c_names[i] = 'emscripten_bind_%s_%d' % (bindings_name, i) 550 | body += ' if (%s === undefined) { %s%s(%s)%s%s }\n' % (args[i], call_prefix, '_' + c_names[i], ', '.join(pre_arg + args[:i]), call_postfix, '' if 'return ' in call_prefix else '; ' + (cache or ' ') + 'return') 551 | c_names[max_args] = 'emscripten_bind_%s_%d' % (bindings_name, max_args) 552 | body += ' %s%s(%s)%s;\n' % (call_prefix, '_' + c_names[max_args], ', '.join(pre_arg + args), call_postfix) 553 | if cache: 554 | body += ' ' + cache + '\n' 555 | mid_js.append(r'''%sfunction%s(%s) { 556 | %s 557 | };''' % (CONSTRUCTOR_CLOSURE_SUPPRESSIONS, (' ' + func_name) if constructor else '', ', '.join(args), body[:-1])) 558 | 559 | # C 560 | 561 | for i in range(min_args, max_args + 1): 562 | raw = sigs.get(i) 563 | if raw is None: 564 | continue 565 | sig = list(map(full_typename, raw)) 566 | if array_attribute: 567 | sig = [x.replace('[]', '') for x in sig] # for arrays, ignore that this is an array - our get/set methods operate on the elements 568 | 569 | c_arg_types = list(map(type_to_c, sig)) 570 | 571 | normal_args = ', '.join(['%s %s' % (c_arg_types[j], args[j]) for j in range(i)]) 572 | if constructor: 573 | full_args = normal_args 574 | else: 575 | full_args = type_to_c(class_name, non_pointing=True) + '* self' + ('' if not normal_args else ', ' + normal_args) 576 | call_args = ', '.join(['%s%s' % ('*' if raw[j].getExtendedAttribute('Ref') else '', args[j]) for j in range(i)]) 577 | if constructor: 578 | call = 'new ' + type_to_c(class_name, non_pointing=True) 579 | call += '(' + call_args + ')' 580 | elif call_content is not None: 581 | call = call_content 582 | else: 583 | call = 'self->' + func_name 584 | call += '(' + call_args + ')' 585 | 586 | if operator: 587 | cast_self = 'self' 588 | if class_name != func_scope: 589 | # this function comes from an ancestor class; for operators, we must cast it 590 | cast_self = 'dynamic_cast<' + type_to_c(func_scope) + '>(' + cast_self + ')' 591 | maybe_deref = deref_if_nonpointer(raw[0]) 592 | if '=' in operator: 593 | call = '(*%s %s %s%s)' % (cast_self, operator, maybe_deref, args[0]) 594 | elif operator == '[]': 595 | call = '((*%s)[%s%s])' % (cast_self, maybe_deref, args[0]) 596 | else: 597 | raise Exception('unfamiliar operator ' + operator) 598 | 599 | pre = '' 600 | 601 | basic_return = 'return ' if constructor or return_type != 'Void' else '' 602 | return_prefix = basic_return 603 | return_postfix = '' 604 | if non_pointer: 605 | return_prefix += '&' 606 | if copy: 607 | pre += ' static %s temp;\n' % type_to_c(return_type, non_pointing=True) 608 | return_prefix += '(temp = ' 609 | return_postfix += ', &temp)' 610 | 611 | c_return_type = type_to_c(return_type) 612 | maybe_const = 'const ' if const else '' 613 | mid_c.append(r''' 614 | %s%s EMSCRIPTEN_KEEPALIVE %s(%s) { 615 | %s %s%s%s; 616 | } 617 | ''' % (maybe_const, type_to_c(class_name) if constructor else c_return_type, c_names[i], full_args, pre, return_prefix, call, return_postfix)) 618 | 619 | if not constructor: 620 | if i == max_args: 621 | dec_args = ', '.join([type_to_cdec(raw[j]) + ' ' + args[j] for j in range(i)]) 622 | js_call_args = ', '.join(['%s%s' % (('(ptrdiff_t)' if sig[j] in interfaces else '') + take_addr_if_nonpointer(raw[j]), args[j]) for j in range(i)]) 623 | 624 | js_impl_methods.append(r''' %s %s(%s) %s { 625 | %sEM_ASM_%s({ 626 | var self = Module['getCache'](Module['%s'])[$0]; 627 | if (!self.hasOwnProperty('%s')) throw 'a JSImplementation must implement all functions, you forgot %s::%s.'; 628 | %sself['%s'](%s)%s; 629 | }, (ptrdiff_t)this%s); 630 | }''' % (c_return_type, func_name, dec_args, maybe_const, 631 | basic_return, 'INT' if c_return_type not in C_FLOATS else 'DOUBLE', 632 | class_name, 633 | func_name, class_name, func_name, 634 | return_prefix, 635 | func_name, 636 | ','.join(['$%d' % i for i in range(1, max_args + 1)]), 637 | return_postfix, 638 | (', ' if js_call_args else '') + js_call_args)) 639 | 640 | 641 | for name, interface in interfaces.items(): 642 | js_impl = interface.getExtendedAttribute('JSImplementation') 643 | if not js_impl: 644 | continue 645 | implements[name] = [js_impl[0]] 646 | 647 | # Compute the height in the inheritance tree of each node. Note that the order of interation 648 | # of `implements` is irrelevant. 649 | # 650 | # After one iteration of the loop, all ancestors of child are guaranteed to have a a larger 651 | # height number than the child, and this is recursively true for each ancestor. If the height 652 | # of child is later increased, all its ancestors will be readjusted at that time to maintain 653 | # that invariant. Further, the height of a node never decreases. Therefore, when the loop 654 | # finishes, all ancestors of a given node should have a larger height number than that node. 655 | nodeHeight = {} 656 | for child, parent in implements.items(): 657 | parent = parent[0] 658 | while parent: 659 | nodeHeight[parent] = max(nodeHeight.get(parent, 0), nodeHeight.get(child, 0) + 1) 660 | grandParent = implements.get(parent) 661 | if grandParent: 662 | child = parent 663 | parent = grandParent[0] 664 | else: 665 | parent = None 666 | 667 | names = sorted(interfaces.keys(), key=lambda x: nodeHeight.get(x, 0), reverse=True) 668 | 669 | for name in names: 670 | interface = interfaces[name] 671 | 672 | mid_js += ['\n// ' + name + '\n'] 673 | mid_c += ['\n// ' + name + '\n'] 674 | 675 | js_impl_methods = [] 676 | 677 | cons = interface.getExtendedAttribute('Constructor') 678 | if type(cons) == list: 679 | raise Exception('do not use "Constructor", instead create methods with the name of the interface') 680 | 681 | js_impl = interface.getExtendedAttribute('JSImplementation') 682 | if js_impl: 683 | js_impl = js_impl[0] 684 | 685 | # Methods 686 | 687 | # Ensure a constructor even if one is not specified. 688 | if not any(m.identifier.name == name for m in interface.members): 689 | mid_js += ['%sfunction %s() { throw "cannot construct a %s, no constructor in IDL" }\n' % (CONSTRUCTOR_CLOSURE_SUPPRESSIONS, name, name)] 690 | mid_js += build_constructor(name) 691 | 692 | for m in interface.members: 693 | if not m.isMethod(): 694 | continue 695 | constructor = m.identifier.name == name 696 | if not constructor: 697 | parent_constructor = False 698 | temp = m.parentScope 699 | while temp.parentScope: 700 | if temp.identifier.name == m.identifier.name: 701 | parent_constructor = True 702 | temp = temp.parentScope 703 | if parent_constructor: 704 | continue 705 | if not constructor: 706 | mid_js += [r''' 707 | %s.prototype['%s'] = %s.prototype.%s = ''' % (name, m.identifier.name, name, m.identifier.name)] 708 | sigs = {} 709 | return_type = None 710 | for ret, args in m.signatures(): 711 | if return_type is None: 712 | return_type = ret.name 713 | else: 714 | assert return_type == ret.name, 'overloads must have the same return type' 715 | for i in range(len(args) + 1): 716 | if i == len(args) or args[i].optional: 717 | assert i not in sigs, 'overloading must differentiate by # of arguments (cannot have two signatures that differ by types but not by length)' 718 | sigs[i] = args[:i] 719 | render_function(name, 720 | m.identifier.name, sigs, return_type, 721 | m.getExtendedAttribute('Ref'), 722 | m.getExtendedAttribute('Value'), 723 | (m.getExtendedAttribute('Operator') or [None])[0], 724 | constructor, 725 | func_scope=m.parentScope.identifier.name, 726 | const=m.getExtendedAttribute('Const')) 727 | mid_js += [';\n'] 728 | if constructor: 729 | mid_js += build_constructor(name) 730 | 731 | for m in interface.members: 732 | if not m.isAttr(): 733 | continue 734 | attr = m.identifier.name 735 | 736 | if m.type.isArray(): 737 | get_sigs = {1: [Dummy({'type': WebIDL.BuiltinTypes[WebIDL.IDLBuiltinType.Types.long]})]} 738 | set_sigs = {2: [Dummy({'type': WebIDL.BuiltinTypes[WebIDL.IDLBuiltinType.Types.long]}), 739 | Dummy({'type': m.type})]} 740 | get_call_content = take_addr_if_nonpointer(m) + 'self->' + attr + '[arg0]' 741 | set_call_content = 'self->' + attr + '[arg0] = ' + deref_if_nonpointer(m) + 'arg1' 742 | if m.getExtendedAttribute('BoundsChecked'): 743 | bounds_check = "array_bounds_check(sizeof(self->%s) / sizeof(self->%s[0]), arg0)" % (attr, attr) 744 | get_call_content = "(%s, %s)" % (bounds_check, get_call_content) 745 | set_call_content = "(%s, %s)" % (bounds_check, set_call_content) 746 | else: 747 | get_sigs = {0: []} 748 | set_sigs = {1: [Dummy({'type': m.type})]} 749 | get_call_content = take_addr_if_nonpointer(m) + 'self->' + attr 750 | set_call_content = 'self->' + attr + ' = ' + deref_if_nonpointer(m) + 'arg0' 751 | 752 | get_name = 'get_' + attr 753 | mid_js += [r''' 754 | %s.prototype['%s'] = %s.prototype.%s = ''' % (name, get_name, name, get_name)] 755 | render_function(name, 756 | get_name, get_sigs, m.type.name, 757 | None, 758 | None, 759 | None, 760 | False, 761 | func_scope=interface, 762 | call_content=get_call_content, 763 | const=m.getExtendedAttribute('Const'), 764 | owned=m.getExtendedAttribute('Owner'), 765 | array_attribute=m.type.isArray()) 766 | 767 | if m.readonly: 768 | mid_js += [r''' 769 | Object.defineProperty(%s.prototype, '%s', { get: %s.prototype.%s });''' % (name, attr, name, get_name)] 770 | else: 771 | set_name = 'set_' + attr 772 | mid_js += [r''' 773 | %s.prototype['%s'] = %s.prototype.%s = ''' % (name, set_name, name, set_name)] 774 | render_function(name, 775 | set_name, set_sigs, 'Void', 776 | None, 777 | None, 778 | None, 779 | False, 780 | func_scope=interface, 781 | call_content=set_call_content, 782 | const=m.getExtendedAttribute('Const'), 783 | owned=m.getExtendedAttribute('Owner'), 784 | array_attribute=m.type.isArray()) 785 | mid_js += [r''' 786 | Object.defineProperty(%s.prototype, '%s', { get: %s.prototype.%s, set: %s.prototype.%s });''' % (name, attr, name, get_name, name, set_name)] 787 | 788 | if not interface.getExtendedAttribute('NoDelete'): 789 | mid_js += [r''' 790 | %s.prototype['__destroy__'] = %s.prototype.__destroy__ = ''' % (name, name)] 791 | render_function(name, 792 | '__destroy__', {0: []}, 'Void', 793 | None, 794 | None, 795 | None, 796 | False, 797 | func_scope=interface, 798 | call_content='delete self') 799 | 800 | # Emit C++ class implementation that calls into JS implementation 801 | 802 | if js_impl: 803 | pre_c += [r''' 804 | class %s : public %s { 805 | public: 806 | %s 807 | }; 808 | ''' % (name, type_to_c(js_impl, non_pointing=True), '\n'.join(js_impl_methods))] 809 | 810 | deferred_js = [] 811 | 812 | for name, enum in enums.items(): 813 | mid_c += ['\n// ' + name + '\n'] 814 | deferred_js += ['\n', '// ' + name + '\n'] 815 | for value in enum.values(): 816 | function_id = "%s_%s" % (name, value.split('::')[-1]) 817 | function_id = 'emscripten_enum_%s' % function_id 818 | mid_c += [r'''%s EMSCRIPTEN_KEEPALIVE %s() { 819 | return %s; 820 | } 821 | ''' % (name, function_id, value)] 822 | symbols = value.split('::') 823 | if len(symbols) == 1: 824 | identifier = symbols[0] 825 | deferred_js += ["Module['%s'] = _%s();\n" % (identifier, function_id)] 826 | elif len(symbols) == 2: 827 | [namespace, identifier] = symbols 828 | if namespace in interfaces: 829 | # namespace is a class 830 | deferred_js += ["Module['%s']['%s'] = _%s();\n" % (namespace, identifier, function_id)] 831 | else: 832 | # namespace is a namespace, so the enums get collapsed into the top level namespace. 833 | deferred_js += ["Module['%s'] = _%s();\n" % (identifier, function_id)] 834 | else: 835 | raise Exception("Illegal enum value %s" % value) 836 | 837 | mid_c += ['\n}\n\n'] 838 | if len(deferred_js): 839 | mid_js += [''' 840 | (function() { 841 | function setupEnums() { 842 | %s 843 | } 844 | if (runtimeInitialized) setupEnums(); 845 | else addOnInit(setupEnums); 846 | })(); 847 | ''' % '\n '.join(deferred_js)] 848 | 849 | # Write 850 | 851 | with open(output_base + '.cpp', 'w') as c: 852 | for x in pre_c: 853 | c.write(x) 854 | for x in mid_c: 855 | c.write(x) 856 | 857 | with open(output_base + '.js', 'w') as js: 858 | for x in mid_js: 859 | js.write(x) 860 | -------------------------------------------------------------------------------- /functions.mk: -------------------------------------------------------------------------------- 1 | # For inclusion in toplevel Makefile 2 | # Defines some useful macros and variables for building etc 3 | # If arguments are expected (macro) it needs to be invoked with $(call ...), 4 | # if no arguments are supported the definition is aregular avariable and can be used as such. 5 | # Special macros of the name TR_... create targets (and always take arguments) 6 | # and thus also need to be $(eval ...)'ed 7 | 8 | ## Build stuff 9 | 10 | # @arg1: name of submodule 11 | define PREPARE_SRC_PATCHED 12 | rm -rf build/lib/$(1) 13 | mkdir -p build/lib 14 | cp -r lib/$(1) build/lib/$(1) 15 | $(foreach file, $(wildcard $(BASE_DIR)build/patches/$(1)/*.patch), \ 16 | patch -d "$(BASE_DIR)build/lib/$(1)" -Np1 -i $(file) && \ 17 | ) : 18 | endef 19 | 20 | # @arg1: name of submodule 21 | define PREPARE_SRC_VPATH 22 | rm -rf build/lib/$(1) 23 | mkdir -p build/lib/$(1) 24 | endef 25 | 26 | # All projects we build have autogen.sh, otherwise we could also fallback to `autoreconf -ivf .` 27 | RECONF_AUTO := NOCONFIGURE=1 ./autogen.sh 28 | 29 | # @arg1: path to source directory; defaults to current working directory 30 | define CONFIGURE_AUTO 31 | emconfigure $(or $(1),.)/configure \ 32 | --prefix="$(DIST_DIR)" \ 33 | --host=x86-none-linux \ 34 | --build=x86_64 \ 35 | --enable-static \ 36 | --disable-shared 37 | endef 38 | 39 | # @arg1: path to source directory; defaults to current working directory 40 | define CONFIGURE_CMAKE 41 | emcmake cmake -S "$(or $(1),.)" -DCMAKE_INSTALL_PREFIX="$(DIST_DIR)" 42 | endef 43 | 44 | # FIXME: Propagate jobserver info with $(MAKE) and set up our makefile for fully parallel builds 45 | JSO_MAKE := emmake make -j "$(shell nproc)" 46 | 47 | ## Clean and git related 48 | 49 | # @arg1: submodule name 50 | define TR_GIT_SM_RESET 51 | git-$(1): 52 | cd lib/$(1) && \ 53 | git reset --hard && \ 54 | git clean -dfx 55 | git submodule update --force lib/$(1) 56 | 57 | .PHONY: git-$(1) 58 | endef 59 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libass-wasm", 3 | "version": "4.2.3", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jellyfin/libass-wasm", 3 | "version": "4.2.3", 4 | "description": "libass Subtitle Renderer and Parser library for browsers", 5 | "main": "dist/js/subtitles-octopus.js", 6 | "files": [ 7 | "dist/js/default.woff2", 8 | "dist/js/subtitles*", 9 | "dist/js/COPYRIGHT" 10 | ], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "prepare": "if test 0$SKIP_PREPARE -eq 0; then ./run-docker-build.sh; fi" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git" 18 | }, 19 | "keywords": [ 20 | "libass", 21 | "subtitle", 22 | "wasm", 23 | "emscripten" 24 | ], 25 | "author": "SubtitlesOctopus contributors", 26 | "license": "LGPL-2.1-or-later AND (FTL OR GPL-2.0-or-later) AND MIT AND MIT-Modern-Variant AND ISC AND NTP AND Zlib AND BSL-1.0", 27 | "bugs": { 28 | "url": "https://github.com/jellyfin/JavascriptSubtitlesOctopus/issues" 29 | }, 30 | "homepage": "https://jellyfin.github.io/JavascriptSubtitlesOctopus/" 31 | } 32 | -------------------------------------------------------------------------------- /run-buildah-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -e 3 | cd "$(dirname "$0")" 4 | 5 | . ./run-common.sh 6 | 7 | if [ "$#" -eq 0 ] ; then 8 | cmd="make" 9 | else 10 | cmd="" 11 | fi 12 | 13 | if [ "$FAST" -eq 0 ] ; then 14 | buildah bud -t "$IMAGE" . 15 | buildah rm "$CONTAINER" >/dev/null 2>&1 || : 16 | buildah from --name "$CONTAINER" "$IMAGE":latest 17 | fi 18 | buildah run -t --env LC_ALL=C.UTF-8 -v "${PWD}":/code "$CONTAINER" $cmd "$@" 19 | -------------------------------------------------------------------------------- /run-common.sh: -------------------------------------------------------------------------------- 1 | usage() { 2 | echo "$0 [-f] [-c con_name] [-i img_name] [commmand [command_args...]]" 3 | echo " -f: Skip building the container and reuse existing (\"fast\")" 4 | echo " -c: Name of the container to create/use;" 5 | echo " defaults to libass_javascriptsubtitlesoctopus-build" 6 | echo " -i: Name of the image to buld/use;" 7 | echo " defaults to libass/javascriptsubtitlesoctopus-build" 8 | echo "If no command is given `make` without arguments will be executed" 9 | exit 2 10 | } 11 | 12 | OPTIND=1 13 | CONTAINER="libass_javascriptsubtitlesoctopus-build" 14 | IMAGE="libass/javascriptsubtitlesoctopus-build" 15 | FAST=0 16 | while getopts "fc:s:" opt ; do 17 | case "$opt" in 18 | f) FAST=1 ;; 19 | c) CONTAINER="$OPTARG" ;; 20 | i) IMAGE="$OPTARG" ;; 21 | *) usage ;; 22 | esac 23 | done 24 | 25 | if [ "$OPTIND" -gt 1 ] ; then 26 | shift $(( OPTIND - 1 )) 27 | fi 28 | -------------------------------------------------------------------------------- /run-docker-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -e 3 | cd "$(dirname "$0")" 4 | 5 | . ./run-common.sh 6 | 7 | if [ "$FAST" -eq 0 ] ; then 8 | docker build -t "$IMAGE" . 9 | fi 10 | if [ "$#" -eq 0 ] ; then 11 | docker run -it --rm --env LC_ALL=C.UTF-8 -v "${PWD}":/code --name "$CONTAINER" "$IMAGE":latest 12 | else 13 | docker run -it --rm --env LC_ALL=C.UTF-8 -v "${PWD}":/code --name "$CONTAINER" "$IMAGE":latest "$@" 14 | fi 15 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # If needed, override the following variables via `make VARNAME=VAL ...` 2 | # Note: we only compile and not(!) link here, thus -c 3 | CXX = em++ -c 4 | CXXFLAGS = -O3 -g4 5 | LIBASS_CXXFLAGS = $(shell pkg-config --static --cflags "libass >= 0.14.0") 6 | LOCAL_CXXFLAGS = -Wall -Wno-deprecated $(LIBASS_CXXFLAGS) 7 | 8 | all: subtitles-octopus-worker.bc 9 | 10 | SubOctpInterface.cpp: SubtitleOctopus.idl ../build/webidl_binder.py 11 | @# Our version of WebIDL contains JSO-specific patches 12 | python3 ../build/webidl_binder.py SubtitleOctopus.idl SubOctpInterface 13 | 14 | subtitles-octopus-worker.bc: SubtitleOctopus.cpp libass.cpp SubOctpInterface.cpp 15 | @# All later dependencies are already included into the first by CPP 16 | $(CXX) $(LOCAL_CXXFLAGS) $(CXXFLAGS) -o $@ $< 17 | 18 | clean: 19 | rm -f subtitles-octopus-worker.bc 20 | rm -f SubOctpInterface.cpp WebIDLGrammar.pkl parser.out 21 | 22 | .PHONY: all clean 23 | -------------------------------------------------------------------------------- /src/SubtitleOctopus.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SubtitleOctopus.js 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "libass.cpp" 13 | 14 | #ifdef __EMSCRIPTEN__ 15 | #include 16 | #else 17 | // make IDE happy 18 | #define emscripten_get_now() 0.0 19 | #endif 20 | 21 | int log_level = 3; 22 | 23 | class ReusableBuffer { 24 | public: 25 | ReusableBuffer(): buffer(NULL), size(0), lessen_counter(0) {} 26 | 27 | ~ReusableBuffer() { 28 | free(buffer); 29 | } 30 | 31 | void clear() { 32 | free(buffer); 33 | buffer = NULL; 34 | size = 0; 35 | lessen_counter = 0; 36 | } 37 | 38 | void *take(size_t new_size, bool keep_content) { 39 | if (size >= new_size) { 40 | if (size >= 1.3 * new_size) { 41 | // big reduction request 42 | lessen_counter++; 43 | } else { 44 | lessen_counter = 0; 45 | } 46 | if (lessen_counter < 10) { 47 | // not reducing the buffer yet 48 | return buffer; 49 | } 50 | } 51 | 52 | void *newbuf; 53 | if (keep_content) { 54 | newbuf = realloc(buffer, new_size); 55 | } else { 56 | newbuf = malloc(new_size); 57 | } 58 | if (!newbuf) return NULL; 59 | 60 | if (!keep_content) free(buffer); 61 | buffer = newbuf; 62 | size = new_size; 63 | lessen_counter = 0; 64 | return buffer; 65 | } 66 | 67 | size_t capacity() const { 68 | return size; 69 | } 70 | 71 | private: 72 | void *buffer; 73 | size_t size; 74 | size_t lessen_counter; 75 | }; 76 | 77 | void msg_callback(int level, const char *fmt, va_list va, void *data) { 78 | if (level > log_level) // 6 for verbose 79 | return; 80 | 81 | const int ERR_LEVEL = 1; 82 | FILE* stream = level <= ERR_LEVEL ? stderr : stdout; 83 | 84 | fprintf(stream, "libass: "); 85 | vfprintf(stream, fmt, va); 86 | fprintf(stream, "\n"); 87 | } 88 | 89 | const float MIN_UINT8_CAST = 0.9 / 255; 90 | const float MAX_UINT8_CAST = 255.9 / 255; 91 | 92 | #define CLAMP_UINT8(value) ((value > MIN_UINT8_CAST) ? ((value < MAX_UINT8_CAST) ? (int)(value * 255) : 255) : 0) 93 | 94 | struct RenderBlendPart { 95 | int dest_x, dest_y, dest_width, dest_height; 96 | unsigned char *image; 97 | RenderBlendPart *next; 98 | }; 99 | 100 | struct RenderBlendResult { 101 | int changed; 102 | double blend_time; 103 | RenderBlendPart *part; 104 | }; 105 | 106 | // maximum regions - a grid of 3x3 107 | #define MAX_BLEND_STORAGES (3 * 3) 108 | struct RenderBlendStorage { 109 | RenderBlendPart part; 110 | ReusableBuffer buf; 111 | bool taken; 112 | }; 113 | 114 | struct EventStopTimesResult { 115 | double eventFinish, emptyFinish; 116 | int is_animated; 117 | }; 118 | 119 | #define MIN(x, y) (((x) < (y)) ? (x) : (y)) 120 | #define MAX(x, y) (((x) > (y)) ? (x) : (y)) 121 | 122 | class BoundingBox { 123 | public: 124 | int min_x, max_x, min_y, max_y; 125 | 126 | BoundingBox(): min_x(-1), max_x(-1), min_y(-1), max_y(-1) {} 127 | 128 | bool empty() const { 129 | return min_x == -1; 130 | } 131 | 132 | void add(int x1, int y1, int w, int h) { 133 | int x2 = x1 + w - 1, y2 = y1 + h - 1; 134 | min_x = (min_x < 0) ? x1 : MIN(min_x, x1); 135 | min_y = (min_y < 0) ? y1 : MIN(min_y, y1); 136 | max_x = (max_x < 0) ? x2 : MAX(max_x, x2); 137 | max_y = (max_y < 0) ? y2 : MAX(max_y, y2); 138 | } 139 | 140 | bool intersets(const BoundingBox& other) const { 141 | return !(other.min_x > max_x || 142 | other.max_x < min_x || 143 | other.min_y > max_y || 144 | other.max_y < min_y); 145 | } 146 | 147 | bool tryMerge(BoundingBox& other) { 148 | if (!intersets(other)) return false; 149 | 150 | min_x = MIN(min_x, other.min_x); 151 | min_y = MIN(min_y, other.min_y); 152 | max_x = MAX(max_x, other.max_x); 153 | max_y = MAX(max_y, other.max_y); 154 | return true; 155 | } 156 | 157 | void clear() { 158 | min_x = max_x = min_y = max_y = -1; 159 | } 160 | }; 161 | 162 | /** 163 | * \brief Overwrite tag with whitespace to nullify its effect 164 | * Boundaries are inclusive at both ends. 165 | */ 166 | static void _remove_tag(char *begin, char *end) { 167 | if (end < begin) 168 | return; 169 | memset(begin, ' ', end - begin + 1); 170 | } 171 | 172 | /** 173 | * \param begin point to the first character of the tag name (after backslash) 174 | * \param end last character that can be read; at least the name itself 175 | and the following character if any must be included 176 | * \return true if tag may cause animations, false if it will definitely not 177 | */ 178 | static bool _is_animated_tag(char *begin, char *end) { 179 | if (end <= begin) 180 | return false; 181 | 182 | size_t length = end - begin + 1; 183 | 184 | #define check_simple_tag(tag) (sizeof(tag)-1 < length && !strncmp(begin, tag, sizeof(tag)-1)) 185 | #define check_complex_tag(tag) (check_simple_tag(tag) && (begin[sizeof(tag)-1] == '(' \ 186 | || begin[sizeof(tag)-1] == ' ' || begin[sizeof(tag)-1] == '\t')) 187 | switch (begin[0]) { 188 | case 'k': //-fallthrough 189 | case 'K': 190 | // Karaoke: k, kf, ko, K and kt ; no other valid ASS-tag starts with k/K 191 | return true; 192 | case 't': 193 | // Animated transform: no other valid tag begins with t 194 | // non-nested t-tags have to be complex tags even in single argument 195 | // form, but nested t-tags (which act like independent t-tags) are allowed to be 196 | // simple-tags without parentheses due to VSF-parsing quirk. 197 | // Since all valid simple t-tags require the existence of a complex t-tag, we only check for complex tags 198 | // to avoid false positives from invalid simple t-tags. This makes animation-dropping somewhat incorrect 199 | // but as animation detection remains accurate, we consider this to be "good enough" 200 | return check_complex_tag("t"); 201 | case 'm': 202 | // Movement: complex tag; again no other valid tag begins with m 203 | // but ensure it's complex just to be sure 204 | return check_complex_tag("move"); 205 | case 'f': 206 | // Fade: \fad and Fade (complex): \fade; both complex 207 | // there are several other valid tags beginning with f 208 | return check_complex_tag("fad") || check_complex_tag("fade"); 209 | } 210 | 211 | return false; 212 | #undef check_complex_tag 213 | #undef check_simple_tag 214 | } 215 | 216 | /** 217 | * \param start First character after { (optionally spaces can be dropped) 218 | * \param end Last character before } (optionally spaces can be dropped) 219 | * \param drop_animations If true animation tags will be discarded 220 | * \return true if after processing the event may contain animations 221 | (i.e. when dropping animations this is always false) 222 | */ 223 | static bool _is_block_animated(char *start, char *end, bool drop_animations) 224 | { 225 | char *tag_start = NULL; // points to beginning backslash 226 | for (char *p = start; p <= end; p++) { 227 | if (*p == '\\') { 228 | // It is safe to go one before and beyond unconditionally 229 | // because the text passed in must be surronded by { } 230 | if (tag_start && _is_animated_tag(tag_start + 1, p - 1)) { 231 | if (!drop_animations) 232 | return true; 233 | // For \t transforms this will assume the final state 234 | _remove_tag(tag_start, p - 1); 235 | } 236 | tag_start = p; 237 | } 238 | } 239 | 240 | if (tag_start && _is_animated_tag(tag_start + 1, end)) { 241 | if (!drop_animations) 242 | return true; 243 | _remove_tag(tag_start, end); 244 | } 245 | 246 | return false; 247 | } 248 | 249 | /** 250 | * \param event ASS event to be processed 251 | * \param drop_animations If true animation tags will be discarded 252 | * \return true if after processing the event may contain animations 253 | (i.e. when dropping animations this is always false) 254 | */ 255 | static bool _is_event_animated(ASS_Event *event, bool drop_animations) { 256 | // Event is animated if it has an Effect or animated override tags 257 | if (event->Effect && event->Effect[0] != '\0') { 258 | if (!drop_animations) return 1; 259 | event->Effect[0] = '\0'; 260 | } 261 | 262 | // Search for override blocks 263 | // Only closed {...}-blocks are parsed by VSFilters and libass 264 | char *block_start = NULL; // points to opening { 265 | for (char *p = event->Text; *p != '\0'; p++) { 266 | switch (*p) { 267 | case '{': 268 | // Escaping the opening curly bracket to not start an override block is 269 | // a VSFilter-incompatible libass extension. But we only use libass, so... 270 | if (!block_start && (p == event->Text || *(p-1) != '\\')) 271 | block_start = p; 272 | break; 273 | case '}': 274 | if (block_start && p - block_start > 2 275 | && _is_block_animated(block_start + 1, p - 1, drop_animations)) 276 | return true; 277 | block_start = NULL; 278 | break; 279 | default: 280 | break; 281 | } 282 | } 283 | 284 | return false; 285 | } 286 | 287 | /** 288 | * \brief Convert time in seconds to time in milliseconds 289 | * \param seconds Time in seconds 290 | * \return Time in milliseconds (rounded) 291 | */ 292 | static long long _convert_time_to_ms(double seconds) 293 | { 294 | return (long long)(seconds * 1e+3 + 0.5); 295 | } 296 | 297 | /** 298 | * \brief Convert time in milliseconds to time in seconds 299 | * \param ms Time in milliseconds 300 | * \return Time in seconds 301 | */ 302 | static double _convert_time_to_seconds(long long ms) 303 | { 304 | return ms * 1e-3; 305 | } 306 | 307 | class SubtitleOctopus { 308 | public: 309 | ASS_Library* ass_library; 310 | ASS_Renderer* ass_renderer; 311 | ASS_Track* track; 312 | 313 | int canvas_w; 314 | int canvas_h; 315 | 316 | int status; 317 | 318 | std::string defaultFont; 319 | 320 | SubtitleOctopus(): ass_library(NULL), ass_renderer(NULL), track(NULL), canvas_w(0), canvas_h(0), status(0), m_is_event_animated(NULL), m_drop_animations(false) { 321 | } 322 | 323 | void setLogLevel(int level) { 324 | log_level = level; 325 | } 326 | 327 | void setDropAnimations(int value) { 328 | bool rescan = m_drop_animations != bool(value) && track != NULL; 329 | m_drop_animations = bool(value); 330 | if (rescan) rescanAllAnimations(); 331 | } 332 | 333 | int getDropAnimations() const { 334 | return m_drop_animations; 335 | } 336 | 337 | void initLibrary(int frame_w, int frame_h, char* default_font) { 338 | if (default_font != NULL) { 339 | defaultFont.assign(default_font); 340 | } 341 | 342 | ass_library = ass_library_init(); 343 | if (!ass_library) { 344 | fprintf(stderr, "jso: ass_library_init failed!\n"); 345 | exit(2); 346 | } 347 | 348 | ass_set_message_cb(ass_library, msg_callback, NULL); 349 | ass_set_extract_fonts(ass_library, 1); 350 | 351 | ass_renderer = ass_renderer_init(ass_library); 352 | if (!ass_renderer) { 353 | fprintf(stderr, "jso: ass_renderer_init failed!\n"); 354 | exit(3); 355 | } 356 | 357 | resizeCanvas(frame_w, frame_h); 358 | 359 | reloadFonts(); 360 | m_blend.clear(); 361 | m_is_event_animated = NULL; 362 | } 363 | 364 | /* TRACK */ 365 | void createTrack(char* subfile) { 366 | reloadLibrary(); 367 | track = ass_read_file(ass_library, subfile, NULL); 368 | if (!track) { 369 | fprintf(stderr, "jso: Failed to start a track\n"); 370 | exit(4); 371 | } 372 | rescanAllAnimations(); 373 | } 374 | 375 | void createTrackMem(char *buf, unsigned long bufsize) { 376 | reloadLibrary(); 377 | track = ass_read_memory(ass_library, buf, (size_t)bufsize, NULL); 378 | if (!track) { 379 | fprintf(stderr, "jso: Failed to start a track\n"); 380 | exit(4); 381 | } 382 | rescanAllAnimations(); 383 | } 384 | 385 | void removeTrack() { 386 | if (track != NULL) { 387 | ass_free_track(track); 388 | track = NULL; 389 | } 390 | free(m_is_event_animated); 391 | m_is_event_animated = NULL; 392 | } 393 | /* TRACK */ 394 | 395 | /* CANVAS */ 396 | void resizeCanvas(int frame_w, int frame_h) { 397 | ass_set_frame_size(ass_renderer, frame_w, frame_h); 398 | ass_set_storage_size(ass_renderer, frame_w, frame_h); 399 | canvas_h = frame_h; 400 | canvas_w = frame_w; 401 | } 402 | 403 | ASS_Image* renderImage(double time, int* changed) { 404 | ASS_Image *img = ass_render_frame(ass_renderer, track, _convert_time_to_ms(time), changed); 405 | return img; 406 | } 407 | /* CANVAS */ 408 | 409 | void quitLibrary() { 410 | removeTrack(); 411 | ass_renderer_done(ass_renderer); 412 | ass_library_done(ass_library); 413 | m_blend.clear(); 414 | free(m_is_event_animated); 415 | m_is_event_animated = NULL; 416 | } 417 | 418 | void reloadLibrary() { 419 | quitLibrary(); 420 | 421 | initLibrary(canvas_w, canvas_h, NULL); 422 | } 423 | 424 | void reloadFonts() { 425 | ass_set_fonts(ass_renderer, defaultFont.c_str(), NULL, ASS_FONTPROVIDER_FONTCONFIG, "/assets/fonts.conf", 1); 426 | } 427 | 428 | void setMargin(int top, int bottom, int left, int right) { 429 | ass_set_margins(ass_renderer, top, bottom, left, right); 430 | } 431 | 432 | int getEventCount() const { 433 | return track->n_events; 434 | } 435 | 436 | int allocEvent() { 437 | free(m_is_event_animated); 438 | m_is_event_animated = NULL; 439 | return ass_alloc_event(track); 440 | } 441 | 442 | void removeEvent(int eid) { 443 | free(m_is_event_animated); 444 | m_is_event_animated = NULL; 445 | ass_free_event(track, eid); 446 | } 447 | 448 | int getStyleCount() const { 449 | return track->n_styles; 450 | } 451 | 452 | int getStyleByName(const char* name) const { 453 | for (int n = 0; n < track->n_styles; n++) { 454 | if (track->styles[n].Name && strcmp(track->styles[n].Name, name) == 0) 455 | return n; 456 | } 457 | return 0; 458 | } 459 | 460 | int allocStyle() { 461 | return ass_alloc_style(track); 462 | } 463 | 464 | void removeStyle(int sid) { 465 | ass_free_event(track, sid); 466 | } 467 | 468 | void removeAllEvents() { 469 | free(m_is_event_animated); 470 | m_is_event_animated = NULL; 471 | ass_flush_events(track); 472 | } 473 | 474 | void setMemoryLimits(int glyph_limit, int bitmap_cache_limit) { 475 | printf("jso: setting total libass memory limits to: glyph=%d MiB, bitmap cache=%d MiB\n", 476 | glyph_limit, bitmap_cache_limit); 477 | ass_set_cache_limits(ass_renderer, glyph_limit, bitmap_cache_limit); 478 | } 479 | 480 | RenderBlendResult* renderBlend(double tm, int force) { 481 | m_blendResult.blend_time = 0.0; 482 | m_blendResult.part = NULL; 483 | 484 | ASS_Image *img = ass_render_frame(ass_renderer, track, _convert_time_to_ms(tm), &m_blendResult.changed); 485 | if (img == NULL || (m_blendResult.changed == 0 && !force)) { 486 | return &m_blendResult; 487 | } 488 | 489 | double start_blend_time = emscripten_get_now(); 490 | for (int i = 0; i < MAX_BLEND_STORAGES; i++) { 491 | m_blendParts[i].taken = false; 492 | } 493 | 494 | // split rendering region in 9 pieces (as on 3x3 grid) 495 | int split_x_low = canvas_w / 3, split_x_high = 2 * canvas_w / 3; 496 | int split_y_low = canvas_h / 3, split_y_high = 2 * canvas_h / 3; 497 | BoundingBox boxes[MAX_BLEND_STORAGES]; 498 | for (ASS_Image *cur = img; cur != NULL; cur = cur->next) { 499 | if (cur->w == 0 || cur->h == 0) continue; // skip empty images 500 | int index = 0; 501 | int middle_x = cur->dst_x + (cur->w >> 1), middle_y = cur->dst_y + (cur->h >> 1); 502 | if (middle_y > split_y_high) { 503 | index += 2 * 3; 504 | } else if (middle_y > split_y_low) { 505 | index += 1 * 3; 506 | } 507 | if (middle_x > split_x_high) { 508 | index += 2; 509 | } else if (middle_y > split_x_low) { 510 | index += 1; 511 | } 512 | boxes[index].add(cur->dst_x, cur->dst_y, cur->w, cur->h); 513 | } 514 | 515 | // now merge regions as long as there are intersecting regions 516 | for (;;) { 517 | bool merged = false; 518 | for (int box1 = 0; box1 < MAX_BLEND_STORAGES - 1; box1++) { 519 | if (boxes[box1].empty()) continue; 520 | for (int box2 = box1 + 1; box2 < MAX_BLEND_STORAGES; box2++) { 521 | if (boxes[box2].empty()) continue; 522 | if (boxes[box1].tryMerge(boxes[box2])) { 523 | boxes[box2].clear(); 524 | merged = true; 525 | } 526 | } 527 | } 528 | if (!merged) break; 529 | } 530 | 531 | for (int box = 0; box < MAX_BLEND_STORAGES; box++) { 532 | if (boxes[box].empty()) continue; 533 | RenderBlendPart *part = renderBlendPart(boxes[box], img); 534 | if (part == NULL) break; // memory allocation error 535 | part->next = m_blendResult.part; 536 | m_blendResult.part = part; 537 | } 538 | m_blendResult.blend_time = emscripten_get_now() - start_blend_time; 539 | 540 | return &m_blendResult; 541 | } 542 | 543 | double findNextEventStart(double tm) const { 544 | if (!track || track->n_events == 0) return -1; 545 | 546 | ASS_Event *cur = track->events; 547 | long long now = _convert_time_to_ms(tm); 548 | long long closest = -1; 549 | 550 | for (int i = 0; i < track->n_events; i++, cur++) { 551 | long long start = cur->Start; 552 | if (start <= now) { 553 | if (now < start + cur->Duration) { 554 | // there's currently an event being displayed, we should render it 555 | closest = now; 556 | break; 557 | } 558 | } else if (start < closest || closest == -1) { 559 | closest = start; 560 | } 561 | } 562 | 563 | return _convert_time_to_seconds(closest); 564 | } 565 | 566 | EventStopTimesResult* findEventStopTimes(double tm) const { 567 | static EventStopTimesResult result; 568 | if (!track || track->n_events == 0) { 569 | result.eventFinish = result.emptyFinish = -1; 570 | return &result; 571 | } 572 | 573 | ASS_Event *cur = track->events; 574 | long long now = _convert_time_to_ms(tm); 575 | 576 | long long minFinish = -1, maxFinish = -1, minStart = -1; 577 | int current_animated = 0; 578 | 579 | for (int i = 0; i < track->n_events; i++, cur++) { 580 | long long start = cur->Start; 581 | long long finish = start + cur->Duration; 582 | if (start <= now) { 583 | if (finish > now) { 584 | if (finish < minFinish || minFinish == -1) { 585 | minFinish = finish; 586 | } 587 | if (finish > maxFinish) { 588 | maxFinish = finish; 589 | } 590 | if (!current_animated && m_is_event_animated) current_animated = m_is_event_animated[i]; 591 | } 592 | } else if (start < minStart || minStart == -1) { 593 | minStart = start; 594 | } 595 | } 596 | result.is_animated = current_animated; 597 | 598 | if (minFinish != -1) { 599 | // some event is going on, so we need to re-draw either when it stops 600 | // or when some other event starts 601 | result.eventFinish = _convert_time_to_seconds((minStart == -1 || minFinish < minStart) ? minFinish : minStart); 602 | } else { 603 | // there's no current event, so no need to draw anything 604 | result.eventFinish = -1; 605 | } 606 | 607 | if (minFinish == maxFinish && (minStart == -1 || minStart > maxFinish)) { 608 | // there's empty space after this event ends 609 | result.emptyFinish = _convert_time_to_seconds(minStart); 610 | } else { 611 | // there's no empty space after eventFinish happens 612 | result.emptyFinish = result.eventFinish; 613 | } 614 | 615 | return &result; 616 | } 617 | 618 | void rescanAllAnimations() { 619 | free(m_is_event_animated); 620 | m_is_event_animated = (int*)malloc(sizeof(int) * track->n_events); 621 | if (m_is_event_animated == NULL) { 622 | printf("cannot parse animated events\n"); 623 | exit(5); 624 | } 625 | 626 | ASS_Event *cur = track->events; 627 | int *animated = m_is_event_animated; 628 | for (int i = 0; i < track->n_events; i++, cur++, animated++) { 629 | *animated = _is_event_animated(cur, m_drop_animations); 630 | } 631 | } 632 | 633 | private: 634 | RenderBlendPart* renderBlendPart(const BoundingBox& rect, ASS_Image* img) { 635 | int width = rect.max_x - rect.min_x + 1, height = rect.max_y - rect.min_y + 1; 636 | 637 | // make float buffer for blending 638 | const size_t buffer_size = width * height * 4 * sizeof(float); 639 | float* buf = (float*)m_blend.take(buffer_size, 0); 640 | if (buf == NULL) { 641 | fprintf(stderr, "jso: cannot allocate buffer for blending\n"); 642 | return NULL; 643 | } 644 | memset(buf, 0, buffer_size); 645 | 646 | // blend things in 647 | for (ASS_Image *cur = img; cur != NULL; cur = cur->next) { 648 | if (cur->dst_x < rect.min_x || cur->dst_y < rect.min_y) continue; // skip images not fully within render region 649 | int curw = cur->w, curh = cur->h; 650 | if (curw == 0 || curh == 0 || cur->dst_x + curw - 1 > rect.max_x || cur->dst_y + curh - 1 > rect.max_y) continue; // skip empty images or images outside render region 651 | int a = (255 - (cur->color & 0xFF)); 652 | if (a == 0) continue; // skip transparent images 653 | 654 | int curs = (cur->stride >= curw) ? cur->stride : curw; 655 | int curx = cur->dst_x - rect.min_x, cury = cur->dst_y - rect.min_y; 656 | 657 | unsigned char *bitmap = cur->bitmap; 658 | float normalized_a = a / 255.0; 659 | float r = ((cur->color >> 24) & 0xFF) / 255.0; 660 | float g = ((cur->color >> 16) & 0xFF) / 255.0; 661 | float b = ((cur->color >> 8) & 0xFF) / 255.0; 662 | 663 | int buf_line_coord = cury * width; 664 | for (int y = 0, bitmap_offset = 0; y < curh; y++, bitmap_offset += curs, buf_line_coord += width) 665 | { 666 | for (int x = 0; x < curw; x++) 667 | { 668 | float pix_alpha = bitmap[bitmap_offset + x] * normalized_a / 255.0; 669 | float inv_alpha = 1.0 - pix_alpha; 670 | 671 | int buf_coord = (buf_line_coord + curx + x) << 2; 672 | float *buf_r = buf + buf_coord; 673 | float *buf_g = buf + buf_coord + 1; 674 | float *buf_b = buf + buf_coord + 2; 675 | float *buf_a = buf + buf_coord + 3; 676 | 677 | // do the compositing, pre-multiply image RGB with alpha for current pixel 678 | *buf_a = pix_alpha + *buf_a * inv_alpha; 679 | *buf_r = r * pix_alpha + *buf_r * inv_alpha; 680 | *buf_g = g * pix_alpha + *buf_g * inv_alpha; 681 | *buf_b = b * pix_alpha + *buf_b * inv_alpha; 682 | } 683 | } 684 | } 685 | 686 | // find closest free buffer 687 | size_t needed = sizeof(unsigned int) * width * height; 688 | RenderBlendStorage *storage = m_blendParts, *bigBuffer = NULL, *smallBuffer = NULL; 689 | for (int buffer_index = 0; buffer_index < MAX_BLEND_STORAGES; buffer_index++, storage++) { 690 | if (storage->taken) continue; 691 | if (storage->buf.capacity() >= needed) { 692 | if (bigBuffer == NULL || bigBuffer->buf.capacity() > storage->buf.capacity()) bigBuffer = storage; 693 | } else { 694 | if (smallBuffer == NULL || smallBuffer->buf.capacity() > storage->buf.capacity()) smallBuffer = storage; 695 | } 696 | } 697 | 698 | if (bigBuffer != NULL) { 699 | storage = bigBuffer; 700 | } else if (smallBuffer != NULL) { 701 | storage = smallBuffer; 702 | } else { 703 | printf("jso: cannot get a buffer for rendering part!\n"); 704 | return NULL; 705 | } 706 | 707 | unsigned int *result = (unsigned int*)storage->buf.take(needed, false); 708 | if (result == NULL) { 709 | printf("jso: cannot make a buffer for rendering part!\n"); 710 | return NULL; 711 | } 712 | storage->taken = true; 713 | 714 | // now build the result; 715 | for (int y = 0, buf_line_coord = 0; y < height; y++, buf_line_coord += width) { 716 | for (int x = 0; x < width; x++) { 717 | unsigned int pixel = 0; 718 | int buf_coord = (buf_line_coord + x) << 2; 719 | float alpha = buf[buf_coord + 3]; 720 | if (alpha > MIN_UINT8_CAST) { 721 | // need to un-multiply the result 722 | float value = buf[buf_coord] / alpha; 723 | pixel |= CLAMP_UINT8(value); // R 724 | value = buf[buf_coord + 1] / alpha; 725 | pixel |= CLAMP_UINT8(value) << 8; // G 726 | value = buf[buf_coord + 2] / alpha; 727 | pixel |= CLAMP_UINT8(value) << 16; // B 728 | pixel |= CLAMP_UINT8(alpha) << 24; // A 729 | } 730 | result[buf_line_coord + x] = pixel; 731 | } 732 | } 733 | 734 | // return the thing 735 | storage->part.dest_x = rect.min_x; 736 | storage->part.dest_y = rect.min_y; 737 | storage->part.dest_width = width; 738 | storage->part.dest_height = height; 739 | storage->part.image = (unsigned char*)result; 740 | return &storage->part; 741 | } 742 | 743 | ReusableBuffer m_blend; 744 | RenderBlendResult m_blendResult; 745 | RenderBlendStorage m_blendParts[MAX_BLEND_STORAGES]; 746 | int *m_is_event_animated; 747 | bool m_drop_animations; 748 | }; 749 | 750 | int main(int argc, char *argv[]) { return 0; } 751 | 752 | #ifdef __EMSCRIPTEN__ 753 | #include "./SubOctpInterface.cpp" 754 | #endif 755 | -------------------------------------------------------------------------------- /src/SubtitleOctopus.idl: -------------------------------------------------------------------------------- 1 | [NoDelete] 2 | interface ASS_Image { 3 | attribute long w; 4 | attribute long h; 5 | attribute long stride; 6 | attribute ByteString bitmap; 7 | attribute unsigned long color; 8 | attribute long dst_x; 9 | attribute long dst_y; 10 | attribute ASS_Image next; 11 | }; 12 | 13 | [NoDelete] 14 | interface ASS_Style { 15 | [Owner] attribute DOMString Name; 16 | [Owner] attribute DOMString FontName; 17 | attribute double FontSize; 18 | attribute unsigned long PrimaryColour; 19 | attribute unsigned long SecondaryColour; 20 | attribute unsigned long OutlineColour; 21 | attribute unsigned long BackColour; 22 | attribute long Bold; 23 | attribute long Italic; 24 | attribute long Underline; 25 | attribute long StrikeOut; 26 | attribute double ScaleX; 27 | attribute double ScaleY; 28 | attribute double Spacing; 29 | attribute double Angle; 30 | attribute long BorderStyle; 31 | attribute double Outline; 32 | attribute double Shadow; 33 | attribute long Alignment; 34 | attribute long MarginL; 35 | attribute long MarginR; 36 | attribute long MarginV; 37 | attribute long Encoding; 38 | attribute long treat_fontname_as_pattern; 39 | attribute double Blur; 40 | attribute long Justify; 41 | }; 42 | 43 | [NoDelete] 44 | interface ASS_Event { 45 | attribute long long Start; 46 | attribute long long Duration; 47 | attribute long ReadOrder; 48 | attribute long Layer; 49 | attribute long Style; 50 | [Owner] attribute DOMString Name; 51 | attribute long MarginL; 52 | attribute long MarginR; 53 | attribute long MarginV; 54 | [Owner] attribute DOMString Effect; 55 | [Owner] attribute DOMString Text; 56 | }; 57 | 58 | [NoDelete] 59 | interface ASS_Track { 60 | attribute long n_styles; 61 | attribute long max_styles; 62 | attribute long n_events; 63 | attribute long max_events; 64 | [Value] attribute ASS_Style[] styles; 65 | [Value] attribute ASS_Event[] events; 66 | [Owner] attribute DOMString style_format; 67 | [Owner] attribute DOMString event_format; 68 | attribute long PlayResX; 69 | attribute long PlayResY; 70 | attribute double Timer; 71 | attribute long WrapStyle; 72 | attribute long ScaledBorderAndShadow; 73 | attribute long Kerning; 74 | [Owner] attribute DOMString Language; 75 | attribute long default_style; 76 | [Owner] attribute DOMString name; 77 | }; 78 | 79 | enum ASS_Hinting { 80 | "ASS_HINTING_NONE", 81 | "ASS_HINTING_LIGHT", 82 | "ASS_HINTING_NORMAL", 83 | "ASS_HINTING_NATIVE" 84 | }; 85 | 86 | enum ASS_ShapingLevel { 87 | "ASS_SHAPING_SIMPLE", 88 | "ASS_SHAPING_COMPLEX", 89 | }; 90 | 91 | enum ASS_OverrideBits { 92 | "ASS_OVERRIDE_DEFAULT", 93 | "ASS_OVERRIDE_BIT_STYLE", 94 | "ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE", 95 | "ASS_OVERRIDE_BIT_FONT_SIZE", 96 | "ASS_OVERRIDE_BIT_FONT_SIZE_FIELDS", 97 | "ASS_OVERRIDE_BIT_FONT_NAME", 98 | "ASS_OVERRIDE_BIT_COLORS", 99 | "ASS_OVERRIDE_BIT_ATTRIBUTES", 100 | "ASS_OVERRIDE_BIT_BORDER", 101 | "ASS_OVERRIDE_BIT_ALIGNMENT", 102 | "ASS_OVERRIDE_BIT_MARGINS", 103 | "ASS_OVERRIDE_FULL_STYLE", 104 | "ASS_OVERRIDE_BIT_JUSTIFY" 105 | }; 106 | 107 | [NoDelete] 108 | interface ASS_Library {}; 109 | [NoDelete] 110 | interface ASS_RenderPriv {}; 111 | [NoDelete] 112 | interface ASS_ParserPriv {}; 113 | [NoDelete] 114 | interface ASS_Renderer {}; 115 | 116 | [NoDelete] 117 | interface libass { 118 | void libass(); 119 | long oct_library_version(); 120 | ASS_Library oct_library_init(); 121 | void oct_library_done(ASS_Library priv); 122 | void oct_set_fonts_dir(ASS_Library priv, [Const] DOMString fonts_dir); 123 | void oct_set_extract_fonts(ASS_Library priv, long extract); 124 | void oct_set_style_overrides(ASS_Library priv, DOMString[] list); 125 | void oct_process_force_style(ASS_Track track); 126 | ASS_Renderer oct_renderer_init(ASS_Library priv); 127 | void oct_renderer_done(ASS_Renderer priv); 128 | void oct_set_frame_size(ASS_Renderer priv, long w, long h); 129 | void oct_set_storage_size(ASS_Renderer priv, long w, long h); 130 | void oct_set_shaper(ASS_Renderer priv, ASS_ShapingLevel level); 131 | void oct_set_margins(ASS_Renderer priv, long t, long b, long l, long r); 132 | void oct_set_use_margins(ASS_Renderer priv, long use); 133 | void oct_set_pixel_aspect(ASS_Renderer priv, double par); 134 | void oct_set_aspect_ratio(ASS_Renderer priv, double dar, double sar); 135 | void oct_set_font_scale(ASS_Renderer priv, double font_scale); 136 | void oct_set_hinting(ASS_Renderer priv, ASS_Hinting ht); 137 | void oct_set_line_spacing(ASS_Renderer priv, double line_spacing); 138 | void oct_set_line_position(ASS_Renderer priv, double line_position); 139 | void oct_set_fonts(ASS_Renderer priv, DOMString default_font, DOMString default_family, long dfp, DOMString config, long update); 140 | void oct_set_selective_style_override_enabled(ASS_Renderer priv, long bits); 141 | void oct_set_selective_style_override(ASS_Renderer priv, ASS_Style style); 142 | void oct_set_cache_limits(ASS_Renderer priv, long glyph_max, long bitmap_max_size); 143 | ASS_Image oct_render_frame(ASS_Renderer priv, ASS_Track track, long long now, IntPtr detect_change); 144 | ASS_Track oct_new_track(ASS_Library priv); 145 | void oct_free_track(ASS_Track track); 146 | long oct_alloc_style(ASS_Track track); 147 | long oct_alloc_event(ASS_Track track); 148 | void oct_free_style(ASS_Track track, long sid); 149 | void oct_free_event(ASS_Track track, long eid); 150 | void oct_flush_events(ASS_Track track); 151 | ASS_Track oct_read_file(ASS_Library library, DOMString fname, DOMString codepage); 152 | void oct_add_font(ASS_Library library, DOMString name, DOMString data, long data_size); 153 | void oct_clear_fonts(ASS_Library library); 154 | long long oct_step_sub(ASS_Track track, long long now, long movement); 155 | }; 156 | 157 | [NoDelete] 158 | interface RenderBlendPart { 159 | attribute long dest_x; 160 | attribute long dest_y; 161 | attribute long dest_width; 162 | attribute long dest_height; 163 | attribute ByteString image; 164 | attribute RenderBlendPart next; 165 | }; 166 | 167 | [NoDelete] 168 | interface RenderBlendResult { 169 | attribute long changed; 170 | attribute double blend_time; 171 | attribute RenderBlendPart part; 172 | }; 173 | 174 | [NoDelete] 175 | interface EventStopTimesResult { 176 | attribute double eventFinish; 177 | attribute double emptyFinish; 178 | attribute long is_animated; 179 | }; 180 | 181 | interface SubtitleOctopus { 182 | void SubtitleOctopus(); 183 | attribute ASS_Track track; 184 | attribute ASS_Renderer ass_renderer; 185 | attribute ASS_Library ass_library; 186 | void setLogLevel(long level); 187 | void setDropAnimations(long value); 188 | long getDropAnimations(); 189 | void initLibrary(long frame_w, long frame_h, DOMString default_font); 190 | void createTrack(DOMString subfile); 191 | void createTrackMem(DOMString buf, unsigned long bufsize); 192 | void removeTrack(); 193 | void resizeCanvas(long frame_w, long frame_h); 194 | ASS_Image renderImage(double time, IntPtr changed); 195 | void quitLibrary(); 196 | void reloadLibrary(); 197 | void reloadFonts(); 198 | void setMargin(long top, long bottom, long left, long right); 199 | long getEventCount(); 200 | long allocEvent(); 201 | long allocStyle(); 202 | void removeEvent(long eid); 203 | long getStyleCount(); 204 | long getStyleByName([Const] DOMString name); 205 | void removeStyle(long eid); 206 | void removeAllEvents(); 207 | void setMemoryLimits(long glyph_limit, long bitmap_cache_limit); 208 | RenderBlendResult renderBlend(double tm, long force); 209 | double findNextEventStart(double tm); 210 | EventStopTimesResult findEventStopTimes(double tm); 211 | void rescanAllAnimations(); 212 | }; 213 | -------------------------------------------------------------------------------- /src/libass.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | /** 8 | * This class is a wrapper for Emscripten WebIDL for interface with Javascript 9 | */ 10 | class libass { 11 | public: 12 | static long oct_library_version() { 13 | return ass_library_version(); 14 | } 15 | static ASS_Library* oct_library_init() { 16 | return ass_library_init(); 17 | } 18 | static void oct_library_done(ASS_Library *priv) { 19 | ass_library_done(priv); 20 | } 21 | static void oct_set_fonts_dir(ASS_Library *priv, const char *fonts_dir) { 22 | ass_set_fonts_dir(priv, fonts_dir); 23 | } 24 | static void oct_set_extract_fonts(ASS_Library *priv, int extract) { 25 | ass_set_extract_fonts(priv, extract); 26 | } 27 | static void oct_set_style_overrides(ASS_Library *priv, char **list) { 28 | ass_set_style_overrides(priv, list); 29 | } 30 | static void oct_process_force_style(ASS_Track *track) { 31 | ass_process_force_style(track); 32 | } 33 | static ASS_Renderer *oct_renderer_init(ASS_Library *priv) { 34 | return ass_renderer_init(priv); 35 | } 36 | static void oct_renderer_done(ASS_Renderer *priv) { 37 | ass_renderer_done(priv); 38 | } 39 | static void oct_set_frame_size(ASS_Renderer *priv, int w, int h) { 40 | ass_set_frame_size(priv, w, h); 41 | } 42 | static void oct_set_storage_size(ASS_Renderer *priv, int w, int h) { 43 | ass_set_storage_size(priv, w, h); 44 | } 45 | static void oct_set_shaper(ASS_Renderer *priv, ASS_ShapingLevel level) { 46 | ass_set_shaper(priv, level); 47 | } 48 | static void oct_set_margins(ASS_Renderer *priv, int t, int b, int l, int r) { 49 | ass_set_margins(priv, t, b, l, r); 50 | } 51 | static void oct_set_use_margins(ASS_Renderer *priv, int use) { 52 | ass_set_use_margins(priv, use); 53 | } 54 | static void oct_set_pixel_aspect(ASS_Renderer *priv, double par) { 55 | ass_set_pixel_aspect(priv, par); 56 | } 57 | static void oct_set_aspect_ratio(ASS_Renderer *priv, double dar, double sar) { 58 | ass_set_aspect_ratio(priv, dar, sar); 59 | } 60 | static void oct_set_font_scale(ASS_Renderer *priv, double font_scale) { 61 | ass_set_font_scale(priv, font_scale); 62 | } 63 | static void oct_set_hinting(ASS_Renderer *priv, ASS_Hinting ht) { 64 | ass_set_hinting(priv, ht); 65 | } 66 | static void oct_set_line_spacing(ASS_Renderer *priv, double line_spacing) { 67 | ass_set_line_spacing(priv, line_spacing); 68 | } 69 | static void oct_set_line_position(ASS_Renderer *priv, double line_position) { 70 | ass_set_line_position(priv, line_position); 71 | } 72 | static void oct_set_fonts(ASS_Renderer *priv, const char *default_font, const char *default_family, int dfp, const char *config, int update) { 73 | ass_set_fonts(priv, default_font, default_family, dfp, config, update); 74 | } 75 | static void oct_set_selective_style_override_enabled(ASS_Renderer *priv, int bits) { 76 | ass_set_selective_style_override_enabled(priv, bits); 77 | } 78 | static void oct_set_selective_style_override(ASS_Renderer *priv, ASS_Style *style) { 79 | ass_set_selective_style_override(priv, style); 80 | } 81 | static void oct_set_cache_limits(ASS_Renderer *priv, int glyph_max, int bitmap_max_size) { 82 | ass_set_cache_limits(priv, glyph_max, bitmap_max_size); 83 | } 84 | static ASS_Image *oct_render_frame(ASS_Renderer *priv, ASS_Track *track, long long now, int *detect_change) { 85 | return ass_render_frame(priv, track, now, detect_change); 86 | } 87 | static ASS_Track *oct_new_track(ASS_Library *priv) { 88 | return ass_new_track(priv); 89 | } 90 | static void oct_free_track(ASS_Track *track) { 91 | ass_free_track(track); 92 | } 93 | static int oct_alloc_style(ASS_Track *track) { 94 | return ass_alloc_style(track); 95 | } 96 | static int oct_alloc_event(ASS_Track *track) { 97 | return ass_alloc_event(track); 98 | } 99 | static void oct_free_style(ASS_Track *track, int sid) { 100 | ass_free_style(track, sid); 101 | } 102 | static void oct_free_event(ASS_Track *track, int eid) { 103 | ass_free_event(track, eid); 104 | } 105 | static void oct_set_check_readorder(ASS_Track *track, int check_readorder) { 106 | ass_set_check_readorder(track, check_readorder); 107 | } 108 | static void oct_flush_events(ASS_Track *track) { 109 | ass_flush_events(track); 110 | } 111 | static ASS_Track *oct_read_file(ASS_Library *library, char *fname, char *codepage) { 112 | return ass_read_file(library, fname, codepage); 113 | } 114 | static ASS_Track *oct_read_memory(ASS_Library *library, char *buf, unsigned long bufsize, char *codepage) { 115 | return ass_read_memory(library, buf, bufsize, codepage); 116 | } 117 | static int oct_read_styles(ASS_Track *track, char *fname, char *codepage) { 118 | return ass_read_styles(track, fname, codepage); 119 | } 120 | static void oct_add_font(ASS_Library *library, char *name, char *data, int data_size) { 121 | ass_add_font(library, name, data, data_size); 122 | } 123 | static void oct_clear_fonts(ASS_Library *library) { 124 | ass_clear_fonts(library); 125 | } 126 | static long long oct_step_sub(ASS_Track *track, long long now, int movement) { 127 | return ass_step_sub(track, now, movement); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.startsWith) { 2 | String.prototype.startsWith = function (search, pos) { 3 | if (pos === undefined) { 4 | pos = 0; 5 | } 6 | return this.substring(pos, search.length) === search; 7 | }; 8 | } 9 | 10 | if (!String.prototype.endsWith) { 11 | String.prototype.endsWith = function (search, this_len) { 12 | if (this_len === undefined || this_len > this.length) { 13 | this_len = this.length; 14 | } 15 | return this.substring(this_len - search.length, this_len) === search; 16 | }; 17 | } 18 | 19 | if (!String.prototype.includes) { 20 | String.prototype.includes = function (search, pos) { 21 | return this.indexOf(search, pos) !== -1; 22 | }; 23 | } 24 | 25 | if (!ArrayBuffer.isView) { 26 | var typedArrays = [ 27 | Int8Array, 28 | Uint8Array, 29 | Uint8ClampedArray, 30 | Int16Array, 31 | Uint16Array, 32 | Int32Array, 33 | Uint32Array, 34 | Float32Array, 35 | Float64Array 36 | ]; 37 | 38 | ArrayBuffer.isView = function (obj) { 39 | return obj && obj.constructor && typedArrays.indexOf(obj.constructor) !== -1; 40 | }; 41 | } 42 | 43 | if (!Int8Array.prototype.slice) { 44 | Object.defineProperty(Int8Array.prototype, 'slice', { 45 | value: function (begin, end) 46 | { 47 | return new Int8Array(this.subarray(begin, end)); 48 | } 49 | }); 50 | } 51 | 52 | if (!Uint8Array.prototype.slice) { 53 | Object.defineProperty(Uint8Array.prototype, 'slice', { 54 | value: function (begin, end) 55 | { 56 | return new Uint8Array(this.subarray(begin, end)); 57 | } 58 | }); 59 | } 60 | 61 | if (!Int16Array.from) { 62 | // Doesn't work for String 63 | Int16Array.from = function (source) { 64 | var arr = new Int16Array(source.length); 65 | arr.set(source, 0); 66 | return arr; 67 | }; 68 | } 69 | 70 | if (!Int32Array.from) { 71 | // Doesn't work for String 72 | Int32Array.from = function (source) { 73 | var arr = new Int32Array(source.length); 74 | arr.set(source, 0); 75 | return arr; 76 | }; 77 | } 78 | 79 | // performance.now() polyfill 80 | if ("performance" in self === false) { 81 | self.performance = {}; 82 | } 83 | Date.now = (Date.now || function () { 84 | return new Date().getTime(); 85 | }); 86 | if ("now" in self.performance === false) { 87 | var nowOffset = Date.now(); 88 | if (performance.timing && performance.timing.navigationStart) { 89 | nowOffset = performance.timing.navigationStart 90 | } 91 | self.performance.now = function now() { 92 | return Date.now() - nowOffset; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/post-worker.js: -------------------------------------------------------------------------------- 1 | Module['FS'] = FS; 2 | 3 | self.delay = 0; // approximate delay (time of render + postMessage + drawImage), for example 1/60 or 0 4 | self.lastCurrentTime = 0; 5 | self.rate = 1; 6 | self.rafId = null; 7 | self.nextIsRaf = false; 8 | self.lastCurrentTimeReceivedAt = Date.now(); 9 | self.targetFps = 24; 10 | self.libassMemoryLimit = 0; // in MiB 11 | self.renderOnDemand = false; // determines if only rendering on demand 12 | self.dropAllAnimations = false; // set to true to enable "lite mode" with all animations disabled for speed 13 | 14 | self.width = 0; 15 | self.height = 0; 16 | 17 | self.fontMap_ = {}; 18 | self.fontId = 0; 19 | 20 | /** 21 | * Make the font accessible by libass by writing it to the virtual FS. 22 | * @param {!string} font the font name. 23 | */ 24 | self.writeFontToFS = function(font) { 25 | font = font.trim().toLowerCase(); 26 | 27 | if (font.startsWith("@")) { 28 | font = font.substr(1); 29 | } 30 | 31 | if (self.fontMap_.hasOwnProperty(font)) return; 32 | 33 | self.fontMap_[font] = true; 34 | 35 | if (!self.availableFonts.hasOwnProperty(font)) return; 36 | 37 | self.loadFontFile('font' + (self.fontId++) + '-', self.availableFonts[font]); 38 | }; 39 | 40 | self.loadFontFile = function (fontId, path) { 41 | if (self.lazyFileLoading && path.indexOf("blob:") !== 0) { 42 | Module["FS"].createLazyFile("/fonts", fontId + path.split('/').pop(), path, true, false); 43 | } else { 44 | Module["FS"].createPreloadedFile("/fonts", fontId + path.split('/').pop(), path, true, false); 45 | } 46 | } 47 | 48 | /** 49 | * Write all font's mentioned in the .ass file to the virtual FS. 50 | * @param {!string} content the file content. 51 | */ 52 | self.writeAvailableFontsToFS = function(content) { 53 | if (!self.availableFonts) return; 54 | 55 | var sections = parseAss(content); 56 | 57 | for (var i = 0; i < sections.length; i++) { 58 | for (var j = 0; j < sections[i].body.length; j++) { 59 | if (sections[i].body[j].key === 'Style') { 60 | self.writeFontToFS(sections[i].body[j].value['Fontname']); 61 | } 62 | } 63 | } 64 | 65 | var regex = /\\fn([^\\}]*?)[\\}]/g; 66 | var matches; 67 | while (matches = regex.exec(self.subContent)) { 68 | self.writeFontToFS(matches[1]); 69 | } 70 | }; 71 | 72 | self.getRenderMethod = function () { 73 | switch (self.renderMode) { 74 | case 'lossy': 75 | return self.lossyRender; 76 | case 'js-blend': 77 | return self.render; 78 | default: 79 | console.error('Unrecognised renderMode, falling back to default!'); 80 | self.renderMode = 'wasm-blend'; 81 | // fallthrough 82 | case 'wasm-blend': 83 | return self.blendRender; 84 | } 85 | } 86 | 87 | /** 88 | * Set the subtitle track. 89 | * @param {!string} content the content of the subtitle file. 90 | */ 91 | self.setTrack = function (content) { 92 | // Make sure that the fonts are loaded 93 | self.writeAvailableFontsToFS(content); 94 | 95 | // Write the subtitle file to the virtual FS. 96 | Module["FS"].writeFile("/sub.ass", content); 97 | 98 | // Tell libass to render the new track 99 | self.octObj.createTrack("/sub.ass"); 100 | self.ass_track = self.octObj.track; 101 | self.ass_renderer = self.octObj.ass_renderer; 102 | self.ass_library = self.octObj.ass_library; 103 | if (!self.renderOnDemand) { 104 | self.getRenderMethod()(); 105 | } 106 | }; 107 | 108 | /** 109 | * Remove subtitle track. 110 | */ 111 | self.freeTrack = function () { 112 | self.octObj.removeTrack(); 113 | if (!self.renderOnDemand) { 114 | self.getRenderMethod()(); 115 | } 116 | }; 117 | 118 | /** 119 | * Set the subtitle track. 120 | * @param {!string} url the URL of the subtitle file. 121 | */ 122 | self.setTrackByUrl = function (url) { 123 | var content = ""; 124 | if (isBrotliFile(url)) { 125 | content = Module["BrotliDecode"](readBinary(url)) 126 | } else { 127 | content = read_(url); 128 | } 129 | self.setTrack(content); 130 | }; 131 | 132 | self.resize = function (width, height) { 133 | self.width = width; 134 | self.height = height; 135 | self.octObj.resizeCanvas(width, height); 136 | }; 137 | 138 | self.getCurrentTime = function () { 139 | var diff = (Date.now() - self.lastCurrentTimeReceivedAt) / 1000; 140 | if (self._isPaused) { 141 | return self.lastCurrentTime; 142 | } 143 | else { 144 | if (diff > 5) { 145 | console.error('Didn\'t received currentTime > 5 seconds. Assuming video was paused.'); 146 | self.setIsPaused(true); 147 | } 148 | return self.lastCurrentTime + (diff * self.rate); 149 | } 150 | }; 151 | self.setCurrentTime = function (currentTime) { 152 | self.lastCurrentTime = currentTime; 153 | self.lastCurrentTimeReceivedAt = Date.now(); 154 | if (!self.rafId) { 155 | if (self.nextIsRaf) { 156 | if (!self.renderOnDemand) { 157 | self.rafId = self.requestAnimationFrame(self.getRenderMethod()); 158 | } 159 | } 160 | else { 161 | if (!self.renderOnDemand) { 162 | self.getRenderMethod()(); 163 | } 164 | 165 | // Give onmessage chance to receive all queued messages 166 | setTimeout(function () { 167 | self.nextIsRaf = false; 168 | }, 20); 169 | } 170 | } 171 | }; 172 | 173 | self._isPaused = true; 174 | self.getIsPaused = function () { 175 | return self._isPaused; 176 | }; 177 | self.setIsPaused = function (isPaused) { 178 | if (isPaused != self._isPaused) { 179 | self._isPaused = isPaused; 180 | if (isPaused) { 181 | if (self.rafId) { 182 | clearTimeout(self.rafId); 183 | self.rafId = null; 184 | } 185 | } 186 | else { 187 | self.lastCurrentTimeReceivedAt = Date.now(); 188 | if (!self.renderOnDemand) { 189 | self.rafId = self.requestAnimationFrame(self.getRenderMethod()); 190 | } 191 | } 192 | } 193 | }; 194 | 195 | self.render = function (force) { 196 | self.rafId = 0; 197 | self.renderPending = false; 198 | var startTime = performance.now(); 199 | var renderResult = self.octObj.renderImage(self.getCurrentTime() + self.delay, self.changed); 200 | var changed = Module.getValue(self.changed, 'i32'); 201 | if (changed != 0 || force) { 202 | var result = self.buildResult(renderResult); 203 | var spentTime = performance.now() - startTime; 204 | postMessage({ 205 | target: 'canvas', 206 | op: 'renderCanvas', 207 | time: Date.now(), 208 | spentTime: spentTime, 209 | canvases: result[0] 210 | }, result[1]); 211 | } 212 | 213 | if (!self._isPaused) { 214 | self.rafId = self.requestAnimationFrame(self.render); 215 | } 216 | }; 217 | 218 | self.blendRenderTiming = function (timing, force) { 219 | var startTime = performance.now(); 220 | 221 | var renderResult = self.octObj.renderBlend(timing, force); 222 | 223 | var canvases = []; 224 | var buffers = []; 225 | 226 | // make a copy, as we should free the memory so subsequent calls can utilize it 227 | for (var part = renderResult.part; part.ptr != 0; part = part.next) { 228 | var result = new Uint8Array(HEAPU8.subarray(part.image, part.image + part.dest_width * part.dest_height * 4)); 229 | canvases.push({w: part.dest_width, h: part.dest_height, x: part.dest_x, y: part.dest_y, buffer: result.buffer}); 230 | buffers.push(result.buffer); 231 | } 232 | 233 | return { 234 | changed: renderResult.changed || force || false, 235 | time: Date.now(), 236 | spentTime: performance.now() - startTime, 237 | blendTime: renderResult.blend_time, 238 | canvases: canvases, 239 | buffers: buffers 240 | } 241 | } 242 | 243 | self.blendRender = function (force) { 244 | self.rafId = 0; 245 | self.renderPending = false; 246 | 247 | var rendered = self.blendRenderTiming(self.getCurrentTime() + self.delay, force); 248 | if (rendered.changed) { 249 | postMessage({ 250 | target: 'canvas', 251 | op: 'renderCanvas', 252 | time: rendered.time, 253 | spentTime: rendered.spentTime, 254 | blendTime: rendered.blendTime, 255 | canvases: rendered.canvases 256 | }, rendered.buffers); 257 | } 258 | 259 | if (!self._isPaused) { 260 | self.rafId = self.requestAnimationFrame(self.blendRender); 261 | } 262 | }; 263 | 264 | self.oneshotRender = function (lastRenderedTime, renderNow, iteration) { 265 | var eventStart = renderNow ? lastRenderedTime : self.octObj.findNextEventStart(lastRenderedTime); 266 | var eventFinish = -1.0, emptyFinish = -1.0, animated = false; 267 | var rendered = {}; 268 | if (eventStart >= 0) { 269 | var eventTimes = self.octObj.findEventStopTimes(eventStart); 270 | eventFinish = eventTimes.eventFinish; 271 | emptyFinish = eventTimes.emptyFinish; 272 | animated = eventTimes.is_animated; 273 | 274 | rendered = self.blendRenderTiming(eventStart, true); 275 | } 276 | 277 | postMessage({ 278 | target: 'canvas', 279 | op: 'oneshot-result', 280 | iteration: iteration, 281 | lastRenderedTime: lastRenderedTime, 282 | eventStart: eventStart, 283 | eventFinish: eventFinish, 284 | emptyFinish: emptyFinish, 285 | animated: animated, 286 | viewport: { 287 | width: self.width, 288 | height: self.height 289 | }, 290 | spentTime: rendered.spentTime || 0, 291 | blendTime: rendered.blendTime || 0, 292 | canvases: rendered.canvases || [] 293 | }, rendered.buffers || []); 294 | } 295 | 296 | self.lossyRender = function (force) { 297 | self.rafId = 0; 298 | self.renderPending = false; 299 | var startTime = performance.now(); 300 | var renderResult = self.octObj.renderImage(self.getCurrentTime() + self.delay, self.changed); 301 | var changed = Module.getValue(self.changed, "i32"); 302 | if (changed != 0 || force) { 303 | var result = self.buildResult(renderResult); 304 | var newTime = performance.now(); 305 | var libassTime = newTime - startTime; 306 | var promises = []; 307 | for (var i = 0; i < result[0].length; i++) { 308 | var image = result[0][i]; 309 | var imageBuffer = new Uint8ClampedArray(image.buffer); 310 | var imageData = new ImageData(imageBuffer, image.w, image.h); 311 | promises[i] = createImageBitmap(imageData, 0, 0, image.w, image.h); 312 | } 313 | Promise.all(promises).then(function (imgs) { 314 | var decodeTime = performance.now() - newTime; 315 | var bitmaps = []; 316 | for (var i = 0; i < imgs.length; i++) { 317 | var image = result[0][i]; 318 | bitmaps[i] = { x: image.x, y: image.y, bitmap: imgs[i] }; 319 | } 320 | postMessage({ 321 | target: "canvas", 322 | op: "renderFastCanvas", 323 | time: Date.now(), 324 | libassTime: libassTime, 325 | decodeTime: decodeTime, 326 | bitmaps: bitmaps 327 | }, imgs); 328 | }); 329 | } 330 | if (!self._isPaused) { 331 | self.rafId = self.requestAnimationFrame(self.lossyRender); 332 | } 333 | }; 334 | 335 | self.buildResult = function (ptr) { 336 | var items = []; 337 | var transferable = []; 338 | var item; 339 | 340 | while (ptr.ptr != 0) { 341 | item = self.buildResultItem(ptr); 342 | if (item !== null) { 343 | items.push(item); 344 | transferable.push(item.buffer); 345 | } 346 | ptr = ptr.next; 347 | } 348 | 349 | return [items, transferable]; 350 | } 351 | 352 | self.buildResultItem = function (ptr) { 353 | var bitmap = ptr.bitmap, 354 | stride = ptr.stride, 355 | w = ptr.w, 356 | h = ptr.h, 357 | color = ptr.color; 358 | 359 | if (w == 0 || h == 0) { 360 | return null; 361 | } 362 | 363 | var r = (color >> 24) & 0xFF, 364 | g = (color >> 16) & 0xFF, 365 | b = (color >> 8) & 0xFF, 366 | a = 255 - (color & 0xFF); 367 | 368 | var result = new Uint8ClampedArray(4 * w * h); 369 | 370 | var bitmapPosition = 0; 371 | var resultPosition = 0; 372 | 373 | for (var y = 0; y < h; ++y) { 374 | for (var x = 0; x < w; ++x) { 375 | var k = Module.HEAPU8[bitmap + bitmapPosition + x] * a / 255; 376 | result[resultPosition] = r; 377 | result[resultPosition + 1] = g; 378 | result[resultPosition + 2] = b; 379 | result[resultPosition + 3] = k; 380 | resultPosition += 4; 381 | } 382 | bitmapPosition += stride; 383 | } 384 | 385 | x = ptr.dst_x; 386 | y = ptr.dst_y; 387 | 388 | return {w: w, h: h, x: x, y: y, buffer: result.buffer}; 389 | }; 390 | 391 | if (typeof SDL !== 'undefined') { 392 | SDL.defaults.copyOnLock = false; 393 | SDL.defaults.discardOnLock = false; 394 | SDL.defaults.opaqueFrontBuffer = false; 395 | } 396 | 397 | function FPSTracker(text) { 398 | var last = 0; 399 | var mean = 0; 400 | var counter = 0; 401 | this.tick = function () { 402 | var now = Date.now(); 403 | if (last > 0) { 404 | var diff = now - last; 405 | mean = 0.99 * mean + 0.01 * diff; 406 | if (counter++ === 60) { 407 | counter = 0; 408 | dump(text + ' fps: ' + (1000 / mean).toFixed(2) + '\n'); 409 | } 410 | } 411 | last = now; 412 | } 413 | } 414 | 415 | /** 416 | * Parse the content of an .ass file. 417 | * @param {!string} content the content of the file 418 | */ 419 | function parseAss(content) { 420 | var m, format, lastPart, parts, key, value, tmp, i, j, body; 421 | var sections = []; 422 | var lines = content.split(/[\r\n]+/g); 423 | for (i = 0; i < lines.length; i++) { 424 | m = lines[i].match(/^\[(.*)\]$/); 425 | if (m) { 426 | format = null; 427 | sections.push({ 428 | name: m[1], 429 | body: [] 430 | }); 431 | } else { 432 | if (/^\s*$/.test(lines[i])) continue; 433 | if (sections.length === 0) continue; 434 | body = sections[sections.length - 1].body; 435 | if (lines[i][0] === ';') { 436 | body.push({ 437 | type: 'comment', 438 | value: lines[i].substring(1) 439 | }); 440 | } else { 441 | parts = lines[i].split(":"); 442 | key = parts[0]; 443 | value = parts.slice(1).join(':').trim(); 444 | if (format || key === 'Format') { 445 | value = value.split(','); 446 | if (format && value.length > format.length) { 447 | lastPart = value.slice(format.length - 1).join(','); 448 | value = value.slice(0, format.length - 1); 449 | value.push(lastPart); 450 | } 451 | value = value.map(function(s) { 452 | return s.trim(); 453 | }); 454 | if (format) { 455 | tmp = {}; 456 | for (j = 0; j < value.length; j++) { 457 | tmp[format[j]] = value[j]; 458 | } 459 | value = tmp; 460 | } 461 | } 462 | if (key === 'Format') { 463 | format = value; 464 | } 465 | body.push({ 466 | key: key, 467 | value: value 468 | }); 469 | } 470 | } 471 | } 472 | 473 | return sections; 474 | }; 475 | 476 | self.requestAnimationFrame = (function () { 477 | // similar to Browser.requestAnimationFrame 478 | var nextRAF = 0; 479 | return function (func) { 480 | // try to keep target fps (30fps) between calls to here 481 | var now = Date.now(); 482 | if (nextRAF === 0) { 483 | nextRAF = now + 1000 / self.targetFps; 484 | } else { 485 | while (now + 2 >= nextRAF) { // fudge a little, to avoid timer jitter causing us to do lots of delay:0 486 | nextRAF += 1000 / self.targetFps; 487 | } 488 | } 489 | var delay = Math.max(nextRAF - now, 0); 490 | return setTimeout(func, delay); 491 | //return setTimeout(func, 1); 492 | }; 493 | })(); 494 | 495 | var screen = { 496 | width: 0, 497 | height: 0 498 | }; 499 | 500 | Module.print = function Module_print(x) { 501 | //dump('OUT: ' + x + '\n'); 502 | postMessage({target: 'stdout', content: x}); 503 | }; 504 | Module.printErr = function Module_printErr(x) { 505 | //dump('ERR: ' + x + '\n'); 506 | postMessage({target: 'stderr', content: x}); 507 | }; 508 | 509 | // Frame throttling 510 | 511 | var frameId = 0; 512 | var clientFrameId = 0; 513 | var commandBuffer = []; 514 | 515 | var postMainLoop = Module['postMainLoop']; 516 | Module['postMainLoop'] = function () { 517 | if (postMainLoop) postMainLoop(); 518 | // frame complete, send a frame id 519 | postMessage({target: 'tick', id: frameId++}); 520 | commandBuffer = []; 521 | }; 522 | 523 | // Wait to start running until we receive some info from the client 524 | //addRunDependency('gl-prefetch'); 525 | addRunDependency('worker-init'); 526 | 527 | // buffer messages until the program starts to run 528 | 529 | var messageBuffer = null; 530 | var messageResenderTimeout = null; 531 | 532 | function messageResender() { 533 | if (calledMain) { 534 | assert(messageBuffer && messageBuffer.length > 0); 535 | messageResenderTimeout = null; 536 | messageBuffer.forEach(function (message) { 537 | onmessage(message); 538 | }); 539 | messageBuffer = null; 540 | } else { 541 | messageResenderTimeout = setTimeout(messageResender, 50); 542 | } 543 | } 544 | 545 | function _applyKeys(input, output) { 546 | var vargs = Object.keys(input); 547 | 548 | for (var i = 0; i < vargs.length; i++) { 549 | output[vargs[i]] = input[vargs[i]]; 550 | } 551 | } 552 | 553 | function onMessageFromMainEmscriptenThread(message) { 554 | if (!calledMain && !message.data.preMain) { 555 | if (!messageBuffer) { 556 | messageBuffer = []; 557 | messageResenderTimeout = setTimeout(messageResender, 50); 558 | } 559 | messageBuffer.push(message); 560 | return; 561 | } 562 | if (calledMain && messageResenderTimeout) { 563 | clearTimeout(messageResenderTimeout); 564 | messageResender(); 565 | } 566 | //console.log('worker got ' + JSON.stringify(message.data).substr(0, 150) + '\n'); 567 | switch (message.data.target) { 568 | case 'window': { 569 | self.fireEvent(message.data.event); 570 | break; 571 | } 572 | case 'canvas': { 573 | if (message.data.event) { 574 | Module.canvas.fireEvent(message.data.event); 575 | } else if (message.data.width) { 576 | if (Module.canvas && message.data.boundingClientRect) { 577 | Module.canvas.boundingClientRect = message.data.boundingClientRect; 578 | } 579 | self.resize(message.data.width, message.data.height); 580 | if (!self.renderOnDemand) { 581 | self.getRenderMethod()(); 582 | } 583 | } else throw 'ey?'; 584 | break; 585 | } 586 | case 'video': { 587 | if (message.data.currentTime !== undefined) { 588 | self.setCurrentTime(message.data.currentTime); 589 | } 590 | if (message.data.isPaused !== undefined) { 591 | self.setIsPaused(message.data.isPaused); 592 | } 593 | if (message.data.rate) { 594 | self.rate = message.data.rate; 595 | } 596 | break; 597 | } 598 | case 'tock': { 599 | clientFrameId = message.data.id; 600 | break; 601 | } 602 | case 'worker-init': { 603 | //Module.canvas = document.createElement('canvas'); 604 | screen.width = self.width = message.data.width; 605 | screen.height = self.height = message.data.height; 606 | self.subUrl = message.data.subUrl; 607 | self.subContent = message.data.subContent; 608 | self.fontFiles = message.data.fonts; 609 | self.renderMode = message.data.renderMode; 610 | // Force fallback if engine does not support 'lossy' mode. 611 | // We only use createImageBitmap in the worker and historic WebKit versions supported 612 | // the API in the normal but not the worker scope, so we can't check this earlier. 613 | if (self.renderMode == 'lossy' && typeof createImageBitmap === 'undefined') { 614 | self.renderMode = 'wasm-blend'; 615 | console.error("'createImageBitmap' needed for 'lossy' unsupported. Falling back to default!"); 616 | } 617 | 618 | self.availableFonts = message.data.availableFonts; 619 | self.fallbackFont = message.data.fallbackFont; 620 | self.lazyFileLoading = message.data.lazyFileLoading; 621 | self.debug = message.data.debug; 622 | if (!hasNativeConsole && self.debug) { 623 | console = makeCustomConsole(); 624 | console.log("overridden console"); 625 | } 626 | if (Module.canvas) { 627 | Module.canvas.width_ = message.data.width; 628 | Module.canvas.height_ = message.data.height; 629 | if (message.data.boundingClientRect) { 630 | Module.canvas.boundingClientRect = message.data.boundingClientRect; 631 | } 632 | } 633 | self.targetFps = message.data.targetFps || self.targetFps; 634 | self.libassMemoryLimit = message.data.libassMemoryLimit || self.libassMemoryLimit; 635 | self.libassGlyphLimit = message.data.libassGlyphLimit || 0; 636 | self.renderOnDemand = message.data.renderOnDemand || false; 637 | self.dropAllAnimations = message.data.dropAllAnimations || false; 638 | removeRunDependency('worker-init'); 639 | postMessage({ 640 | target: "ready", 641 | }); 642 | break; 643 | } 644 | case 'oneshot-render': 645 | self.oneshotRender(message.data.lastRendered, 646 | message.data.renderNow || false, 647 | message.data.iteration); 648 | break; 649 | case 'destroy': 650 | self.octObj.quitLibrary(); 651 | break; 652 | case 'free-track': 653 | self.freeTrack(); 654 | break; 655 | case 'set-track': 656 | self.setTrack(message.data.content); 657 | break; 658 | case 'set-track-by-url': 659 | self.setTrackByUrl(message.data.url); 660 | break; 661 | case 'create-event': 662 | var event = message.data.event; 663 | var i = self.octObj.allocEvent(); 664 | var evnt_ptr = self.octObj.track.get_events(i); 665 | _applyKeys(event, evnt_ptr); 666 | break; 667 | case 'get-events': 668 | var events = []; 669 | for (var i = 0; i < self.octObj.getEventCount(); i++) { 670 | var evnt_ptr = self.octObj.track.get_events(i); 671 | var event = { 672 | _index: i, 673 | Start: evnt_ptr.get_Start(), 674 | Duration: evnt_ptr.get_Duration(), 675 | ReadOrder: evnt_ptr.get_ReadOrder(), 676 | Layer: evnt_ptr.get_Layer(), 677 | Style: evnt_ptr.get_Style(), 678 | Name: evnt_ptr.get_Name(), 679 | MarginL: evnt_ptr.get_MarginL(), 680 | MarginR: evnt_ptr.get_MarginR(), 681 | MarginV: evnt_ptr.get_MarginV(), 682 | Effect: evnt_ptr.get_Effect(), 683 | Text: evnt_ptr.get_Text() 684 | }; 685 | 686 | events.push(event); 687 | } 688 | postMessage({ 689 | target: "get-events", 690 | time: Date.now(), 691 | events: events 692 | }); 693 | break; 694 | case 'set-event': 695 | var event = message.data.event; 696 | var i = message.data.index; 697 | var evnt_ptr = self.octObj.track.get_events(i); 698 | _applyKeys(event, evnt_ptr); 699 | break; 700 | case 'remove-event': 701 | var i = message.data.index; 702 | self.octObj.removeEvent(i); 703 | break; 704 | case 'create-style': 705 | var style = message.data.style; 706 | var i = self.octObj.allocStyle(); 707 | var styl_ptr = self.octObj.track.get_styles(i); 708 | _applyKeys(style, styl_ptr); 709 | break; 710 | case 'get-styles': 711 | var styles = []; 712 | for (var i = 0; i < self.octObj.getStyleCount(); i++) { 713 | var styl_ptr = self.octObj.track.get_styles(i); 714 | var style = { 715 | _index: i, 716 | Name: styl_ptr.get_Name(), 717 | FontName: styl_ptr.get_FontName(), 718 | FontSize: styl_ptr.get_FontSize(), 719 | PrimaryColour: styl_ptr.get_PrimaryColour(), 720 | SecondaryColour: styl_ptr.get_SecondaryColour(), 721 | OutlineColour: styl_ptr.get_OutlineColour(), 722 | BackColour: styl_ptr.get_BackColour(), 723 | Bold: styl_ptr.get_Bold(), 724 | Italic: styl_ptr.get_Italic(), 725 | Underline: styl_ptr.get_Underline(), 726 | StrikeOut: styl_ptr.get_StrikeOut(), 727 | ScaleX: styl_ptr.get_ScaleX(), 728 | ScaleY: styl_ptr.get_ScaleY(), 729 | Spacing: styl_ptr.get_Spacing(), 730 | Angle: styl_ptr.get_Angle(), 731 | BorderStyle: styl_ptr.get_BorderStyle(), 732 | Outline: styl_ptr.get_Outline(), 733 | Shadow: styl_ptr.get_Shadow(), 734 | Alignment: styl_ptr.get_Alignment(), 735 | MarginL: styl_ptr.get_MarginL(), 736 | MarginR: styl_ptr.get_MarginR(), 737 | MarginV: styl_ptr.get_MarginV(), 738 | Encoding: styl_ptr.get_Encoding(), 739 | treat_fontname_as_pattern: styl_ptr.get_treat_fontname_as_pattern(), 740 | Blur: styl_ptr.get_Blur(), 741 | Justify: styl_ptr.get_Justify() 742 | }; 743 | styles.push(style); 744 | } 745 | postMessage({ 746 | target: "get-styles", 747 | time: Date.now(), 748 | styles: styles 749 | }); 750 | break; 751 | case 'set-style': 752 | var style = message.data.style; 753 | var i = message.data.index; 754 | var styl_ptr = self.octObj.track.get_styles(i); 755 | _applyKeys(style, styl_ptr); 756 | break; 757 | case 'remove-style': 758 | var i = message.data.index; 759 | self.octObj.removeStyle(i); 760 | break; 761 | case 'runBenchmark': { 762 | self.runBenchmark(); 763 | break; 764 | } 765 | case 'custom': { 766 | if (Module['onCustomMessage']) { 767 | Module['onCustomMessage'](message); 768 | } else { 769 | throw 'Custom message received but worker Module.onCustomMessage not implemented.'; 770 | } 771 | break; 772 | } 773 | case 'setimmediate': { 774 | if (Module['setImmediates']) Module['setImmediates'].shift()(); 775 | break; 776 | } 777 | default: 778 | throw 'wha? ' + message.data.target; 779 | } 780 | }; 781 | 782 | onmessage = onMessageFromMainEmscriptenThread; 783 | 784 | function postCustomMessage(data) { 785 | postMessage({target: 'custom', userData: data}); 786 | } 787 | 788 | self.runBenchmark = function (seconds, pos, async) { 789 | var totalTime = 0; 790 | var i = 0; 791 | pos = pos || 0; 792 | seconds = seconds || 60; 793 | var count = seconds * self.targetFps; 794 | var start = performance.now(); 795 | var longestFrame = 0; 796 | var run = function () { 797 | var t0 = performance.now(); 798 | 799 | pos += 1 / self.targetFps; 800 | self.setCurrentTime(pos); 801 | 802 | var t1 = performance.now(); 803 | var diff = t1 - t0; 804 | totalTime += diff; 805 | if (diff > longestFrame) { 806 | longestFrame = diff; 807 | } 808 | 809 | if (i < count) { 810 | i++; 811 | 812 | if (async) { 813 | self.requestAnimationFrame(run); 814 | return false; 815 | } 816 | else { 817 | return true; 818 | } 819 | } 820 | else { 821 | console.log("Performance fps: " + Math.round(1000 / (totalTime / count)) + ""); 822 | console.log("Real fps: " + Math.round(1000 / ((t1 - start) / count)) + ""); 823 | console.log("Total time: " + totalTime); 824 | console.log("Longest frame: " + Math.ceil(longestFrame) + "ms (" + Math.floor(1000 / longestFrame) + " fps)"); 825 | 826 | return false; 827 | } 828 | }; 829 | 830 | while (true) { 831 | if (!run()) { 832 | break; 833 | } 834 | } 835 | }; 836 | -------------------------------------------------------------------------------- /src/pre-worker.js: -------------------------------------------------------------------------------- 1 | var hasNativeConsole = typeof console !== "undefined"; 2 | 3 | // implement console methods if they're missing 4 | function makeCustomConsole() { 5 | var console = (function () { 6 | function postConsoleMessage(prefix, args) { 7 | postMessage({ 8 | target: "console-" + prefix, 9 | content: JSON.stringify(Array.prototype.slice.call(args)), 10 | }) 11 | } 12 | 13 | return { 14 | log: function() { 15 | postConsoleMessage("log", arguments); 16 | }, 17 | debug: function() { 18 | postConsoleMessage("debug", arguments); 19 | }, 20 | info: function() { 21 | postConsoleMessage("info", arguments); 22 | }, 23 | warn: function() { 24 | postConsoleMessage("warn", arguments); 25 | }, 26 | error: function() { 27 | postConsoleMessage("error", arguments); 28 | } 29 | } 30 | })(); 31 | 32 | return console; 33 | } 34 | 35 | /** 36 | * Test the subtitle file for Brotli compression. 37 | * @param {!string} url the URL of the subtitle file. 38 | * @returns {boolean} Brotli compression found or not. 39 | */ 40 | function isBrotliFile(url) { 41 | // Search for parameters 42 | var len = url.indexOf("?"); 43 | 44 | if (len === -1) { 45 | len = url.length; 46 | } 47 | 48 | if (url.endsWith(".br", len)) { 49 | console.warn("Support for manual brotli decompression is tentatively deprecated and " 50 | + "may be removed with the next release. Instead use HTTP's Content-Encoding.") 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | Module = Module || {}; 58 | 59 | Module["preRun"] = Module["preRun"] || []; 60 | 61 | Module["preRun"].push(function () { 62 | Module["FS_createPath"]("/", "fonts", true, true); 63 | Module["FS_createPath"]("/", "fontconfig", true, true); 64 | 65 | if (!self.subContent) { 66 | // We can use sync xhr cause we're inside Web Worker 67 | if (isBrotliFile(self.subUrl)) { 68 | self.subContent = Module["BrotliDecode"](readBinary(self.subUrl)) 69 | } else { 70 | self.subContent = read_(self.subUrl); 71 | } 72 | } 73 | 74 | if (self.availableFonts && self.availableFonts.length !== 0) { 75 | var sections = parseAss(self.subContent); 76 | for (var i = 0; i < sections.length; i++) { 77 | for (var j = 0; j < sections[i].body.length; j++) { 78 | if (sections[i].body[j].key === 'Style') { 79 | self.writeFontToFS(sections[i].body[j].value['Fontname']); 80 | } 81 | } 82 | } 83 | 84 | var regex = /\\fn([^\\}]*?)[\\}]/g; 85 | var matches; 86 | while (matches = regex.exec(self.subContent)) { 87 | self.writeFontToFS(matches[1]); 88 | } 89 | } 90 | 91 | if (self.subContent) { 92 | Module["FS"].writeFile("/sub.ass", self.subContent); 93 | } 94 | 95 | self.subContent = null; 96 | 97 | self.loadFontFile(".fallback-", self.fallbackFont); 98 | 99 | //Module["FS"].mount(Module["FS"].filesystems.IDBFS, {}, '/fonts'); 100 | var fontFiles = self.fontFiles || []; 101 | for (var i = 0; i < fontFiles.length; i++) { 102 | self.loadFontFile('font' + i + '-', fontFiles[i]); 103 | } 104 | }); 105 | 106 | Module['onRuntimeInitialized'] = function () { 107 | self.octObj = new Module.SubtitleOctopus(); 108 | 109 | self.changed = Module._malloc(4); 110 | 111 | self.octObj.initLibrary(screen.width, screen.height, "/fonts/.fallback-" + self.fallbackFont.split('/').pop()); 112 | self.octObj.setDropAnimations(!!self.dropAllAnimations); 113 | self.octObj.createTrack("/sub.ass"); 114 | self.ass_track = self.octObj.track; 115 | self.ass_library = self.octObj.ass_library; 116 | self.ass_renderer = self.octObj.ass_renderer; 117 | 118 | if (self.libassMemoryLimit > 0 || self.libassGlyphLimit > 0) { 119 | self.octObj.setMemoryLimits(self.libassGlyphLimit, self.libassMemoryLimit); 120 | } 121 | }; 122 | 123 | Module["print"] = function (text) { 124 | if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); 125 | console.log(text); 126 | }; 127 | Module["printErr"] = function (text) { 128 | if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); 129 | console.error(text); 130 | }; 131 | 132 | // Modified from https://github.com/kripken/emscripten/blob/6dc4ac5f9e4d8484e273e4dcc554f809738cedd6/src/proxyWorker.js 133 | if (!hasNativeConsole) { 134 | // we can't call Module.printErr because that might be circular 135 | var console = { 136 | log: function (x) { 137 | if (typeof dump === 'function') dump('log: ' + x + '\n'); 138 | }, 139 | debug: function (x) { 140 | if (typeof dump === 'function') dump('debug: ' + x + '\n'); 141 | }, 142 | info: function (x) { 143 | if (typeof dump === 'function') dump('info: ' + x + '\n'); 144 | }, 145 | warn: function (x) { 146 | if (typeof dump === 'function') dump('warn: ' + x + '\n'); 147 | }, 148 | error: function (x) { 149 | if (typeof dump === 'function') dump('error: ' + x + '\n'); 150 | }, 151 | }; 152 | } 153 | --------------------------------------------------------------------------------