├── .dockerignore ├── .github └── workflows │ └── emscripten.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile_licence ├── README.md ├── build ├── license_defaults ├── license_extract.sh ├── license_fullnotice ├── license_lint.awk └── patches │ ├── brotli │ ├── .gitkeep │ └── 0001-upstream_Enable-install-with-emscripten.patch │ ├── fontconfig │ ├── .gitkeep │ └── 0001-disable-pthreads.patch │ ├── freetype │ ├── .gitkeep │ └── 0002-disable-pthread.patch │ └── harfbuzz │ ├── .gitkeep │ └── 0001-force_disable_pthread.patch ├── dist ├── COPYRIGHT ├── default.woff2 ├── jassub-worker-modern.wasm ├── jassub-worker.js ├── jassub-worker.wasm ├── jassub-worker.wasm.js ├── jassub.es.js └── jassub.umd.js ├── functions.mk ├── index.d.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── run-buildah-build.sh ├── run-common.sh ├── run-docker-build.sh ├── src ├── JASSUB.cpp ├── jassub.js ├── pre-worker.js └── worker.js └── vite.build.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything for build image step, /code is passed via a volume 2 | * 3 | -------------------------------------------------------------------------------- /.github/workflows/emscripten.yml: -------------------------------------------------------------------------------- 1 | name: Emscripten 2 | 3 | on: 4 | push: 5 | branches: [main, 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.9.1 15 | with: 16 | access_token: ${{ github.token }} 17 | all_but_latest: true 18 | 19 | - name: Checkout Base Repo 20 | uses: actions/checkout@v4 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 -v "${PWD}":/code libass/jso:latest 34 | 35 | - name: Upload Nightly Build 36 | uses: actions/upload-artifact@v4 37 | if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') 38 | with: 39 | name: js 40 | path: dist 41 | 42 | 43 | update_gh-pages: 44 | if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' 45 | needs: build 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | with: 51 | ref: gh-pages 52 | 53 | - name: Download nightly build 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: js 57 | path: jassub/assets 58 | 59 | - name: Push new version 60 | run: | 61 | git config --global user.email "actions@noreply.github.com" 62 | git config --global user.name "GitHub Action" 63 | git add jassub/assets 64 | git commit -m "$(printf \ 65 | "Update binaries to latest nightly\n\nFrom %s" "${{ github.sha }}")" 66 | git push origin gh-pages 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | a.out.js 2 | a.out.wasm 3 | a.out.js.mem 4 | a.out 5 | a.wasm 6 | src/parser.out 7 | lib/expat/xmlwf/xmlwf 8 | .libs/ 9 | *.bc 10 | *.lo 11 | *.la 12 | *.o 13 | *.pyc 14 | *.tgz 15 | venv/ 16 | node_modules/ 17 | build/lib/* 18 | dist/js 19 | dist/libraries 20 | dist/license 21 | test 22 | .pnpm-store 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "array": "cpp", 4 | "string": "cpp", 5 | "string_view": "cpp" 6 | } 7 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/emscripten/emsdk:3.1.40 2 | 3 | RUN apt-get update && \ 4 | apt-get install curl -y --no-install-recommends && \ 5 | curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - &&\ 6 | apt-get install -y --no-install-recommends \ 7 | build-essential \ 8 | cmake \ 9 | git \ 10 | ragel \ 11 | patch \ 12 | libtool \ 13 | itstool \ 14 | pkg-config \ 15 | python3 \ 16 | gettext \ 17 | autopoint \ 18 | automake \ 19 | autoconf \ 20 | m4 \ 21 | gperf \ 22 | licensecheck \ 23 | nodejs \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | WORKDIR /code 27 | CMD ["bash", "-c", "sudo rm -rf dist/libraries; sudo rm -rf build/lib; make; sudo rm -rf dist/libraries; sudo rm -rf build/lib; env MODERN=1 make; sudo npm i; sudo node vite.build.js"] 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 JASSUB contributors 4 | Copyright (c) 2017-2021 JavascriptSubtitlesOctopus contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # JASSUB.js - Makefile 2 | 3 | # make - Build Dependencies and the JASSUB.js 4 | BASE_DIR:=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 5 | DIST_DIR:=$(BASE_DIR)dist/libraries 6 | 7 | export CFLAGS = -O3 -flto -s USE_PTHREADS=0 -fno-rtti -fno-exceptions 8 | export CXXFLAGS = $(CFLAGS) 9 | export PKG_CONFIG_PATH = $(DIST_DIR)/lib/pkgconfig 10 | export EM_PKG_CONFIG_PATH = $(PKG_CONFIG_PATH) 11 | 12 | SIMD_ARGS = \ 13 | -msimd128 \ 14 | -msse \ 15 | -msse2 \ 16 | -msse3 \ 17 | -mssse3 \ 18 | -msse4 \ 19 | -msse4.1 \ 20 | -msse4.2 \ 21 | -mavx \ 22 | -mavx2 \ 23 | -matomics \ 24 | -mnontrapping-fptoint 25 | 26 | ifeq (${MODERN},1) 27 | WORKER_NAME = jassub-worker-modern 28 | WORKER_ARGS = \ 29 | -s WASM=1 \ 30 | $(SIMD_ARGS) 31 | 32 | override CFLAGS += $(SIMD_ARGS) 33 | override CXXFLAGS += $(SIMD_ARGS) 34 | 35 | else 36 | WORKER_NAME = jassub-worker 37 | WORKER_ARGS = \ 38 | -s WASM=2 39 | 40 | endif 41 | 42 | all: jassub 43 | jassub: dist 44 | 45 | .PHONY: all jassub dist 46 | 47 | include functions.mk 48 | 49 | # FriBidi 50 | build/lib/fribidi/configure: lib/fribidi $(wildcard $(BASE_DIR)build/patches/fribidi/*.patch) 51 | $(call PREPARE_SRC_PATCHED,fribidi) 52 | cd build/lib/fribidi && $(RECONF_AUTO) 53 | 54 | $(DIST_DIR)/lib/libfribidi.a: build/lib/fribidi/configure 55 | cd build/lib/fribidi && \ 56 | $(call CONFIGURE_AUTO) && \ 57 | $(JSO_MAKE) -C lib/ fribidi-unicode-version.h && \ 58 | $(JSO_MAKE) -C lib/ install && \ 59 | $(JSO_MAKE) install-pkgconfigDATA 60 | 61 | # Expat 62 | build/lib/expat/configured: lib/expat 63 | $(call PREPARE_SRC_VPATH,expat) 64 | touch build/lib/expat/configured 65 | 66 | $(DIST_DIR)/lib/libexpat.a: build/lib/expat/configured 67 | cd build/lib/expat && \ 68 | $(call CONFIGURE_CMAKE,$(BASE_DIR)lib/expat/expat) \ 69 | -DEXPAT_BUILD_DOCS=off \ 70 | -DEXPAT_SHARED_LIBS=off \ 71 | -DEXPAT_BUILD_EXAMPLES=off \ 72 | -DEXPAT_BUILD_FUZZERS=off \ 73 | -DEXPAT_BUILD_TESTS=off \ 74 | -DEXPAT_BUILD_TOOLS=off \ 75 | && \ 76 | $(JSO_MAKE) install 77 | 78 | # Brotli 79 | build/lib/brotli/configured: lib/brotli $(wildcard $(BASE_DIR)build/patches/brotli/*.patch) 80 | $(call PREPARE_SRC_PATCHED,brotli) 81 | touch build/lib/brotli/configured 82 | 83 | $(DIST_DIR)/lib/libbrotlidec.a: $(DIST_DIR)/lib/libbrotlicommon.a 84 | $(DIST_DIR)/lib/libbrotlicommon.a: build/lib/brotli/configured 85 | cd build/lib/brotli && \ 86 | $(call CONFIGURE_CMAKE) && \ 87 | $(JSO_MAKE) install 88 | # Normalise static lib names 89 | cd $(DIST_DIR)/lib/ && \ 90 | for lib in *-static.a ; do mv "$$lib" "$${lib%-static.a}.a" ; done 91 | 92 | 93 | # Freetype without Harfbuzz 94 | build/lib/freetype/configure: lib/freetype $(wildcard $(BASE_DIR)build/patches/freetype/*.patch) 95 | $(call PREPARE_SRC_PATCHED,freetype) 96 | cd build/lib/freetype && $(RECONF_AUTO) 97 | 98 | build/lib/freetype/build_hb/dist_hb/lib/libfreetype.a: $(DIST_DIR)/lib/libbrotlidec.a build/lib/freetype/configure 99 | cd build/lib/freetype && \ 100 | mkdir -p build_hb && \ 101 | cd build_hb && \ 102 | $(call CONFIGURE_AUTO,..) \ 103 | --prefix="$$(pwd)/dist_hb" \ 104 | --with-brotli=yes \ 105 | --without-harfbuzz \ 106 | && \ 107 | $(JSO_MAKE) install 108 | 109 | # Harfbuzz 110 | build/lib/harfbuzz/configure: lib/harfbuzz $(wildcard $(BASE_DIR)build/patches/harfbuzz/*.patch) 111 | $(call PREPARE_SRC_PATCHED,harfbuzz) 112 | cd build/lib/harfbuzz && $(RECONF_AUTO) 113 | 114 | $(DIST_DIR)/lib/libharfbuzz.a: build/lib/freetype/build_hb/dist_hb/lib/libfreetype.a build/lib/harfbuzz/configure 115 | cd build/lib/harfbuzz && \ 116 | EM_PKG_CONFIG_PATH=$(PKG_CONFIG_PATH):$(BASE_DIR)build/lib/freetype/build_hb/dist_hb/lib/pkgconfig \ 117 | CFLAGS="-DHB_NO_MT $(CFLAGS)" \ 118 | CXXFLAGS="-DHB_NO_MT $(CFLAGS)" \ 119 | $(call CONFIGURE_AUTO) \ 120 | --with-freetype \ 121 | && \ 122 | cd src && \ 123 | $(JSO_MAKE) install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfigDATA 124 | 125 | # Freetype with Harfbuzz 126 | $(DIST_DIR)/lib/libfreetype.a: $(DIST_DIR)/lib/libharfbuzz.a $(DIST_DIR)/lib/libbrotlidec.a 127 | cd build/lib/freetype && \ 128 | EM_PKG_CONFIG_PATH=$(PKG_CONFIG_PATH):$(BASE_DIR)build/lib/freetype/build_hb/dist_hb/lib/pkgconfig \ 129 | $(call CONFIGURE_AUTO) \ 130 | --with-brotli=yes \ 131 | --with-harfbuzz \ 132 | && \ 133 | $(JSO_MAKE) install 134 | 135 | # Fontconfig 136 | build/lib/fontconfig/configure: lib/fontconfig $(wildcard $(BASE_DIR)build/patches/fontconfig/*.patch) 137 | $(call PREPARE_SRC_PATCHED,fontconfig) 138 | cd build/lib/fontconfig && $(RECONF_AUTO) 139 | 140 | $(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 141 | cd build/lib/fontconfig && \ 142 | $(call CONFIGURE_AUTO) \ 143 | --disable-docs \ 144 | --with-default-fonts=/fonts \ 145 | && \ 146 | $(JSO_MAKE) -C src/ install && \ 147 | $(JSO_MAKE) -C fontconfig/ install && \ 148 | $(JSO_MAKE) install-pkgconfigDATA 149 | 150 | 151 | # libass 152 | build/lib/libass/configured: lib/libass 153 | cd lib/libass && $(RECONF_AUTO) 154 | $(call PREPARE_SRC_VPATH,libass) 155 | touch build/lib/libass/configured 156 | 157 | $(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 158 | cd build/lib/libass && \ 159 | $(call CONFIGURE_AUTO,../../../lib/libass) \ 160 | --enable-large-tiles \ 161 | --enable-fontconfig \ 162 | && \ 163 | $(JSO_MAKE) install 164 | 165 | LIBASS_DEPS = \ 166 | $(DIST_DIR)/lib/libfribidi.a \ 167 | $(DIST_DIR)/lib/libbrotlicommon.a \ 168 | $(DIST_DIR)/lib/libbrotlidec.a \ 169 | $(DIST_DIR)/lib/libfreetype.a \ 170 | $(DIST_DIR)/lib/libexpat.a \ 171 | $(DIST_DIR)/lib/libharfbuzz.a \ 172 | $(DIST_DIR)/lib/libfontconfig.a \ 173 | $(DIST_DIR)/lib/libass.a 174 | 175 | 176 | dist: $(LIBASS_DEPS) dist/js/$(WORKER_NAME).js dist/js/jassub.js 177 | 178 | # Dist Files https://github.com/emscripten-core/emscripten/blob/3.1.38/src/settings.js 179 | 180 | # args for increasing performance 181 | # https://github.com/emscripten-core/emscripten/issues/13899 182 | PERFORMANCE_ARGS = \ 183 | -s BINARYEN_EXTRA_PASSES=--one-caller-inline-max-function-size=19306 \ 184 | -s INVOKE_RUN=0 \ 185 | -s DISABLE_EXCEPTION_CATCHING=1 \ 186 | -s TEXTDECODER=1 \ 187 | -s MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION=1 \ 188 | --no-heap-copy \ 189 | -flto \ 190 | -fno-exceptions \ 191 | -O3 192 | 193 | # args for reducing size 194 | SIZE_ARGS = \ 195 | -s POLYFILL=0 \ 196 | -s FILESYSTEM=0 \ 197 | -s AUTO_JS_LIBRARIES=0 \ 198 | -s AUTO_NATIVE_LIBRARIES=0 \ 199 | -s HTML5_SUPPORT_DEFERRING_USER_SENSITIVE_REQUESTS=0 \ 200 | -s INCOMING_MODULE_JS_API="[]" \ 201 | -s USE_SDL=0 \ 202 | -s MINIMAL_RUNTIME=1 203 | 204 | # args that are required for this to even work at all 205 | COMPAT_ARGS = \ 206 | -s EXPORTED_FUNCTIONS="['_malloc']" \ 207 | -s EXPORT_KEEPALIVE=1 \ 208 | -s EXPORTED_RUNTIME_METHODS="['getTempRet0', 'setTempRet0']" \ 209 | -s IMPORTED_MEMORY=1 \ 210 | -s MIN_CHROME_VERSION=27 \ 211 | -s MIN_SAFARI_VERSION=60005 \ 212 | -mbulk-memory \ 213 | --memory-init-file 0 214 | 215 | dist/js/$(WORKER_NAME).js: src/JASSUB.cpp src/worker.js src/pre-worker.js 216 | mkdir -p dist/js 217 | emcc src/JASSUB.cpp $(LIBASS_DEPS) \ 218 | $(WORKER_ARGS) \ 219 | $(PERFORMANCE_ARGS) \ 220 | $(SIZE_ARGS) \ 221 | $(COMPAT_ARGS) \ 222 | --pre-js src/pre-worker.js \ 223 | -s ENVIRONMENT=worker \ 224 | -s EXIT_RUNTIME=0 \ 225 | -s ALLOW_MEMORY_GROWTH=1 \ 226 | -s MODULARIZE=1 \ 227 | -s EXPORT_ES6=1 \ 228 | -lembind \ 229 | -o $@ 230 | 231 | dist/js/jassub.js: src/jassub.js 232 | mkdir -p dist/js 233 | cp src/jassub.js $@ 234 | 235 | # dist/license/all: 236 | # @#FIXME: allow -j in toplevel Makefile and reintegrate licence extraction into this file 237 | # make -j "$$(nproc)" -f Makefile_licence all 238 | 239 | # dist/js/COPYRIGHT: dist/license/all 240 | # cp "$<" "$@" 241 | 242 | # Clean Tasks 243 | 244 | clean: clean-dist clean-libs clean-jassub 245 | 246 | clean-dist: 247 | rm -frv dist/libraries/* 248 | rm -frv dist/js/* 249 | rm -frv dist/license/* 250 | clean-libs: 251 | rm -frv dist/libraries build/lib 252 | clean-jassub: 253 | cd src && git clean -fdX 254 | 255 | git-checkout: 256 | git submodule sync --recursive && \ 257 | git submodule update --init --recursive 258 | 259 | SUBMODULES := brotli expat fontconfig freetype fribidi harfbuzz libass 260 | git-smreset: $(addprefix git-, $(SUBMODULES)) 261 | 262 | $(foreach subm, $(SUBMODULES), $(eval $(call TR_GIT_SM_RESET,$(subm)))) 263 | 264 | server: # Node http server npm i -g http-server 265 | http-server 266 | 267 | .PHONY: clean clean-dist clean-libs clean-jassub git-checkout git-smreset server 268 | -------------------------------------------------------------------------------- /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 | 7 | LIB_LICENSES := brotli expat freetype fribidi fontconfig harfbuzz libass 8 | LIB_LICENSES_FINDOPT_brotli := -path ./research -prune -false -o 9 | LIB_LICENSES_FINDOPT_expat := -path ./expat/fuzz -prune -false -o 10 | LIB_LICENSES_FINDOPT_freetype := -path ./src/tools -prune -false -o 11 | LIB_LICENSES_FINDOPT_fribidi := -path ./bin -prune -false -o 12 | LIB_LICENSES_FINDOPT_harfbuzz := -path ./test -prune -false -o 13 | 14 | $(addprefix dist/license/, $(LIB_LICENSES)): dist/license/%: .git/modules/lib/%/HEAD build/license_extract.sh build/license_defaults 15 | @mkdir -p dist/license 16 | (cd "lib/$*" && FINDOPTS="$(LIB_LICENSES_FINDOPT_$*)" \ 17 | ../../build/license_extract.sh ../../build/license_defaults "$*" .) > $@ 18 | 19 | dist/license/jassub: .git/HEAD build/license_extract.sh 20 | @mkdir -p dist/license 21 | build/license_extract.sh build/license_defaults jassub src > dist/license/jassub 22 | 23 | dist/license/all: dist/license/jassub $(addprefix dist/license/, $(LIB_LICENSES)) build/license_fullnotice build/license_lint.awk 24 | @echo "# The following lists all copyright notices and licenses for the" > dist/license/all 25 | @echo "# work contained in JASSUB per project." >> dist/license/all 26 | @echo "" >> dist/license/all 27 | 28 | @echo "Concatenate extracted license info..." 29 | @$(foreach LIB_PROJ, jassub $(LIB_LICENSES), \ 30 | echo "# Project: $(LIB_PROJ)" >> dist/license/all && \ 31 | cat dist/license/$(LIB_PROJ) >> dist/license/all && \ 32 | ) : 33 | 34 | mv dist/license/all dist/license/all.tmp 35 | build/license_lint.awk dist/license/all.tmp build/license_fullnotice 36 | cat dist/license/all.tmp build/license_fullnotice > dist/license/all 37 | 38 | dist/js/COPYRIGHT: dist/license/all 39 | cp "$<" "$@" 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | JASSUB 3 |

4 |

5 | JavaScript SSA/ASS Subtitle Renderer. 6 |

7 | JASSUB is a JS wrapper for libass, which renders SSA/ASS subtitles directly in your browser. It uses Emscripten to compile libass' C++ code to WASM. 8 | 9 |

10 | Online Demos 11 |

12 | 13 | ## Features 14 | - Supports most SSA/ASS features (everything libass supports) 15 | - Supports all OpenType, TrueType and WOFF fonts, as well as embedded fonts 16 | - Supports anamorphic videos [(on browsers which support it)](https://caniuse.com/mdn-api_htmlvideoelement_requestvideoframecallback) 17 | - Supports different video color spaces [(on browsers which support it)](https://caniuse.com/mdn-api_videocolorspace) 18 | - Capable of using local fonts [(on browsers which support it)](https://caniuse.com/mdn-api_window_querylocalfonts) 19 | - Works fast (all the heavy lifting is done by WebAssembly) 20 | - Is fully threaded (on browsers which support it, it's capable of working fully on a separate thread) 21 | - Is asynchronous (renders when available, not in order of execution) 22 | - Benefits from hardware acceleration (uses hardware accelerated canvas API's) 23 | - Doesn't manipulate the DOM to render subtitles 24 | - Easy to use - just connect it to video element 25 | 26 | ## Isn't this just the same thing as JavascriptSubtitlesOctopus? 27 | No. See this comparison. 28 | 29 | ## Usage 30 | By default all you need to do is copy the files from the `dist/` folder of the repository into the same folder as where your JS runs, then do: 31 | ```js 32 | import JASSUB from './jassub.es.js' 33 | 34 | const renderer = new JASSUB({ 35 | video: document.querySelector('video'), 36 | subUrl: './tracks/sub.ass' 37 | }) 38 | ``` 39 | `Note:` while the `dist/` folder includes a UMD dist it still uses modern syntax. If you want backwards compatibility with older browsers I recommend you run it tru babel. 40 | 41 | If you use a bundler like Vite, you can instead do: 42 | ```shell 43 | npm i jassub 44 | ``` 45 | 46 | ```js 47 | import JASSUB from 'jassub' 48 | import workerUrl from 'jassub/dist/jassub-worker.js?url' 49 | import wasmUrl from 'jassub/dist/jassub-worker.wasm?url' 50 | 51 | const renderer = new JASSUB({ 52 | video: document.querySelector('video'), 53 | subContent: subtitleString, 54 | workerUrl, // you can also use: `new URL('jassub/dist/jassub-worker.js', import.meta.url)` instead of importing it as an url 55 | wasmUrl 56 | }) 57 | ``` 58 | ## Using only with canvas 59 | You're also able to use it without any video. However, that requires you to set the time the subtitles should render at yourself: 60 | ```js 61 | import JASSUB from './jassub.es.js' 62 | 63 | const renderer = new JASSUB({ 64 | canvas: document.querySelector('canvas'), 65 | subUrl: './tracks/sub.ass' 66 | }) 67 | 68 | renderer.setCurrentTime(15) 69 | ``` 70 | ## Changing subtitles 71 | You're not limited to only display the subtitle file you referenced in your options. You're able to dynamically change subtitles on the fly. There's three methods that you can use for this specifically: 72 | 73 | - `setTrackByUrl(url):` works the same as the `subUrl` option. It will set the subtitle to display by its URL. 74 | - `setTrack(content):` works the same as the `subContent` option. It will set the subtitle to dispaly by its content. 75 | - `freeTrack():` this simply removes the subtitles. You can use the two methods above to set a new subtitle file to be displayed. 76 | ```js 77 | renderer.setTrackByUrl('/newsub.ass') 78 | ``` 79 | ## Cleaning up the object 80 | After you're finished with rendering the subtitles. You need to call the `destroy()` method to correctly destroy the object. 81 | ```js 82 | const renderer = new JASSUB(options) 83 | // After you've finished using it... 84 | renderer.destroy() 85 | ``` 86 | ## Options 87 | The default options are best, and automatically fallback to the next fastest options in line, when the API's they use are unsupported. You can however forcefully change this behavior by specifying options. These options are included in the JSDoc of the object, so if your editor supports JSDoc IntelliSense you will see these exact descriptions when calling methods and specifying options. 88 | 89 | - `{Object} options` Settings object. 90 | - `{HTMLVideoElement} options.video` Video to use as target for rendering and event listeners. Optional if canvas is specified instead. 91 | - `{HTMLCanvasElement} options.canvas` { Optional } Canvas to use for manual handling. Not required if video is specified. 92 | - `{'js'|'wasm'} options.blendMode` { Optional = 'js' } Which image blending mode to use. WASM will perform better on lower end devices, JS will perform better if the device and browser supports hardware acceleration. 93 | - `{Boolean} options.asyncRender` { Optional = true } Whether or not to use async rendering, which offloads the CPU by creating image bitmaps on the GPU. 94 | - `{Boolean} options.offscreenRender` { Optional = true } Whether or not to render things fully on the worker, greatly reduces CPU usage. 95 | - `{Boolean} options.onDemandRender` { Optional = true } Whether or not to render subtitles as the video player renders frames, rather than predicting which frame the player is on using events. 96 | - `{Number} options.targetFps` { Optional = true } Target FPS to render subtitles at. Ignored when onDemandRender is enabled. 97 | - `{Number} options.timeOffset` { Optional = 0 } Subtitle time offset in seconds. 98 | - `{Boolean} options.debug` { Optional = false } Whether or not to print debug information. 99 | - `{Number} options.prescaleFactor` { Optional = 1.0 } Scale down (< 1.0) the subtitles canvas to improve performance at the expense of quality, or scale it up (> 1.0). 100 | - `{Number} options.prescaleHeightLimit` { Optional = 1080 } The height in pixels beyond which the subtitles canvas won't be prescaled. 101 | - `{Number} options.maxRenderHeight` { Optional = 0 } The maximum rendering height in pixels of the subtitles canvas. Beyond this subtitles will be upscaled by the browser. 102 | - `{Boolean} options.dropAllAnimations` { Optional = false } Attempt to discard all animated tags. Enabling this may severly mangle complex subtitles and should only be considered as an last ditch effort of uncertain success for hardware otherwise incapable of displaing anything. Will not reliably work with manually edited or allocated events. 103 | - `{Boolean} options.dropAllBlur` { Optional = false } The holy grail of performance gains. If heavy TS lags a lot, disabling this will make it ~x10 faster. This drops blur from all added subtitle tracks making most text and backgrounds look sharper, this is way less intrusive than dropping all animations, while still offering major performance gains. 104 | - `{String} options.workerUrl` { Optional = 'jassub-worker.js' } The URL of the worker. 105 | - `{String} options.wasmUrl` { Optional = 'jassub-worker.wasm' } The URL of the worker WASM. 106 | - `{String} options.legacyWasmUrl` { Optional = 'jassub-worker.wasm.js' } The URL of the legacy worker WASM. Only loaded if the browser doesn't support WASM. 107 | - `{String} options.modernWasmUrl` { Optional } The URL of the modern worker WASM. This includes faster ASM instructions, but is only supported by newer browsers, disabled if the URL isn't defined. 108 | - `{String} [options.subUrl=options.subContent]` The URL of the subtitle file to play. 109 | - `{String} [options.subContent=options.subUrl]` The content of the subtitle file to play. 110 | - `{String[]|Uint8Array[]} options.fonts` { Optional } An array of links or Uint8Arrays to the fonts used in the subtitle. If Uint8Array is used the array is copied, not referenced. This forces all the fonts in this array to be loaded by the renderer, regardless of if they are used. 111 | - `{Object} options.availableFonts` { Optional = {'liberation sans': './default.woff2'}} Object with all available fonts - Key is font family in lower case, value is link or Uint8Array: { arial: '/font1.ttf' }. These fonts are selectively loaded if detected as used in the current subtitle track. 112 | - `{String} options.fallbackFont` { Optional = 'liberation sans' } The font family key of the fallback font in availableFonts to use if the other font for the style is missing special glyphs or unicode. 113 | - `{Boolean} options.useLocalFonts` { Optional = false } If the Local Font Access API is enabled [chrome://flags/#font-access], the library will query for permissions to use local fonts and use them if any are missing. The permission can be queried beforehand using navigator.permissions.request({ name: 'local-fonts' }). 114 | - `{Number} options.libassMemoryLimit` { Optional } libass bitmap cache memory limit in MiB (approximate). 115 | - `{Number} options.libassGlyphLimit` { Optional } libass glyph cache memory limit in MiB (approximate). 116 | 117 | ## Methods and properties 118 | This library has a lot of methods and properties, however many aren't made for manual use or have no effect when changing, those are usually prefixed with `_`. Most of these never need to be called by the user. 119 | 120 | ### List of properties: 121 | - `debug` - -||- 122 | - `prescaleFactor` - -||- 123 | - `prescaleHeightLimit` - -||- 124 | - `maxRenderHeight` - -||- 125 | - `busy` - Boolean which specifies if the renderer is currently busy. 126 | - `timeOffset` - -||- 127 | ### List of methods: 128 | - `resize(width = 0, height = 0, top = 0, left = 0, force)` - Resize the canvas to given parameters. Auto-generated if values are ommited. 129 | - {Number} [width=0] 130 | - {Number} [height=0] 131 | - {Number} [top=0] 132 | - {Number} [left=0] 133 | - {Boolean} force 134 | - `setVideo(video)` - Change the video to use as target for event listeners. 135 | - {HTMLVideoElement} video 136 | - `setTrackByUrl(url)` - Overwrites the current subtitle content. 137 | - {String} url URL to load subtitles from. 138 | - `setTrack(content)` - Overwrites the current subtitle content. 139 | - {String} content Content of the ASS file. 140 | - `freeTrack()` - Free currently used subtitle track. 141 | - `setIsPaused(isPaused)` - Sets the playback state of the media. 142 | - {Boolean} isPaused Pause/Play subtitle playback. 143 | - `setRate(rate)` - Sets the playback rate of the media [speed multiplier]. 144 | - {Number} rate Playback rate. 145 | - `setCurrentTime(isPaused, currentTime, rate)` - Sets the current time, playback state and rate of the subtitles. 146 | - {Boolean} [isPaused] Pause/Play subtitle playback. 147 | - {Number} [currentTime] Time in seconds. 148 | - {Number} [rate] Playback rate. 149 | - `destroy(err)` - Destroy the object, worker, listeners and all data. 150 | - {String} [err] Error to throw when destroying. 151 | - `sendMessage(target, data = {}, transferable)` - Send data and execute function in the worker. 152 | - {String} target Target function. 153 | - {Object} [data] Data for function. 154 | - {Transferable[]} [transferable] Array of transferables. 155 | - `createEvent(event)` - Create a new ASS event directly. 156 | - {ASS_Event} event 157 | - `setEvent(event, index)` - Overwrite the data of the event with the specified index. 158 | - {ASS_Event} event 159 | - {Number} index 160 | - `removeEvent(index)` - Remove the event with the specified index. 161 | - {Number} index 162 | - `getEvents(callback)` - Get all ASS events. 163 | - {function(Error|null, ASS_Event)} callback Function to callback when worker returns the events. 164 | - `createStyle(style)` - Create a new ASS style directly. 165 | - {ASS_Style} event 166 | - `setStyle (event, index)` - Overwrite the data of the style with the specified index. 167 | - {ASS_Style} event 168 | - {Number} index 169 | - `removeStyle (index)` - Remove the style with the specified index. 170 | - {Number} index 171 | - `getStyles (callback)` - Get all ASS styles. 172 | - {function(Error|null, ASS_Style)} callback Function to callback when worker returns the styles. 173 | - `addfont (font)` - Adds a font to the renderer. 174 | - {String|Uint8Array} font Font to add. 175 | 176 | ### ASS_Event object properties 177 | - `{Number} Start` - Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will appear onscreen. Note that there is a single digit for the hours! 178 | - `{Number} Duration` - End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will disappear offscreen. Note that there is a single digit for the hours! 179 | - `{String} Style` - Style name. If it is "Default", then your own *Default style will be subtituted. 180 | - `{String} Name` - Character name. This is the name of the character who speaks the dialogue. It is for information only, to make the script is easier to follow when editing/timing. 181 | - `{Number} MarginL` - 4-figure Left Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 182 | - `{Number} MarginR` - 4-figure Right Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 183 | - `{Number} MarginV` - 4-figure Bottom Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 184 | - `{String} Effect` - Transition Effect. This is either empty, or contains information for one of the three transition effects implemented in SSA v4.x 185 | - `{String} Text` - Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. Everything after the 9th comma is treated as the subtitle text, so it can include commas. 186 | - `{Number} ReadOrder` - Number in order of which to read this event. 187 | - `{Number} Layer` - Z-index overlap in which to render this event. 188 | - `{Number} _index` - (Internal) index of the event. 189 | 190 | ### ASS_Style object properties 191 | - `{String} Name` The name of the Style. Case sensitive. Cannot include commas. 192 | - `{String} FontName` The fontname as used by Windows. Case-sensitive. 193 | - `{Number} FontSize` Font size. 194 | - `{Number} PrimaryColour` A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 195 | - `{Number} SecondaryColour` A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 196 | - `{Number} OutlineColour` A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 197 | - `{Number} BackColour` This is the colour of the subtitle outline or shadow, if these are used. A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR. 198 | - `{Number} Bold` This defines whether text is bold (true) or not (false). -1 is True, 0 is False. This is independant of the Italic attribute - you can have have text which is both bold and italic. 199 | - `{Number} Italic` Italic. This defines whether text is italic (true) or not (false). -1 is True, 0 is False. This is independant of the bold attribute - you can have have text which is both bold and italic. 200 | - `{Number} Underline` -1 or 0 201 | - `{Number} StrikeOut` -1 or 0 202 | - `{Number} ScaleX` Modifies the width of the font. [percent] 203 | - `{Number} ScaleY` Modifies the height of the font. [percent] 204 | - `{Number} Spacing` Extra space between characters. [pixels] 205 | - `{Number} Angle` The origin of the rotation is defined by the alignment. Can be a floating point number. [degrees] 206 | - `{Number} BorderStyle` 1=Outline + drop shadow, 3=Opaque box 207 | - `{Number} Outline` If BorderStyle is 1, then this specifies the width of the outline around the text, in pixels. Values may be 0, 1, 2, 3 or 4. 208 | - `{Number} Shadow` If BorderStyle is 1, then this specifies the depth of the drop shadow behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in addition to an outline - SSA will force an outline of 1 pixel if no outline width is given. 209 | - `{Number} Alignment` This sets how text is "justified" within the Left/Right onscreen margins, and also the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified toptitle 210 | - `{Number} MarginL` This defines the Left Margin in pixels. It is the distance from the left-hand edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed. 211 | - `{Number} MarginR` This defines the Right Margin in pixels. It is the distance from the right-hand edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed. 212 | - `{Number} MarginV` This defines the vertical Left Margin in pixels. For a subtitle, it is the distance from the bottom of the screen. For a toptitle, it is the distance from the top of the screen. For a midtitle, the value is ignored - the text will be vertically centred. 213 | - `{Number} Encoding` This specifies the font character set or encoding and on multi-lingual Windows installations it provides access to characters used in multiple than one languages. It is usually 0 (zero) for English (Western, ANSI) Windows. 214 | - `{Number} treat_fontname_as_pattern` 215 | - `{Number} Blur` 216 | - `{Number} Justify` 217 | 218 | # How to build? 219 | ## Dependencies 220 | - git 221 | - emscripten (Configure the enviroment) 222 | - make 223 | - python3 224 | - cmake 225 | - pkgconfig 226 | - patch 227 | - libtool 228 | - autotools (autoconf, automake, autopoint) 229 | - gettext 230 | - ragel - Required by Harfbuzz 231 | - itstool - Required by Fontconfig 232 | - gperf - Required by Fontconfig 233 | - licensecheck 234 | 235 | ## Get the Source 236 | Run git clone --recursive https://github.com/ThaUnknown/jassub.git 237 | 238 | ## Build inside a Container 239 | ### Docker 240 | 1. Install Docker 241 | 2. ./run-docker-build.sh 242 | 3. Artifacts are in /dist/js 243 | ### Buildah 244 | 1. Install Buildah and a suitable backend for buildah run like crun or runc 245 | 2. ./run-buildah-build.sh 246 | 3. Artifacts are in /dist/js 247 | ## Build without Containers 248 | 1. Install the dependency packages listed above 249 | 2. make 250 | - If on macOS with libtool from brew, LIBTOOLIZE=glibtoolize make 251 | 3. Artifacts are in /dist/js 252 | -------------------------------------------------------------------------------- /build/license_defaults: -------------------------------------------------------------------------------- 1 | # Tab seperated list of default license 2 | # and if given copyrightholder per project 3 | jassub Expat 2022-2022 JASSUB contributors 4 | subtitlesoctopus Expat 2017-2021 JavascriptSubtitlesOctopus contributors 5 | 6 | brotli Expat 2009, 2010, 2013-2016 by the Brotli Authors 7 | expat Expat 2000-2017 Expat development team / 1997-2000 Thai Open Source Software Center Ltd 8 | libass ISC 2006-2016 libass contributors 9 | fontconfig NTP~disclaimer 2000-2007 Keith Packard / 2005 Patrick Lam / 2009 Roozbeh Pournader / 2008,2009 Red Hat, Inc. / 2008 Danilo Šegan / 2012 Google, Inc. 10 | 11 | fribidi LGPL-2.1+ 12 | harfbuzz MIT~old 13 | freetype FTL 14 | -------------------------------------------------------------------------------- /build/license_extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 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 | 44 | find "$base_dir" $FINDOPTS -type f -regextype egrep -regex '.*\.(c|h|cpp|hpp|js)$' -exec \ 45 | licensecheck --machine --copyright --deb-fmt '{}' \; \ 46 | | awk -F"$tabulator" -v base_dir="$base_dir" \ 47 | -v def_license="$def_license" -v def_copy="$def_copy" ' 48 | BEGIN { 49 | split("", lcfiles) # Clear array with only pre-Issue 8 POSIX 50 | SUBSEP = sprintf("\n") # Seperator for pseudo-multidim arrays 51 | 52 | # Add default 53 | if(def_copy && def_license) 54 | ++lcfiles[def_license, def_copy] 55 | } 56 | 57 | 1 { 58 | if ($2 == "UNKNOWN") 59 | if (def_license) { 60 | $2 = def_license 61 | }else { 62 | print "ERROR: Unlicensed file "$1" matched!" | "cat>&2" 63 | print " If there is no default license, then" | "cat>&2" 64 | print " reporting this upstream might be a good idea." | "cat>&2" 65 | exit 1 66 | } 67 | if ($3 == "*No copyright*") { 68 | if (def_copy) { 69 | $3 = def_copy 70 | } else if (def_license != $2) { 71 | print "ERROR: Orphaned file "$1" and no default attribution!" | "cat>&2" 72 | exit 1 73 | } else { 74 | # Appears eg in freetype ; hope default copyright holder is correct 75 | next 76 | } 77 | } 78 | 79 | split($3, copyh, " / ") 80 | for(i in copyh) 81 | ++lcfiles[$2, copyh[i]]; 82 | } 83 | 84 | END { 85 | # Group copyright holders per license 86 | # The second pass in END is required to only add each (license, copy) pair once 87 | # using pure POSIX-only awk. 88 | split("", tmp) 89 | for(lc in lcfiles) { 90 | split(lc, lico, SUBSEP) 91 | if(lico[1] in tmp) 92 | tmp[lico[1]] = tmp[lico[1]]""SUBSEP" "lico[2] 93 | else 94 | tmp[lico[1]] = lico[2] 95 | } 96 | 97 | for(license in tmp) { 98 | printf "License: %s\n", license 99 | printf "Copyright: %s\n", tmp[license] 100 | printf "\n" 101 | } 102 | } 103 | ' 104 | -------------------------------------------------------------------------------- /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 JASSUB 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: Expat 15 | Permission is hereby granted, free of charge, to any person obtaining 16 | a copy of this software and associated documentation files (the 17 | "Software"), to deal in the Software without restriction, including 18 | without limitation the rights to use, copy, modify, merge, publish, 19 | distribute, sublicense, and/or sell copies of the Software, and to permit 20 | persons to whom the Software is furnished to do so, subject to the 21 | following conditions: 22 | . 23 | The above copyright notice and this permission notice shall be included 24 | in all copies or substantial portions of the Software. 25 | . 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 29 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 30 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 31 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 32 | USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | License: FTL 35 | Portions of this software are copyright © 2021 The FreeType 36 | Project (www.freetype.org). All rights reserved. 37 | A copy of the FreeType License can be obtained at 38 | https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/docs/FTL.TXT 39 | 40 | License: ISC 41 | Permission to use, copy, modify, and/or distribute this software for any 42 | purpose with or without fee is hereby granted, provided that the above 43 | copyright notice and this permission notice appear in all copies. 44 | . 45 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 46 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 47 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 48 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 49 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 50 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 51 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 52 | 53 | License: MIT~old 54 | Permission is hereby granted, without written agreement and without 55 | license or royalty fees, to use, copy, modify, and distribute this 56 | software and its documentation for any purpose, provided that the 57 | above copyright notice and the following two paragraphs appear in 58 | all copies of this software. 59 | . 60 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR 61 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 62 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 63 | IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 64 | DAMAGE. 65 | . 66 | THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 67 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 68 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 69 | ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO 70 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS 71 | 72 | License: NTP 73 | Permission to use, copy, modify, and distribute this 74 | software and its documentation for any purpose and without 75 | fee is hereby granted, provided that the above copyright 76 | notice appear in all copies. The University of California 77 | makes no representations about the suitability of this 78 | software for any purpose. It is provided "as is" without 79 | express or implied warranty. 80 | 81 | License: NTP~disclaimer 82 | Permission to use, copy, modify, distribute, and sell this software and its 83 | documentation for any purpose is hereby granted without fee, provided that 84 | the above copyright notice appear in all copies and that both that 85 | copyright notice and this permission notice appear in supporting 86 | documentation, and that the name of the author(s) not be used in 87 | advertising or publicity pertaining to distribution of the software without 88 | specific, written prior permission. The authors make no 89 | representations about the suitability of this software for any purpose. It 90 | is provided "as is" without express or implied warranty. 91 | . 92 | THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, 93 | INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO 94 | EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR 95 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 96 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 97 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 98 | PERFORMANCE OF THIS SOFTWARE. 99 | 100 | License: Zlib 101 | This software is provided 'as-is', without any express or implied 102 | warranty. In no event will the authors be held liable for any damages 103 | arising from the use of this software. 104 | . 105 | Permission is granted to anyone to use this software for any purpose, 106 | including commercial applications, and to alter it and redistribute it 107 | freely, subject to the following restrictions: 108 | . 109 | 1. The origin of this software must not be misrepresented; you must not 110 | claim that you wrote the original software. If you use this software 111 | in a product, an acknowledgment in the product documentation would be 112 | appreciated but is not required. 113 | 2. Altered source versions must be plainly marked as such, and must not be 114 | misrepresented as being the original software. 115 | 3. This notice may not be removed or altered from any source distribution. 116 | 117 | License: BSL-1 118 | Boost Software License - Version 1.0 - August 17th, 2003 119 | . 120 | Permission is hereby granted, free of charge, to any person or organization 121 | obtaining a copy of the software and accompanying documentation covered by 122 | this license (the "Software") to use, reproduce, display, distribute, 123 | execute, and transmit the Software, and to prepare derivative works of the 124 | Software, and to permit third-parties to whom the Software is furnished to 125 | do so, all subject to the following: 126 | . 127 | The copyright notices in the Software and this entire statement, including 128 | the above license grant, this restriction and the following disclaimer, 129 | must be included in all copies of the Software, in whole or in part, and 130 | all derivative works of the Software, unless such copies or derivative 131 | works are solely in the form of machine-executable object code generated by 132 | a source language processor. 133 | . 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 135 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 136 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 137 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 138 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 139 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 140 | DEALINGS IN THE SOFTWARE. 141 | 142 | License: LGPL-2.1 143 | GNU LESSER GENERAL PUBLIC LICENSE 144 | Version 2.1, February 1999 145 | . 146 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 147 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 148 | Everyone is permitted to copy and distribute verbatim copies 149 | of this license document, but changing it is not allowed. 150 | . 151 | [This is the first released version of the Lesser GPL. It also counts 152 | as the successor of the GNU Library Public License, version 2, hence 153 | the version number 2.1.] 154 | . 155 | Preamble 156 | . 157 | The licenses for most software are designed to take away your 158 | freedom to share and change it. By contrast, the GNU General Public 159 | Licenses are intended to guarantee your freedom to share and change 160 | free software--to make sure the software is free for all its users. 161 | . 162 | This license, the Lesser General Public License, applies to some 163 | specially designated software packages--typically libraries--of the 164 | Free Software Foundation and other authors who decide to use it. You 165 | can use it too, but we suggest you first think carefully about whether 166 | this license or the ordinary General Public License is the better 167 | strategy to use in any particular case, based on the explanations below. 168 | . 169 | When we speak of free software, we are referring to freedom of use, 170 | not price. Our General Public Licenses are designed to make sure that 171 | you have the freedom to distribute copies of free software (and charge 172 | for this service if you wish); that you receive source code or can get 173 | it if you want it; that you can change the software and use pieces of 174 | it in new free programs; and that you are informed that you can do 175 | these things. 176 | . 177 | To protect your rights, we need to make restrictions that forbid 178 | distributors to deny you these rights or to ask you to surrender these 179 | rights. These restrictions translate to certain responsibilities for 180 | you if you distribute copies of the library or if you modify it. 181 | . 182 | For example, if you distribute copies of the library, whether gratis 183 | or for a fee, you must give the recipients all the rights that we gave 184 | you. You must make sure that they, too, receive or can get the source 185 | code. If you link other code with the library, you must provide 186 | complete object files to the recipients, so that they can relink them 187 | with the library after making changes to the library and recompiling 188 | it. And you must show them these terms so they know their rights. 189 | . 190 | We protect your rights with a two-step method: (1) we copyright the 191 | library, and (2) we offer you this license, which gives you legal 192 | permission to copy, distribute and/or modify the library. 193 | . 194 | To protect each distributor, we want to make it very clear that 195 | there is no warranty for the free library. Also, if the library is 196 | modified by someone else and passed on, the recipients should know 197 | that what they have is not the original version, so that the original 198 | author's reputation will not be affected by problems that might be 199 | introduced by others. 200 | . 201 | Finally, software patents pose a constant threat to the existence of 202 | any free program. We wish to make sure that a company cannot 203 | effectively restrict the users of a free program by obtaining a 204 | restrictive license from a patent holder. Therefore, we insist that 205 | any patent license obtained for a version of the library must be 206 | consistent with the full freedom of use specified in this license. 207 | . 208 | Most GNU software, including some libraries, is covered by the 209 | ordinary GNU General Public License. This license, the GNU Lesser 210 | General Public License, applies to certain designated libraries, and 211 | is quite different from the ordinary General Public License. We use 212 | this license for certain libraries in order to permit linking those 213 | libraries into non-free programs. 214 | . 215 | When a program is linked with a library, whether statically or using 216 | a shared library, the combination of the two is legally speaking a 217 | combined work, a derivative of the original library. The ordinary 218 | General Public License therefore permits such linking only if the 219 | entire combination fits its criteria of freedom. The Lesser General 220 | Public License permits more lax criteria for linking other code with 221 | the library. 222 | . 223 | We call this license the "Lesser" General Public License because it 224 | does Less to protect the user's freedom than the ordinary General 225 | Public License. It also provides other free software developers Less 226 | of an advantage over competing non-free programs. These disadvantages 227 | are the reason we use the ordinary General Public License for many 228 | libraries. However, the Lesser license provides advantages in certain 229 | special circumstances. 230 | . 231 | For example, on rare occasions, there may be a special need to 232 | encourage the widest possible use of a certain library, so that it becomes 233 | a de-facto standard. To achieve this, non-free programs must be 234 | allowed to use the library. A more frequent case is that a free 235 | library does the same job as widely used non-free libraries. In this 236 | case, there is little to gain by limiting the free library to free 237 | software only, so we use the Lesser General Public License. 238 | . 239 | In other cases, permission to use a particular library in non-free 240 | programs enables a greater number of people to use a large body of 241 | free software. For example, permission to use the GNU C Library in 242 | non-free programs enables many more people to use the whole GNU 243 | operating system, as well as its variant, the GNU/Linux operating 244 | system. 245 | . 246 | Although the Lesser General Public License is Less protective of the 247 | users' freedom, it does ensure that the user of a program that is 248 | linked with the Library has the freedom and the wherewithal to run 249 | that program using a modified version of the Library. 250 | . 251 | The precise terms and conditions for copying, distribution and 252 | modification follow. Pay close attention to the difference between a 253 | "work based on the library" and a "work that uses the library". The 254 | former contains code derived from the library, whereas the latter must 255 | be combined with the library in order to run. 256 | . 257 | GNU LESSER GENERAL PUBLIC LICENSE 258 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 259 | . 260 | 0. This License Agreement applies to any software library or other 261 | program which contains a notice placed by the copyright holder or 262 | other authorized party saying it may be distributed under the terms of 263 | this Lesser General Public License (also called "this License"). 264 | Each licensee is addressed as "you". 265 | . 266 | A "library" means a collection of software functions and/or data 267 | prepared so as to be conveniently linked with application programs 268 | (which use some of those functions and data) to form executables. 269 | . 270 | The "Library", below, refers to any such software library or work 271 | which has been distributed under these terms. A "work based on the 272 | Library" means either the Library or any derivative work under 273 | copyright law: that is to say, a work containing the Library or a 274 | portion of it, either verbatim or with modifications and/or translated 275 | straightforwardly into another language. (Hereinafter, translation is 276 | included without limitation in the term "modification".) 277 | . 278 | "Source code" for a work means the preferred form of the work for 279 | making modifications to it. For a library, complete source code means 280 | all the source code for all modules it contains, plus any associated 281 | interface definition files, plus the scripts used to control compilation 282 | and installation of the library. 283 | . 284 | Activities other than copying, distribution and modification are not 285 | covered by this License; they are outside its scope. The act of 286 | running a program using the Library is not restricted, and output from 287 | such a program is covered only if its contents constitute a work based 288 | on the Library (independent of the use of the Library in a tool for 289 | writing it). Whether that is true depends on what the Library does 290 | and what the program that uses the Library does. 291 | 292 | 1. You may copy and distribute verbatim copies of the Library's 293 | complete source code as you receive it, in any medium, provided that 294 | you conspicuously and appropriately publish on each copy an 295 | appropriate copyright notice and disclaimer of warranty; keep intact 296 | all the notices that refer to this License and to the absence of any 297 | warranty; and distribute a copy of this License along with the 298 | Library. 299 | . 300 | You may charge a fee for the physical act of transferring a copy, 301 | and you may at your option offer warranty protection in exchange for a 302 | fee. 303 | . 304 | 2. You may modify your copy or copies of the Library or any portion 305 | of it, thus forming a work based on the Library, and copy and 306 | distribute such modifications or work under the terms of Section 1 307 | above, provided that you also meet all of these conditions: 308 | . 309 | a) The modified work must itself be a software library. 310 | . 311 | b) You must cause the files modified to carry prominent notices 312 | stating that you changed the files and the date of any change. 313 | . 314 | c) You must cause the whole of the work to be licensed at no 315 | charge to all third parties under the terms of this License. 316 | . 317 | d) If a facility in the modified Library refers to a function or a 318 | table of data to be supplied by an application program that uses 319 | the facility, other than as an argument passed when the facility 320 | is invoked, then you must make a good faith effort to ensure that, 321 | in the event an application does not supply such function or 322 | table, the facility still operates, and performs whatever part of 323 | its purpose remains meaningful. 324 | . 325 | (For example, a function in a library to compute square roots has 326 | a purpose that is entirely well-defined independent of the 327 | application. Therefore, Subsection 2d requires that any 328 | application-supplied function or table used by this function must 329 | be optional: if the application does not supply it, the square 330 | root function must still compute square roots.) 331 | . 332 | These requirements apply to the modified work as a whole. If 333 | identifiable sections of that work are not derived from the Library, 334 | and can be reasonably considered independent and separate works in 335 | themselves, then this License, and its terms, do not apply to those 336 | sections when you distribute them as separate works. But when you 337 | distribute the same sections as part of a whole which is a work based 338 | on the Library, the distribution of the whole must be on the terms of 339 | this License, whose permissions for other licensees extend to the 340 | entire whole, and thus to each and every part regardless of who wrote 341 | it. 342 | . 343 | Thus, it is not the intent of this section to claim rights or contest 344 | your rights to work written entirely by you; rather, the intent is to 345 | exercise the right to control the distribution of derivative or 346 | collective works based on the Library. 347 | . 348 | In addition, mere aggregation of another work not based on the Library 349 | with the Library (or with a work based on the Library) on a volume of 350 | a storage or distribution medium does not bring the other work under 351 | the scope of this License. 352 | . 353 | 3. You may opt to apply the terms of the ordinary GNU General Public 354 | License instead of this License to a given copy of the Library. To do 355 | this, you must alter all the notices that refer to this License, so 356 | that they refer to the ordinary GNU General Public License, version 2, 357 | instead of to this License. (If a newer version than version 2 of the 358 | ordinary GNU General Public License has appeared, then you can specify 359 | that version instead if you wish.) Do not make any other change in 360 | these notices. 361 | . 362 | Once this change is made in a given copy, it is irreversible for 363 | that copy, so the ordinary GNU General Public License applies to all 364 | subsequent copies and derivative works made from that copy. 365 | . 366 | This option is useful when you wish to copy part of the code of 367 | the Library into a program that is not a library. 368 | . 369 | 4. You may copy and distribute the Library (or a portion or 370 | derivative of it, under Section 2) in object code or executable form 371 | under the terms of Sections 1 and 2 above provided that you accompany 372 | it with the complete corresponding machine-readable source code, which 373 | must be distributed under the terms of Sections 1 and 2 above on a 374 | medium customarily used for software interchange. 375 | . 376 | If distribution of object code is made by offering access to copy 377 | from a designated place, then offering equivalent access to copy the 378 | source code from the same place satisfies the requirement to 379 | distribute the source code, even though third parties are not 380 | compelled to copy the source along with the object code. 381 | . 382 | 5. A program that contains no derivative of any portion of the 383 | Library, but is designed to work with the Library by being compiled or 384 | linked with it, is called a "work that uses the Library". Such a 385 | work, in isolation, is not a derivative work of the Library, and 386 | therefore falls outside the scope of this License. 387 | . 388 | However, linking a "work that uses the Library" with the Library 389 | creates an executable that is a derivative of the Library (because it 390 | contains portions of the Library), rather than a "work that uses the 391 | library". The executable is therefore covered by this License. 392 | Section 6 states terms for distribution of such executables. 393 | . 394 | When a "work that uses the Library" uses material from a header file 395 | that is part of the Library, the object code for the work may be a 396 | derivative work of the Library even though the source code is not. 397 | Whether this is true is especially significant if the work can be 398 | linked without the Library, or if the work is itself a library. The 399 | threshold for this to be true is not precisely defined by law. 400 | . 401 | If such an object file uses only numerical parameters, data 402 | structure layouts and accessors, and small macros and small inline 403 | functions (ten lines or less in length), then the use of the object 404 | file is unrestricted, regardless of whether it is legally a derivative 405 | work. (Executables containing this object code plus portions of the 406 | Library will still fall under Section 6.) 407 | . 408 | Otherwise, if the work is a derivative of the Library, you may 409 | distribute the object code for the work under the terms of Section 6. 410 | Any executables containing that work also fall under Section 6, 411 | whether or not they are linked directly with the Library itself. 412 | . 413 | 6. As an exception to the Sections above, you may also combine or 414 | link a "work that uses the Library" with the Library to produce a 415 | work containing portions of the Library, and distribute that work 416 | under terms of your choice, provided that the terms permit 417 | modification of the work for the customer's own use and reverse 418 | engineering for debugging such modifications. 419 | . 420 | You must give prominent notice with each copy of the work that the 421 | Library is used in it and that the Library and its use are covered by 422 | this License. You must supply a copy of this License. If the work 423 | during execution displays copyright notices, you must include the 424 | copyright notice for the Library among them, as well as a reference 425 | directing the user to the copy of this License. Also, you must do one 426 | of these things: 427 | . 428 | a) Accompany the work with the complete corresponding 429 | machine-readable source code for the Library including whatever 430 | changes were used in the work (which must be distributed under 431 | Sections 1 and 2 above); and, if the work is an executable linked 432 | with the Library, with the complete machine-readable "work that 433 | uses the Library", as object code and/or source code, so that the 434 | user can modify the Library and then relink to produce a modified 435 | executable containing the modified Library. (It is understood 436 | that the user who changes the contents of definitions files in the 437 | Library will not necessarily be able to recompile the application 438 | to use the modified definitions.) 439 | . 440 | b) Use a suitable shared library mechanism for linking with the 441 | Library. A suitable mechanism is one that (1) uses at run time a 442 | copy of the library already present on the user's computer system, 443 | rather than copying library functions into the executable, and (2) 444 | will operate properly with a modified version of the library, if 445 | the user installs one, as long as the modified version is 446 | interface-compatible with the version that the work was made with. 447 | . 448 | c) Accompany the work with a written offer, valid for at 449 | least three years, to give the same user the materials 450 | specified in Subsection 6a, above, for a charge no more 451 | than the cost of performing this distribution. 452 | . 453 | d) If distribution of the work is made by offering access to copy 454 | from a designated place, offer equivalent access to copy the above 455 | specified materials from the same place. 456 | . 457 | e) Verify that the user has already received a copy of these 458 | materials or that you have already sent this user a copy. 459 | . 460 | For an executable, the required form of the "work that uses the 461 | Library" must include any data and utility programs needed for 462 | reproducing the executable from it. However, as a special exception, 463 | the materials to be distributed need not include anything that is 464 | normally distributed (in either source or binary form) with the major 465 | components (compiler, kernel, and so on) of the operating system on 466 | which the executable runs, unless that component itself accompanies 467 | the executable. 468 | . 469 | It may happen that this requirement contradicts the license 470 | restrictions of other proprietary libraries that do not normally 471 | accompany the operating system. Such a contradiction means you cannot 472 | use both them and the Library together in an executable that you 473 | distribute. 474 | . 475 | 7. You may place library facilities that are a work based on the 476 | Library side-by-side in a single library together with other library 477 | facilities not covered by this License, and distribute such a combined 478 | library, provided that the separate distribution of the work based on 479 | the Library and of the other library facilities is otherwise 480 | permitted, and provided that you do these two things: 481 | . 482 | a) Accompany the combined library with a copy of the same work 483 | based on the Library, uncombined with any other library 484 | facilities. This must be distributed under the terms of the 485 | Sections above. 486 | . 487 | b) Give prominent notice with the combined library of the fact 488 | that part of it is a work based on the Library, and explaining 489 | where to find the accompanying uncombined form of the same work. 490 | . 491 | 8. You may not copy, modify, sublicense, link with, or distribute 492 | the Library except as expressly provided under this License. Any 493 | attempt otherwise to copy, modify, sublicense, link with, or 494 | distribute the Library is void, and will automatically terminate your 495 | rights under this License. However, parties who have received copies, 496 | or rights, from you under this License will not have their licenses 497 | terminated so long as such parties remain in full compliance. 498 | . 499 | 9. You are not required to accept this License, since you have not 500 | signed it. However, nothing else grants you permission to modify or 501 | distribute the Library or its derivative works. These actions are 502 | prohibited by law if you do not accept this License. Therefore, by 503 | modifying or distributing the Library (or any work based on the 504 | Library), you indicate your acceptance of this License to do so, and 505 | all its terms and conditions for copying, distributing or modifying 506 | the Library or works based on it. 507 | . 508 | 10. Each time you redistribute the Library (or any work based on the 509 | Library), the recipient automatically receives a license from the 510 | original licensor to copy, distribute, link with or modify the Library 511 | subject to these terms and conditions. You may not impose any further 512 | restrictions on the recipients' exercise of the rights granted herein. 513 | You are not responsible for enforcing compliance by third parties with 514 | this License. 515 | . 516 | 11. If, as a consequence of a court judgment or allegation of patent 517 | infringement or for any other reason (not limited to patent issues), 518 | conditions are imposed on you (whether by court order, agreement or 519 | otherwise) that contradict the conditions of this License, they do not 520 | excuse you from the conditions of this License. If you cannot 521 | distribute so as to satisfy simultaneously your obligations under this 522 | License and any other pertinent obligations, then as a consequence you 523 | may not distribute the Library at all. For example, if a patent 524 | license would not permit royalty-free redistribution of the Library by 525 | all those who receive copies directly or indirectly through you, then 526 | the only way you could satisfy both it and this License would be to 527 | refrain entirely from distribution of the Library. 528 | . 529 | If any portion of this section is held invalid or unenforceable under any 530 | particular circumstance, the balance of the section is intended to apply, 531 | and the section as a whole is intended to apply in other circumstances. 532 | . 533 | It is not the purpose of this section to induce you to infringe any 534 | patents or other property right claims or to contest validity of any 535 | such claims; this section has the sole purpose of protecting the 536 | integrity of the free software distribution system which is 537 | implemented by public license practices. Many people have made 538 | generous contributions to the wide range of software distributed 539 | through that system in reliance on consistent application of that 540 | system; it is up to the author/donor to decide if he or she is willing 541 | to distribute software through any other system and a licensee cannot 542 | impose that choice. 543 | . 544 | This section is intended to make thoroughly clear what is believed to 545 | be a consequence of the rest of this License. 546 | . 547 | 12. If the distribution and/or use of the Library is restricted in 548 | certain countries either by patents or by copyrighted interfaces, the 549 | original copyright holder who places the Library under this License may add 550 | an explicit geographical distribution limitation excluding those countries, 551 | so that distribution is permitted only in or among countries not thus 552 | excluded. In such case, this License incorporates the limitation as if 553 | written in the body of this License. 554 | . 555 | 13. The Free Software Foundation may publish revised and/or new 556 | versions of the Lesser General Public License from time to time. 557 | Such new versions will be similar in spirit to the present version, 558 | but may differ in detail to address new problems or concerns. 559 | . 560 | Each version is given a distinguishing version number. If the Library 561 | specifies a version number of this License which applies to it and 562 | "any later version", you have the option of following the terms and 563 | conditions either of that version or of any later version published by 564 | the Free Software Foundation. If the Library does not specify a 565 | license version number, you may choose any version ever published by 566 | the Free Software Foundation. 567 | . 568 | 14. If you wish to incorporate parts of the Library into other free 569 | programs whose distribution conditions are incompatible with these, 570 | write to the author to ask for permission. For software which is 571 | copyrighted by the Free Software Foundation, write to the Free 572 | Software Foundation; we sometimes make exceptions for this. Our 573 | decision will be guided by the two goals of preserving the free status 574 | of all derivatives of our free software and of promoting the sharing 575 | and reuse of software generally. 576 | . 577 | NO WARRANTY 578 | . 579 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 580 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 581 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 582 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 583 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 584 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 585 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 586 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 587 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 588 | . 589 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 590 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 591 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 592 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 593 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 594 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 595 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 596 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 597 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 598 | DAMAGES. 599 | . 600 | END OF TERMS AND CONDITIONS 601 | . 602 | How to Apply These Terms to Your New Libraries 603 | . 604 | If you develop a new library, and you want it to be of the greatest 605 | possible use to the public, we recommend making it free software that 606 | everyone can redistribute and change. You can do so by permitting 607 | redistribution under these terms (or, alternatively, under the terms of the 608 | ordinary General Public License). 609 | . 610 | To apply these terms, attach the following notices to the library. It is 611 | safest to attach them to the start of each source file to most effectively 612 | convey the exclusion of warranty; and each file should have at least the 613 | "copyright" line and a pointer to where the full notice is found. 614 | . 615 | 616 | Copyright (C) 617 | . 618 | This library is free software; you can redistribute it and/or 619 | modify it under the terms of the GNU Lesser General Public 620 | License as published by the Free Software Foundation; either 621 | version 2.1 of the License, or (at your option) any later version. 622 | . 623 | This library is distributed in the hope that it will be useful, 624 | but WITHOUT ANY WARRANTY; without even the implied warranty of 625 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 626 | Lesser General Public License for more details. 627 | . 628 | You should have received a copy of the GNU Lesser General Public 629 | License along with this library; if not, write to the Free Software 630 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 631 | . 632 | Also add information on how to contact you by electronic and paper mail. 633 | . 634 | You should also get your employer (if you work as a programmer) or your 635 | school, if any, to sign a "copyright disclaimer" for the library, if 636 | necessary. Here is a sample; alter the names: 637 | . 638 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 639 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 640 | . 641 | , 1 April 1990 642 | Ty Coon, President of Vice 643 | . 644 | That's all there is to it! 645 | -------------------------------------------------------------------------------- /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") 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/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/build/patches/brotli/.gitkeep -------------------------------------------------------------------------------- /build/patches/brotli/0001-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/fontconfig/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/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/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/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/harfbuzz/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/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 | -------------------------------------------------------------------------------- /dist/default.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/dist/default.woff2 -------------------------------------------------------------------------------- /dist/jassub-worker-modern.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/dist/jassub-worker-modern.wasm -------------------------------------------------------------------------------- /dist/jassub-worker.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThaUnknown/jassub/c43b19cd46a0e88685059432c135ddfb5eb3a3bc/dist/jassub-worker.wasm -------------------------------------------------------------------------------- /dist/jassub.es.js: -------------------------------------------------------------------------------- 1 | typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype && (HTMLVideoElement.prototype._rvfcpolyfillmap = {}, HTMLVideoElement.prototype.requestVideoFrameCallback = function(c) { 2 | const e = performance.now(), t = this.getVideoPlaybackQuality(), s = this.mozPresentedFrames || this.mozPaintedFrames || t.totalVideoFrames - t.droppedVideoFrames, r = (n, a) => { 3 | const i = this.getVideoPlaybackQuality(), h = this.mozPresentedFrames || this.mozPaintedFrames || i.totalVideoFrames - i.droppedVideoFrames; 4 | if (h > s) { 5 | const d = this.mozFrameDelay || i.totalFrameDelay - t.totalFrameDelay || 0, l = a - n; 6 | c(a, { 7 | presentationTime: a + d * 1e3, 8 | expectedDisplayTime: a + l, 9 | width: this.videoWidth, 10 | height: this.videoHeight, 11 | mediaTime: Math.max(0, this.currentTime || 0) + l / 1e3, 12 | presentedFrames: h, 13 | processingDuration: d 14 | }), delete this._rvfcpolyfillmap[e]; 15 | } else 16 | this._rvfcpolyfillmap[e] = requestAnimationFrame((d) => r(a, d)); 17 | }; 18 | return this._rvfcpolyfillmap[e] = requestAnimationFrame((n) => r(e, n)), e; 19 | }, HTMLVideoElement.prototype.cancelVideoFrameCallback = function(c) { 20 | cancelAnimationFrame(this._rvfcpolyfillmap[c]), delete this._rvfcpolyfillmap[c]; 21 | }); 22 | const _ = { 23 | bt709: "BT709", 24 | // these might not be exactly correct? oops? 25 | bt470bg: "BT601", 26 | // alias BT.601 PAL... whats the difference? 27 | smpte170m: "BT601" 28 | // alias BT.601 NTSC... whats the difference? 29 | }, f = { 30 | BT601: { 31 | BT709: "1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418" 32 | }, 33 | BT709: { 34 | BT601: "0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582" 35 | }, 36 | FCC: { 37 | BT709: "1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378", 38 | BT601: "1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996" 39 | }, 40 | SMPTE240M: { 41 | BT709: "0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148", 42 | BT601: "0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973" 43 | } 44 | }; 45 | class o extends EventTarget { 46 | /** 47 | * @param {Object} options Settings object. 48 | * @param {HTMLVideoElement} options.video Video to use as target for rendering and event listeners. Optional if canvas is specified instead. 49 | * @param {HTMLCanvasElement} [options.canvas=HTMLCanvasElement] Canvas to use for manual handling. Not required if video is specified. 50 | * @param {'js'|'wasm'} [options.blendMode='js'] Which image blending mode to use. WASM will perform better on lower end devices, JS will perform better if the device and browser supports hardware acceleration. 51 | * @param {Boolean} [options.asyncRender=true] Whether or not to use async rendering, which offloads the CPU by creating image bitmaps on the GPU. 52 | * @param {Boolean} [options.offscreenRender=true] Whether or not to render things fully on the worker, greatly reduces CPU usage. 53 | * @param {Boolean} [options.onDemandRender=true] Whether or not to render subtitles as the video player decodes renders, rather than predicting which frame the player is on using events. 54 | * @param {Number} [options.targetFps=24] Target FPS to render subtitles at. Ignored when onDemandRender is enabled. 55 | * @param {Number} [options.timeOffset=0] Subtitle time offset in seconds. 56 | * @param {Boolean} [options.debug=false] Whether or not to print debug information. 57 | * @param {Number} [options.prescaleFactor=1.0] Scale down (< 1.0) the subtitles canvas to improve performance at the expense of quality, or scale it up (> 1.0). 58 | * @param {Number} [options.prescaleHeightLimit=1080] The height in pixels beyond which the subtitles canvas won't be prescaled. 59 | * @param {Number} [options.maxRenderHeight=0] The maximum rendering height in pixels of the subtitles canvas. Beyond this subtitles will be upscaled by the browser. 60 | * @param {Boolean} [options.dropAllAnimations=false] Attempt to discard all animated tags. Enabling this may severly mangle complex subtitles and should only be considered as an last ditch effort of uncertain success for hardware otherwise incapable of displaing anything. Will not reliably work with manually edited or allocated events. 61 | * @param {Boolean} [options.dropAllBlur=false] The holy grail of performance gains. If heavy TS lags a lot, disabling this will make it ~x10 faster. This drops blur from all added subtitle tracks making most text and backgrounds look sharper, this is way less intrusive than dropping all animations, while still offering major performance gains. 62 | * @param {String} [options.workerUrl='jassub-worker.js'] The URL of the worker. 63 | * @param {String} [options.wasmUrl='jassub-worker.wasm'] The URL of the worker WASM. 64 | * @param {String} [options.legacyWasmUrl='jassub-worker.wasm.js'] The URL of the worker WASM. Only loaded if the browser doesn't support WASM. 65 | * @param {String} options.modernWasmUrl The URL of the modern worker WASM. This includes faster ASM instructions, but is only supported by newer browsers, disabled if the URL isn't defined. 66 | * @param {String} [options.subUrl=options.subContent] The URL of the subtitle file to play. 67 | * @param {String} [options.subContent=options.subUrl] The content of the subtitle file to play. 68 | * @param {String[]|Uint8Array[]} [options.fonts] An array of links or Uint8Arrays to the fonts used in the subtitle. If Uint8Array is used the array is copied, not referenced. This forces all the fonts in this array to be loaded by the renderer, regardless of if they are used. 69 | * @param {Object} [options.availableFonts={'liberation sans': './default.woff2'}] Object with all available fonts - Key is font family in lower case, value is link or Uint8Array: { arial: '/font1.ttf' }. These fonts are selectively loaded if detected as used in the current subtitle track. 70 | * @param {String} [options.fallbackFont='liberation sans'] The font family key of the fallback font in availableFonts to use if the other font for the style is missing special glyphs or unicode. 71 | * @param {Boolean} [options.useLocalFonts=false] If the Local Font Access API is enabled [chrome://flags/#font-access], the library will query for permissions to use local fonts and use them if any are missing. The permission can be queried beforehand using navigator.permissions.request({ name: 'local-fonts' }). 72 | * @param {Number} [options.libassMemoryLimit] libass bitmap cache memory limit in MiB (approximate). 73 | * @param {Number} [options.libassGlyphLimit] libass glyph cache memory limit in MiB (approximate). 74 | */ 75 | constructor(e) { 76 | if (super(), !globalThis.Worker) 77 | throw this.destroy("Worker not supported"); 78 | if (!e) 79 | throw this.destroy("No options provided"); 80 | this._loaded = /** @type {Promise} */ 81 | new Promise((s) => { 82 | this._init = s; 83 | }); 84 | const t = o._test(); 85 | if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? !0), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? !0), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas) 86 | this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent); 87 | else if (!this._canvas) 88 | throw this.destroy("Don't know where to render: you should give video or canvas in options."); 89 | if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx) 90 | throw this.destroy("Canvas rendering not supported"); 91 | this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = this.setRate.bind(this), this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = !1, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => { 92 | this._worker.postMessage({ 93 | target: "init", 94 | wasmUrl: o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm", 95 | legacyWasmUrl: e.legacyWasmUrl ?? "jassub-worker.wasm.js", 96 | asyncRender: typeof createImageBitmap < "u" && (e.asyncRender ?? !0), 97 | onDemandRender: this._onDemandRender, 98 | width: this._canvasctrl.width || 0, 99 | height: this._canvasctrl.height || 0, 100 | blendMode: e.blendMode || "js", 101 | subUrl: e.subUrl, 102 | subContent: e.subContent || null, 103 | fonts: e.fonts || [], 104 | availableFonts: e.availableFonts || { "liberation sans": "./default.woff2" }, 105 | fallbackFont: e.fallbackFont || "liberation sans", 106 | debug: this.debug, 107 | targetFps: e.targetFps || 24, 108 | dropAllAnimations: e.dropAllAnimations, 109 | dropAllBlur: e.dropAllBlur, 110 | libassMemoryLimit: e.libassMemoryLimit || 0, 111 | libassGlyphLimit: e.libassGlyphLimit || 0, 112 | // @ts-ignore 113 | useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? !0), 114 | hasBitmapBug: o._hasBitmapBug 115 | }), this._offscreenRender === !0 && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]); 116 | }); 117 | } 118 | _createCanvas() { 119 | return this._canvas = document.createElement("canvas"), this._canvas.style.display = "block", this._canvas.style.position = "absolute", this._canvas.style.pointerEvents = "none", this._canvasParent.appendChild(this._canvas), this._canvas; 120 | } 121 | // test support for WASM, ImageData, alphaBug, but only once, on init so it doesn't run when first running the page 122 | /** @type {boolean|null} */ 123 | static _supportsSIMD = null; 124 | /** @type {boolean|null} */ 125 | static _hasAlphaBug = null; 126 | /** @type {boolean|null} */ 127 | static _hasBitmapBug = null; 128 | static _testSIMD() { 129 | if (o._supportsSIMD === null) 130 | try { 131 | o._supportsSIMD = WebAssembly.validate(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11)); 132 | } catch { 133 | o._supportsSIMD = !1; 134 | } 135 | } 136 | static async _testImageBugs() { 137 | if (o._hasBitmapBug !== null) 138 | return; 139 | const e = document.createElement("canvas"), t = e.getContext("2d", { willReadFrequently: !0 }); 140 | if (!t) 141 | throw new Error("Canvas rendering not supported"); 142 | if (typeof ImageData.prototype.constructor == "function") 143 | try { 144 | new ImageData(new Uint8ClampedArray([0, 0, 0, 0]), 1, 1); 145 | } catch { 146 | console.log("Detected that ImageData is not constructable despite browser saying so"), self.ImageData = function(h, d, l) { 147 | const m = t.createImageData(d, l); 148 | return h && m.data.set(h), m; 149 | }; 150 | } 151 | const s = document.createElement("canvas"), r = s.getContext("2d", { willReadFrequently: !0 }); 152 | if (!r) 153 | throw new Error("Canvas rendering not supported"); 154 | e.width = s.width = 1, e.height = s.height = 1, t.clearRect(0, 0, 1, 1), r.clearRect(0, 0, 1, 1); 155 | const n = r.getImageData(0, 0, 1, 1).data; 156 | t.putImageData(new ImageData(new Uint8ClampedArray([0, 255, 0, 0]), 1, 1), 0, 0), r.drawImage(e, 0, 0); 157 | const a = r.getImageData(0, 0, 1, 1).data; 158 | if (o._hasAlphaBug = n[1] !== a[1], o._hasAlphaBug && console.log("Detected a browser having issue with transparent pixels, applying workaround"), typeof createImageBitmap < "u") { 159 | const i = new Uint8ClampedArray([255, 0, 255, 0, 255]).subarray(1, 5); 160 | r.drawImage(await createImageBitmap(new ImageData(i, 1)), 0, 0); 161 | const { data: h } = r.getImageData(0, 0, 1, 1); 162 | o._hasBitmapBug = !1; 163 | for (const [d, l] of h.entries()) 164 | if (Math.abs(i[d] - l) > 15) { 165 | o._hasBitmapBug = !0, console.log("Detected a browser having issue with partial bitmaps, applying workaround"); 166 | break; 167 | } 168 | } else 169 | o._hasBitmapBug = !1; 170 | e.remove(), s.remove(); 171 | } 172 | static async _test() { 173 | o._testSIMD(), await o._testImageBugs(); 174 | } 175 | /** 176 | * Resize the canvas to given parameters. Auto-generated if values are ommited. 177 | * @param {Number} [width=0] 178 | * @param {Number} [height=0] 179 | * @param {Number} [top=0] 180 | * @param {Number} [left=0] 181 | * @param {Boolean} [force=false] 182 | */ 183 | resize(e = 0, t = 0, s = 0, r = 0, n = this._video?.paused) { 184 | if ((!e || !t) && this._video) { 185 | const a = this._getVideoPosition(); 186 | let i = null; 187 | if (this._videoWidth) { 188 | const h = this._video.videoWidth / this._videoWidth, d = this._video.videoHeight / this._videoHeight; 189 | i = this._computeCanvasSize((a.width || 0) / h, (a.height || 0) / d); 190 | } else 191 | i = this._computeCanvasSize(a.width || 0, a.height || 0); 192 | e = i.width, t = i.height, this._canvasParent && (s = a.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top), r = a.x), this._canvas.style.width = a.width + "px", this._canvas.style.height = a.height + "px"; 193 | } 194 | this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === !1 ? this.busy = !0 : n = !1, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video.videoWidth, videoHeight: this._videoHeight || this._video.videoHeight, force: n }); 195 | } 196 | _getVideoPosition(e = this._video.videoWidth, t = this._video.videoHeight) { 197 | const s = e / t, { offsetWidth: r, offsetHeight: n } = this._video, a = r / n; 198 | e = r, t = n, a > s ? e = Math.floor(n * s) : t = Math.floor(r / s); 199 | const i = (r - e) / 2, h = (n - t) / 2; 200 | return { width: e, height: t, x: i, y: h }; 201 | } 202 | _computeCanvasSize(e = 0, t = 0) { 203 | const s = this.prescaleFactor <= 0 ? 1 : this.prescaleFactor, r = self.devicePixelRatio || 1; 204 | if (t <= 0 || e <= 0) 205 | e = 0, t = 0; 206 | else { 207 | const n = s < 1 ? -1 : 1; 208 | let a = t * r; 209 | n * a * s <= n * this.prescaleHeightLimit ? a *= s : n * a < n * this.prescaleHeightLimit && (a = this.prescaleHeightLimit), this.maxRenderHeight > 0 && a > this.maxRenderHeight && (a = this.maxRenderHeight), e *= a / t, t = a; 210 | } 211 | return { width: e, height: t }; 212 | } 213 | _timeupdate({ type: e }) { 214 | const s = { 215 | seeking: !0, 216 | waiting: !0, 217 | playing: !1 218 | }[e]; 219 | s != null && (this._playstate = s), this.setCurrentTime(this._video.paused || this._playstate, this._video.currentTime + this.timeOffset); 220 | } 221 | /** 222 | * Change the video to use as target for event listeners. 223 | * @param {HTMLVideoElement} video 224 | */ 225 | setVideo(e) { 226 | e instanceof HTMLVideoElement ? (this._removeListeners(), this._video = e, this._onDemandRender ? this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)) : (this._playstate = e.paused, e.addEventListener("timeupdate", this._boundTimeUpdate, !1), e.addEventListener("progress", this._boundTimeUpdate, !1), e.addEventListener("waiting", this._boundTimeUpdate, !1), e.addEventListener("seeking", this._boundTimeUpdate, !1), e.addEventListener("playing", this._boundTimeUpdate, !1), e.addEventListener("ratechange", this._boundSetRate, !1), e.addEventListener("resize", this._boundResize, !1)), "VideoFrame" in window && (e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, !1), e.readyState > 2 && this._updateColorSpace()), e.videoWidth > 0 && this.resize(), typeof ResizeObserver < "u" && (this._ro || (this._ro = new ResizeObserver(() => this.resize())), this._ro.observe(e))) : this._error("Video element invalid!"); 227 | } 228 | runBenchmark() { 229 | this.sendMessage("runBenchmark"); 230 | } 231 | /** 232 | * Overwrites the current subtitle content. 233 | * @param {String} url URL to load subtitles from. 234 | */ 235 | setTrackByUrl(e) { 236 | this.sendMessage("setTrackByUrl", { url: e }), this._reAttachOffscreen(), this._ctx && (this._ctx.filter = "none"); 237 | } 238 | /** 239 | * Overwrites the current subtitle content. 240 | * @param {String} content Content of the ASS file. 241 | */ 242 | setTrack(e) { 243 | this.sendMessage("setTrack", { content: e }), this._reAttachOffscreen(), this._ctx && (this._ctx.filter = "none"); 244 | } 245 | /** 246 | * Free currently used subtitle track. 247 | */ 248 | freeTrack() { 249 | this.sendMessage("freeTrack"); 250 | } 251 | /** 252 | * Sets the playback state of the media. 253 | * @param {Boolean} isPaused Pause/Play subtitle playback. 254 | */ 255 | setIsPaused(e) { 256 | this.sendMessage("video", { isPaused: e }); 257 | } 258 | /** 259 | * Sets the playback rate of the media [speed multiplier]. 260 | * @param {Number} rate Playback rate. 261 | */ 262 | setRate(e) { 263 | this.sendMessage("video", { rate: e }); 264 | } 265 | /** 266 | * Sets the current time, playback state and rate of the subtitles. 267 | * @param {Boolean} [isPaused] Pause/Play subtitle playback. 268 | * @param {Number} [currentTime] Time in seconds. 269 | * @param {Number} [rate] Playback rate. 270 | */ 271 | setCurrentTime(e, t, s) { 272 | this.sendMessage("video", { isPaused: e, currentTime: t, rate: s, colorSpace: this._videoColorSpace }); 273 | } 274 | /** 275 | * @typedef {Object} ASS_Event 276 | * @property {Number} Start Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will appear onscreen. Note that there is a single digit for the hours! 277 | * @property {Number} Duration End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will disappear offscreen. Note that there is a single digit for the hours! 278 | * @property {String} Style Style name. If it is "Default", then your own *Default style will be subtituted. 279 | * @property {String} Name Character name. This is the name of the character who speaks the dialogue. It is for information only, to make the script is easier to follow when editing/timing. 280 | * @property {Number} MarginL 4-figure Left Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 281 | * @property {Number} MarginR 4-figure Right Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 282 | * @property {Number} MarginV 4-figure Bottom Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used. 283 | * @property {String} Effect Transition Effect. This is either empty, or contains information for one of the three transition effects implemented in SSA v4.x 284 | * @property {String} Text Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. Everything after the 9th comma is treated as the subtitle text, so it can include commas. 285 | * @property {Number} ReadOrder Number in order of which to read this event. 286 | * @property {Number} Layer Z-index overlap in which to render this event. 287 | * @property {Number} _index (Internal) index of the event. 288 | */ 289 | /** 290 | * Create a new ASS event directly. 291 | * @param {ASS_Event} event 292 | */ 293 | createEvent(e) { 294 | this.sendMessage("createEvent", { event: e }); 295 | } 296 | /** 297 | * Overwrite the data of the event with the specified index. 298 | * @param {ASS_Event} event 299 | * @param {Number} index 300 | */ 301 | setEvent(e, t) { 302 | this.sendMessage("setEvent", { event: e, index: t }); 303 | } 304 | /** 305 | * Remove the event with the specified index. 306 | * @param {Number} index 307 | */ 308 | removeEvent(e) { 309 | this.sendMessage("removeEvent", { index: e }); 310 | } 311 | /** 312 | * Get all ASS events. 313 | * @param {function(Error|null, ASS_Event): void} callback Function to callback when worker returns the events. 314 | */ 315 | getEvents(e) { 316 | this._fetchFromWorker({ 317 | target: "getEvents" 318 | }, (t, { events: s }) => { 319 | e(t, s); 320 | }); 321 | } 322 | /** 323 | * Set a style override. 324 | * @param {ASS_Style} style 325 | */ 326 | styleOverride(e) { 327 | this.sendMessage("styleOverride", { style: e }); 328 | } 329 | /** 330 | * Disable style override. 331 | */ 332 | disableStyleOverride() { 333 | this.sendMessage("disableStyleOverride"); 334 | } 335 | /** 336 | * @typedef {Object} ASS_Style 337 | * @property {String} Name The name of the Style. Case sensitive. Cannot include commas. 338 | * @property {String} FontName The fontname as used by Windows. Case-sensitive. 339 | * @property {Number} FontSize Font size. 340 | * @property {Number} PrimaryColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 341 | * @property {Number} SecondaryColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 342 | * @property {Number} OutlineColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR 343 | * @property {Number} BackColour This is the colour of the subtitle outline or shadow, if these are used. A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR. 344 | * @property {Number} Bold This defines whether text is bold (true) or not (false). -1 is True, 0 is False. This is independant of the Italic attribute - you can have have text which is both bold and italic. 345 | * @property {Number} Italic Italic. This defines whether text is italic (true) or not (false). -1 is True, 0 is False. This is independant of the bold attribute - you can have have text which is both bold and italic. 346 | * @property {Number} Underline -1 or 0 347 | * @property {Number} StrikeOut -1 or 0 348 | * @property {Number} ScaleX Modifies the width of the font. [percent] 349 | * @property {Number} ScaleY Modifies the height of the font. [percent] 350 | * @property {Number} Spacing Extra space between characters. [pixels] 351 | * @property {Number} Angle The origin of the rotation is defined by the alignment. Can be a floating point number. [degrees] 352 | * @property {Number} BorderStyle 1=Outline + drop shadow, 3=Opaque box 353 | * @property {Number} Outline If BorderStyle is 1, then this specifies the width of the outline around the text, in pixels. Values may be 0, 1, 2, 3 or 4. 354 | * @property {Number} Shadow If BorderStyle is 1, then this specifies the depth of the drop shadow behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in addition to an outline - SSA will force an outline of 1 pixel if no outline width is given. 355 | * @property {Number} Alignment This sets how text is "justified" within the Left/Right onscreen margins, and also the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified toptitle 356 | * @property {Number} MarginL This defines the Left Margin in pixels. It is the distance from the left-hand edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed. 357 | * @property {Number} MarginR This defines the Right Margin in pixels. It is the distance from the right-hand edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed. 358 | * @property {Number} MarginV This defines the vertical Left Margin in pixels. For a subtitle, it is the distance from the bottom of the screen. For a toptitle, it is the distance from the top of the screen. For a midtitle, the value is ignored - the text will be vertically centred. 359 | * @property {Number} Encoding This specifies the font character set or encoding and on multi-lingual Windows installations it provides access to characters used in multiple than one languages. It is usually 0 (zero) for English (Western, ANSI) Windows. 360 | * @property {Number} treat_fontname_as_pattern 361 | * @property {Number} Blur 362 | * @property {Number} Justify 363 | */ 364 | /** 365 | * Create a new ASS style directly. 366 | * @param {ASS_Style} style 367 | */ 368 | createStyle(e) { 369 | this.sendMessage("createStyle", { style: e }); 370 | } 371 | /** 372 | * Overwrite the data of the style with the specified index. 373 | * @param {ASS_Style} style 374 | * @param {Number} index 375 | */ 376 | setStyle(e, t) { 377 | this.sendMessage("setStyle", { style: e, index: t }); 378 | } 379 | /** 380 | * Remove the style with the specified index. 381 | * @param {Number} index 382 | */ 383 | removeStyle(e) { 384 | this.sendMessage("removeStyle", { index: e }); 385 | } 386 | /** 387 | * Get all ASS styles. 388 | * @param {function(Error|null, ASS_Style): void} callback Function to callback when worker returns the styles. 389 | */ 390 | getStyles(e) { 391 | this._fetchFromWorker({ 392 | target: "getStyles" 393 | }, (t, { styles: s }) => { 394 | e(t, s); 395 | }); 396 | } 397 | /** 398 | * Adds a font to the renderer. 399 | * @param {String|Uint8Array} font Font to add. 400 | */ 401 | addFont(e) { 402 | this.sendMessage("addFont", { font: e }); 403 | } 404 | /** 405 | * Changes the font family of the default font, this font needs to be previously added via addFont or fonts array on construction. 406 | * @param {String} font Font family to change to. 407 | */ 408 | setDefaultFont(e) { 409 | this.sendMessage("defaultFont", { font: e }); 410 | } 411 | _sendLocalFont(e) { 412 | try { 413 | queryLocalFonts().then((t) => { 414 | const s = t?.find((r) => r.fullName.toLowerCase() === e); 415 | s && s.blob().then((r) => { 416 | r.arrayBuffer().then((n) => { 417 | this.addFont(new Uint8Array(n)); 418 | }); 419 | }); 420 | }); 421 | } catch (t) { 422 | console.warn("Local fonts API:", t); 423 | } 424 | } 425 | _getLocalFont({ font: e }) { 426 | try { 427 | navigator?.permissions?.query ? navigator.permissions.query({ name: "local-fonts" }).then((t) => { 428 | t.state === "granted" && this._sendLocalFont(e); 429 | }) : this._sendLocalFont(e); 430 | } catch (t) { 431 | console.warn("Local fonts API:", t); 432 | } 433 | } 434 | _unbusy() { 435 | this._lastDemandTime ? this._demandRender(this._lastDemandTime) : this.busy = !1; 436 | } 437 | _handleRVFC(e, { mediaTime: t, width: s, height: r }) { 438 | if (this._destroyed) 439 | return null; 440 | this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = !0, this._demandRender({ mediaTime: t, width: s, height: r })), this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)); 441 | } 442 | _demandRender({ mediaTime: e, width: t, height: s }) { 443 | this._lastDemandTime = null, (t !== this._videoWidth || s !== this._videoHeight) && (this._videoWidth = t, this._videoHeight = s, this.resize()), this.sendMessage("demand", { time: e + this.timeOffset }); 444 | } 445 | // if we're using offscreen render, we can't use ctx filters, so we can't use a transfered canvas 446 | _detachOffscreen() { 447 | if (!this._offscreenRender || this._ctx) 448 | return null; 449 | this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas, this._ctx = this._canvasctrl.getContext("2d"), this.sendMessage("detachOffscreen"), this.busy = !1, this.resize(0, 0, 0, 0, !0); 450 | } 451 | // if the video or track changed, we need to re-attach the offscreen canvas 452 | _reAttachOffscreen() { 453 | if (!this._offscreenRender || !this._ctx) 454 | return null; 455 | this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = !1, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, !0); 456 | } 457 | _updateColorSpace() { 458 | this._video.requestVideoFrameCallback(() => { 459 | try { 460 | const e = new VideoFrame(this._video); 461 | this._videoColorSpace = _[e.colorSpace.matrix], e.close(), this.sendMessage("getColorSpace"); 462 | } catch (e) { 463 | console.warn(e); 464 | } 465 | }); 466 | } 467 | /** 468 | * Veryify the color spaces for subtitles and videos, then apply filters to correct the color of subtitles. 469 | * @param {Object} options 470 | * @param {String} options.subtitleColorSpace Subtitle color space. One of: BT601 BT709 SMPTE240M FCC 471 | * @param {String=} options.videoColorSpace Video color space. One of: BT601 BT709 472 | */ 473 | _verifyColorSpace({ subtitleColorSpace: e, videoColorSpace: t = this._videoColorSpace }) { 474 | !e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,#f")`); 475 | } 476 | _render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) { 477 | this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height); 478 | for (const i of e) 479 | i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y))); 480 | if (this.debug) { 481 | s.JSRenderTime = Date.now() - s.JSRenderTime - s.IPCTime; 482 | let i = 0; 483 | const h = s.bitmaps || e.length; 484 | delete s.bitmaps; 485 | for (const d in s) 486 | i += s[d]; 487 | console.log("Bitmaps: " + h + " Total: " + (i | 0) + "ms", s); 488 | } 489 | } 490 | _fixAlpha(e) { 491 | if (o._hasAlphaBug) 492 | for (let t = 3; t < e.length; t += 4) 493 | e[t] = e[t] > 1 ? e[t] : 1; 494 | return e; 495 | } 496 | _ready() { 497 | this._init(), this.dispatchEvent(new CustomEvent("ready")); 498 | } 499 | /** 500 | * Send data and execute function in the worker. 501 | * @param {String} target Target function. 502 | * @param {Object} [data] Data for function. 503 | * @param {Transferable[]} [transferable] Array of transferables. 504 | */ 505 | async sendMessage(e, t = {}, s) { 506 | await this._loaded, s ? this._worker.postMessage({ 507 | target: e, 508 | transferable: s, 509 | ...t 510 | }, [...s]) : this._worker.postMessage({ 511 | target: e, 512 | ...t 513 | }); 514 | } 515 | _fetchFromWorker(e, t) { 516 | try { 517 | const s = e.target, r = setTimeout(() => { 518 | a(new Error("Error: Timeout while try to fetch " + s)); 519 | }, 5e3), n = ({ data: i }) => { 520 | i.target === s && (t(null, i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r)); 521 | }, a = (i) => { 522 | t(i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r); 523 | }; 524 | this._worker.addEventListener("message", n), this._worker.addEventListener("error", a), this._worker.postMessage(e); 525 | } catch (s) { 526 | this._error(s); 527 | } 528 | } 529 | _console({ content: e, command: t }) { 530 | console[t].apply(console, JSON.parse(e)); 531 | } 532 | _onmessage({ data: e }) { 533 | this["_" + e.target] && this["_" + e.target](e); 534 | } 535 | _error(e) { 536 | const t = e instanceof Error ? e : e instanceof ErrorEvent ? e.error : new Error(e), s = e instanceof Event ? new ErrorEvent(e.type, e) : new ErrorEvent("error", { error: t }); 537 | return this.dispatchEvent(s), console.error(t), t; 538 | } 539 | _removeListeners() { 540 | this._video && (this._ro && this._ro.unobserve(this._video), this._ctx && (this._ctx.filter = "none"), this._video.removeEventListener("timeupdate", this._boundTimeUpdate), this._video.removeEventListener("progress", this._boundTimeUpdate), this._video.removeEventListener("waiting", this._boundTimeUpdate), this._video.removeEventListener("seeking", this._boundTimeUpdate), this._video.removeEventListener("playing", this._boundTimeUpdate), this._video.removeEventListener("ratechange", this._boundSetRate), this._video.removeEventListener("resize", this._boundResize), this._video.removeEventListener("loadedmetadata", this._boundUpdateColorSpace)); 541 | } 542 | /** 543 | * Destroy the object, worker, listeners and all data. 544 | * @param {String|Error} [err] Error to throw when destroying. 545 | */ 546 | destroy(e) { 547 | return e && (e = this._error(e)), this._video && this._canvasParent && this._video.parentNode?.removeChild(this._canvasParent), this._destroyed = !0, this._removeListeners(), this.sendMessage("destroy"), this._worker?.terminate(), e; 548 | } 549 | } 550 | export { 551 | o as default 552 | }; 553 | -------------------------------------------------------------------------------- /dist/jassub.umd.js: -------------------------------------------------------------------------------- 1 | (function(c,m){typeof exports=="object"&&typeof module<"u"?module.exports=m():typeof define=="function"&&define.amd?define(m):(c=typeof globalThis<"u"?globalThis:c||self,c.JASSUB=m())})(this,function(){"use strict";typeof HTMLVideoElement<"u"&&!("requestVideoFrameCallback"in HTMLVideoElement.prototype)&&"getVideoPlaybackQuality"in HTMLVideoElement.prototype&&(HTMLVideoElement.prototype._rvfcpolyfillmap={},HTMLVideoElement.prototype.requestVideoFrameCallback=function(_){const e=performance.now(),t=this.getVideoPlaybackQuality(),s=this.mozPresentedFrames||this.mozPaintedFrames||t.totalVideoFrames-t.droppedVideoFrames,r=(n,a)=>{const i=this.getVideoPlaybackQuality(),h=this.mozPresentedFrames||this.mozPaintedFrames||i.totalVideoFrames-i.droppedVideoFrames;if(h>s){const d=this.mozFrameDelay||i.totalFrameDelay-t.totalFrameDelay||0,l=a-n;_(a,{presentationTime:a+d*1e3,expectedDisplayTime:a+l,width:this.videoWidth,height:this.videoHeight,mediaTime:Math.max(0,this.currentTime||0)+l/1e3,presentedFrames:h,processingDuration:d}),delete this._rvfcpolyfillmap[e]}else this._rvfcpolyfillmap[e]=requestAnimationFrame(d=>r(a,d))};return this._rvfcpolyfillmap[e]=requestAnimationFrame(n=>r(e,n)),e},HTMLVideoElement.prototype.cancelVideoFrameCallback=function(_){cancelAnimationFrame(this._rvfcpolyfillmap[_]),delete this._rvfcpolyfillmap[_]});const c={bt709:"BT709",bt470bg:"BT601",smpte170m:"BT601"},m={BT601:{BT709:"1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"},BT709:{BT601:"0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"},FCC:{BT709:"1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",BT601:"1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"},SMPTE240M:{BT709:"0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",BT601:"0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"}};class o extends EventTarget{constructor(e){if(super(),!globalThis.Worker)throw this.destroy("Worker not supported");if(!e)throw this.destroy("No options provided");this._loaded=new Promise(s=>{this._init=s});const t=o._test();if(this._onDemandRender="requestVideoFrameCallback"in HTMLVideoElement.prototype&&(e.onDemandRender??!0),this._offscreenRender="transferControlToOffscreen"in HTMLCanvasElement.prototype&&!e.canvas&&(e.offscreenRender??!0),this.timeOffset=e.timeOffset||0,this._video=e.video,this._videoHeight=0,this._videoWidth=0,this._videoColorSpace=null,this._canvas=e.canvas,this._video&&!this._canvas)this._canvasParent=document.createElement("div"),this._canvasParent.className="JASSUB",this._canvasParent.style.position="relative",this._canvas=this._createCanvas(),this._video.insertAdjacentElement("afterend",this._canvasParent);else if(!this._canvas)throw this.destroy("Don't know where to render: you should give video or canvas in options.");if(this._bufferCanvas=document.createElement("canvas"),this._bufferCtx=this._bufferCanvas.getContext("2d"),!this._bufferCtx)throw this.destroy("Canvas rendering not supported");this._canvasctrl=this._offscreenRender?this._canvas.transferControlToOffscreen():this._canvas,this._ctx=!this._offscreenRender&&this._canvasctrl.getContext("2d"),this._lastRenderTime=0,this.debug=!!e.debug,this.prescaleFactor=e.prescaleFactor||1,this.prescaleHeightLimit=e.prescaleHeightLimit||1080,this.maxRenderHeight=e.maxRenderHeight||0,this._boundResize=this.resize.bind(this),this._boundTimeUpdate=this._timeupdate.bind(this),this._boundSetRate=this.setRate.bind(this),this._boundUpdateColorSpace=this._updateColorSpace.bind(this),this._video&&this.setVideo(e.video),this._onDemandRender&&(this.busy=!1,this._lastDemandTime=null),this._worker=new Worker(e.workerUrl||"jassub-worker.js"),this._worker.onmessage=s=>this._onmessage(s),this._worker.onerror=s=>this._error(s),t.then(()=>{this._worker.postMessage({target:"init",wasmUrl:o._supportsSIMD&&e.modernWasmUrl?e.modernWasmUrl:e.wasmUrl??"jassub-worker.wasm",legacyWasmUrl:e.legacyWasmUrl??"jassub-worker.wasm.js",asyncRender:typeof createImageBitmap<"u"&&(e.asyncRender??!0),onDemandRender:this._onDemandRender,width:this._canvasctrl.width||0,height:this._canvasctrl.height||0,blendMode:e.blendMode||"js",subUrl:e.subUrl,subContent:e.subContent||null,fonts:e.fonts||[],availableFonts:e.availableFonts||{"liberation sans":"./default.woff2"},fallbackFont:e.fallbackFont||"liberation sans",debug:this.debug,targetFps:e.targetFps||24,dropAllAnimations:e.dropAllAnimations,dropAllBlur:e.dropAllBlur,libassMemoryLimit:e.libassMemoryLimit||0,libassGlyphLimit:e.libassGlyphLimit||0,useLocalFonts:typeof queryLocalFonts<"u"&&(e.useLocalFonts??!0),hasBitmapBug:o._hasBitmapBug}),this._offscreenRender===!0&&this.sendMessage("offscreenCanvas",null,[this._canvasctrl])})}_createCanvas(){return this._canvas=document.createElement("canvas"),this._canvas.style.display="block",this._canvas.style.position="absolute",this._canvas.style.pointerEvents="none",this._canvasParent.appendChild(this._canvas),this._canvas}static _supportsSIMD=null;static _hasAlphaBug=null;static _hasBitmapBug=null;static _testSIMD(){if(o._supportsSIMD===null)try{o._supportsSIMD=WebAssembly.validate(Uint8Array.of(0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11))}catch{o._supportsSIMD=!1}}static async _testImageBugs(){if(o._hasBitmapBug!==null)return;const e=document.createElement("canvas"),t=e.getContext("2d",{willReadFrequently:!0});if(!t)throw new Error("Canvas rendering not supported");if(typeof ImageData.prototype.constructor=="function")try{new ImageData(new Uint8ClampedArray([0,0,0,0]),1,1)}catch{console.log("Detected that ImageData is not constructable despite browser saying so"),self.ImageData=function(h,d,l){const f=t.createImageData(d,l);return h&&f.data.set(h),f}}const s=document.createElement("canvas"),r=s.getContext("2d",{willReadFrequently:!0});if(!r)throw new Error("Canvas rendering not supported");e.width=s.width=1,e.height=s.height=1,t.clearRect(0,0,1,1),r.clearRect(0,0,1,1);const n=r.getImageData(0,0,1,1).data;t.putImageData(new ImageData(new Uint8ClampedArray([0,255,0,0]),1,1),0,0),r.drawImage(e,0,0);const a=r.getImageData(0,0,1,1).data;if(o._hasAlphaBug=n[1]!==a[1],o._hasAlphaBug&&console.log("Detected a browser having issue with transparent pixels, applying workaround"),typeof createImageBitmap<"u"){const i=new Uint8ClampedArray([255,0,255,0,255]).subarray(1,5);r.drawImage(await createImageBitmap(new ImageData(i,1)),0,0);const{data:h}=r.getImageData(0,0,1,1);o._hasBitmapBug=!1;for(const[d,l]of h.entries())if(Math.abs(i[d]-l)>15){o._hasBitmapBug=!0,console.log("Detected a browser having issue with partial bitmaps, applying workaround");break}}else o._hasBitmapBug=!1;e.remove(),s.remove()}static async _test(){o._testSIMD(),await o._testImageBugs()}resize(e=0,t=0,s=0,r=0,n=this._video?.paused){if((!e||!t)&&this._video){const a=this._getVideoPosition();let i=null;if(this._videoWidth){const h=this._video.videoWidth/this._videoWidth,d=this._video.videoHeight/this._videoHeight;i=this._computeCanvasSize((a.width||0)/h,(a.height||0)/d)}else i=this._computeCanvasSize(a.width||0,a.height||0);e=i.width,t=i.height,this._canvasParent&&(s=a.y-(this._canvasParent.getBoundingClientRect().top-this._video.getBoundingClientRect().top),r=a.x),this._canvas.style.width=a.width+"px",this._canvas.style.height=a.height+"px"}this._canvas.style.top=s+"px",this._canvas.style.left=r+"px",n&&this.busy===!1?this.busy=!0:n=!1,this.sendMessage("canvas",{width:e,height:t,videoWidth:this._videoWidth||this._video.videoWidth,videoHeight:this._videoHeight||this._video.videoHeight,force:n})}_getVideoPosition(e=this._video.videoWidth,t=this._video.videoHeight){const s=e/t,{offsetWidth:r,offsetHeight:n}=this._video,a=r/n;e=r,t=n,a>s?e=Math.floor(n*s):t=Math.floor(r/s);const i=(r-e)/2,h=(n-t)/2;return{width:e,height:t,x:i,y:h}}_computeCanvasSize(e=0,t=0){const s=this.prescaleFactor<=0?1:this.prescaleFactor,r=self.devicePixelRatio||1;if(t<=0||e<=0)e=0,t=0;else{const n=s<1?-1:1;let a=t*r;n*a*s<=n*this.prescaleHeightLimit?a*=s:n*a0&&a>this.maxRenderHeight&&(a=this.maxRenderHeight),e*=a/t,t=a}return{width:e,height:t}}_timeupdate({type:e}){const s={seeking:!0,waiting:!0,playing:!1}[e];s!=null&&(this._playstate=s),this.setCurrentTime(this._video.paused||this._playstate,this._video.currentTime+this.timeOffset)}setVideo(e){e instanceof HTMLVideoElement?(this._removeListeners(),this._video=e,this._onDemandRender?this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)):(this._playstate=e.paused,e.addEventListener("timeupdate",this._boundTimeUpdate,!1),e.addEventListener("progress",this._boundTimeUpdate,!1),e.addEventListener("waiting",this._boundTimeUpdate,!1),e.addEventListener("seeking",this._boundTimeUpdate,!1),e.addEventListener("playing",this._boundTimeUpdate,!1),e.addEventListener("ratechange",this._boundSetRate,!1),e.addEventListener("resize",this._boundResize,!1)),"VideoFrame"in window&&(e.addEventListener("loadedmetadata",this._boundUpdateColorSpace,!1),e.readyState>2&&this._updateColorSpace()),e.videoWidth>0&&this.resize(),typeof ResizeObserver<"u"&&(this._ro||(this._ro=new ResizeObserver(()=>this.resize())),this._ro.observe(e))):this._error("Video element invalid!")}runBenchmark(){this.sendMessage("runBenchmark")}setTrackByUrl(e){this.sendMessage("setTrackByUrl",{url:e}),this._reAttachOffscreen(),this._ctx&&(this._ctx.filter="none")}setTrack(e){this.sendMessage("setTrack",{content:e}),this._reAttachOffscreen(),this._ctx&&(this._ctx.filter="none")}freeTrack(){this.sendMessage("freeTrack")}setIsPaused(e){this.sendMessage("video",{isPaused:e})}setRate(e){this.sendMessage("video",{rate:e})}setCurrentTime(e,t,s){this.sendMessage("video",{isPaused:e,currentTime:t,rate:s,colorSpace:this._videoColorSpace})}createEvent(e){this.sendMessage("createEvent",{event:e})}setEvent(e,t){this.sendMessage("setEvent",{event:e,index:t})}removeEvent(e){this.sendMessage("removeEvent",{index:e})}getEvents(e){this._fetchFromWorker({target:"getEvents"},(t,{events:s})=>{e(t,s)})}styleOverride(e){this.sendMessage("styleOverride",{style:e})}disableStyleOverride(){this.sendMessage("disableStyleOverride")}createStyle(e){this.sendMessage("createStyle",{style:e})}setStyle(e,t){this.sendMessage("setStyle",{style:e,index:t})}removeStyle(e){this.sendMessage("removeStyle",{index:e})}getStyles(e){this._fetchFromWorker({target:"getStyles"},(t,{styles:s})=>{e(t,s)})}addFont(e){this.sendMessage("addFont",{font:e})}setDefaultFont(e){this.sendMessage("defaultFont",{font:e})}_sendLocalFont(e){try{queryLocalFonts().then(t=>{const s=t?.find(r=>r.fullName.toLowerCase()===e);s&&s.blob().then(r=>{r.arrayBuffer().then(n=>{this.addFont(new Uint8Array(n))})})})}catch(t){console.warn("Local fonts API:",t)}}_getLocalFont({font:e}){try{navigator?.permissions?.query?navigator.permissions.query({name:"local-fonts"}).then(t=>{t.state==="granted"&&this._sendLocalFont(e)}):this._sendLocalFont(e)}catch(t){console.warn("Local fonts API:",t)}}_unbusy(){this._lastDemandTime?this._demandRender(this._lastDemandTime):this.busy=!1}_handleRVFC(e,{mediaTime:t,width:s,height:r}){if(this._destroyed)return null;this.busy?this._lastDemandTime={mediaTime:t,width:s,height:r}:(this.busy=!0,this._demandRender({mediaTime:t,width:s,height:r})),this._video.requestVideoFrameCallback(this._handleRVFC.bind(this))}_demandRender({mediaTime:e,width:t,height:s}){this._lastDemandTime=null,(t!==this._videoWidth||s!==this._videoHeight)&&(this._videoWidth=t,this._videoHeight=s,this.resize()),this.sendMessage("demand",{time:e+this.timeOffset})}_detachOffscreen(){if(!this._offscreenRender||this._ctx)return null;this._canvas.remove(),this._createCanvas(),this._canvasctrl=this._canvas,this._ctx=this._canvasctrl.getContext("2d"),this.sendMessage("detachOffscreen"),this.busy=!1,this.resize(0,0,0,0,!0)}_reAttachOffscreen(){if(!this._offscreenRender||!this._ctx)return null;this._canvas.remove(),this._createCanvas(),this._canvasctrl=this._canvas.transferControlToOffscreen(),this._ctx=!1,this.sendMessage("offscreenCanvas",null,[this._canvasctrl]),this.resize(0,0,0,0,!0)}_updateColorSpace(){this._video.requestVideoFrameCallback(()=>{try{const e=new VideoFrame(this._video);this._videoColorSpace=c[e.colorSpace.matrix],e.close(),this.sendMessage("getColorSpace")}catch(e){console.warn(e)}})}_verifyColorSpace({subtitleColorSpace:e,videoColorSpace:t=this._videoColorSpace}){!e||!t||e!==t&&(this._detachOffscreen(),this._ctx.filter=`url("data:image/svg+xml;utf8,#f")`)}_render({images:e,asyncRender:t,times:s,width:r,height:n,colorSpace:a}){this._unbusy(),this.debug&&(s.IPCTime=Date.now()-s.JSRenderTime),(this._canvasctrl.width!==r||this._canvasctrl.height!==n)&&(this._canvasctrl.width=r,this._canvasctrl.height=n,this._verifyColorSpace({subtitleColorSpace:a})),this._ctx.clearRect(0,0,this._canvasctrl.width,this._canvasctrl.height);for(const i of e)i.image&&(t?(this._ctx.drawImage(i.image,i.x,i.y),i.image.close()):(this._bufferCanvas.width=i.w,this._bufferCanvas.height=i.h,this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)),i.w,i.h),0,0),this._ctx.drawImage(this._bufferCanvas,i.x,i.y)));if(this.debug){s.JSRenderTime=Date.now()-s.JSRenderTime-s.IPCTime;let i=0;const h=s.bitmaps||e.length;delete s.bitmaps;for(const d in s)i+=s[d];console.log("Bitmaps: "+h+" Total: "+(i|0)+"ms",s)}}_fixAlpha(e){if(o._hasAlphaBug)for(let t=3;t1?e[t]:1;return e}_ready(){this._init(),this.dispatchEvent(new CustomEvent("ready"))}async sendMessage(e,t={},s){await this._loaded,s?this._worker.postMessage({target:e,transferable:s,...t},[...s]):this._worker.postMessage({target:e,...t})}_fetchFromWorker(e,t){try{const s=e.target,r=setTimeout(()=>{a(new Error("Error: Timeout while try to fetch "+s))},5e3),n=({data:i})=>{i.target===s&&(t(null,i),this._worker.removeEventListener("message",n),this._worker.removeEventListener("error",a),clearTimeout(r))},a=i=>{t(i),this._worker.removeEventListener("message",n),this._worker.removeEventListener("error",a),clearTimeout(r)};this._worker.addEventListener("message",n),this._worker.addEventListener("error",a),this._worker.postMessage(e)}catch(s){this._error(s)}}_console({content:e,command:t}){console[t].apply(console,JSON.parse(e))}_onmessage({data:e}){this["_"+e.target]&&this["_"+e.target](e)}_error(e){const t=e instanceof Error?e:e instanceof ErrorEvent?e.error:new Error(e),s=e instanceof Event?new ErrorEvent(e.type,e):new ErrorEvent("error",{error:t});return this.dispatchEvent(s),console.error(t),t}_removeListeners(){this._video&&(this._ro&&this._ro.unobserve(this._video),this._ctx&&(this._ctx.filter="none"),this._video.removeEventListener("timeupdate",this._boundTimeUpdate),this._video.removeEventListener("progress",this._boundTimeUpdate),this._video.removeEventListener("waiting",this._boundTimeUpdate),this._video.removeEventListener("seeking",this._boundTimeUpdate),this._video.removeEventListener("playing",this._boundTimeUpdate),this._video.removeEventListener("ratechange",this._boundSetRate),this._video.removeEventListener("resize",this._boundResize),this._video.removeEventListener("loadedmetadata",this._boundUpdateColorSpace))}destroy(e){return e&&(e=this._error(e)),this._video&&this._canvasParent&&this._video.parentNode?.removeChild(this._canvasParent),this._destroyed=!0,this._removeListeners(),this.sendMessage("destroy"),this._worker?.terminate(),e}}return o}); 2 | -------------------------------------------------------------------------------- /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 submdolue 21 | define PREPARE_SRC_VPATH 22 | rm -rf build/lib/$(1) 23 | mkdir -p build/lib/$(1) 24 | touch build/lib/$(1)/configured 25 | endef 26 | 27 | # All projects we build have autogen.sh, otherwise we could also fallback to `autoreconf -ivf .` 28 | RECONF_AUTO := NOCONFIGURE=1 ./autogen.sh 29 | 30 | CONF_ARGS = --enable-optimize 31 | 32 | ifeq (${MODERN},1) 33 | override CONF_ARGS += --enable-simd 34 | endif 35 | 36 | # @arg1: path to source directory; defaults to current working directory 37 | define CONFIGURE_AUTO 38 | emconfigure $(or $(1),.)/configure \ 39 | --prefix="$(DIST_DIR)" \ 40 | --host=x86-none-linux \ 41 | --build=x86_64 \ 42 | --enable-static \ 43 | --disable-shared \ 44 | --disable-debug \ 45 | $(CONF_ARGS) 46 | endef 47 | 48 | # @arg1: path to source directory; defaults to current working directory 49 | define CONFIGURE_CMAKE 50 | emcmake cmake -S "$(or $(1),.)" -DCMAKE_INSTALL_PREFIX="$(DIST_DIR)" 51 | endef 52 | 53 | # FIXME: Propagate jobserver info with $(MAKE) and set up our makefile for fully parallel builds 54 | JSO_MAKE := emmake make -j "$(shell nproc)" 55 | 56 | ## Clean and git related 57 | 58 | # @arg1: submodule name 59 | define TR_GIT_SM_RESET 60 | git-$(1): 61 | cd lib/$(1) && \ 62 | git reset --hard && \ 63 | git clean -dfx 64 | git submodule update --force lib/$(1) 65 | 66 | .PHONY: git-$(1) 67 | endef 68 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface ASS_Event { 2 | Start: number; 3 | Duration: number; 4 | Style: string; 5 | Name: string; 6 | MarginL: number; 7 | MarginR: number; 8 | MarginV: number; 9 | Effect: string; 10 | Text: string; 11 | ReadOrder: number; 12 | Layer: number; 13 | _index: number; 14 | } 15 | 16 | interface ASS_Style { 17 | Name: string; 18 | FontName: string; 19 | FontSize: number; 20 | PrimaryColour: number; // uint32_t RGBA 21 | SecondaryColour: number; // uint32_t RGBA 22 | OutlineColour: number; // uint32_t RGBA 23 | BackColour: number; // uint32_t RGBA 24 | Bold: number; 25 | Italic: number; 26 | Underline: number; 27 | StrikeOut: number; 28 | ScaleX: number; 29 | ScaleY: number; 30 | Spacing: number; 31 | Angle: number; 32 | BorderStyle: number; 33 | Outline: number; 34 | Shadow: number; 35 | Alignment: number; 36 | MarginL: number; 37 | MarginR: number; 38 | MarginV: number; 39 | Encoding: number; 40 | treat_fontname_as_pattern: number; 41 | Blur: number; 42 | Justify: number; 43 | } 44 | 45 | interface JassubOptions { 46 | 47 | video?: HTMLVideoElement; 48 | canvas?: HTMLCanvasElement; 49 | 50 | blendMode?: 'js' | 'wasm'; 51 | 52 | asyncRender?: boolean; 53 | offscreenRender?: boolean; 54 | onDemandRender?: boolean; 55 | targetFps?: number; 56 | timeOffset?: number; 57 | 58 | debug?: boolean; 59 | prescaleFactor?: number; 60 | prescaleHeightLimit?: number; 61 | maxRenderHeight?: number; 62 | dropAllAnimations?: boolean; 63 | dropAllBlur?: boolean 64 | 65 | workerUrl?: string; 66 | wasmUrl?: string; 67 | legacyWasmUrl?: string; 68 | modernWasmUrl?: string; 69 | 70 | subUrl?: string; 71 | subContent?: string; 72 | 73 | fonts?: string[] | Uint8Array[]; 74 | availableFonts?: Record; 75 | fallbackFont?: string; 76 | useLocalFonts?: boolean; 77 | 78 | libassMemoryLimit?: number; 79 | libassGlyphLimit?: number; 80 | } 81 | 82 | type ASS_EventCallback = (error: Error | null, event: ASS_Event[]) => void; 83 | type ASS_StyleCallback = (error: Error | null, event: ASS_Style[]) => void; 84 | 85 | export default class JASSUB { 86 | constructor (options: JassubOptions); 87 | 88 | resize (width?: number, height?: number, top?: number, left?: number): void; 89 | setVideo (video: HTMLVideoElement): void; 90 | runBenchmark (): void; 91 | 92 | setTrackByUrl (url: string): void; 93 | setTrack (content: string): void; 94 | freeTrack (): void; 95 | 96 | setIsPaused (isPaused: boolean): void; 97 | setRate (rate: number): void; 98 | setCurrentTime (isPaused?: boolean, currentTime?: number, rate?: number): void; 99 | 100 | createEvent (event: ASS_Event): void; 101 | setEvent (event: ASS_Event, index: number): void; 102 | removeEvent (index: number): void; 103 | getEvents (callback: ASS_EventCallback): void; 104 | 105 | createStyle (style: ASS_Style): void; 106 | setStyle (style: ASS_Style, index: number): void; 107 | removeStyle (index: number): void; 108 | getStyles (callback: ASS_StyleCallback): void; 109 | styleOverride (style: ASS_Style): void; 110 | disableStyleOverride(): void; 111 | setDefaultFont(font: string): void; 112 | 113 | addFont (font: string | Uint8Array): void; 114 | 115 | sendMessage (target: string, data?: Record, transferable?: Transferable[]): void; 116 | destroy (err?: string): void; 117 | 118 | static _hasAlphaBug: boolean; 119 | static _hasBitmapBug: boolean; 120 | _ctx: CanvasRenderingContext2D; 121 | _canvas: HTMLCanvasElement; 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jassub", 3 | "version": "1.8.6", 4 | "description": "libass Subtitle Renderer and Parser library for browsers", 5 | "main": "src/jassub.js", 6 | "type": "module", 7 | "files": [ 8 | "dist/COPYRIGHT", 9 | "dist/default.woff2", 10 | "dist/jassub*", 11 | "src/jassub.js", 12 | "index.d.ts" 13 | ], 14 | "scripts": { 15 | "build": "node vite.build.js", 16 | "docker:build": "docker build -t thaunknown/jassub-build .", 17 | "docker:run": "docker run -it --rm -v ${PWD}:/code --name thaunknown_jassub-build thaunknown/jassub-build:latest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/ThaUnknown/jassub.git" 22 | }, 23 | "keywords": [ 24 | "libass", 25 | "subtitle", 26 | "wasm", 27 | "emscripten" 28 | ], 29 | "author": "ThaUnknown", 30 | "contributors": [ 31 | "SubtitlesOctopus Contributors" 32 | ], 33 | "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", 34 | "bugs": { 35 | "url": "https://github.com/ThaUnknown/jassub/issues" 36 | }, 37 | "homepage": "https://github.com/ThaUnknown/jassub", 38 | "dependencies": { 39 | "rvfc-polyfill": "^1.0.7" 40 | }, 41 | "devDependencies": { 42 | "@types/emscripten": "^1.39.7", 43 | "terser": "^5.19.4", 44 | "vite": "^4.4.9", 45 | "vite-plugin-static-copy": "^0.17.0" 46 | } 47 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | rvfc-polyfill: 12 | specifier: ^1.0.7 13 | version: 1.0.7 14 | devDependencies: 15 | '@types/emscripten': 16 | specifier: ^1.39.7 17 | version: 1.39.7 18 | terser: 19 | specifier: ^5.19.4 20 | version: 5.19.4 21 | vite: 22 | specifier: ^4.4.9 23 | version: 4.4.9(terser@5.19.4) 24 | vite-plugin-static-copy: 25 | specifier: ^0.17.0 26 | version: 0.17.0(vite@4.4.9) 27 | 28 | packages: 29 | 30 | '@esbuild/android-arm64@0.18.20': 31 | resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 32 | engines: {node: '>=12'} 33 | cpu: [arm64] 34 | os: [android] 35 | 36 | '@esbuild/android-arm@0.18.20': 37 | resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 38 | engines: {node: '>=12'} 39 | cpu: [arm] 40 | os: [android] 41 | 42 | '@esbuild/android-x64@0.18.20': 43 | resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 44 | engines: {node: '>=12'} 45 | cpu: [x64] 46 | os: [android] 47 | 48 | '@esbuild/darwin-arm64@0.18.20': 49 | resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 50 | engines: {node: '>=12'} 51 | cpu: [arm64] 52 | os: [darwin] 53 | 54 | '@esbuild/darwin-x64@0.18.20': 55 | resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 56 | engines: {node: '>=12'} 57 | cpu: [x64] 58 | os: [darwin] 59 | 60 | '@esbuild/freebsd-arm64@0.18.20': 61 | resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 62 | engines: {node: '>=12'} 63 | cpu: [arm64] 64 | os: [freebsd] 65 | 66 | '@esbuild/freebsd-x64@0.18.20': 67 | resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 68 | engines: {node: '>=12'} 69 | cpu: [x64] 70 | os: [freebsd] 71 | 72 | '@esbuild/linux-arm64@0.18.20': 73 | resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 74 | engines: {node: '>=12'} 75 | cpu: [arm64] 76 | os: [linux] 77 | 78 | '@esbuild/linux-arm@0.18.20': 79 | resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 80 | engines: {node: '>=12'} 81 | cpu: [arm] 82 | os: [linux] 83 | 84 | '@esbuild/linux-ia32@0.18.20': 85 | resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 86 | engines: {node: '>=12'} 87 | cpu: [ia32] 88 | os: [linux] 89 | 90 | '@esbuild/linux-loong64@0.18.20': 91 | resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 92 | engines: {node: '>=12'} 93 | cpu: [loong64] 94 | os: [linux] 95 | 96 | '@esbuild/linux-mips64el@0.18.20': 97 | resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 98 | engines: {node: '>=12'} 99 | cpu: [mips64el] 100 | os: [linux] 101 | 102 | '@esbuild/linux-ppc64@0.18.20': 103 | resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 104 | engines: {node: '>=12'} 105 | cpu: [ppc64] 106 | os: [linux] 107 | 108 | '@esbuild/linux-riscv64@0.18.20': 109 | resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 110 | engines: {node: '>=12'} 111 | cpu: [riscv64] 112 | os: [linux] 113 | 114 | '@esbuild/linux-s390x@0.18.20': 115 | resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 116 | engines: {node: '>=12'} 117 | cpu: [s390x] 118 | os: [linux] 119 | 120 | '@esbuild/linux-x64@0.18.20': 121 | resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 122 | engines: {node: '>=12'} 123 | cpu: [x64] 124 | os: [linux] 125 | 126 | '@esbuild/netbsd-x64@0.18.20': 127 | resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 128 | engines: {node: '>=12'} 129 | cpu: [x64] 130 | os: [netbsd] 131 | 132 | '@esbuild/openbsd-x64@0.18.20': 133 | resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 134 | engines: {node: '>=12'} 135 | cpu: [x64] 136 | os: [openbsd] 137 | 138 | '@esbuild/sunos-x64@0.18.20': 139 | resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 140 | engines: {node: '>=12'} 141 | cpu: [x64] 142 | os: [sunos] 143 | 144 | '@esbuild/win32-arm64@0.18.20': 145 | resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 146 | engines: {node: '>=12'} 147 | cpu: [arm64] 148 | os: [win32] 149 | 150 | '@esbuild/win32-ia32@0.18.20': 151 | resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 152 | engines: {node: '>=12'} 153 | cpu: [ia32] 154 | os: [win32] 155 | 156 | '@esbuild/win32-x64@0.18.20': 157 | resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 158 | engines: {node: '>=12'} 159 | cpu: [x64] 160 | os: [win32] 161 | 162 | '@jridgewell/gen-mapping@0.3.3': 163 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 164 | engines: {node: '>=6.0.0'} 165 | 166 | '@jridgewell/resolve-uri@3.1.1': 167 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 168 | engines: {node: '>=6.0.0'} 169 | 170 | '@jridgewell/set-array@1.1.2': 171 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 172 | engines: {node: '>=6.0.0'} 173 | 174 | '@jridgewell/source-map@0.3.5': 175 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 176 | 177 | '@jridgewell/sourcemap-codec@1.4.15': 178 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 179 | 180 | '@jridgewell/trace-mapping@0.3.19': 181 | resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} 182 | 183 | '@nodelib/fs.scandir@2.1.5': 184 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 185 | engines: {node: '>= 8'} 186 | 187 | '@nodelib/fs.stat@2.0.5': 188 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 189 | engines: {node: '>= 8'} 190 | 191 | '@nodelib/fs.walk@1.2.8': 192 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 193 | engines: {node: '>= 8'} 194 | 195 | '@types/emscripten@1.39.7': 196 | resolution: {integrity: sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==} 197 | 198 | acorn@8.10.0: 199 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 200 | engines: {node: '>=0.4.0'} 201 | hasBin: true 202 | 203 | anymatch@3.1.3: 204 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 205 | engines: {node: '>= 8'} 206 | 207 | binary-extensions@2.2.0: 208 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 209 | engines: {node: '>=8'} 210 | 211 | braces@3.0.2: 212 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 213 | engines: {node: '>=8'} 214 | 215 | buffer-from@1.1.2: 216 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 217 | 218 | chokidar@3.5.3: 219 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 220 | engines: {node: '>= 8.10.0'} 221 | 222 | commander@2.20.3: 223 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 224 | 225 | esbuild@0.18.20: 226 | resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 227 | engines: {node: '>=12'} 228 | hasBin: true 229 | 230 | fast-glob@3.3.1: 231 | resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} 232 | engines: {node: '>=8.6.0'} 233 | 234 | fastq@1.15.0: 235 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 236 | 237 | fill-range@7.0.1: 238 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 239 | engines: {node: '>=8'} 240 | 241 | fs-extra@11.1.1: 242 | resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} 243 | engines: {node: '>=14.14'} 244 | 245 | fsevents@2.3.3: 246 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 247 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 248 | os: [darwin] 249 | 250 | glob-parent@5.1.2: 251 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 252 | engines: {node: '>= 6'} 253 | 254 | graceful-fs@4.2.11: 255 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 256 | 257 | is-binary-path@2.1.0: 258 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 259 | engines: {node: '>=8'} 260 | 261 | is-extglob@2.1.1: 262 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 263 | engines: {node: '>=0.10.0'} 264 | 265 | is-glob@4.0.3: 266 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 267 | engines: {node: '>=0.10.0'} 268 | 269 | is-number@7.0.0: 270 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 271 | engines: {node: '>=0.12.0'} 272 | 273 | jsonfile@6.1.0: 274 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 275 | 276 | merge2@1.4.1: 277 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 278 | engines: {node: '>= 8'} 279 | 280 | micromatch@4.0.5: 281 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 282 | engines: {node: '>=8.6'} 283 | 284 | nanoid@3.3.6: 285 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 286 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 287 | hasBin: true 288 | 289 | normalize-path@3.0.0: 290 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 291 | engines: {node: '>=0.10.0'} 292 | 293 | picocolors@1.0.0: 294 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 295 | 296 | picomatch@2.3.1: 297 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 298 | engines: {node: '>=8.6'} 299 | 300 | postcss@8.4.29: 301 | resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} 302 | engines: {node: ^10 || ^12 || >=14} 303 | 304 | queue-microtask@1.2.3: 305 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 306 | 307 | readdirp@3.6.0: 308 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 309 | engines: {node: '>=8.10.0'} 310 | 311 | reusify@1.0.4: 312 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 313 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 314 | 315 | rollup@3.29.2: 316 | resolution: {integrity: sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==} 317 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 318 | hasBin: true 319 | 320 | run-parallel@1.2.0: 321 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 322 | 323 | rvfc-polyfill@1.0.7: 324 | resolution: {integrity: sha512-seBl7J1J3/k0LuzW2T9fG6JIOpni5AbU+/87LA+zTYKgTVhsfShmS8K/yOo1eeEjGJHnAdkVAUUM+PEjN9Mpkw==} 325 | 326 | source-map-js@1.0.2: 327 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 328 | engines: {node: '>=0.10.0'} 329 | 330 | source-map-support@0.5.21: 331 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 332 | 333 | source-map@0.6.1: 334 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 335 | engines: {node: '>=0.10.0'} 336 | 337 | terser@5.19.4: 338 | resolution: {integrity: sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==} 339 | engines: {node: '>=10'} 340 | hasBin: true 341 | 342 | to-regex-range@5.0.1: 343 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 344 | engines: {node: '>=8.0'} 345 | 346 | universalify@2.0.0: 347 | resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} 348 | engines: {node: '>= 10.0.0'} 349 | 350 | vite-plugin-static-copy@0.17.0: 351 | resolution: {integrity: sha512-2HpNbHfDt8SDy393AGXh9llHkc8FJMQkI8s3T5WsH3SWLMO+f5cFIyPErl4yGKU9Uh3Vaqsd4lHZYTf042fQ2A==} 352 | engines: {node: ^14.18.0 || >=16.0.0} 353 | peerDependencies: 354 | vite: ^3.0.0 || ^4.0.0 355 | 356 | vite@4.4.9: 357 | resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} 358 | engines: {node: ^14.18.0 || >=16.0.0} 359 | hasBin: true 360 | peerDependencies: 361 | '@types/node': '>= 14' 362 | less: '*' 363 | lightningcss: ^1.21.0 364 | sass: '*' 365 | stylus: '*' 366 | sugarss: '*' 367 | terser: ^5.4.0 368 | peerDependenciesMeta: 369 | '@types/node': 370 | optional: true 371 | less: 372 | optional: true 373 | lightningcss: 374 | optional: true 375 | sass: 376 | optional: true 377 | stylus: 378 | optional: true 379 | sugarss: 380 | optional: true 381 | terser: 382 | optional: true 383 | 384 | snapshots: 385 | 386 | '@esbuild/android-arm64@0.18.20': 387 | optional: true 388 | 389 | '@esbuild/android-arm@0.18.20': 390 | optional: true 391 | 392 | '@esbuild/android-x64@0.18.20': 393 | optional: true 394 | 395 | '@esbuild/darwin-arm64@0.18.20': 396 | optional: true 397 | 398 | '@esbuild/darwin-x64@0.18.20': 399 | optional: true 400 | 401 | '@esbuild/freebsd-arm64@0.18.20': 402 | optional: true 403 | 404 | '@esbuild/freebsd-x64@0.18.20': 405 | optional: true 406 | 407 | '@esbuild/linux-arm64@0.18.20': 408 | optional: true 409 | 410 | '@esbuild/linux-arm@0.18.20': 411 | optional: true 412 | 413 | '@esbuild/linux-ia32@0.18.20': 414 | optional: true 415 | 416 | '@esbuild/linux-loong64@0.18.20': 417 | optional: true 418 | 419 | '@esbuild/linux-mips64el@0.18.20': 420 | optional: true 421 | 422 | '@esbuild/linux-ppc64@0.18.20': 423 | optional: true 424 | 425 | '@esbuild/linux-riscv64@0.18.20': 426 | optional: true 427 | 428 | '@esbuild/linux-s390x@0.18.20': 429 | optional: true 430 | 431 | '@esbuild/linux-x64@0.18.20': 432 | optional: true 433 | 434 | '@esbuild/netbsd-x64@0.18.20': 435 | optional: true 436 | 437 | '@esbuild/openbsd-x64@0.18.20': 438 | optional: true 439 | 440 | '@esbuild/sunos-x64@0.18.20': 441 | optional: true 442 | 443 | '@esbuild/win32-arm64@0.18.20': 444 | optional: true 445 | 446 | '@esbuild/win32-ia32@0.18.20': 447 | optional: true 448 | 449 | '@esbuild/win32-x64@0.18.20': 450 | optional: true 451 | 452 | '@jridgewell/gen-mapping@0.3.3': 453 | dependencies: 454 | '@jridgewell/set-array': 1.1.2 455 | '@jridgewell/sourcemap-codec': 1.4.15 456 | '@jridgewell/trace-mapping': 0.3.19 457 | 458 | '@jridgewell/resolve-uri@3.1.1': {} 459 | 460 | '@jridgewell/set-array@1.1.2': {} 461 | 462 | '@jridgewell/source-map@0.3.5': 463 | dependencies: 464 | '@jridgewell/gen-mapping': 0.3.3 465 | '@jridgewell/trace-mapping': 0.3.19 466 | 467 | '@jridgewell/sourcemap-codec@1.4.15': {} 468 | 469 | '@jridgewell/trace-mapping@0.3.19': 470 | dependencies: 471 | '@jridgewell/resolve-uri': 3.1.1 472 | '@jridgewell/sourcemap-codec': 1.4.15 473 | 474 | '@nodelib/fs.scandir@2.1.5': 475 | dependencies: 476 | '@nodelib/fs.stat': 2.0.5 477 | run-parallel: 1.2.0 478 | 479 | '@nodelib/fs.stat@2.0.5': {} 480 | 481 | '@nodelib/fs.walk@1.2.8': 482 | dependencies: 483 | '@nodelib/fs.scandir': 2.1.5 484 | fastq: 1.15.0 485 | 486 | '@types/emscripten@1.39.7': {} 487 | 488 | acorn@8.10.0: {} 489 | 490 | anymatch@3.1.3: 491 | dependencies: 492 | normalize-path: 3.0.0 493 | picomatch: 2.3.1 494 | 495 | binary-extensions@2.2.0: {} 496 | 497 | braces@3.0.2: 498 | dependencies: 499 | fill-range: 7.0.1 500 | 501 | buffer-from@1.1.2: {} 502 | 503 | chokidar@3.5.3: 504 | dependencies: 505 | anymatch: 3.1.3 506 | braces: 3.0.2 507 | glob-parent: 5.1.2 508 | is-binary-path: 2.1.0 509 | is-glob: 4.0.3 510 | normalize-path: 3.0.0 511 | readdirp: 3.6.0 512 | optionalDependencies: 513 | fsevents: 2.3.3 514 | 515 | commander@2.20.3: {} 516 | 517 | esbuild@0.18.20: 518 | optionalDependencies: 519 | '@esbuild/android-arm': 0.18.20 520 | '@esbuild/android-arm64': 0.18.20 521 | '@esbuild/android-x64': 0.18.20 522 | '@esbuild/darwin-arm64': 0.18.20 523 | '@esbuild/darwin-x64': 0.18.20 524 | '@esbuild/freebsd-arm64': 0.18.20 525 | '@esbuild/freebsd-x64': 0.18.20 526 | '@esbuild/linux-arm': 0.18.20 527 | '@esbuild/linux-arm64': 0.18.20 528 | '@esbuild/linux-ia32': 0.18.20 529 | '@esbuild/linux-loong64': 0.18.20 530 | '@esbuild/linux-mips64el': 0.18.20 531 | '@esbuild/linux-ppc64': 0.18.20 532 | '@esbuild/linux-riscv64': 0.18.20 533 | '@esbuild/linux-s390x': 0.18.20 534 | '@esbuild/linux-x64': 0.18.20 535 | '@esbuild/netbsd-x64': 0.18.20 536 | '@esbuild/openbsd-x64': 0.18.20 537 | '@esbuild/sunos-x64': 0.18.20 538 | '@esbuild/win32-arm64': 0.18.20 539 | '@esbuild/win32-ia32': 0.18.20 540 | '@esbuild/win32-x64': 0.18.20 541 | 542 | fast-glob@3.3.1: 543 | dependencies: 544 | '@nodelib/fs.stat': 2.0.5 545 | '@nodelib/fs.walk': 1.2.8 546 | glob-parent: 5.1.2 547 | merge2: 1.4.1 548 | micromatch: 4.0.5 549 | 550 | fastq@1.15.0: 551 | dependencies: 552 | reusify: 1.0.4 553 | 554 | fill-range@7.0.1: 555 | dependencies: 556 | to-regex-range: 5.0.1 557 | 558 | fs-extra@11.1.1: 559 | dependencies: 560 | graceful-fs: 4.2.11 561 | jsonfile: 6.1.0 562 | universalify: 2.0.0 563 | 564 | fsevents@2.3.3: 565 | optional: true 566 | 567 | glob-parent@5.1.2: 568 | dependencies: 569 | is-glob: 4.0.3 570 | 571 | graceful-fs@4.2.11: {} 572 | 573 | is-binary-path@2.1.0: 574 | dependencies: 575 | binary-extensions: 2.2.0 576 | 577 | is-extglob@2.1.1: {} 578 | 579 | is-glob@4.0.3: 580 | dependencies: 581 | is-extglob: 2.1.1 582 | 583 | is-number@7.0.0: {} 584 | 585 | jsonfile@6.1.0: 586 | dependencies: 587 | universalify: 2.0.0 588 | optionalDependencies: 589 | graceful-fs: 4.2.11 590 | 591 | merge2@1.4.1: {} 592 | 593 | micromatch@4.0.5: 594 | dependencies: 595 | braces: 3.0.2 596 | picomatch: 2.3.1 597 | 598 | nanoid@3.3.6: {} 599 | 600 | normalize-path@3.0.0: {} 601 | 602 | picocolors@1.0.0: {} 603 | 604 | picomatch@2.3.1: {} 605 | 606 | postcss@8.4.29: 607 | dependencies: 608 | nanoid: 3.3.6 609 | picocolors: 1.0.0 610 | source-map-js: 1.0.2 611 | 612 | queue-microtask@1.2.3: {} 613 | 614 | readdirp@3.6.0: 615 | dependencies: 616 | picomatch: 2.3.1 617 | 618 | reusify@1.0.4: {} 619 | 620 | rollup@3.29.2: 621 | optionalDependencies: 622 | fsevents: 2.3.3 623 | 624 | run-parallel@1.2.0: 625 | dependencies: 626 | queue-microtask: 1.2.3 627 | 628 | rvfc-polyfill@1.0.7: {} 629 | 630 | source-map-js@1.0.2: {} 631 | 632 | source-map-support@0.5.21: 633 | dependencies: 634 | buffer-from: 1.1.2 635 | source-map: 0.6.1 636 | 637 | source-map@0.6.1: {} 638 | 639 | terser@5.19.4: 640 | dependencies: 641 | '@jridgewell/source-map': 0.3.5 642 | acorn: 8.10.0 643 | commander: 2.20.3 644 | source-map-support: 0.5.21 645 | 646 | to-regex-range@5.0.1: 647 | dependencies: 648 | is-number: 7.0.0 649 | 650 | universalify@2.0.0: {} 651 | 652 | vite-plugin-static-copy@0.17.0(vite@4.4.9): 653 | dependencies: 654 | chokidar: 3.5.3 655 | fast-glob: 3.3.1 656 | fs-extra: 11.1.1 657 | picocolors: 1.0.0 658 | vite: 4.4.9(terser@5.19.4) 659 | 660 | vite@4.4.9(terser@5.19.4): 661 | dependencies: 662 | esbuild: 0.18.20 663 | postcss: 8.4.29 664 | rollup: 3.29.2 665 | terser: 5.19.4 666 | optionalDependencies: 667 | fsevents: 2.3.3 668 | -------------------------------------------------------------------------------- /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 -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 thaunknown_jassub-build" 6 | echo " -i: Name of the image to buld/use;" 7 | echo " defaults to thaunknown/jassub-build" 8 | echo "If no command is given `make` without arguments will be executed" 9 | exit 2 10 | } 11 | 12 | OPTIND=1 13 | CONTAINER="thaunknown_jassub-build" 14 | IMAGE="thaunknown/jassub-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 -v "${PWD}":/code --name "$CONTAINER" "$IMAGE":latest 12 | else 13 | docker run -it --rm -v "${PWD}":/code --name "$CONTAINER" "$IMAGE":latest "$@" 14 | fi 15 | -------------------------------------------------------------------------------- /src/JASSUB.cpp: -------------------------------------------------------------------------------- 1 | #include "../lib/libass/libass/ass.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | int log_level = 3; 13 | 14 | class ReusableBuffer { 15 | private: 16 | void *buffer; 17 | size_t lessen_counter; 18 | 19 | public: 20 | size_t size; 21 | ReusableBuffer() : buffer(NULL), size(0), lessen_counter(0) { 22 | } 23 | 24 | ~ReusableBuffer() { 25 | free(buffer); 26 | } 27 | 28 | void clear() { 29 | free(buffer); 30 | buffer = NULL; 31 | size = 0; 32 | lessen_counter = 0; 33 | } 34 | 35 | void *take(size_t new_size) { 36 | if (size >= new_size) { 37 | if (size >= 1.3 * new_size) { 38 | // big reduction request 39 | lessen_counter++; 40 | } else { 41 | lessen_counter = 0; 42 | } 43 | if (lessen_counter < 10) { 44 | // not reducing the buffer yet 45 | memset(buffer, 0, new_size); 46 | return buffer; 47 | } 48 | } 49 | 50 | free(buffer); 51 | buffer = malloc(new_size); 52 | if (buffer) { 53 | size = new_size; 54 | memset(buffer, 0, size); 55 | } else 56 | size = 0; 57 | lessen_counter = 0; 58 | return buffer; 59 | } 60 | }; 61 | 62 | void msg_callback(int level, const char *fmt, va_list va, void *data) { 63 | if (level > log_level) // 6 for verbose 64 | return; 65 | 66 | const int ERR_LEVEL = 1; 67 | FILE *stream = level <= ERR_LEVEL ? stderr : stdout; 68 | 69 | fprintf(stream, "JASSUB: "); 70 | vfprintf(stream, fmt, va); 71 | fprintf(stream, "\n"); 72 | } 73 | 74 | const float MIN_UINT8_CAST = 0.9 / 255; 75 | const float MAX_UINT8_CAST = 255.9 / 255; 76 | 77 | #define CLAMP_UINT8(value) ((value > MIN_UINT8_CAST) ? ((value < MAX_UINT8_CAST) ? (int)(value * 255) : 255) : 0) 78 | 79 | typedef struct RenderResult { 80 | public: 81 | int x, y, w, h; 82 | size_t image; 83 | RenderResult *next; 84 | } RenderResult; 85 | 86 | // maximum regions - a grid of 3x3 87 | #define MAX_BLEND_STORAGES (3 * 3) 88 | struct RenderBlendStorage { 89 | RenderResult next; 90 | ReusableBuffer buf; 91 | bool taken; 92 | }; 93 | 94 | #define MIN(x, y) (((x) < (y)) ? (x) : (y)) 95 | #define MAX(x, y) (((x) > (y)) ? (x) : (y)) 96 | 97 | class BoundingBox { 98 | public: 99 | int min_x, max_x, min_y, max_y; 100 | 101 | BoundingBox() : min_x(-1), max_x(-1), min_y(-1), max_y(-1) { 102 | } 103 | 104 | bool empty() const { 105 | return min_x == -1; 106 | } 107 | 108 | void add(int x1, int y1, int w, int h) { 109 | int x2 = x1 + w - 1, y2 = y1 + h - 1; 110 | min_x = (min_x < 0) ? x1 : MIN(min_x, x1); 111 | min_y = (min_y < 0) ? y1 : MIN(min_y, y1); 112 | max_x = (max_x < 0) ? x2 : MAX(max_x, x2); 113 | max_y = (max_y < 0) ? y2 : MAX(max_y, y2); 114 | } 115 | 116 | bool intersets(const BoundingBox &other) const { 117 | return !(other.min_x > max_x || other.max_x < min_x || other.min_y > max_y || other.max_y < min_y); 118 | } 119 | 120 | bool tryMerge(BoundingBox &other) { 121 | if (!intersets(other)) 122 | return false; 123 | 124 | min_x = MIN(min_x, other.min_x); 125 | min_y = MIN(min_y, other.min_y); 126 | max_x = MAX(max_x, other.max_x); 127 | max_y = MAX(max_y, other.max_y); 128 | return true; 129 | } 130 | 131 | void clear() { 132 | min_x = max_x = min_y = max_y = -1; 133 | } 134 | }; 135 | 136 | /** 137 | * \brief Overwrite tag with whitespace to nullify its effect 138 | * Boundaries are inclusive at both ends. 139 | */ 140 | static void _remove_tag(char *begin, char *end) { 141 | if (end < begin) 142 | return; 143 | memset(begin, ' ', end - begin + 1); 144 | } 145 | 146 | /** 147 | * \param begin point to the first character of the tag name (after backslash) 148 | * \param end last character that can be read; at least the name itself 149 | and the following character if any must be included 150 | * \return true if tag may cause animations, false if it will definitely not 151 | */ 152 | static bool _is_animated_tag(char *begin, char *end) { 153 | if (end <= begin) 154 | return false; 155 | 156 | size_t length = end - begin + 1; 157 | 158 | #define check_simple_tag(tag) (sizeof(tag) - 1 < length && !strncmp(begin, tag, sizeof(tag) - 1)) 159 | #define check_complex_tag(tag) (check_simple_tag(tag) && (begin[sizeof(tag) - 1] == '(' || begin[sizeof(tag) - 1] == ' ' || begin[sizeof(tag) - 1] == '\t')) 160 | switch (begin[0]) { 161 | case 'k': //-fallthrough 162 | case 'K': 163 | // Karaoke: k, kf, ko, K and kt ; no other valid ASS-tag starts with k/K 164 | return true; 165 | case 't': 166 | // Animated transform: no other valid tag begins with t 167 | // non-nested t-tags have to be complex tags even in single argument 168 | // form, but nested t-tags (which act like independent t-tags) are allowed to be 169 | // simple-tags without parentheses due to VSF-parsing quirk. 170 | // Since all valid simple t-tags require the existence of a complex t-tag, we only check for complex tags 171 | // to avoid false positives from invalid simple t-tags. This makes animation-dropping somewhat incorrect 172 | // but as animation detection remains accurate, we consider this to be "good enough" 173 | return check_complex_tag("t"); 174 | case 'm': 175 | // Movement: complex tag; again no other valid tag begins with m 176 | // but ensure it's complex just to be sure 177 | return check_complex_tag("move"); 178 | case 'f': 179 | // Fade: \fad and Fade (complex): \fade; both complex 180 | // there are several other valid tags beginning with f 181 | return check_complex_tag("fad") || check_complex_tag("fade"); 182 | } 183 | 184 | return false; 185 | #undef check_complex_tag 186 | #undef check_simple_tag 187 | } 188 | 189 | /** 190 | * \param start First character after { (optionally spaces can be dropped) 191 | * \param end Last character before } (optionally spaces can be dropped) 192 | * \param drop_animations If true animation tags will be discarded 193 | * \return true if after processing the event may contain animations 194 | (i.e. when dropping animations this is always false) 195 | */ 196 | static bool _is_block_animated(char *start, char *end, bool drop_animations) { 197 | char *tag_start = NULL; // points to beginning backslash 198 | for (char *p = start; p <= end; p++) { 199 | if (*p == '\\') { 200 | // It is safe to go one before and beyond unconditionally 201 | // because the text passed in must be surronded by { } 202 | if (tag_start && _is_animated_tag(tag_start + 1, p - 1)) { 203 | if (!drop_animations) 204 | return true; 205 | // For \t transforms this will assume the final state 206 | _remove_tag(tag_start, p - 1); 207 | } 208 | tag_start = p; 209 | } 210 | } 211 | 212 | if (tag_start && _is_animated_tag(tag_start + 1, end)) { 213 | if (!drop_animations) 214 | return true; 215 | _remove_tag(tag_start, end); 216 | } 217 | 218 | return false; 219 | } 220 | 221 | /** 222 | * \param event ASS event to be processed 223 | * \param drop_animations If true animation tags will be discarded 224 | * \return true if after processing the event may contain animations 225 | (i.e. when dropping animations this is always false) 226 | */ 227 | static bool _is_event_animated(ASS_Event *event, bool drop_animations) { 228 | // Event is animated if it has an Effect or animated override tags 229 | if (event->Effect && event->Effect[0] != '\0') { 230 | if (!drop_animations) 231 | return 1; 232 | event->Effect[0] = '\0'; 233 | } 234 | 235 | // Search for override blocks 236 | // Only closed {...}-blocks are parsed by VSFilters and libass 237 | char *block_start = NULL; // points to opening { 238 | for (char *p = event->Text; *p != '\0'; p++) { 239 | switch (*p) { 240 | case '{': 241 | // Escaping the opening curly bracket to not start an override block is 242 | // a VSFilter-incompatible libass extension. But we only use libass, so... 243 | if (!block_start && (p == event->Text || *(p - 1) != '\\')) 244 | block_start = p; 245 | break; 246 | case '}': 247 | if (block_start && p - block_start > 2 && _is_block_animated(block_start + 1, p - 1, drop_animations)) 248 | return true; 249 | block_start = NULL; 250 | break; 251 | default: 252 | break; 253 | } 254 | } 255 | 256 | return false; 257 | } 258 | 259 | static char *copyString(const std::string &str) { 260 | char *result = new char[str.length() + 1]; 261 | strcpy(result, str.data()); 262 | return result; 263 | } 264 | 265 | class JASSUB { 266 | private: 267 | ReusableBuffer m_buffer; 268 | RenderBlendStorage m_blendParts[MAX_BLEND_STORAGES]; 269 | bool drop_animations; 270 | int scanned_events; // next unscanned event index 271 | ASS_Library *ass_library; 272 | ASS_Renderer *ass_renderer; 273 | bool debug; 274 | 275 | int canvas_w; 276 | int canvas_h; 277 | 278 | int status; 279 | 280 | const char *defaultFont; 281 | 282 | public: 283 | ASS_Track *track; 284 | 285 | int trackColorSpace; 286 | int changed = 0; 287 | int count = 0; 288 | int time = 0; 289 | JASSUB(int canvas_w, int canvas_h, const std::string &df, bool debug) { 290 | status = 0; 291 | ass_library = NULL; 292 | ass_renderer = NULL; 293 | track = NULL; 294 | this->canvas_w = canvas_w; 295 | this->canvas_h = canvas_h; 296 | drop_animations = false; 297 | scanned_events = 0; 298 | this->debug = debug; 299 | 300 | defaultFont = copyString(df); 301 | ass_library = ass_library_init(); 302 | if (!ass_library) { 303 | fprintf(stderr, "JASSUB: ass_library_init failed!\n"); 304 | exit(2); 305 | } 306 | 307 | ass_set_message_cb(ass_library, msg_callback, NULL); 308 | 309 | ass_renderer = ass_renderer_init(ass_library); 310 | if (!ass_renderer) { 311 | fprintf(stderr, "JASSUB: ass_renderer_init failed!\n"); 312 | exit(3); 313 | } 314 | ass_set_extract_fonts(ass_library, true); 315 | 316 | resizeCanvas(canvas_w, canvas_h, canvas_w, canvas_h); 317 | 318 | reloadFonts(); 319 | m_buffer.clear(); 320 | } 321 | 322 | void setLogLevel(int level) { 323 | log_level = level; 324 | } 325 | 326 | void setDropAnimations(int value) { 327 | drop_animations = !!value; 328 | if (drop_animations) 329 | scanAnimations(scanned_events); 330 | } 331 | 332 | /* 333 | * \brief Scan events starting at index i for animations 334 | * and discard animated tags when found. 335 | * Note that once animated tags were dropped they cannot be restored. 336 | * Updates the class member scanned_events to last scanned index. 337 | */ 338 | void scanAnimations(int i) { 339 | for (; i < track->n_events; i++) { 340 | _is_event_animated(track->events + i, drop_animations); 341 | } 342 | scanned_events = i; 343 | } 344 | 345 | /* TRACK */ 346 | void createTrackMem(std::string buf) { 347 | removeTrack(); 348 | track = ass_read_memory(ass_library, buf.data(), buf.size(), NULL); 349 | if (!track) { 350 | fprintf(stderr, "JASSUB: Failed to start a track\n"); 351 | exit(4); 352 | } 353 | scanAnimations(0); 354 | 355 | trackColorSpace = track->YCbCrMatrix; 356 | } 357 | 358 | void removeTrack() { 359 | if (track != NULL) { 360 | ass_free_track(track); 361 | track = NULL; 362 | } 363 | } 364 | /* TRACK */ 365 | 366 | /* CANVAS */ 367 | void resizeCanvas(int canvas_w, int canvas_h, int video_w, int video_h) { 368 | ass_set_storage_size(ass_renderer, video_w, video_h); 369 | ass_set_frame_size(ass_renderer, canvas_w, canvas_h); 370 | this->canvas_h = canvas_h; 371 | this->canvas_w = canvas_w; 372 | } 373 | int getBufferSize(ASS_Image *img) { 374 | int size = 0; 375 | for (ASS_Image *tmp = img; tmp; tmp = tmp->next) { 376 | if (tmp->w == 0 || tmp->h == 0) { 377 | continue; 378 | } 379 | size += sizeof(uint32_t) * tmp->w * tmp->h + sizeof(RenderResult); 380 | } 381 | return size; 382 | } 383 | RenderResult *processImages(ASS_Image *img) { 384 | RenderResult *renderResult = NULL; 385 | char *rawbuffer = (char *)m_buffer.take(getBufferSize(img)); 386 | if (rawbuffer == NULL) { 387 | fprintf(stderr, "JASSUB: cannot allocate buffer for rendering\n"); 388 | return NULL; 389 | } 390 | for (RenderResult *tmp = renderResult; img; img = img->next) { 391 | int w = img->w, h = img->h; 392 | if (w == 0 || h == 0) continue; 393 | 394 | double alpha = (255 - (img->color & 255)) / 255.0; 395 | if (alpha == 0.0) continue; 396 | 397 | unsigned int datasize = sizeof(uint32_t) * w * h; 398 | size_t *data = (size_t *)rawbuffer; 399 | decodeBitmap(alpha, data, img, w, h); 400 | RenderResult *result = (RenderResult *)(rawbuffer + datasize); 401 | result->w = w; 402 | result->h = h; 403 | result->x = img->dst_x; 404 | result->y = img->dst_y; 405 | result->image = (size_t)data; 406 | result->next = NULL; 407 | 408 | if (tmp) { 409 | tmp->next = result; 410 | } else { 411 | renderResult = result; 412 | } 413 | tmp = result; 414 | 415 | rawbuffer += datasize + sizeof(RenderResult); 416 | ++count; 417 | } 418 | return renderResult; 419 | } 420 | 421 | void decodeBitmap(double alpha, size_t *data, ASS_Image *img, int w, int h) { 422 | uint32_t color = ((img->color << 8) & 0xff0000) | ((img->color >> 8) & 0xff00) | ((img->color >> 24) & 0xff); 423 | uint8_t *pos = img->bitmap; 424 | uint32_t res = 0; 425 | for (uint32_t y = 0; y < h; ++y, pos += img->stride) { 426 | for (uint32_t z = 0; z < w; ++z, ++res) { 427 | uint8_t mask = pos[z]; 428 | if (mask != 0) 429 | data[res] = ((uint32_t)(alpha * mask) << 24) | color; 430 | } 431 | } 432 | } 433 | 434 | RenderResult *renderImage(double tm, int force) { 435 | time = 0; 436 | count = 0; 437 | 438 | ASS_Image *imgs = ass_render_frame(ass_renderer, track, (int)(tm * 1000), &changed); 439 | if (imgs == NULL || (changed == 0 && !force)) return NULL; 440 | 441 | if (debug) time = emscripten_get_now(); 442 | 443 | return processImages(imgs); 444 | } 445 | 446 | void quitLibrary() { 447 | removeTrack(); 448 | ass_renderer_done(ass_renderer); 449 | ass_library_done(ass_library); 450 | m_buffer.clear(); 451 | } 452 | 453 | void setDefaultFont(const std::string &name) { 454 | defaultFont = copyString(name); 455 | reloadFonts(); 456 | } 457 | 458 | void reloadFonts() { 459 | ass_set_fonts(ass_renderer, NULL, defaultFont, ASS_FONTPROVIDER_NONE, NULL, 1); 460 | } 461 | 462 | void addFont(const std::string &name, int data, unsigned long data_size) { 463 | ass_add_font(ass_library, name.c_str(), (char *)data, (size_t)data_size); 464 | free((char *)data); 465 | } 466 | 467 | void setMargin(int top, int bottom, int left, int right) { 468 | ass_set_margins(ass_renderer, top, bottom, left, right); 469 | } 470 | 471 | int getEventCount() const { 472 | return track->n_events; 473 | } 474 | 475 | int allocEvent() { 476 | return ass_alloc_event(track); 477 | } 478 | 479 | void removeEvent(int eid) { 480 | ass_free_event(track, eid); 481 | } 482 | 483 | int getStyleCount() const { 484 | return track->n_styles; 485 | } 486 | 487 | int allocStyle() { 488 | return ass_alloc_style(track); 489 | } 490 | 491 | void removeStyle(int sid) { 492 | ass_free_event(track, sid); 493 | } 494 | 495 | void removeAllEvents() { 496 | ass_flush_events(track); 497 | } 498 | 499 | void setMemoryLimits(int glyph_limit, int bitmap_cache_limit) { 500 | printf("JASSUB: setting total libass memory limits to: glyph=%d MiB, bitmap cache=%d MiB\n", glyph_limit, bitmap_cache_limit); 501 | ass_set_cache_limits(ass_renderer, glyph_limit, bitmap_cache_limit); 502 | } 503 | 504 | RenderResult *renderBlend(double tm, int force) { 505 | time = 0; 506 | count = 0; 507 | 508 | ASS_Image *img = ass_render_frame(ass_renderer, track, (int)(tm * 1000), &changed); 509 | if (img == NULL || (changed == 0 && !force)) { 510 | return NULL; 511 | } 512 | 513 | if (debug) time = emscripten_get_now(); 514 | for (int i = 0; i < MAX_BLEND_STORAGES; i++) { 515 | m_blendParts[i].taken = false; 516 | } 517 | 518 | // split rendering region in 9 pieces (as on 3x3 grid) 519 | int split_x_low = canvas_w / 3, split_x_high = 2 * canvas_w / 3; 520 | int split_y_low = canvas_h / 3, split_y_high = 2 * canvas_h / 3; 521 | BoundingBox boxes[MAX_BLEND_STORAGES]; 522 | for (ASS_Image *cur = img; cur != NULL; cur = cur->next) { 523 | if (cur->w == 0 || cur->h == 0) 524 | continue; // skip empty images 525 | int index = 0; 526 | int middle_x = cur->dst_x + (cur->w >> 1), middle_y = cur->dst_y + (cur->h >> 1); 527 | if (middle_y > split_y_high) { 528 | index += 2 * 3; 529 | } else if (middle_y > split_y_low) { 530 | index += 1 * 3; 531 | } 532 | if (middle_x > split_x_high) { 533 | index += 2; 534 | } else if (middle_y > split_x_low) { 535 | index += 1; 536 | } 537 | boxes[index].add(cur->dst_x, cur->dst_y, cur->w, cur->h); 538 | } 539 | 540 | // now merge regions as long as there are intersecting regions 541 | for (;;) { 542 | bool merged = false; 543 | for (int box1 = 0; box1 < MAX_BLEND_STORAGES - 1; box1++) { 544 | if (boxes[box1].empty()) 545 | continue; 546 | for (int box2 = box1 + 1; box2 < MAX_BLEND_STORAGES; box2++) { 547 | if (boxes[box2].empty()) 548 | continue; 549 | if (boxes[box1].tryMerge(boxes[box2])) { 550 | boxes[box2].clear(); 551 | merged = true; 552 | } 553 | } 554 | } 555 | if (!merged) 556 | break; 557 | } 558 | 559 | RenderResult *renderResult = NULL; 560 | for (int box = 0; box < MAX_BLEND_STORAGES; box++) { 561 | if (boxes[box].empty()) { 562 | continue; 563 | } 564 | RenderResult *part = renderBlendPart(boxes[box], img); 565 | if (part == NULL) { 566 | break; // memory allocation error 567 | } 568 | if (renderResult) { 569 | part->next = renderResult->next; 570 | renderResult->next = part; 571 | } else { 572 | renderResult = part; 573 | } 574 | 575 | ++count; 576 | } 577 | 578 | return renderResult; 579 | } 580 | 581 | RenderResult *renderBlendPart(const BoundingBox &rect, ASS_Image *img) { 582 | int width = rect.max_x - rect.min_x + 1, height = rect.max_y - rect.min_y + 1; 583 | 584 | // make float buffer for blending 585 | const size_t buffer_size = width * height * 4 * sizeof(float); 586 | float *buf = (float *)m_buffer.take(buffer_size); 587 | if (buf == NULL) { 588 | fprintf(stderr, "JASSUB: cannot allocate buffer for blending\n"); 589 | return NULL; 590 | } 591 | 592 | // blend things in 593 | for (ASS_Image *cur = img; cur != NULL; cur = cur->next) { 594 | if (cur->dst_x < rect.min_x || cur->dst_y < rect.min_y) 595 | continue; // skip images not fully within render region 596 | int curw = cur->w, curh = cur->h; 597 | if (curw == 0 || curh == 0 || cur->dst_x + curw - 1 > rect.max_x || cur->dst_y + curh - 1 > rect.max_y) 598 | continue; // skip empty images or images outside render region 599 | int a = (255 - (cur->color & 0xFF)); 600 | if (a == 0) 601 | continue; // skip transparent images 602 | 603 | int curs = (cur->stride >= curw) ? cur->stride : curw; 604 | int curx = cur->dst_x - rect.min_x, cury = cur->dst_y - rect.min_y; 605 | 606 | unsigned char *bitmap = cur->bitmap; 607 | float normalized_a = a / 255.0; 608 | float r = ((cur->color >> 24) & 0xFF) / 255.0; 609 | float g = ((cur->color >> 16) & 0xFF) / 255.0; 610 | float b = ((cur->color >> 8) & 0xFF) / 255.0; 611 | 612 | int buf_line_coord = cury * width; 613 | for (int y = 0, bitmap_offset = 0; y < curh; y++, bitmap_offset += curs, buf_line_coord += width) { 614 | for (int x = 0; x < curw; x++) { 615 | float pix_alpha = bitmap[bitmap_offset + x] * normalized_a / 255.0; 616 | float inv_alpha = 1.0 - pix_alpha; 617 | 618 | int buf_coord = (buf_line_coord + curx + x) << 2; 619 | float *buf_r = buf + buf_coord; 620 | float *buf_g = buf + buf_coord + 1; 621 | float *buf_b = buf + buf_coord + 2; 622 | float *buf_a = buf + buf_coord + 3; 623 | 624 | // do the compositing, pre-multiply image RGB with alpha for current pixel 625 | *buf_a = pix_alpha + *buf_a * inv_alpha; 626 | *buf_r = r * pix_alpha + *buf_r * inv_alpha; 627 | *buf_g = g * pix_alpha + *buf_g * inv_alpha; 628 | *buf_b = b * pix_alpha + *buf_b * inv_alpha; 629 | } 630 | } 631 | } 632 | 633 | // find closest free buffer 634 | size_t needed = sizeof(unsigned int) * width * height; 635 | RenderBlendStorage *storage = m_blendParts, *bigBuffer = NULL, *smallBuffer = NULL; 636 | for (int buffer_index = 0; buffer_index < MAX_BLEND_STORAGES; buffer_index++, storage++) { 637 | if (storage->taken) 638 | continue; 639 | if (storage->buf.size >= needed) { 640 | if (bigBuffer == NULL || bigBuffer->buf.size > storage->buf.size) 641 | bigBuffer = storage; 642 | } else { 643 | if (smallBuffer == NULL || smallBuffer->buf.size > storage->buf.size) 644 | smallBuffer = storage; 645 | } 646 | } 647 | 648 | if (bigBuffer != NULL) { 649 | storage = bigBuffer; 650 | } else if (smallBuffer != NULL) { 651 | storage = smallBuffer; 652 | } else { 653 | printf("JASSUB: cannot get a buffer for rendering part!\n"); 654 | return NULL; 655 | } 656 | 657 | unsigned int *result = (unsigned int *)storage->buf.take(needed); 658 | if (result == NULL) { 659 | printf("JASSUB: cannot make a buffer for rendering part!\n"); 660 | return NULL; 661 | } 662 | storage->taken = true; 663 | 664 | // now build the result; 665 | for (int y = 0, buf_line_coord = 0; y < height; y++, buf_line_coord += width) { 666 | for (int x = 0; x < width; x++) { 667 | unsigned int pixel = 0; 668 | int buf_coord = (buf_line_coord + x) << 2; 669 | float alpha = buf[buf_coord + 3]; 670 | if (alpha > MIN_UINT8_CAST) { 671 | // need to un-multiply the result 672 | float value = buf[buf_coord] / alpha; 673 | pixel |= CLAMP_UINT8(value); // R 674 | value = buf[buf_coord + 1] / alpha; 675 | pixel |= CLAMP_UINT8(value) << 8; // G 676 | value = buf[buf_coord + 2] / alpha; 677 | pixel |= CLAMP_UINT8(value) << 16; // B 678 | pixel |= CLAMP_UINT8(alpha) << 24; // A 679 | } 680 | result[buf_line_coord + x] = pixel; 681 | } 682 | } 683 | 684 | // return the thing 685 | storage->next.x = rect.min_x; 686 | storage->next.y = rect.min_y; 687 | storage->next.w = width; 688 | storage->next.h = height; 689 | storage->next.image = (size_t)result; 690 | 691 | return &storage->next; 692 | } 693 | 694 | // BINDING 695 | ASS_Event *getEvent(int i) { 696 | return &track->events[i]; 697 | } 698 | 699 | ASS_Style *getStyle(int i) { 700 | return &track->styles[i]; 701 | } 702 | 703 | void styleOverride(ASS_Style style) { 704 | int set_force_flags = ASS_OVERRIDE_BIT_STYLE 705 | | ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE; 706 | 707 | ass_set_selective_style_override_enabled(ass_renderer, set_force_flags); 708 | ass_set_selective_style_override(ass_renderer, &style); 709 | ass_set_font_scale(ass_renderer, 0.3); 710 | } 711 | 712 | void disableStyleOverride() { 713 | ass_set_selective_style_override_enabled(ass_renderer, 0); 714 | ass_set_font_scale(ass_renderer, 1); 715 | } 716 | }; 717 | 718 | static uint32_t getDuration(const ASS_Event &evt) { 719 | return (uint32_t)evt.Duration; 720 | } 721 | 722 | static void setDuration(ASS_Event &evt, const long ms) { 723 | evt.Duration = ms; 724 | } 725 | 726 | static uint32_t getStart(const ASS_Event &evt) { 727 | return (uint32_t)evt.Start; 728 | } 729 | 730 | static void setStart(ASS_Event &evt, const long ms) { 731 | evt.Start = ms; 732 | } 733 | 734 | static std::string getEventName(const ASS_Event &evt) { 735 | return evt.Name; 736 | } 737 | 738 | static void setEventName(ASS_Event &evt, const std::string &str) { 739 | evt.Name = copyString(str); 740 | } 741 | 742 | static std::string getText(const ASS_Event &evt) { 743 | return evt.Text; 744 | } 745 | 746 | static void setText(ASS_Event &evt, const std::string &str) { 747 | evt.Text = copyString(str); 748 | } 749 | 750 | static std::string getEffect(const ASS_Event &evt) { 751 | return evt.Effect; 752 | } 753 | 754 | static void setEffect(ASS_Event &evt, const std::string &str) { 755 | evt.Effect = copyString(str); 756 | } 757 | 758 | static std::string getStyleName(const ASS_Style &style) { 759 | return style.Name; 760 | } 761 | 762 | static void setStyleName(ASS_Style &style, const std::string &str) { 763 | style.Name = copyString(str); 764 | } 765 | 766 | static std::string getFontName(const ASS_Style &style) { 767 | return style.FontName; 768 | } 769 | 770 | static void setFontName(ASS_Style &style, const std::string &str) { 771 | style.FontName = copyString(str); 772 | } 773 | 774 | static RenderResult getNext(const RenderResult &res) { 775 | return *res.next; 776 | } 777 | 778 | EMSCRIPTEN_BINDINGS(JASSUB) { 779 | emscripten::class_("RenderResult") 780 | .property("x", &RenderResult::x) 781 | .property("y", &RenderResult::y) 782 | .property("w", &RenderResult::w) 783 | .property("h", &RenderResult::h) 784 | .property("next", &getNext) 785 | .property("image", &RenderResult::image); 786 | 787 | emscripten::class_("ASS_Style") 788 | .property("Name", &getStyleName, &setStyleName) 789 | .property("FontName", &getFontName, &setFontName) 790 | .property("FontSize", &ASS_Style::FontSize) 791 | .property("PrimaryColour", &ASS_Style::PrimaryColour) 792 | .property("SecondaryColour", &ASS_Style::SecondaryColour) 793 | .property("OutlineColour", &ASS_Style::OutlineColour) 794 | .property("BackColour", &ASS_Style::BackColour) 795 | .property("Bold", &ASS_Style::Bold) 796 | .property("Italic", &ASS_Style::Italic) 797 | .property("Underline", &ASS_Style::Underline) 798 | .property("StrikeOut", &ASS_Style::StrikeOut) 799 | .property("ScaleX", &ASS_Style::ScaleX) 800 | .property("ScaleY", &ASS_Style::ScaleY) 801 | .property("Spacing", &ASS_Style::Spacing) 802 | .property("Angle", &ASS_Style::Angle) 803 | .property("BorderStyle", &ASS_Style::BorderStyle) 804 | .property("Outline", &ASS_Style::Outline) 805 | .property("Shadow", &ASS_Style::Shadow) 806 | .property("Alignment", &ASS_Style::Alignment) 807 | .property("MarginL", &ASS_Style::MarginL) 808 | .property("MarginR", &ASS_Style::MarginR) 809 | .property("MarginV", &ASS_Style::MarginV) 810 | .property("Encoding", &ASS_Style::Encoding) 811 | .property("treat_fontname_as_pattern", &ASS_Style::treat_fontname_as_pattern) 812 | .property("Blur", &ASS_Style::Blur) 813 | .property("Justify", &ASS_Style::Justify); 814 | 815 | emscripten::class_("ASS_Event") 816 | .property("Start", &getStart, &setStart) 817 | .property("Duration", &getDuration, &setDuration) 818 | .property("Name", &getEventName, &setEventName) 819 | .property("Effect", &getEffect, &setEffect) 820 | .property("Text", &getText, &setText) 821 | .property("ReadOrder", &ASS_Event::ReadOrder) 822 | .property("Layer", &ASS_Event::Layer) 823 | .property("Style", &ASS_Event::Style) 824 | .property("MarginL", &ASS_Event::MarginL) 825 | .property("MarginR", &ASS_Event::MarginR) 826 | .property("MarginV", &ASS_Event::MarginV); 827 | 828 | emscripten::class_("JASSUB") 829 | .constructor() 830 | .function("setLogLevel", &JASSUB::setLogLevel) 831 | .function("setDropAnimations", &JASSUB::setDropAnimations) 832 | .function("createTrackMem", &JASSUB::createTrackMem) 833 | .function("removeTrack", &JASSUB::removeTrack) 834 | .function("resizeCanvas", &JASSUB::resizeCanvas) 835 | .function("quitLibrary", &JASSUB::quitLibrary) 836 | .function("addFont", &JASSUB::addFont) 837 | .function("reloadFonts", &JASSUB::reloadFonts) 838 | .function("setMargin", &JASSUB::setMargin) 839 | .function("getEventCount", &JASSUB::getEventCount) 840 | .function("allocEvent", &JASSUB::allocEvent) 841 | .function("allocStyle", &JASSUB::allocStyle) 842 | .function("removeEvent", &JASSUB::removeEvent) 843 | .function("getStyleCount", &JASSUB::getStyleCount) 844 | .function("removeStyle", &JASSUB::removeStyle) 845 | .function("removeAllEvents", &JASSUB::removeAllEvents) 846 | .function("setMemoryLimits", &JASSUB::setMemoryLimits) 847 | .function("renderBlend", &JASSUB::renderBlend, emscripten::allow_raw_pointers()) 848 | .function("renderImage", &JASSUB::renderImage, emscripten::allow_raw_pointers()) 849 | .function("getEvent", &JASSUB::getEvent, emscripten::allow_raw_pointers()) 850 | .function("getStyle", &JASSUB::getStyle, emscripten::allow_raw_pointers()) 851 | .function("styleOverride", &JASSUB::styleOverride, emscripten::allow_raw_pointers()) 852 | .function("disableStyleOverride", &JASSUB::disableStyleOverride) 853 | .function("setDefaultFont", &JASSUB::setDefaultFont) 854 | .property("trackColorSpace", &JASSUB::trackColorSpace) 855 | .property("changed", &JASSUB::changed) 856 | .property("count", &JASSUB::count) 857 | .property("time", &JASSUB::time); 858 | } 859 | -------------------------------------------------------------------------------- /src/pre-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-global-assign, no-unused-vars, prefer-const, no-extend-native */ 2 | /* global out, err, updateMemoryViews, wasmMemory */ 3 | 4 | function assert (c, m) { 5 | if (!c) throw m 6 | } 7 | 8 | let asm = null 9 | 10 | out = text => { 11 | if (text === 'JASSUB: No usable fontconfig configuration file found, using fallback.') { 12 | console.debug(text) 13 | } else { 14 | console.log(text) 15 | } 16 | } 17 | 18 | err = text => { 19 | if (text === 'Fontconfig error: Cannot load default config file: No such file: (null)') { 20 | console.debug(text) 21 | } else { 22 | console.error(text) 23 | } 24 | } 25 | 26 | // patch EMS function to include Uint8Clamped, but call old function too 27 | updateMemoryViews = (_super => { 28 | return () => { 29 | _super() 30 | self.wasmMemory = wasmMemory 31 | self.HEAPU8C = new Uint8ClampedArray(wasmMemory.buffer) 32 | self.HEAPU8 = new Uint8Array(wasmMemory.buffer) 33 | } 34 | })(updateMemoryViews) 35 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extend-native */ 2 | // @ts-ignore 3 | import WASM from 'wasm' 4 | 5 | // polyfills for old or weird engines 6 | 7 | if (!String.prototype.startsWith) { 8 | String.prototype.startsWith = function (s, p = 0) { 9 | return this.substring(p, s.length) === s 10 | } 11 | } 12 | 13 | if (!String.prototype.includes) { 14 | String.prototype.includes = function (s, p) { 15 | return this.indexOf(s, p) !== -1 16 | } 17 | } 18 | 19 | if (!Uint8Array.prototype.slice) { 20 | Uint8Array.prototype.slice = function (b, e) { 21 | return new Uint8Array(this.subarray(b, e)) 22 | } 23 | } 24 | 25 | function toAbsoluteIndex (index, length) { 26 | const integer = index >> 0 27 | return integer < 0 ? Math.max(integer + length, 0) : Math.min(integer, length) 28 | } 29 | 30 | if (!Uint8Array.prototype.fill) { 31 | Int8Array.prototype.fill = Int16Array.prototype.fill = Int32Array.prototype.fill = Uint8Array.prototype.fill = Uint16Array.prototype.fill = Uint32Array.prototype.fill = Float32Array.prototype.fill = Float64Array.prototype.fill = Array.prototype.fill = function (value) { 32 | if (this == null) throw new TypeError('this is null or not defined') 33 | 34 | const O = Object(this) 35 | 36 | const length = O.length >>> 0 37 | 38 | const argumentsLength = arguments.length 39 | let index = toAbsoluteIndex(argumentsLength > 1 ? arguments[1] : undefined, length) 40 | const end = argumentsLength > 2 ? arguments[2] : undefined 41 | const endPos = end === undefined ? length : toAbsoluteIndex(end, length) 42 | while (endPos > index) O[index++] = value 43 | return O 44 | } 45 | } 46 | 47 | if (!Uint8Array.prototype.copyWithin) { 48 | Int8Array.prototype.copyWithin = Int16Array.prototype.copyWithin = Int32Array.prototype.copyWithin = Uint8Array.prototype.copyWithin = Uint16Array.prototype.copyWithin = Uint32Array.prototype.copyWithin = Float32Array.prototype.copyWithin = Float64Array.prototype.copyWithin = Array.prototype.copyWithin = function (target, start) { 49 | const O = Object(this) 50 | const len = O.length >>> 0 51 | 52 | let to = toAbsoluteIndex(target, len) 53 | let from = toAbsoluteIndex(start, len) 54 | const end = arguments.length > 2 ? arguments[2] : undefined 55 | let count = Math.min((end === undefined ? len : toAbsoluteIndex(end, len)) - from, len - to) 56 | let inc = 1 57 | if (from < to && to < from + count) { 58 | inc = -1 59 | from += count - 1 60 | to += count - 1 61 | } 62 | while (count-- > 0) { 63 | if (from in O) O[to] = O[from] 64 | else delete O[to] 65 | to += inc 66 | from += inc 67 | } return O 68 | } 69 | } 70 | 71 | if (!Date.now) Date.now = () => new Date().getTime() 72 | // @ts-ignore 73 | if (!('performance' in self)) self.performance = { now: () => Date.now() } 74 | 75 | // implement console methods if they're missing 76 | if (typeof console === 'undefined') { 77 | const msg = (command, a) => { 78 | postMessage({ 79 | target: 'console', 80 | command, 81 | content: JSON.stringify(Array.prototype.slice.call(a)) 82 | }) 83 | } 84 | // @ts-ignore 85 | self.console = { 86 | log: function () { 87 | msg('log', arguments) 88 | }, 89 | debug: function () { 90 | msg('debug', arguments) 91 | }, 92 | info: function () { 93 | msg('info', arguments) 94 | }, 95 | warn: function () { 96 | msg('warn', arguments) 97 | }, 98 | error: function () { 99 | msg('error', arguments) 100 | } 101 | } 102 | console.log('Detected lack of console, overridden console') 103 | } 104 | 105 | let promiseSupported = typeof Promise !== 'undefined' 106 | 107 | // some engines report that Promise resolve isn't a function... what?... 108 | if (promiseSupported) { 109 | try { 110 | let res 111 | // eslint-disable-next-line no-new 112 | new Promise(resolve => { res = resolve }) 113 | res() 114 | } catch (error) { 115 | promiseSupported = false 116 | } 117 | } 118 | 119 | // very bad promise polyfill, it's absolutely minimal just to make emscripten work 120 | // in engines that don't have promises, Promise should never be used otherwise 121 | if (!promiseSupported) { 122 | // @ts-ignore 123 | self.Promise = function (cb) { 124 | let then = () => {} 125 | cb(a => setTimeout(() => then(a), 0)) 126 | return { then: fn => then = fn } 127 | } 128 | } 129 | 130 | const read_ = (url, ab) => { 131 | const xhr = new XMLHttpRequest() 132 | xhr.open('GET', url, false) 133 | xhr.responseType = ab ? 'arraybuffer' : 'text' 134 | xhr.send(null) 135 | return xhr.response 136 | } 137 | const readAsync = (url, load, err) => { 138 | const xhr = new XMLHttpRequest() 139 | xhr.open('GET', url, true) 140 | xhr.responseType = 'arraybuffer' 141 | xhr.onload = () => { 142 | if ((xhr.status === 200 || xhr.status === 0) && xhr.response) { 143 | return load(xhr.response) 144 | } 145 | } 146 | xhr.onerror = err 147 | xhr.send(null) 148 | } 149 | 150 | let lastCurrentTime = 0 151 | const rate = 1 152 | let rafId = null 153 | let nextIsRaf = false 154 | let lastCurrentTimeReceivedAt = Date.now() 155 | let targetFps = 24 156 | let useLocalFonts = false 157 | let blendMode = 'js' 158 | let availableFonts = {} 159 | const fontMap_ = {} 160 | let fontId = 0 161 | let debug 162 | 163 | self.width = 0 164 | self.height = 0 165 | 166 | let asyncRender = false 167 | 168 | self.addFont = ({ font }) => asyncWrite(font) 169 | 170 | const findAvailableFonts = font => { 171 | font = font.trim().toLowerCase() 172 | 173 | if (font.startsWith('@')) font = font.substring(1) 174 | 175 | if (fontMap_[font]) return 176 | 177 | fontMap_[font] = true 178 | 179 | if (!availableFonts[font]) { 180 | if (useLocalFonts) postMessage({ target: 'getLocalFont', font }) 181 | } else { 182 | asyncWrite(availableFonts[font]) 183 | } 184 | } 185 | 186 | const asyncWrite = font => { 187 | if (typeof font === 'string') { 188 | readAsync(font, fontData => { 189 | allocFont(new Uint8Array(fontData)) 190 | }, console.error) 191 | } else { 192 | allocFont(font) 193 | } 194 | } 195 | 196 | // TODO: this should re-draw last frame! 197 | const allocFont = uint8 => { 198 | const ptr = _malloc(uint8.byteLength) 199 | self.HEAPU8.set(uint8, ptr) 200 | jassubObj.addFont('font-' + (fontId++), ptr, uint8.byteLength) 201 | jassubObj.reloadFonts() 202 | } 203 | 204 | const processAvailableFonts = content => { 205 | if (!availableFonts) return 206 | 207 | const sections = parseAss(content) 208 | 209 | for (let i = 0; i < sections.length; i++) { 210 | for (let j = 0; j < sections[i].body.length; j++) { 211 | if (sections[i].body[j].key === 'Style') { 212 | findAvailableFonts(sections[i].body[j].value.Fontname) 213 | } 214 | } 215 | } 216 | 217 | const regex = /\\fn([^\\}]*?)[\\}]/g 218 | let matches 219 | while ((matches = regex.exec(content)) !== null) { 220 | findAvailableFonts(matches[1]) 221 | } 222 | } 223 | /** 224 | * Set the subtitle track. 225 | * @param {!string} content the content of the subtitle file. 226 | */ 227 | self.setTrack = ({ content }) => { 228 | // Make sure that the fonts are loaded 229 | processAvailableFonts(content) 230 | 231 | if (dropAllBlur) content = dropBlur(content) 232 | // Tell libass to render the new track 233 | jassubObj.createTrackMem(content) 234 | 235 | subtitleColorSpace = libassYCbCrMap[jassubObj.trackColorSpace] 236 | postMessage({ target: 'verifyColorSpace', subtitleColorSpace }) 237 | } 238 | 239 | self.getColorSpace = () => postMessage({ target: 'verifyColorSpace', subtitleColorSpace }) 240 | 241 | /** 242 | * Remove subtitle track. 243 | */ 244 | self.freeTrack = () => { 245 | jassubObj.removeTrack() 246 | } 247 | 248 | /** 249 | * Set the subtitle track. 250 | * @param {!string} url the URL of the subtitle file. 251 | */ 252 | self.setTrackByUrl = ({ url }) => { 253 | self.setTrack({ content: read_(url) }) 254 | } 255 | 256 | const getCurrentTime = () => { 257 | const diff = (Date.now() - lastCurrentTimeReceivedAt) / 1000 258 | if (_isPaused) { 259 | return lastCurrentTime 260 | } else { 261 | if (diff > 5) { 262 | console.error('Didn\'t received currentTime > 5 seconds. Assuming video was paused.') 263 | setIsPaused(true) 264 | } 265 | return lastCurrentTime + (diff * rate) 266 | } 267 | } 268 | const setCurrentTime = currentTime => { 269 | lastCurrentTime = currentTime 270 | lastCurrentTimeReceivedAt = Date.now() 271 | if (!rafId) { 272 | if (nextIsRaf) { 273 | rafId = requestAnimationFrame(renderLoop) 274 | } else { 275 | renderLoop() 276 | 277 | // Give onmessage chance to receive all queued messages 278 | setTimeout(() => { 279 | nextIsRaf = false 280 | }, 20) 281 | } 282 | } 283 | } 284 | 285 | let _isPaused = true 286 | const setIsPaused = isPaused => { 287 | if (isPaused !== _isPaused) { 288 | _isPaused = isPaused 289 | if (isPaused) { 290 | if (rafId) { 291 | clearTimeout(rafId) 292 | rafId = null 293 | } 294 | } else { 295 | lastCurrentTimeReceivedAt = Date.now() 296 | rafId = requestAnimationFrame(renderLoop) 297 | } 298 | } 299 | } 300 | 301 | const a = 'BT601' 302 | const b = 'BT709' 303 | const c = 'SMPTE240M' 304 | const d = 'FCC' 305 | 306 | const libassYCbCrMap = [null, a, null, a, a, b, b, c, c, d, d] 307 | 308 | const render = (time, force) => { 309 | const times = {} 310 | const renderStartTime = performance.now() 311 | const renderResult = blendMode === 'wasm' ? jassubObj.renderBlend(time, force || 0) : jassubObj.renderImage(time, force || 0) 312 | if (debug) { 313 | const decodeEndTime = performance.now() 314 | const renderEndTime = jassubObj.time 315 | times.WASMRenderTime = renderEndTime - renderStartTime 316 | times.WASMBitmapDecodeTime = decodeEndTime - renderEndTime 317 | // performance.now is relative to the creation of the scope, since this time MIGHT be used to calculate a time difference 318 | // on the main thread, we need absolute time, not relative 319 | times.JSRenderTime = Date.now() 320 | } 321 | if (jassubObj.changed !== 0 || force) { 322 | const images = [] 323 | const buffers = [] 324 | if (!renderResult) return paintImages({ images, buffers, times }) 325 | if (asyncRender) { 326 | const promises = [] 327 | for (let result = renderResult, i = 0; i < jassubObj.count; result = result.next, ++i) { 328 | const reassigned = { w: result.w, h: result.h, x: result.x, y: result.y } 329 | const pointer = result.image 330 | const data = hasBitmapBug ? self.HEAPU8C.slice(pointer, pointer + reassigned.w * reassigned.h * 4) : self.HEAPU8C.subarray(pointer, pointer + reassigned.w * reassigned.h * 4) 331 | promises.push(createImageBitmap(new ImageData(data, reassigned.w, reassigned.h))) 332 | images.push(reassigned) 333 | } 334 | // use callback to not rely on async/await 335 | Promise.all(promises).then(bitmaps => { 336 | for (let i = 0; i < images.length; i++) { 337 | images[i].image = bitmaps[i] 338 | } 339 | if (debug) times.JSBitmapGenerationTime = Date.now() - times.JSRenderTime 340 | paintImages({ images, buffers: bitmaps, times }) 341 | }) 342 | } else { 343 | for (let image = renderResult, i = 0; i < jassubObj.count; image = image.next, ++i) { 344 | const reassigned = { w: image.w, h: image.h, x: image.x, y: image.y, image: image.image } 345 | if (!offCanvasCtx) { 346 | const buf = self.wasmMemory.buffer.slice(image.image, image.image + image.w * image.h * 4) 347 | buffers.push(buf) 348 | reassigned.image = buf 349 | } 350 | images.push(reassigned) 351 | } 352 | paintImages({ images, buffers, times }) 353 | } 354 | } else { 355 | postMessage({ 356 | target: 'unbusy' 357 | }) 358 | } 359 | } 360 | 361 | self.demand = ({ time }) => { 362 | lastCurrentTime = time 363 | render(time) 364 | } 365 | 366 | const renderLoop = force => { 367 | rafId = 0 368 | render(getCurrentTime(), force) 369 | if (!_isPaused) { 370 | rafId = requestAnimationFrame(renderLoop) 371 | } 372 | } 373 | 374 | const paintImages = ({ times, images, buffers }) => { 375 | const resultObject = { 376 | target: 'render', 377 | asyncRender, 378 | images, 379 | times, 380 | width: self.width, 381 | height: self.height, 382 | colorSpace: subtitleColorSpace 383 | } 384 | 385 | if (offscreenRender) { 386 | if (offCanvas.height !== self.height || offCanvas.width !== self.width) { 387 | offCanvas.width = self.width 388 | offCanvas.height = self.height 389 | } 390 | offCanvasCtx.clearRect(0, 0, self.width, self.height) 391 | for (const image of images) { 392 | if (image.image) { 393 | if (asyncRender) { 394 | offCanvasCtx.drawImage(image.image, image.x, image.y) 395 | image.image.close() 396 | } else { 397 | bufferCanvas.width = image.w 398 | bufferCanvas.height = image.h 399 | bufferCtx.putImageData(new ImageData(self.HEAPU8C.subarray(image.image, image.image + image.w * image.h * 4), image.w, image.h), 0, 0) 400 | offCanvasCtx.drawImage(bufferCanvas, image.x, image.y) 401 | } 402 | } 403 | } 404 | if (offscreenRender === 'hybrid') { 405 | if (!images.length) return postMessage(resultObject) 406 | if (debug) times.bitmaps = images.length 407 | try { 408 | const image = offCanvas.transferToImageBitmap() 409 | resultObject.images = [{ image, x: 0, y: 0 }] 410 | resultObject.asyncRender = true 411 | postMessage(resultObject, [image]) 412 | } catch (e) { 413 | postMessage({ target: 'unbusy' }) 414 | } 415 | } else { 416 | if (debug) { 417 | times.JSRenderTime = Date.now() - times.JSRenderTime - (times.JSBitmapGenerationTime || 0) 418 | let total = 0 419 | for (const key in times) total += times[key] 420 | console.log('Bitmaps: ' + images.length + ' Total: ' + (total | 0) + 'ms', times) 421 | } 422 | postMessage({ target: 'unbusy' }) 423 | } 424 | } else { 425 | postMessage(resultObject, buffers) 426 | } 427 | } 428 | 429 | /** 430 | * Parse the content of an .ass file. 431 | * @param {!string} content the content of the file 432 | */ 433 | const parseAss = content => { 434 | let m, format, lastPart, parts, key, value, tmp, i, j, body 435 | const sections = [] 436 | const lines = content.split(/[\r\n]+/g) 437 | for (i = 0; i < lines.length; i++) { 438 | m = lines[i].match(/^\[(.*)\]$/) 439 | if (m) { 440 | format = null 441 | sections.push({ 442 | name: m[1], 443 | body: [] 444 | }) 445 | } else { 446 | if (/^\s*$/.test(lines[i])) continue 447 | if (sections.length === 0) continue 448 | body = sections[sections.length - 1].body 449 | if (lines[i][0] === ';') { 450 | body.push({ 451 | type: 'comment', 452 | value: lines[i].substring(1) 453 | }) 454 | } else { 455 | parts = lines[i].split(':') 456 | key = parts[0] 457 | value = parts.slice(1).join(':').trim() 458 | if (format || key === 'Format') { 459 | value = value.split(',') 460 | if (format && value.length > format.length) { 461 | lastPart = value.slice(format.length - 1).join(',') 462 | value = value.slice(0, format.length - 1) 463 | value.push(lastPart) 464 | } 465 | value = value.map(s => { 466 | return s.trim() 467 | }) 468 | if (format) { 469 | tmp = {} 470 | for (j = 0; j < value.length; j++) { 471 | tmp[format[j]] = value[j] 472 | } 473 | value = tmp 474 | } 475 | } 476 | if (key === 'Format') { 477 | format = value 478 | } 479 | body.push({ 480 | key, 481 | value 482 | }) 483 | } 484 | } 485 | } 486 | 487 | return sections 488 | } 489 | 490 | const blurRegex = /\\blur(?:[0-9]+\.)?[0-9]+/gm 491 | 492 | const dropBlur = subContent => { 493 | return subContent.replace(blurRegex, '') 494 | } 495 | 496 | const requestAnimationFrame = (() => { 497 | // similar to Browser.requestAnimationFrame 498 | let nextRAF = 0 499 | return func => { 500 | // try to keep target fps (30fps) between calls to here 501 | const now = Date.now() 502 | if (nextRAF === 0) { 503 | nextRAF = now + 1000 / targetFps 504 | } else { 505 | while (now + 2 >= nextRAF) { // fudge a little, to avoid timer jitter causing us to do lots of delay:0 506 | nextRAF += 1000 / targetFps 507 | } 508 | } 509 | const delay = Math.max(nextRAF - now, 0) 510 | return setTimeout(func, delay) 511 | // return setTimeout(func, 1); 512 | } 513 | })() 514 | 515 | const _applyKeys = (input, output) => { 516 | for (const v of Object.keys(input)) { 517 | output[v] = input[v] 518 | } 519 | } 520 | let offCanvas 521 | let offCanvasCtx 522 | let offscreenRender 523 | let bufferCanvas 524 | let bufferCtx 525 | let jassubObj 526 | let subtitleColorSpace 527 | let dropAllBlur 528 | let _malloc 529 | let hasBitmapBug 530 | 531 | self.init = data => { 532 | hasBitmapBug = data.hasBitmapBug 533 | try { 534 | const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)) 535 | if (!(module instanceof WebAssembly.Module) || !(new WebAssembly.Instance(module) instanceof WebAssembly.Instance)) throw new Error('WASM not supported') 536 | } catch (e) { 537 | console.warn(e) 538 | // load WASM2JS code if WASM is unsupported 539 | // eslint-disable-next-line no-eval 540 | eval(read_(data.legacyWasmUrl)) 541 | } 542 | // hack, we want custom WASM URLs 543 | // @ts-ignore 544 | if (WebAssembly.instantiateStreaming) { 545 | const _fetch = self.fetch 546 | self.fetch = _ => _fetch(data.wasmUrl) 547 | } 548 | WASM({ wasm: !WebAssembly.instantiateStreaming && read_(data.wasmUrl, true) }).then((/** @type {EmscriptenModule} */Module) => { 549 | _malloc = Module._malloc 550 | self.width = data.width 551 | self.height = data.height 552 | blendMode = data.blendMode 553 | asyncRender = data.asyncRender 554 | // Force fallback if engine does not support 'lossy' mode. 555 | // We only use createImageBitmap in the worker and historic WebKit versions supported 556 | // the API in the normal but not the worker scope, so we can't check this earlier. 557 | if (asyncRender && typeof createImageBitmap === 'undefined') { 558 | asyncRender = false 559 | console.error("'createImageBitmap' needed for 'asyncRender' unsupported!") 560 | } 561 | 562 | availableFonts = data.availableFonts 563 | debug = data.debug 564 | targetFps = data.targetFps || targetFps 565 | useLocalFonts = data.useLocalFonts 566 | dropAllBlur = data.dropAllBlur 567 | 568 | const fallbackFont = data.fallbackFont.toLowerCase() 569 | jassubObj = new Module.JASSUB(self.width, self.height, fallbackFont || null, debug) 570 | 571 | if (fallbackFont) findAvailableFonts(fallbackFont) 572 | 573 | let subContent = data.subContent 574 | if (!subContent) subContent = read_(data.subUrl) 575 | 576 | processAvailableFonts(subContent) 577 | if (dropAllBlur) subContent = dropBlur(subContent) 578 | 579 | for (const font of data.fonts || []) asyncWrite(font) 580 | 581 | jassubObj.createTrackMem(subContent) 582 | 583 | subtitleColorSpace = libassYCbCrMap[jassubObj.trackColorSpace] 584 | 585 | jassubObj.setDropAnimations(data.dropAllAnimations || 0) 586 | 587 | if (data.libassMemoryLimit > 0 || data.libassGlyphLimit > 0) { 588 | jassubObj.setMemoryLimits(data.libassGlyphLimit || 0, data.libassMemoryLimit || 0) 589 | } 590 | 591 | postMessage({ target: 'ready' }) 592 | postMessage({ target: 'verifyColorSpace', subtitleColorSpace }) 593 | }) 594 | } 595 | 596 | self.offscreenCanvas = ({ transferable }) => { 597 | offCanvas = transferable[0] 598 | offCanvasCtx = offCanvas.getContext('2d') 599 | if (!asyncRender) { 600 | bufferCanvas = new OffscreenCanvas(self.height, self.width) 601 | bufferCtx = bufferCanvas.getContext('2d', { desynchronized: true }) 602 | } 603 | offscreenRender = true 604 | } 605 | 606 | self.detachOffscreen = () => { 607 | offCanvas = new OffscreenCanvas(self.height, self.width) 608 | offCanvasCtx = offCanvas.getContext('2d', { desynchronized: true }) 609 | offscreenRender = 'hybrid' 610 | } 611 | 612 | self.canvas = ({ width, height, videoWidth, videoHeight, force }) => { 613 | if (width == null) throw new Error('Invalid canvas size specified') 614 | self.width = width 615 | self.height = height 616 | if (jassubObj) jassubObj.resizeCanvas(width, height, videoWidth, videoHeight) 617 | if (force) render(lastCurrentTime, true) 618 | } 619 | 620 | self.video = ({ currentTime, isPaused, rate }) => { 621 | if (currentTime != null) setCurrentTime(currentTime) 622 | if (isPaused != null) setIsPaused(isPaused) 623 | rate = rate || rate 624 | } 625 | 626 | self.destroy = () => { 627 | jassubObj.quitLibrary() 628 | } 629 | 630 | self.createEvent = ({ event }) => { 631 | _applyKeys(event, jassubObj.getEvent(jassubObj.allocEvent())) 632 | } 633 | 634 | self.getEvents = () => { 635 | const events = [] 636 | for (let i = 0; i < jassubObj.getEventCount(); i++) { 637 | const { Start, Duration, ReadOrder, Layer, Style, MarginL, MarginR, MarginV, Name, Text, Effect } = jassubObj.getEvent(i) 638 | events.push({ Start, Duration, ReadOrder, Layer, Style, MarginL, MarginR, MarginV, Name, Text, Effect }) 639 | } 640 | postMessage({ 641 | target: 'getEvents', 642 | events 643 | }) 644 | } 645 | 646 | self.setEvent = ({ event, index }) => { 647 | _applyKeys(event, jassubObj.getEvent(index)) 648 | } 649 | 650 | self.removeEvent = ({ index }) => { 651 | jassubObj.removeEvent(index) 652 | } 653 | 654 | self.createStyle = ({ style }) => { 655 | const alloc = jassubObj.getStyle(jassubObj.allocStyle()) 656 | _applyKeys(style, alloc) 657 | return alloc 658 | } 659 | 660 | self.getStyles = () => { 661 | const styles = [] 662 | for (let i = 0; i < jassubObj.getStyleCount(); i++) { 663 | // eslint-disable-next-line camelcase 664 | const { Name, FontName, FontSize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding, treat_fontname_as_pattern, Blur, Justify } = jassubObj.getStyle(i) 665 | // eslint-disable-next-line camelcase 666 | styles.push({ Name, FontName, FontSize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding, treat_fontname_as_pattern, Blur, Justify }) 667 | } 668 | postMessage({ 669 | target: 'getStyles', 670 | time: Date.now(), 671 | styles 672 | }) 673 | } 674 | 675 | self.setStyle = ({ style, index }) => { 676 | _applyKeys(style, jassubObj.getStyle(index)) 677 | } 678 | 679 | self.removeStyle = ({ index }) => { 680 | jassubObj.removeStyle(index) 681 | } 682 | 683 | self.styleOverride = (data) => { 684 | jassubObj.styleOverride(self.createStyle(data)) 685 | } 686 | 687 | self.disableStyleOverride = () => { 688 | jassubObj.disableStyleOverride() 689 | } 690 | 691 | self.defaultFont = ({ font }) => { 692 | jassubObj.setDefaultFont(font) 693 | } 694 | 695 | onmessage = ({ data }) => { 696 | if (self[data.target]) { 697 | self[data.target](data) 698 | } else { 699 | throw new Error('Unknown event target ' + data.target) 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /vite.build.js: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { resolve, dirname } from 'path' 3 | import { build } from 'vite' 4 | import { viteStaticCopy } from 'vite-plugin-static-copy' 5 | import { fileURLToPath } from 'url' 6 | import { appendFile } from 'fs/promises' 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)) 9 | 10 | await build({ 11 | configFile: false, 12 | build: { 13 | target: 'esnext', 14 | emptyOutDir: false, 15 | minify: 'esbuild', 16 | lib: { 17 | entry: resolve(__dirname, 'src/jassub.js'), 18 | name: 'JASSUB', 19 | fileName: (format) => `jassub.${format}.js` 20 | } 21 | } 22 | }) 23 | 24 | await build({ 25 | configFile: false, 26 | plugins: [ 27 | viteStaticCopy({ 28 | targets: [ 29 | { 30 | src: 'dist/js/jassub-worker.wasm', 31 | dest: './' 32 | }, 33 | { 34 | src: 'dist/js/jassub-worker-modern.wasm', 35 | dest: './' 36 | } 37 | ] 38 | }) 39 | ], 40 | resolve: { 41 | alias: { 42 | wasm: 'dist/js/jassub-worker-modern.js' 43 | } 44 | }, 45 | build: { 46 | target: 'esnext', 47 | outDir: './dist', 48 | minify: 'esbuild', 49 | lib: { 50 | fileName: () => 'jassub-worker.js', 51 | entry: 'src/worker.js', 52 | formats: ['cjs'] 53 | }, 54 | emptyOutDir: false 55 | } 56 | }) 57 | 58 | await build({ 59 | configFile: false, 60 | build: { 61 | terserOptions: { 62 | mangle: false, 63 | compress: false, 64 | format: { 65 | comments: false 66 | } 67 | }, 68 | target: 'esnext', 69 | outDir: './dist', 70 | minify: 'terser', 71 | rollupOptions: { 72 | treeshake: false, 73 | output: { 74 | exports: 'none', 75 | entryFileNames: '[name].js' 76 | }, 77 | input: { 78 | 'jassub-worker.wasm': resolve(__dirname, 'dist/js/jassub-worker.wasm.js') 79 | } 80 | }, 81 | emptyOutDir: false 82 | } 83 | }) 84 | 85 | await appendFile(resolve(__dirname, 'dist/jassub-worker.wasm.js'), 'self.WebAssembly=WebAssembly') 86 | --------------------------------------------------------------------------------