├── .github └── workflows │ └── ci.yml ├── .gitignore ├── 569086201.txt ├── LICENSE ├── Makefile ├── README.md ├── assets └── ComicNeue_Bold.otf ├── default.nix ├── sample.txt ├── src ├── aids.hpp ├── diffimg.cpp ├── emote_downloader.cpp ├── stb_image.h ├── stb_image_resize.h ├── stb_image_write.h ├── tzozen.h ├── vodus.cpp ├── vodus_emotes.cpp ├── vodus_encoder.cpp ├── vodus_encoder.hpp ├── vodus_error.cpp ├── vodus_image32.cpp ├── vodus_main.cpp ├── vodus_message.cpp ├── vodus_queue.cpp ├── vodus_video_params.cpp └── vodus_video_params.hpp ├── test ├── pajaWalk │ ├── .gitignore │ ├── emote-107.gif │ ├── emote-96.gif │ ├── expected-frames │ │ ├── 0-frame.png │ │ ├── 1-frame.png │ │ ├── 10-frame.png │ │ ├── 100-frame.png │ │ ├── 101-frame.png │ │ ├── 102-frame.png │ │ ├── 103-frame.png │ │ ├── 104-frame.png │ │ ├── 105-frame.png │ │ ├── 106-frame.png │ │ ├── 107-frame.png │ │ ├── 108-frame.png │ │ ├── 109-frame.png │ │ ├── 11-frame.png │ │ ├── 110-frame.png │ │ ├── 111-frame.png │ │ ├── 112-frame.png │ │ ├── 113-frame.png │ │ ├── 114-frame.png │ │ ├── 115-frame.png │ │ ├── 116-frame.png │ │ ├── 117-frame.png │ │ ├── 118-frame.png │ │ ├── 119-frame.png │ │ ├── 12-frame.png │ │ ├── 120-frame.png │ │ ├── 121-frame.png │ │ ├── 122-frame.png │ │ ├── 123-frame.png │ │ ├── 124-frame.png │ │ ├── 125-frame.png │ │ ├── 126-frame.png │ │ ├── 13-frame.png │ │ ├── 14-frame.png │ │ ├── 15-frame.png │ │ ├── 16-frame.png │ │ ├── 17-frame.png │ │ ├── 18-frame.png │ │ ├── 19-frame.png │ │ ├── 2-frame.png │ │ ├── 20-frame.png │ │ ├── 21-frame.png │ │ ├── 22-frame.png │ │ ├── 23-frame.png │ │ ├── 24-frame.png │ │ ├── 25-frame.png │ │ ├── 26-frame.png │ │ ├── 27-frame.png │ │ ├── 28-frame.png │ │ ├── 29-frame.png │ │ ├── 3-frame.png │ │ ├── 30-frame.png │ │ ├── 31-frame.png │ │ ├── 32-frame.png │ │ ├── 33-frame.png │ │ ├── 34-frame.png │ │ ├── 35-frame.png │ │ ├── 36-frame.png │ │ ├── 37-frame.png │ │ ├── 38-frame.png │ │ ├── 39-frame.png │ │ ├── 4-frame.png │ │ ├── 40-frame.png │ │ ├── 41-frame.png │ │ ├── 42-frame.png │ │ ├── 43-frame.png │ │ ├── 44-frame.png │ │ ├── 45-frame.png │ │ ├── 46-frame.png │ │ ├── 47-frame.png │ │ ├── 48-frame.png │ │ ├── 49-frame.png │ │ ├── 5-frame.png │ │ ├── 50-frame.png │ │ ├── 51-frame.png │ │ ├── 52-frame.png │ │ ├── 53-frame.png │ │ ├── 54-frame.png │ │ ├── 55-frame.png │ │ ├── 56-frame.png │ │ ├── 57-frame.png │ │ ├── 58-frame.png │ │ ├── 59-frame.png │ │ ├── 6-frame.png │ │ ├── 60-frame.png │ │ ├── 61-frame.png │ │ ├── 62-frame.png │ │ ├── 63-frame.png │ │ ├── 64-frame.png │ │ ├── 65-frame.png │ │ ├── 66-frame.png │ │ ├── 67-frame.png │ │ ├── 68-frame.png │ │ ├── 69-frame.png │ │ ├── 7-frame.png │ │ ├── 70-frame.png │ │ ├── 71-frame.png │ │ ├── 72-frame.png │ │ ├── 73-frame.png │ │ ├── 74-frame.png │ │ ├── 75-frame.png │ │ ├── 76-frame.png │ │ ├── 77-frame.png │ │ ├── 78-frame.png │ │ ├── 79-frame.png │ │ ├── 8-frame.png │ │ ├── 80-frame.png │ │ ├── 81-frame.png │ │ ├── 82-frame.png │ │ ├── 83-frame.png │ │ ├── 84-frame.png │ │ ├── 85-frame.png │ │ ├── 86-frame.png │ │ ├── 87-frame.png │ │ ├── 88-frame.png │ │ ├── 89-frame.png │ │ ├── 9-frame.png │ │ ├── 90-frame.png │ │ ├── 91-frame.png │ │ ├── 92-frame.png │ │ ├── 93-frame.png │ │ ├── 94-frame.png │ │ ├── 95-frame.png │ │ ├── 96-frame.png │ │ ├── 97-frame.png │ │ ├── 98-frame.png │ │ └── 99-frame.png │ ├── mapping.csv │ ├── pajaWalk.txt │ ├── pajaWalk1.gif │ ├── pajaWalk2.gif │ ├── pajaWalk3.gif │ ├── renew.sh │ ├── test.conf │ └── test.sh └── utf8 │ ├── .gitignore │ ├── expected-frames │ ├── 0-frame.png │ ├── 1-frame.png │ ├── 10-frame.png │ ├── 11-frame.png │ ├── 12-frame.png │ ├── 13-frame.png │ ├── 14-frame.png │ ├── 15-frame.png │ ├── 16-frame.png │ ├── 17-frame.png │ ├── 18-frame.png │ ├── 19-frame.png │ ├── 2-frame.png │ ├── 20-frame.png │ ├── 21-frame.png │ ├── 22-frame.png │ ├── 23-frame.png │ ├── 24-frame.png │ ├── 25-frame.png │ ├── 26-frame.png │ ├── 27-frame.png │ ├── 28-frame.png │ ├── 29-frame.png │ ├── 3-frame.png │ ├── 30-frame.png │ ├── 31-frame.png │ ├── 32-frame.png │ ├── 33-frame.png │ ├── 34-frame.png │ ├── 35-frame.png │ ├── 36-frame.png │ ├── 37-frame.png │ ├── 38-frame.png │ ├── 39-frame.png │ ├── 4-frame.png │ ├── 40-frame.png │ ├── 41-frame.png │ ├── 42-frame.png │ ├── 43-frame.png │ ├── 44-frame.png │ ├── 45-frame.png │ ├── 46-frame.png │ ├── 47-frame.png │ ├── 48-frame.png │ ├── 49-frame.png │ ├── 5-frame.png │ ├── 50-frame.png │ ├── 51-frame.png │ ├── 52-frame.png │ ├── 53-frame.png │ ├── 54-frame.png │ ├── 55-frame.png │ ├── 56-frame.png │ ├── 57-frame.png │ ├── 58-frame.png │ ├── 59-frame.png │ ├── 6-frame.png │ ├── 60-frame.png │ ├── 61-frame.png │ ├── 62-frame.png │ ├── 63-frame.png │ ├── 64-frame.png │ ├── 65-frame.png │ ├── 66-frame.png │ ├── 67-frame.png │ ├── 68-frame.png │ ├── 69-frame.png │ ├── 7-frame.png │ ├── 70-frame.png │ ├── 71-frame.png │ ├── 72-frame.png │ ├── 73-frame.png │ ├── 74-frame.png │ ├── 75-frame.png │ ├── 76-frame.png │ ├── 77-frame.png │ ├── 78-frame.png │ ├── 8-frame.png │ └── 9-frame.png │ ├── fonts-japanese-gothic.ttf │ ├── mapping.csv │ ├── renew.sh │ ├── test.conf │ ├── test.sh │ └── utf-8.txt ├── third_party ├── .gitignore ├── Makefile.patch └── build_third_party.sh ├── video.conf └── video.params /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build-linux-gcc: 6 | runs-on: ubuntu-18.04 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: install dependencies 10 | run: | 11 | sudo apt-get update 12 | sudo apt-get install -qq nasm libfreetype6-dev libcurl4-openssl-dev libglfw3-dev 13 | - uses: actions/cache@v2 14 | with: 15 | # TODO(#84): centralize third party versions in the build 16 | path: | 17 | ./third_party/ffmpeg-4.3-dist/ 18 | ./third_party/giflib-5.2.1-dist/ 19 | ./third_party/glfw-3.3.2-dist/ 20 | key: ${{ runner.os }}-ffmpeg-4.3-giflib-5.2.1 21 | - name: build third-party things 22 | run: | 23 | cd third_party 24 | ./build_third_party.sh 25 | cd .. 26 | - name: build vodus 27 | run: | 28 | make -B 29 | ./emote_downloader tsoding 110240192 30 | make render 31 | pushd ./test/pajaWalk/ 32 | ./test.sh 33 | popd 34 | pushd ./test/utf8/ 35 | ./test.sh 36 | popd 37 | env: 38 | CC: gcc 39 | CXX: g++ 40 | - uses: actions/upload-artifact@v2 41 | with: 42 | name: output.mpeg 43 | path: ./output.mpeg 44 | 45 | build-macos-clang: 46 | runs-on: macOS-latest 47 | steps: 48 | - uses: actions/checkout@v1 49 | - name: install dependencies 50 | run: | 51 | brew install nasm freetype2 openssl glfw 52 | - uses: actions/cache@v2 53 | with: 54 | path: | 55 | ./third_party/ffmpeg-4.3-dist/ 56 | ./third_party/giflib-5.2.1-dist/ 57 | ./third_party/glfw-3.3.2-dist/ 58 | key: ${{ runner.os }}-ffmpeg-4.3-giflib-5.2.1 59 | - name: build third-party things 60 | run: | 61 | cd third_party 62 | ./build_third_party.sh 63 | cd .. 64 | - name: build vodus 65 | run: | 66 | make -B 67 | ./emote_downloader tsoding 110240192 68 | make render 69 | pushd ./test/pajaWalk/ 70 | ./test.sh 71 | popd 72 | pushd ./test/utf8/ 73 | ./test.sh 74 | popd 75 | env: 76 | CC: clang 77 | CXX: clang++ 78 | - uses: actions/upload-artifact@v2 79 | with: 80 | name: output.mpeg 81 | path: ./output.mpeg 82 | 83 | 84 | build-linux-clang: 85 | runs-on: ubuntu-18.04 86 | steps: 87 | - uses: actions/checkout@v1 88 | - name: install dependencies 89 | run: | 90 | sudo apt-get update 91 | sudo apt-get install -qq nasm libfreetype6-dev libcurl4-openssl-dev libglfw3-dev 92 | - uses: actions/cache@v2 93 | with: 94 | path: | 95 | ./third_party/ffmpeg-4.3-dist/ 96 | ./third_party/giflib-5.2.1-dist/ 97 | ./third_party/glfw-3.3.2-dist/ 98 | key: ${{ runner.os }}-ffmpeg-4.3-giflib-5.2.1 99 | - name: build third-party things 100 | run: | 101 | cd third_party 102 | ./build_third_party.sh 103 | cd .. 104 | - name: build vodus 105 | run: | 106 | make -B 107 | ./emote_downloader tsoding 110240192 108 | make render 109 | pushd ./test/pajaWalk/ 110 | ./test.sh 111 | popd 112 | pushd ./test/utf8/ 113 | ./test.sh 114 | popd 115 | env: 116 | CC: clang 117 | CXX: clang++ 118 | - uses: actions/upload-artifact@v2 119 | with: 120 | name: output.mpeg 121 | path: ./output.mpeg 122 | 123 | # TODO(#8): no Windows build 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vodus.release 2 | vodus.debug 3 | emote_downloader 4 | *.ppm 5 | *.mp4 6 | *.mpeg 7 | emotes/ 8 | TAGS 9 | /mapping.csv 10 | .vscode/ 11 | diffimg 12 | *.o -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Vodus Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UNAME:=$(shell uname) 2 | 3 | VODUS_EXTRA_CXXFLAGS= 4 | 5 | ifdef VODUS_SSE 6 | VODUS_EXTRA_CXXFLAGS += -DVODUS_SSE -msse4 7 | endif 8 | 9 | # TODO(#87): we need an option to build with system libraries 10 | ifeq ($(UNAME), Darwin) 11 | VODUS_PKGS=freetype2 glfw3 12 | else 13 | VODUS_PKGS=freetype2 glfw3 gl 14 | endif 15 | VODUS_CXXFLAGS=-Wall -fno-exceptions -std=c++17 $(VODUS_EXTRA_CXXFLAGS) -ggdb `pkg-config --cflags $(VODUS_PKGS)` -I./third_party/ffmpeg-4.3-dist/usr/local/include/ -I./third_party/giflib-5.2.1-dist/usr/local/include/ 16 | VODUS_LIBS=`pkg-config --libs $(VODUS_PKGS)` -L./third_party/giflib-5.2.1-dist/usr/local/lib/ ./third_party/giflib-5.2.1-dist/usr/local/lib/libgif.a -L./third_party/ffmpeg-4.3-dist/usr/local/lib/ -L./third_party/glfw-3.3.2-dist/usr/local/lib/ -lavcodec -lavutil -lswresample -pthread -lm -llzma -lz -ldl 17 | 18 | ifeq ($(UNAME), Darwin) 19 | VODUS_LIBS += -framework AVFoundation -framework VideoToolbox -framework CoreVideo -framework AudioToolbox -framework CoreMedia -framework CoreFoundation -framework OpenGL -liconv 20 | endif 21 | 22 | EMOTE_DOWNLOADER_PKGS=libcurl 23 | EMOTE_DOWNLOADER_CXXFLAGS=-Wall -fno-exceptions -std=c++17 -ggdb `pkg-config --cflags $(EMOTE_DOWNLOADER_PKGS)` 24 | EMOTE_DOWNLOADER_LIBS=`pkg-config --libs $(EMOTE_DOWNLOADER_PKGS)` 25 | 26 | DIFFIMG_CXXFLAGS=-Wall -fno-exceptions -std=c++17 -ggdb 27 | 28 | .PHONY: all 29 | all: vodus.release vodus.debug emote_downloader diffimg Makefile 30 | 31 | vodus.release: $(wildcard src/vodus*.cpp) $(wildcard src/core*.cpp) $(wildcard src/stb_image*.h) 32 | $(CXX) $(VODUS_CXXFLAGS) -DVODUS_RELEASE -O3 -o vodus.release src/vodus.cpp $(VODUS_LIBS) 33 | 34 | vodus.debug: $(wildcard src/vodus*.cpp) $(wildcard src/core*.cpp) stb_image.o stb_image_resize.o stb_image_write.o 35 | $(CXX) $(VODUS_CXXFLAGS) -O0 -fno-builtin -o vodus.debug src/vodus.cpp $(VODUS_LIBS) stb_image.o stb_image_resize.o stb_image_write.o 36 | 37 | stb_image.o: src/stb_image.h 38 | $(CC) $(CFLAGS) -x c -ggdb -DSTBI_ONLY_PNG -DSTB_IMAGE_IMPLEMENTATION -c -o stb_image.o src/stb_image.h 39 | 40 | stb_image_resize.o: src/stb_image_resize.h 41 | $(CC) $(CFLAGS) -x c -ggdb -DSTB_IMAGE_RESIZE_IMPLEMENTATION -c -o stb_image_resize.o src/stb_image_resize.h 42 | 43 | stb_image_write.o: src/stb_image_write.h 44 | $(CC) $(CFLAGS) -x c -ggdb -DSTB_IMAGE_WRITE_IMPLEMENTATION -c -o stb_image_write.o src/stb_image_write.h 45 | 46 | emote_downloader: src/emote_downloader.cpp $(wildcard src/core*.cpp) 47 | $(CXX) $(EMOTE_DOWNLOADER_CXXFLAGS) -o emote_downloader src/emote_downloader.cpp $(EMOTE_DOWNLOADER_LIBS) 48 | 49 | .PHONY: render 50 | render: vodus.debug 51 | ./vodus.debug sample.txt output.mpeg --config video.conf 52 | 53 | .PHONY: render.release 54 | render.release: vodus.release 55 | ./vodus.release sample.txt output.mpeg --config video.conf 56 | 57 | diffimg: src/diffimg.cpp 58 | $(CXX) $(DIFFIMG_CXXFLAGS) -o diffimg src/diffimg.cpp 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/tsoding/vodus/workflows/CI/badge.svg)](https://github.com/tsoding/vodus/actions) 2 | 3 | # Vodus 4 | 5 | Introduction Video: https://www.twitch.tv/videos/563056420 6 | 7 | **WARNING! The application is in an active development state and is not even 8 | alpha yet. Use it at your own risk. Nothing is documented, anything can be 9 | changed at any moment or stop working at all.** 10 | 11 | ## Quick Start 12 | 13 | ### Install Dependencies 14 | 15 | #### Debian 16 | 17 | ```console 18 | $ sudo apt-get install nasm libfreetype6-dev libcurl4-openssl-dev 19 | ``` 20 | 21 | #### NixOS 22 | 23 | ```console 24 | $ nix-shell 25 | ``` 26 | 27 | #### FreeBSD 28 | ```console 29 | # pkg install nasm xmlto freetype2 curl openssl gcc gmake 30 | ``` 31 | 32 | ### Build Third Party dependencies 33 | 34 | Needs to be done only once. 35 | 36 | ``` console 37 | $ cd third_party/ 38 | $ ./build_third_party.sh 39 | $ cd .. 40 | ``` 41 | 42 | ### Build the Project 43 | 44 | ```console 45 | $ make 46 | ``` 47 | 48 | ### Download Emotes 49 | 50 | ```console 51 | $ ./emote_downloader 52 | ``` 53 | 54 | ### Render the Sample Video 55 | 56 | ```console 57 | $ make render 58 | ``` 59 | 60 | ## Custom Log Format 61 | 62 | 63 | -------------------------------------------------------------------------------- /assets/ComicNeue_Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/assets/ComicNeue_Bold.otf -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; { 2 | vodusEnv = gcc8Stdenv.mkDerivation { 3 | name = "vodus-env"; 4 | buildInputs = [ stdenv 5 | gcc 6 | gdb 7 | pkgconfig 8 | freetype 9 | giflib 10 | ffmpeg-full 11 | ]; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /sample.txt: -------------------------------------------------------------------------------- 1 | [0:00:00] /me is offline 2 | [0:00:01] WAYTOODANK monkaTOS WAYTOODANK monkaTOS WAYTOODANK monkaTOS WAYTOODANK monkaTOS WAYTOODANK monkaTOS 3 | [0:00:01.50] forsenPls forsenPls forsenPls forsenPls forsenPls forsenPls forsenPls forsenPls forsenPls forsenPls 4 | [0:00:02] AYAYA AYAYA AYAYA AYAYA AYAYA AYAYA AYAYA AYAYA AYAYA AYAYA 5 | [0:00:02.50] tsodinW tsodinW tsodinW tsodinW tsodinW tsodinW tsodinW tsodinW 6 | [0:00:03] phpHop phpHop phpHop phpHop phpHop phpHop phpHop phpHop phpHop phpHop 7 | [0:00:03.50] 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 8 | [0:00:03.60] 🅱 🅱 🅱 🅱 🅱 🅱 🅱 🅱 🅱 🅱 🅱 9 | [0:00:03.70] 🇯🇵 👩🏾 ? 10 | [0:00:04] HELL YEEEAH BROTHAA 11 | [0:00:04] KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona 12 | [0:00:05] KKona KKona KKona KKona KKona KKona KKona KKona KKona 13 | [0:00:06] KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona KKoooona 14 | [0:00:06] KKona KKona KKona KKona KKona KKona KKona KKona KKona 15 | [0:00:07] RainbowPls RainbowPls RainbowPls RainbowPls RainbowPls RainbowPls RainbowPls RainbowPls RainbowPls 16 | [0:00:08] DANCE DANCE DANCE DANCE DANCE DANCE DANCE DANCE DANCE 17 | [0:00:09] SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls 18 | [0:00:10] SlavPls OPA DAVAI DAVAI SlavPls 19 | [0:00:11] SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls 20 | [0:00:11] SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls 21 | [0:00:12] SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls 22 | [0:00:12] SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls SlavPls 23 | -------------------------------------------------------------------------------- /src/diffimg.cpp: -------------------------------------------------------------------------------- 1 | #include "aids.hpp" 2 | #define STB_IMAGE_IMPLEMENTATION 3 | #include "stb_image.h" 4 | #include 5 | 6 | using namespace aids; 7 | 8 | void usage(FILE *stream) 9 | { 10 | println(stream, "Usage: diffimg -e -a -t 25.0"); 11 | } 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | Args args = {argc, argv}; 16 | 17 | const char *expected_filepath = NULL; 18 | const char *actual_filepath = NULL; 19 | float threshold = 10.0f; 20 | 21 | args.shift(); 22 | while (!args.empty()) { 23 | auto flag = cstr_as_string_view(args.shift()); 24 | if (flag == "-e"_sv) { 25 | if (args.empty()) { 26 | println(stderr, "No argument for flag `", flag, "`"); 27 | usage(stderr); 28 | exit(1); 29 | } 30 | expected_filepath = args.shift(); 31 | } else if (flag == "-a"_sv) { 32 | if (args.empty()) { 33 | println(stderr, "No argument for flag `", flag, "`"); 34 | usage(stderr); 35 | exit(1); 36 | } 37 | actual_filepath = args.shift(); 38 | } else if (flag == "-t"_sv) { 39 | if (args.empty()) { 40 | println(stderr, "No argument for flag `", flag, "`"); 41 | usage(stderr); 42 | exit(1); 43 | } 44 | 45 | const auto threshold_sv = cstr_as_string_view(args.shift()); 46 | const auto maybe_threshold = threshold_sv.as_float(); 47 | if (!maybe_threshold.has_value) { 48 | println(stderr, threshold_sv, " is not a float"); 49 | usage(stderr); 50 | exit(1); 51 | } 52 | threshold = maybe_threshold.unwrap; 53 | } else { 54 | println(stderr, "Unknown flag `", flag, "`"); 55 | usage(stderr); 56 | exit(1); 57 | } 58 | } 59 | 60 | if (expected_filepath == NULL) { 61 | println(stderr, "Expected path is not provied"); 62 | usage(stderr); 63 | exit(1); 64 | } 65 | 66 | if (actual_filepath == NULL) { 67 | println(stderr, "Actual path is not provied"); 68 | usage(stderr); 69 | exit(1); 70 | } 71 | 72 | int expected_width = 0; 73 | int expected_height = 0; 74 | uint32_t *expected_data = (uint32_t *) stbi_load( 75 | expected_filepath, &expected_width, &expected_height, NULL, 4); 76 | if (expected_data == NULL) { 77 | println(stderr, "Could not read file `", expected_filepath, "`"); 78 | exit(1); 79 | } 80 | 81 | int actual_width = 0; 82 | int actual_height = 0; 83 | uint32_t *actual_data = (uint32_t *) stbi_load( 84 | actual_filepath, &actual_width, &actual_height, NULL, 4); 85 | if (actual_data == NULL) { 86 | println(stderr, "Could not read file `", actual_filepath, "`"); 87 | exit(1); 88 | } 89 | 90 | if (actual_width != expected_width || actual_height != expected_height) { 91 | println(stderr, "Unexpected size!"); 92 | println(stderr, " Expected: ", expected_width, "x", expected_height); 93 | println(stderr, " Actual: ", actual_width, "x", actual_height); 94 | exit(1); 95 | } 96 | 97 | const auto all = expected_width * actual_height; 98 | int different = 0; 99 | for (int i = 0; i < all; ++i) { 100 | if (expected_data[i] != actual_data[i]) { 101 | different += 1; 102 | } 103 | } 104 | float deviation = ((float) different / (float) all) * 100.0f; 105 | println(stdout, "Deviation: ", deviation, "%"); 106 | println(stdout, "Threshold: ", threshold, "%"); 107 | 108 | if (deviation > threshold) { 109 | println(stdout, "TOO DIFFERENT"); 110 | return 1; 111 | } 112 | 113 | println(stdout, "OK"); 114 | return 0; 115 | } 116 | -------------------------------------------------------------------------------- /src/emote_downloader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifdef _WIN32 5 | #include 6 | #else 7 | #include 8 | #include 9 | #include 10 | #endif 11 | 12 | #define CURL_STRICTER 13 | #include 14 | #define TZOZEN_IMPLEMENTATION 15 | #include "./tzozen.h" 16 | #include "./aids.hpp" 17 | using namespace aids; 18 | 19 | template 20 | struct Fixed_Array 21 | { 22 | size_t size; 23 | T elements[Capacity]; 24 | 25 | void push(T x) 26 | { 27 | assert(size < Capacity); 28 | elements[size++] = x; 29 | } 30 | }; 31 | 32 | const size_t DEFAULT_STRING_BUFFER_CAPACITY = 20 * 1024 * 1024; 33 | 34 | template 35 | struct Fixed_Buffer 36 | { 37 | size_t size; 38 | char data[Capacity + 1]; 39 | 40 | size_t write(const char *data, size_t size) 41 | { 42 | if (this->size + size > Capacity) return 0; 43 | memcpy(this->data + this->size, data, size); 44 | this->size += size; 45 | return size; 46 | } 47 | 48 | size_t write(const char *cstr) 49 | { 50 | return write(cstr, strlen(cstr)); 51 | } 52 | 53 | size_t write(size_t x) 54 | { 55 | const auto n = snprintf(data + size, Capacity - size, "%ld", x); 56 | this->size += n; 57 | return n; 58 | } 59 | 60 | void clean() 61 | { 62 | memset(this->data, 0, Capacity + 1); 63 | this->size = 0; 64 | } 65 | }; 66 | 67 | template 68 | void print1(FILE *stream, Fixed_Buffer buffer) 69 | { 70 | fwrite(buffer.data, 1, buffer.size, stream); 71 | } 72 | 73 | size_t curl_string_buffer_write_callback(char *ptr, 74 | size_t size, 75 | size_t nmemb, 76 | Fixed_Buffer<> *string_buffer) 77 | { 78 | return string_buffer->write(ptr, size * nmemb); 79 | } 80 | 81 | CURLcode curl_download_file_to(CURL *curl, const char *url, const char *filepath) 82 | { 83 | FILE *file = fopen(filepath, "wb"); 84 | if (!file) { 85 | println(stderr, "Could not open file `", filepath, "`"); 86 | abort(); 87 | } 88 | defer(fclose(file)); 89 | 90 | curl_easy_setopt(curl, CURLOPT_URL, url); 91 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); 92 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); 93 | return curl_easy_perform(curl); 94 | } 95 | 96 | template 97 | CURLcode curl_perform_to_string_buffer(CURL *curl, 98 | const char *url, 99 | Fixed_Buffer *string_buffer) 100 | { 101 | curl_easy_setopt(curl, CURLOPT_URL, url); 102 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_string_buffer_write_callback); 103 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, string_buffer); 104 | return curl_easy_perform(curl); 105 | } 106 | 107 | const size_t JSON_MEMORY_BUFFER_CAPACITY = 10 * MEGA; 108 | static uint8_t json_memory_buffer[JSON_MEMORY_BUFFER_CAPACITY]; 109 | Fixed_Buffer curl_buffer = {}; 110 | 111 | static inline 112 | void expect_json_type(Json_Value value, Json_Type type) 113 | { 114 | if (value.type != type) { 115 | println(stderr, 116 | "Expected ", json_type_as_cstr(type), ", but got ", 117 | json_type_as_cstr(value.type)); 118 | abort(); 119 | } 120 | } 121 | 122 | const size_t PATH_BUFFER_SIZE = 1024; 123 | const size_t MAX_PARALLEL = 20; 124 | 125 | struct Curl_Download 126 | { 127 | Fixed_Buffer url; 128 | Fixed_Buffer file; 129 | }; 130 | 131 | void add_download_to_multi_handle(Curl_Download download, CURLM *cm) 132 | { 133 | FILE *file = fopen(download.file.data, "wb"); 134 | CURL *eh = curl_easy_init(); 135 | curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, fwrite); 136 | curl_easy_setopt(eh, CURLOPT_WRITEDATA, file); 137 | curl_easy_setopt(eh, CURLOPT_URL, download.url.data); 138 | curl_easy_setopt(eh, CURLOPT_PRIVATE, file); 139 | curl_multi_add_handle(cm, eh); 140 | } 141 | 142 | void print1(FILE *stream, Curl_Download download) 143 | { 144 | print(stream, "Curl_Download { .url = \"", download.url, "\", .file = \"", download.file, "\" }"); 145 | } 146 | 147 | void print1(FILE *stream, String tzozen_string) 148 | { 149 | fwrite(tzozen_string.data, 1, tzozen_string.len, stream); 150 | } 151 | 152 | void append_ffz_set(CURL *curl, 153 | Json_Object set, 154 | FILE *mapping, 155 | Fixed_Array *downloads) 156 | { 157 | auto emoticons = json_object_value_by_key(set, SLT("emoticons")); 158 | expect_json_type(emoticons, JSON_ARRAY); 159 | 160 | FOR_JSON(Json_Array, emoticon, emoticons.array) { 161 | expect_json_type(emoticon->value, JSON_OBJECT); 162 | auto urls = json_object_value_by_key(emoticon->value.object, SLT("urls")); 163 | expect_json_type(urls, JSON_OBJECT); 164 | 165 | if (urls.object.end) { 166 | auto url = urls.object.end->value; 167 | expect_json_type(url, JSON_STRING); 168 | auto emote_id = json_object_value_by_key(emoticon->value.object, SLT("id")); 169 | expect_json_type(emote_id, JSON_NUMBER); 170 | auto name = json_object_value_by_key(emoticon->value.object, SLT("name")); 171 | expect_json_type(name, JSON_STRING); 172 | 173 | Curl_Download download = {}; 174 | 175 | download.file.write("emotes/emote-"); 176 | download.file.write(downloads->size); 177 | download.file.write(".png"); 178 | download.url.write("https:"); 179 | download.url.write(url.string.data, url.string.len); 180 | 181 | println(mapping, name.string, ",", download.file); 182 | downloads->push(download); 183 | } 184 | } 185 | } 186 | 187 | Json_Value curl_perform_to_json_value(CURL *curl, const char *url) 188 | { 189 | curl_buffer.clean(); 190 | auto res = curl_perform_to_string_buffer(curl, url, &curl_buffer); 191 | if (res != CURLE_OK) { 192 | println(stderr, "curl_perform_to_string_buffer() failed: ", 193 | curl_easy_strerror(res)); 194 | abort(); 195 | } 196 | 197 | Memory memory = {}; 198 | memory.capacity = JSON_MEMORY_BUFFER_CAPACITY; 199 | memory.buffer = json_memory_buffer; 200 | 201 | String source = { 202 | curl_buffer.size, 203 | curl_buffer.data 204 | }; 205 | 206 | Json_Result result = parse_json_value(&memory, source); 207 | if (result.is_error) { 208 | print_json_error(stdout, result, source, url); 209 | abort(); 210 | } 211 | 212 | return result.value; 213 | } 214 | 215 | void append_room_ffz_mapping(CURL *curl, 216 | const char *emotes_url, 217 | FILE *mapping, 218 | Fixed_Array *downloads) 219 | { 220 | auto result = curl_perform_to_json_value(curl, emotes_url); 221 | expect_json_type(result, JSON_OBJECT); 222 | 223 | auto room = json_object_value_by_key(result.object, SLT("room")); 224 | expect_json_type(room, JSON_OBJECT); 225 | 226 | auto sets = json_object_value_by_key(result.object, SLT("sets")); 227 | expect_json_type(sets, JSON_OBJECT); 228 | 229 | auto set_id = json_object_value_by_key(room.object, SLT("set")); 230 | expect_json_type(set_id, JSON_NUMBER); 231 | 232 | auto set = json_object_value_by_key(sets.object, set_id.number.integer); 233 | expect_json_type(set, JSON_OBJECT); 234 | 235 | append_ffz_set(curl, set.object, mapping, downloads); 236 | } 237 | 238 | void append_global_ffz_mapping(CURL *curl, 239 | FILE *mapping, 240 | Fixed_Array *downloads) 241 | { 242 | const char *const emotes_url = "https://api.frankerfacez.com/v1/set/global"; 243 | auto result = curl_perform_to_json_value(curl, emotes_url); 244 | 245 | expect_json_type(result, JSON_OBJECT); 246 | auto default_sets = json_object_value_by_key(result.object, SLT("default_sets")); 247 | expect_json_type(default_sets, JSON_ARRAY); 248 | auto sets = json_object_value_by_key(result.object, SLT("sets")); 249 | expect_json_type(sets, JSON_OBJECT); 250 | 251 | FOR_JSON(Json_Array, set_id, default_sets.array) { 252 | expect_json_type(set_id->value, JSON_NUMBER); 253 | auto set = json_object_value_by_key(sets.object, set_id->value.number.integer); 254 | expect_json_type(set, JSON_OBJECT); 255 | append_ffz_set(curl, set.object, mapping, downloads); 256 | } 257 | } 258 | 259 | void append_bttv_emotes_array(Json_Array emotes_array, 260 | FILE *mapping, 261 | Fixed_Array *downloads) 262 | { 263 | FOR_JSON (Json_Array, emote, emotes_array) { 264 | expect_json_type(emote->value, JSON_OBJECT); 265 | 266 | auto emote_id = json_object_value_by_key(emote->value.object, SLT("id")); 267 | expect_json_type(emote_id, JSON_STRING); 268 | 269 | auto emote_code = json_object_value_by_key(emote->value.object, SLT("code")); 270 | expect_json_type(emote_code, JSON_STRING); 271 | 272 | auto emote_imageType = json_object_value_by_key(emote->value.object, SLT("imageType")); 273 | expect_json_type(emote_imageType, JSON_STRING); 274 | 275 | Curl_Download download = {}; 276 | 277 | download.file.write("emotes/emote-"); 278 | download.file.write(downloads->size); 279 | download.file.write("."); 280 | download.file.write(emote_imageType.string.data, emote_imageType.string.len); 281 | 282 | download.url.write("https://cdn.betterttv.net/emote/"); 283 | download.url.write(emote_id.string.data, emote_id.string.len); 284 | download.url.write("/3x"); 285 | 286 | println(mapping, emote_code.string, ",", download.file); 287 | downloads->push(download); 288 | } 289 | } 290 | 291 | void append_global_bttv_mapping(CURL *curl, 292 | const char *emotes_url, 293 | FILE *mapping, 294 | Fixed_Array *downloads) 295 | { 296 | curl_buffer.clean(); 297 | 298 | auto res = curl_perform_to_string_buffer(curl, emotes_url, &curl_buffer); 299 | if (res != CURLE_OK) { 300 | println(stderr, "curl_perform_to_string_buffer() failed: ", 301 | curl_easy_strerror(res)); 302 | abort(); 303 | } 304 | 305 | Memory memory = {}; 306 | memory.capacity = JSON_MEMORY_BUFFER_CAPACITY; 307 | memory.buffer = json_memory_buffer; 308 | 309 | String source = { 310 | curl_buffer.size, 311 | curl_buffer.data 312 | }; 313 | 314 | Json_Result result = parse_json_value(&memory, source); 315 | if (result.is_error) { 316 | print_json_error(stdout, result, source, emotes_url); 317 | abort(); 318 | } 319 | 320 | expect_json_type(result.value, JSON_ARRAY); 321 | append_bttv_emotes_array(result.value.array, mapping, downloads); 322 | } 323 | 324 | void append_channel_twitch_mapping(CURL *curl, 325 | const char *emotes_url, 326 | FILE *mapping, 327 | Fixed_Array *downloads) 328 | { 329 | curl_buffer.clean(); 330 | 331 | auto res = curl_perform_to_string_buffer(curl, emotes_url, &curl_buffer); 332 | if (res != CURLE_OK) { 333 | println(stderr, "curl_perform_to_string_buffer() failed: ", 334 | curl_easy_strerror(res)); 335 | abort(); 336 | } 337 | 338 | Memory memory = {}; 339 | memory.capacity = JSON_MEMORY_BUFFER_CAPACITY; 340 | memory.buffer = json_memory_buffer; 341 | 342 | String source = { 343 | curl_buffer.size, 344 | curl_buffer.data 345 | }; 346 | 347 | Json_Result result = parse_json_value(&memory, source); 348 | if (result.is_error) { 349 | print_json_error(stdout, result, source, emotes_url); 350 | abort(); 351 | } 352 | 353 | expect_json_type(result.value, JSON_OBJECT); 354 | auto channel_emotes = json_object_value_by_key(result.value.object, SLT("emotes")); 355 | expect_json_type(channel_emotes, JSON_ARRAY); 356 | 357 | FOR_JSON (Json_Array, emote, channel_emotes.array) { 358 | expect_json_type(emote->value, JSON_OBJECT); 359 | auto emote_code = json_object_value_by_key(emote->value.object, SLT("code")); 360 | expect_json_type(emote_code, JSON_STRING); 361 | auto emote_id = json_object_value_by_key(emote->value.object, SLT("id")); 362 | expect_json_type(emote_id, JSON_NUMBER); 363 | 364 | Curl_Download download = {}; 365 | 366 | download.file.write("emotes/emote-"); 367 | download.file.write(downloads->size); 368 | download.file.write(".png"); 369 | 370 | download.url.write("https://static-cdn.jtvnw.net/emoticons/v1/"); 371 | download.url.write(emote_id.string.data, emote_id.string.len); 372 | download.url.write("/3.0"); 373 | 374 | println(mapping, emote_code.string, ",", download.file); 375 | downloads->push(download); 376 | } 377 | } 378 | 379 | void append_channel_bttv_mapping(CURL *curl, 380 | const char *emotes_url, 381 | FILE *mapping, 382 | Fixed_Array *downloads) 383 | { 384 | curl_buffer.clean(); 385 | 386 | auto res = curl_perform_to_string_buffer(curl, emotes_url, &curl_buffer); 387 | if (res != CURLE_OK) { 388 | println(stderr, "curl_perform_to_string_buffer() failed: ", 389 | curl_easy_strerror(res)); 390 | abort(); 391 | } 392 | 393 | Memory memory = {}; 394 | memory.capacity = JSON_MEMORY_BUFFER_CAPACITY; 395 | memory.buffer = json_memory_buffer; 396 | 397 | String source = { 398 | curl_buffer.size, 399 | curl_buffer.data 400 | }; 401 | 402 | Json_Result result = parse_json_value(&memory, source); 403 | if (result.is_error) { 404 | print_json_error(stdout, result, source, emotes_url); 405 | abort(); 406 | } 407 | 408 | expect_json_type(result.value, JSON_OBJECT); 409 | 410 | auto channel_emotes = json_object_value_by_key(result.value.object, SLT("channelEmotes")); 411 | expect_json_type(channel_emotes, JSON_ARRAY); 412 | append_bttv_emotes_array(channel_emotes.array, mapping, downloads); 413 | 414 | auto shared_emotes = json_object_value_by_key(result.value.object, SLT("sharedEmotes")); 415 | expect_json_type(shared_emotes, JSON_ARRAY); 416 | append_bttv_emotes_array(shared_emotes.array, mapping, downloads); 417 | } 418 | 419 | bool is_sep(char x) 420 | { 421 | #ifdef _WIN32 422 | return x == '/' || x == '\\'; 423 | #else 424 | return x == '/'; 425 | #endif 426 | } 427 | 428 | struct Path 429 | { 430 | String_View as_string_view; 431 | 432 | Path parent() 433 | { 434 | auto p = as_string_view; 435 | 436 | while (p.count > 0 && is_sep(*(p.data + p.count - 1))) { 437 | p.chop_back(1); 438 | } 439 | 440 | while (p.count > 0 && !is_sep(*(p.data + p.count - 1))) { 441 | p.chop_back(1); 442 | } 443 | 444 | return p.count == 0 ? Path {as_string_view} : Path {p}; 445 | } 446 | 447 | void copy_to(char *buffer, size_t size) 448 | { 449 | memcpy(buffer, as_string_view.data, 450 | min(size, as_string_view.count)); 451 | } 452 | }; 453 | 454 | void print1(FILE *stream, Path path) 455 | { 456 | print1(stream, path.as_string_view); 457 | } 458 | 459 | bool path_exists(Path path) 460 | { 461 | char pathname[256 + 1] = {}; 462 | path.copy_to(pathname, sizeof(pathname) - 1); 463 | #ifdef _WIN32 464 | struct _stat statbuf = {}; 465 | return _stat(pathname, &statbuf) == 0; 466 | #else 467 | struct stat statbuf = {}; 468 | return stat(pathname, &statbuf) == 0; 469 | #endif 470 | } 471 | 472 | int create_directory(Path dirpath) 473 | { 474 | char pathname[256 + 1] = {}; 475 | dirpath.copy_to(pathname, sizeof(pathname) - 1); 476 | #ifdef _WIN32 477 | return _mkdir(pathname); 478 | #else 479 | return mkdir(pathname, 0755); 480 | #endif 481 | } 482 | 483 | void create_all_directories(Path dirpath) 484 | { 485 | while (!path_exists(dirpath)) { 486 | create_all_directories(dirpath.parent()); 487 | create_directory(dirpath); 488 | } 489 | } 490 | 491 | Fixed_Array downloads; 492 | 493 | void usage(FILE *stream) 494 | { 495 | println(stdout, "./emote_downloader "); 496 | } 497 | 498 | int main(int argc, char **argv) 499 | { 500 | Args args = {argc, argv}; 501 | args.shift(); // skip program name 502 | 503 | if (args.empty()) { 504 | usage(stderr); 505 | panic("ERROR: channel-name is not provided"); 506 | } 507 | auto channel_name = args.shift(); 508 | 509 | if (args.empty()) { 510 | usage(stderr); 511 | panic("ERROR: channel-id is not provided"); 512 | } 513 | auto channel_id = args.shift(); 514 | 515 | curl_global_init(CURL_GLOBAL_DEFAULT); 516 | defer(curl_global_cleanup()); 517 | 518 | CURL *curl = curl_easy_init(); 519 | if (!curl) { 520 | println(stderr, "CURL pooped itself: could not initialize the state"); 521 | abort(); 522 | } 523 | defer(curl_easy_cleanup(curl)); 524 | 525 | const char *EMOTE_CACHE_DIRECTORY = "./emotes/"; 526 | const char *MAPPING_FILE = "./mapping.csv"; 527 | 528 | FILE *mapping = fopen(MAPPING_FILE, "w"); 529 | if (mapping == NULL) { 530 | println(stderr, "Could not open file ", MAPPING_FILE); 531 | } 532 | defer(fclose(mapping)); 533 | 534 | create_directory(Path {cstr_as_string_view(EMOTE_CACHE_DIRECTORY)}); 535 | 536 | CURLM *cm = curl_multi_init(); 537 | defer(curl_multi_cleanup(cm)); 538 | 539 | curl_multi_setopt(cm, CURLMOPT_MAXCONNECTS, MAX_PARALLEL); 540 | 541 | char channel_url_buffer[1024]; 542 | String_Buffer channel_url = {sizeof(channel_url_buffer), channel_url_buffer}; 543 | 544 | 545 | // TODO(#152): emote_downloader does not download global Twitch emotes 546 | sprint(&channel_url, "https://api.twitchemotes.com/api/v4/channels/", channel_id); 547 | append_channel_twitch_mapping(curl, channel_url.data, mapping, &downloads); 548 | channel_url.size = 0; 549 | 550 | append_global_bttv_mapping(curl, "https://api.betterttv.net/3/cached/emotes/global", mapping, &downloads); 551 | 552 | sprint(&channel_url, "https://api.betterttv.net/3/cached/users/twitch/", channel_id); 553 | append_channel_bttv_mapping(curl, channel_url.data, mapping, &downloads); 554 | channel_url.size = 0; 555 | 556 | append_global_ffz_mapping(curl, mapping, &downloads); 557 | 558 | sprint(&channel_url, "https://api.frankerfacez.com/v1/room/", channel_name); 559 | append_room_ffz_mapping(curl, channel_url.data, mapping, &downloads); 560 | channel_url.size = 0; 561 | 562 | size_t transfers = 0; 563 | for (transfers = 0; transfers < min(downloads.size, MAX_PARALLEL); ++transfers) { 564 | add_download_to_multi_handle(downloads.elements[transfers], cm); 565 | } 566 | 567 | int still_alive = 1; 568 | do { 569 | curl_multi_perform(cm, &still_alive); 570 | 571 | CURLMsg *msg; 572 | int msgs_left = -1; 573 | while((msg = curl_multi_info_read(cm, &msgs_left))) { 574 | if (msg->msg == CURLMSG_DONE) { 575 | FILE *file = NULL; 576 | curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &file); 577 | fclose(file); 578 | 579 | char *url = NULL; 580 | curl_easy_getinfo(msg->easy_handle, CURLINFO_EFFECTIVE_URL, &url); 581 | println(stderr, 582 | "R: ", msg->data.result, " - ", curl_easy_strerror(msg->data.result), 583 | " <", url, ">"); 584 | 585 | CURL *e = msg->easy_handle; 586 | curl_multi_remove_handle(cm, e); 587 | curl_easy_cleanup(e); 588 | } else { 589 | println(stderr, "E: CURLMsg (", msg->msg, ")"); 590 | } 591 | 592 | if (transfers < downloads.size) { 593 | add_download_to_multi_handle(downloads.elements[transfers++], cm); 594 | } 595 | } 596 | 597 | if(still_alive) curl_multi_wait(cm, NULL, 0, 1000, NULL); 598 | } while (still_alive || (transfers < downloads.size)); 599 | 600 | // TODO(#156): emote_downloader should download and setup the twitter emoji pack 601 | // Twitter Emoji Pack: https://twemoji.twitter.com/ 602 | 603 | return 0; 604 | } 605 | -------------------------------------------------------------------------------- /src/tzozen.h: -------------------------------------------------------------------------------- 1 | #ifndef TZOZEN_H_ 2 | #define TZOZEN_H_ 3 | 4 | // todo: https://github.com/nothings/stb/blob/master/docs/stb_howto.txt 5 | // [+] 1. #define LIBRARYNAME_IMPLEMENTATION 6 | // [+] 2. AVOID DEPENDENCIES 7 | // [+] 3. AVOID MALLOC 8 | // [+] 4. ALLOW STATIC IMPLEMENTATION 9 | // [+] 5. MAKE ACCESSIBLE FROM C 10 | // [ ] 6. NAMESPACE PRIVATE FUNCTIONS 11 | // [-] 7. EASY-TO-COMPLY LICENSE 12 | // > why would you care whether your credit appeared there? 13 | // Because I could put it to my CV and it would be verifiable by Ctrl+F. 14 | // Which is important when you are nobody. 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #define KILO 1024LL 23 | #define MEGA (1024LL * KILO) 24 | #define GIGA (1024LL * MEGA) 25 | 26 | #ifndef TZOZENDEF 27 | # ifdef TZOZEN_STATIC 28 | # define TZOZENDEF static 29 | # else 30 | # define TZOZENDEF extern 31 | # endif 32 | #endif 33 | 34 | typedef struct { 35 | size_t capacity; 36 | size_t size; 37 | uint8_t *buffer; 38 | } Memory; 39 | 40 | TZOZENDEF void *memory_alloc(Memory *memory, size_t size); 41 | 42 | #define UTF8_CHUNK_CAPACITY 4 43 | typedef struct { 44 | size_t size; 45 | uint8_t buffer[UTF8_CHUNK_CAPACITY]; 46 | } Utf8_Chunk; 47 | 48 | TZOZENDEF Utf8_Chunk utf8_encode_rune(uint32_t rune); 49 | TZOZENDEF int json_get_utf8_char_len(unsigned char ch); 50 | 51 | typedef struct { 52 | size_t len; 53 | const char *data; 54 | } String; 55 | 56 | #define SLT(literal) string(sizeof(literal) - 1, literal) 57 | 58 | TZOZENDEF String string(size_t len, const char *data); 59 | TZOZENDEF const char *string_as_cstr(Memory *memory, String s); 60 | TZOZENDEF String string_empty(void); 61 | TZOZENDEF void chop(String *s, size_t n); 62 | TZOZENDEF String chop_until_char(String *input, char delim); 63 | TZOZENDEF String chop_line(String *input); 64 | TZOZENDEF int string_equal(String a, String b); 65 | TZOZENDEF String take(String s, size_t n); 66 | TZOZENDEF String drop(String s, size_t n); 67 | TZOZENDEF int prefix_of(String prefix, String s); 68 | TZOZENDEF int json_isspace(char c); 69 | TZOZENDEF String trim_begin(String s); 70 | TZOZENDEF int64_t stoi64(String integer); 71 | TZOZENDEF String clone_string(Memory *memory, String string); 72 | TZOZENDEF int32_t unhex(char x); 73 | 74 | #ifndef JSON_DEPTH_MAX_LIMIT 75 | #define JSON_DEPTH_MAX_LIMIT 100 76 | #endif 77 | 78 | typedef struct Json_Value Json_Value; 79 | 80 | typedef enum { 81 | JSON_NULL = 0, 82 | JSON_BOOLEAN, 83 | JSON_NUMBER, 84 | JSON_STRING, 85 | JSON_ARRAY, 86 | JSON_OBJECT 87 | } Json_Type; 88 | 89 | TZOZENDEF const char *json_type_as_cstr(Json_Type type); 90 | 91 | typedef struct Json_Array_Elem Json_Array_Elem; 92 | 93 | typedef struct { 94 | Json_Array_Elem *begin; 95 | Json_Array_Elem *end; 96 | } Json_Array; 97 | 98 | TZOZENDEF size_t json_array_size(Json_Array array); 99 | TZOZENDEF void json_array_push(Memory *memory, Json_Array *array, Json_Value value); 100 | 101 | typedef struct Json_Object_Elem Json_Object_Elem; 102 | 103 | typedef struct { 104 | Json_Object_Elem *begin; 105 | Json_Object_Elem *end; 106 | } Json_Object; 107 | 108 | TZOZENDEF size_t json_object_size(Json_Object object); 109 | TZOZENDEF void json_object_push(Memory *memory, Json_Object *object, String key, Json_Value value); 110 | TZOZENDEF Json_Value json_object_value_by_key(Json_Object object, String key); 111 | 112 | typedef struct { 113 | // todo(#26): because of the use of String-s Json_Number can hold an incorrect value 114 | // But you can only get an incorrect Json_Number if you construct it yourself. 115 | // Anything coming from parse_json_value should be always a correct number. 116 | String integer; 117 | String fraction; 118 | String exponent; 119 | } Json_Number; 120 | 121 | TZOZENDEF int64_t json_number_to_integer(Json_Number number); 122 | 123 | struct Json_Value { 124 | Json_Type type; 125 | union 126 | { 127 | int boolean; 128 | Json_Number number; 129 | String string; 130 | Json_Array array; 131 | Json_Object object; 132 | }; 133 | }; 134 | 135 | TZOZENDEF Json_Value json_null(); 136 | TZOZENDEF Json_Value json_true(); 137 | TZOZENDEF Json_Value json_false(); 138 | TZOZENDEF Json_Value json_number(String integer, String fraction, String exponent); 139 | TZOZENDEF Json_Value json_string(String string); 140 | TZOZENDEF Json_Value json_array_empty(); 141 | TZOZENDEF Json_Value json_array(Json_Array array); 142 | TZOZENDEF Json_Value json_object_empty(); 143 | TZOZENDEF Json_Value json_object(Json_Object object); 144 | 145 | struct Json_Array_Elem { 146 | Json_Array_Elem *next; 147 | Json_Value value; 148 | }; 149 | 150 | struct Json_Object_Elem { 151 | Json_Object_Elem *next; 152 | String key; 153 | Json_Value value; 154 | }; 155 | 156 | #define FOR_JSON(container_type, elem, container) \ 157 | for (container_type##_Elem *elem = (container).begin; \ 158 | elem != NULL; \ 159 | elem = elem->next) 160 | 161 | typedef struct { 162 | Json_Value value; 163 | String rest; 164 | int is_error; 165 | const char *message; 166 | } Json_Result; 167 | 168 | TZOZENDEF Json_Result result_success(String rest, Json_Value value); 169 | TZOZENDEF Json_Result result_failure(String rest, const char *message); 170 | TZOZENDEF void print_json_error(FILE *stream, Json_Result result, String source, const char *prefix); 171 | 172 | TZOZENDEF Json_Result parse_token(String source, String token, Json_Value value, const char *message); 173 | TZOZENDEF Json_Result parse_json_number(Memory *memory, String source); 174 | TZOZENDEF Json_Result parse_escape_sequence(Memory *memory, String source); 175 | TZOZENDEF Json_Result parse_json_string_literal(String source); 176 | TZOZENDEF Json_Result parse_json_string(Memory *memory, String source); 177 | TZOZENDEF Json_Result parse_json_array(Memory *memory, String source, int level); 178 | TZOZENDEF Json_Result parse_json_object(Memory *memory, String source, int level); 179 | TZOZENDEF Json_Result parse_json_value_with_depth(Memory *memory, String source, int level); 180 | TZOZENDEF Json_Result parse_json_value(Memory *memory, String source); 181 | 182 | TZOZENDEF void print_json_null(FILE *stream); 183 | TZOZENDEF void print_json_number(FILE *stream, Json_Number number); 184 | TZOZENDEF void print_json_string(FILE *stream, String string); 185 | TZOZENDEF void print_json_array(FILE *stream, Json_Array array); 186 | TZOZENDEF void print_json_object(FILE *stream, Json_Object object); 187 | TZOZENDEF void print_json_value(FILE *stream, Json_Value value); 188 | 189 | #endif // TZOZEN_H_ 190 | 191 | #ifdef TZOZEN_IMPLEMENTATION 192 | // todo: port https://github.com/tsoding/skedudle/pull/74 when it's merged 193 | 194 | TZOZENDEF void *memory_alloc(Memory *memory, size_t size) 195 | { 196 | assert(memory); 197 | assert(memory->size + size <= memory->capacity); 198 | 199 | void *result = memory->buffer + memory->size; 200 | memory->size += size; 201 | 202 | return result; 203 | } 204 | 205 | TZOZENDEF String string(size_t len, const char *data) 206 | { 207 | String result = {len, data}; 208 | return result; 209 | } 210 | 211 | 212 | TZOZENDEF const char *string_as_cstr(Memory *memory, String s) 213 | { 214 | assert(memory); 215 | char *cstr = (char *) memory_alloc(memory, s.len + 1); 216 | memcpy(cstr, s.data, s.len); 217 | cstr[s.len] = '\0'; 218 | return cstr; 219 | } 220 | 221 | TZOZENDEF String string_empty(void) 222 | { 223 | String result = {0, NULL}; 224 | return result; 225 | } 226 | 227 | TZOZENDEF String chop_until_char(String *input, char delim) 228 | { 229 | if (input->len == 0) { 230 | return string_empty(); 231 | } 232 | 233 | size_t i = 0; 234 | while (i < input->len && input->data[i] != delim) 235 | ++i; 236 | 237 | String line; 238 | line.data = input->data; 239 | line.len = i; 240 | 241 | if (i == input->len) { 242 | input->data += input->len; 243 | input->len = 0; 244 | } else { 245 | input->data += i + 1; 246 | input->len -= i + 1; 247 | } 248 | 249 | return line; 250 | } 251 | 252 | TZOZENDEF String chop_line(String *input) 253 | { 254 | return chop_until_char(input, '\n'); 255 | } 256 | 257 | TZOZENDEF int string_equal(String a, String b) 258 | { 259 | if (a.len != b.len) return 0; 260 | return memcmp(a.data, b.data, a.len) == 0; 261 | } 262 | 263 | TZOZENDEF String take(String s, size_t n) 264 | { 265 | if (s.len < n) return s; 266 | String result = { n, s.data }; 267 | return result; 268 | } 269 | 270 | TZOZENDEF String drop(String s, size_t n) 271 | { 272 | if (s.len < n) return SLT(""); 273 | String result = { 274 | s.len - n, 275 | s.data + n 276 | }; 277 | return result; 278 | } 279 | 280 | TZOZENDEF int prefix_of(String prefix, String s) 281 | { 282 | return string_equal(prefix, take(s, prefix.len)); 283 | } 284 | 285 | TZOZENDEF void chop(String *s, size_t n) 286 | { 287 | *s = drop(*s, n); 288 | } 289 | 290 | TZOZENDEF const char *json_type_as_cstr(Json_Type type) 291 | { 292 | switch (type) { 293 | case JSON_NULL: return "JSON_NULL"; 294 | case JSON_BOOLEAN: return "JSON_BOOLEAN"; 295 | case JSON_NUMBER: return "JSON_NUMBER"; 296 | case JSON_STRING: return "JSON_STRING"; 297 | case JSON_ARRAY: return "JSON_ARRAY"; 298 | case JSON_OBJECT: return "JSON_OBJECT"; 299 | } 300 | 301 | assert(0 && "Incorrect Json_Type"); 302 | return NULL; 303 | } 304 | 305 | TZOZENDEF size_t json_array_size(Json_Array array) 306 | { 307 | size_t size = 0; 308 | FOR_JSON (Json_Array, elem, array) size += 1; 309 | return size; 310 | } 311 | 312 | TZOZENDEF size_t json_object_size(Json_Object object) 313 | { 314 | size_t size = 0; 315 | FOR_JSON (Json_Object, elem, object) size += 1; 316 | return size; 317 | } 318 | 319 | 320 | TZOZENDEF Json_Value json_null() 321 | { 322 | Json_Value value; 323 | memset(&value, 0, sizeof(value)); 324 | return value; 325 | } 326 | 327 | TZOZENDEF Json_Value json_true() 328 | { 329 | Json_Value value; 330 | memset(&value, 0, sizeof(value)); 331 | value.type = JSON_BOOLEAN; 332 | value.boolean = 1; 333 | return value; 334 | } 335 | 336 | TZOZENDEF Json_Value json_false() 337 | { 338 | Json_Value value; 339 | memset(&value, 0, sizeof(value)); 340 | value.type = JSON_BOOLEAN; 341 | return value; 342 | } 343 | 344 | TZOZENDEF Json_Value json_number(String integer, String fraction, String exponent) 345 | { 346 | Json_Value value = json_null(); 347 | value.type = JSON_NUMBER; 348 | value.number.integer = integer; 349 | value.number.fraction = fraction; 350 | value.number.exponent = exponent; 351 | return value; 352 | } 353 | 354 | TZOZENDEF Json_Value json_string(String string) 355 | { 356 | Json_Value value; 357 | memset(&value, 0, sizeof(value)); 358 | value.type = JSON_STRING; 359 | value.string = string; 360 | return value; 361 | } 362 | 363 | TZOZENDEF Json_Value json_array_empty() 364 | { 365 | Json_Value value = json_null(); 366 | value.type = JSON_ARRAY; 367 | return value; 368 | } 369 | 370 | TZOZENDEF Json_Value json_object_empty() 371 | { 372 | Json_Value value = json_null(); 373 | value.type = JSON_OBJECT; 374 | return value; 375 | } 376 | 377 | TZOZENDEF Json_Value json_array(Json_Array array) 378 | { 379 | Json_Value value = json_null(); 380 | value.type = JSON_ARRAY; 381 | value.array = array; 382 | return value; 383 | } 384 | 385 | TZOZENDEF Json_Value json_object(Json_Object object) 386 | { 387 | Json_Value value = json_null(); 388 | value.type = JSON_OBJECT; 389 | value.object = object; 390 | return value; 391 | } 392 | 393 | // We are not using isspace from ctype is because it considers more 394 | // characters to be whitespaces than the JSON spec. 395 | TZOZENDEF int json_isspace(char c) 396 | { 397 | return c == 0x20 || c == 0x0A || c == 0x0D || c == 0x09; 398 | } 399 | 400 | TZOZENDEF String trim_begin(String s) 401 | { 402 | while (s.len && json_isspace(*s.data)) { 403 | s.data++; 404 | s.len--; 405 | } 406 | return s; 407 | } 408 | 409 | TZOZENDEF void json_array_push(Memory *memory, Json_Array *array, Json_Value value) 410 | { 411 | Json_Array_Elem *next = (Json_Array_Elem *) memory_alloc(memory, sizeof(Json_Array_Elem)); 412 | memset(next, 0, sizeof(Json_Array_Elem)); 413 | next->value = value; 414 | 415 | if (array->begin == NULL) { 416 | assert(array->end == NULL); 417 | array->begin = next; 418 | } else { 419 | assert(array->end != NULL); 420 | array->end->next = next; 421 | } 422 | 423 | array->end = next; 424 | } 425 | 426 | TZOZENDEF void json_object_push(Memory *memory, Json_Object *object, String key, Json_Value value) 427 | { 428 | Json_Object_Elem *next = (Json_Object_Elem *) memory_alloc(memory, sizeof(Json_Object_Elem)); 429 | memset(next, 0, sizeof(Json_Object_Elem)); 430 | next->key = key; 431 | next->value = value; 432 | 433 | if (object->begin == NULL) { 434 | assert(object->end == NULL); 435 | object->begin = next; 436 | } else { 437 | assert(object->end != NULL); 438 | object->end->next = next; 439 | } 440 | 441 | object->end = next; 442 | } 443 | 444 | TZOZENDEF int64_t stoi64(String integer) 445 | { 446 | if (integer.len == 0) { 447 | return 0; 448 | } 449 | 450 | int64_t result = 0; 451 | int64_t sign = 1; 452 | 453 | if (*integer.data == '-') { 454 | sign = -1; 455 | chop(&integer, 1); 456 | } else if (*integer.data == '+') { 457 | sign = 1; 458 | chop(&integer, 1); 459 | } 460 | 461 | while (integer.len) { 462 | assert(isdigit(*integer.data)); 463 | result = result * 10 + (*integer.data - '0'); 464 | chop(&integer, 1); 465 | } 466 | 467 | return result * sign; 468 | } 469 | 470 | TZOZENDEF int64_t json_number_to_integer(Json_Number number) 471 | { 472 | int64_t exponent = stoi64(number.exponent); 473 | int64_t result = stoi64(number.integer); 474 | 475 | if (exponent > 0) { 476 | int64_t sign = result >= 0 ? 1 : -1; 477 | 478 | for (; exponent > 0; exponent -= 1) { 479 | int64_t x = 0; 480 | 481 | if (number.fraction.len) { 482 | x = *number.fraction.data - '0'; 483 | chop(&number.fraction, 1); 484 | } 485 | 486 | result = result * 10 + sign * x; 487 | } 488 | } 489 | 490 | for (; exponent < 0 && result; exponent += 1) { 491 | result /= 10; 492 | } 493 | 494 | return result; 495 | } 496 | 497 | TZOZENDEF Json_Result result_success(String rest, Json_Value value) 498 | { 499 | Json_Result result; 500 | memset(&result, 0, sizeof(result)); 501 | result.value = value; 502 | result.rest = rest; 503 | return result; 504 | } 505 | 506 | TZOZENDEF Json_Result result_failure(String rest, const char *message) 507 | { 508 | Json_Result result; 509 | memset(&result, 0, sizeof(result)); 510 | result.is_error = 1; 511 | result.rest = rest; 512 | result.message = message; 513 | return result; 514 | } 515 | 516 | TZOZENDEF Json_Result parse_token(String source, String token, 517 | Json_Value value, 518 | const char *message) 519 | { 520 | if (string_equal(take(source, token.len), token)) { 521 | return result_success(drop(source, token.len), value); 522 | } 523 | 524 | return result_failure(source, message); 525 | } 526 | 527 | TZOZENDEF String clone_string(Memory *memory, String string) 528 | { 529 | char *clone_data = (char *)memory_alloc(memory, string.len); 530 | String clone = { string.len, clone_data}; 531 | memcpy(clone_data, string.data, string.len); 532 | return clone; 533 | } 534 | 535 | TZOZENDEF Json_Result parse_json_number(Memory *memory, String source) 536 | { 537 | String integer = {0, NULL}; 538 | String fraction = {0, NULL}; 539 | String exponent = {0, NULL}; 540 | 541 | integer.data = source.data; 542 | 543 | if (source.len && *source.data == '-') { 544 | integer.len += 1; 545 | chop(&source, 1); 546 | } 547 | 548 | while (source.len && isdigit(*source.data)) { 549 | integer.len += 1; 550 | chop(&source, 1); 551 | } 552 | 553 | // todo(#34): empty integer with fraction is not taken into account 554 | if (integer.len == 0 555 | || string_equal(integer, SLT("-")) 556 | || (integer.len > 1 && *integer.data == '0') 557 | || (integer.len > 2 && prefix_of(SLT("-0"), integer))) { 558 | return result_failure(source, "Incorrect number literal"); 559 | } 560 | 561 | if (source.len && *source.data == '.') { 562 | chop(&source, 1); 563 | fraction.data = source.data; 564 | 565 | while (source.len && isdigit(*source.data)) { 566 | fraction.len += 1; 567 | chop(&source, 1); 568 | } 569 | 570 | if (fraction.len == 0) { 571 | return result_failure(source, "Incorrect number literal"); 572 | } 573 | } 574 | 575 | if (source.len && tolower(*source.data) == 'e') { 576 | chop(&source, 1); 577 | 578 | exponent.data = source.data; 579 | 580 | if (source.len && (*source.data == '-' || *source.data == '+')) { 581 | exponent.len += 1; 582 | chop(&source, 1); 583 | } 584 | 585 | while (source.len && isdigit(*source.data)) { 586 | exponent.len += 1; 587 | chop(&source, 1); 588 | } 589 | 590 | if (exponent.len == 0 || 591 | string_equal(exponent, SLT("-")) || 592 | string_equal(exponent, SLT("+"))) { 593 | return result_failure(source, "Incorrect number literal"); 594 | } 595 | } 596 | 597 | return result_success( 598 | source, 599 | json_number( 600 | clone_string(memory, integer), 601 | clone_string(memory, fraction), 602 | clone_string(memory, exponent))); 603 | } 604 | 605 | TZOZENDEF Json_Result parse_json_string_literal(String source) 606 | { 607 | if (source.len == 0 || *source.data != '"') { 608 | return result_failure(source, "Expected '\"'"); 609 | } 610 | 611 | chop(&source, 1); 612 | 613 | String s = { 0, source.data }; 614 | 615 | while (source.len && *source.data != '"') { 616 | if (*source.data == '\\') { 617 | s.len++; 618 | chop(&source, 1); 619 | 620 | if (source.len == 0) { 621 | return result_failure(source, "Unfinished escape sequence"); 622 | } 623 | } 624 | 625 | s.len++; 626 | chop(&source, 1); 627 | } 628 | 629 | if (source.len == 0) { 630 | return result_failure(source, "Expected '\"'"); 631 | } 632 | 633 | chop(&source, 1); 634 | 635 | return result_success(source, json_string(s)); 636 | } 637 | 638 | TZOZENDEF int32_t unhex(char x) 639 | { 640 | x = tolower(x); 641 | 642 | if ('0' <= x && x <= '9') { 643 | return x - '0'; 644 | } else if ('a' <= x && x <= 'f') { 645 | return x - 'a' + 10; 646 | } 647 | 648 | return -1; 649 | } 650 | 651 | TZOZENDEF Utf8_Chunk utf8_encode_rune(uint32_t rune) 652 | { 653 | const uint8_t b00000111 = (1 << 3) - 1; 654 | const uint8_t b00001111 = (1 << 4) - 1; 655 | const uint8_t b00011111 = (1 << 5) - 1; 656 | const uint8_t b00111111 = (1 << 6) - 1; 657 | const uint8_t b10000000 = ~((1 << 7) - 1); 658 | const uint8_t b11000000 = ~((1 << 6) - 1); 659 | const uint8_t b11100000 = ~((1 << 5) - 1); 660 | const uint8_t b11110000 = ~((1 << 4) - 1); 661 | 662 | Utf8_Chunk chunk; 663 | memset(&chunk, 0, sizeof(chunk)); 664 | 665 | if (rune <= 0x007F) { 666 | chunk.size = 1; 667 | chunk.buffer[0] = (uint8_t) rune; 668 | } else if (0x0080 <= rune && rune <= 0x07FF) { 669 | chunk.size = 2; 670 | chunk.buffer[0] = (uint8_t) (((rune >> 6) & b00011111) | b11000000); 671 | chunk.buffer[1] = (uint8_t) ((rune & b00111111) | b10000000); 672 | } else if (0x0800 <= rune && rune <= 0xFFFF) { 673 | chunk.size = 3; 674 | chunk.buffer[0] = (uint8_t) (((rune >> 12) & b00001111) | b11100000); 675 | chunk.buffer[1] = (uint8_t) (((rune >> 6) & b00111111) | b10000000); 676 | chunk.buffer[2] = (uint8_t) ((rune & b00111111) | b10000000); 677 | } else if (0x10000 <= rune && rune <= 0x10FFFF) { 678 | chunk.size = 4; 679 | chunk.buffer[0] = (uint8_t) (((rune >> 18) & b00000111) | b11110000); 680 | chunk.buffer[1] = (uint8_t) (((rune >> 12) & b00111111) | b10000000); 681 | chunk.buffer[2] = (uint8_t) (((rune >> 6) & b00111111) | b10000000); 682 | chunk.buffer[3] = (uint8_t) ((rune & b00111111) | b10000000); 683 | } 684 | 685 | return chunk; 686 | } 687 | 688 | TZOZENDEF Json_Result parse_escape_sequence(Memory *memory, String source) 689 | { 690 | static char unescape_map[][2] = { 691 | {'b', '\b'}, 692 | {'f', '\f'}, 693 | {'n', '\n'}, 694 | {'r', '\r'}, 695 | {'t', '\t'}, 696 | {'/', '/'}, 697 | {'\\', '\\'}, 698 | {'"', '"'}, 699 | }; 700 | static const size_t unescape_map_size = 701 | sizeof(unescape_map) / sizeof(unescape_map[0]); 702 | 703 | if (source.len == 0 || *source.data != '\\') { 704 | return result_failure(source, "Expected '\\'"); 705 | } 706 | chop(&source, 1); 707 | 708 | if (source.len <= 0) { 709 | return result_failure(source, "Unfinished escape sequence"); 710 | } 711 | 712 | for (size_t i = 0; i < unescape_map_size; ++i) { 713 | if (unescape_map[i][0] == *source.data) { 714 | // It may look like we are refering to something outside 715 | // of `memory`, but later (see the places where 716 | // `parse_escape_sequence()` is used 2020-05-05) we are 717 | // copying it to `memory`. 718 | // 719 | // todo: We don't have any policy on what kind of memory we should always refer to int Json_Value-s and Json_Result-s 720 | String s = {1, &unescape_map[i][1]}; 721 | return result_success(drop(source, 1), json_string(s)); 722 | } 723 | } 724 | 725 | if (*source.data != 'u') { 726 | return result_failure(source, "Unknown escape sequence"); 727 | } 728 | chop(&source, 1); 729 | 730 | if (source.len < 4) { 731 | return result_failure(source, "Incomplete unicode point escape sequence"); 732 | } 733 | 734 | uint32_t rune = 0; 735 | for (int i = 0; i < 4; ++i) { 736 | int32_t x = unhex(*source.data); 737 | if (x < 0) { 738 | return result_failure(source, "Incorrect hex digit"); 739 | } 740 | rune = rune * 0x10 + x; 741 | chop(&source, 1); 742 | } 743 | 744 | if (0xD800 <= rune && rune <= 0xDBFF) { 745 | if (source.len < 6) { 746 | return result_failure(source, "Unfinished surrogate pair"); 747 | } 748 | 749 | if (*source.data != '\\') { 750 | return result_failure(source, "Unfinished surrogate pair. Expected '\\'"); 751 | } 752 | chop(&source, 1); 753 | 754 | if (*source.data != 'u') { 755 | return result_failure(source, "Unfinished surrogate pair. Expected 'u'"); 756 | } 757 | chop(&source, 1); 758 | 759 | uint32_t surrogate = 0; 760 | for (int i = 0; i < 4; ++i) { 761 | int32_t x = unhex(*source.data); 762 | if (x < 0) { 763 | return result_failure(source, "Incorrect hex digit"); 764 | } 765 | surrogate = surrogate * 0x10 + x; 766 | chop(&source, 1); 767 | } 768 | 769 | if (!(0xDC00 <= surrogate && surrogate <= 0xDFFF)) { 770 | return result_failure(source, "Invalid surrogate pair"); 771 | } 772 | 773 | rune = 0x10000 + (((rune - 0xD800) << 10) |(surrogate - 0xDC00)); 774 | } 775 | 776 | if (rune > 0x10FFFF) { 777 | rune = 0xFFFD; 778 | } 779 | 780 | Utf8_Chunk utf8_chunk = utf8_encode_rune(rune); 781 | assert(utf8_chunk.size > 0); 782 | 783 | char *data = (char *) memory_alloc(memory, utf8_chunk.size); 784 | memcpy(data, utf8_chunk.buffer, utf8_chunk.size); 785 | 786 | String s = {utf8_chunk.size, data}; 787 | return result_success(source, json_string(s)); 788 | } 789 | 790 | TZOZENDEF Json_Result parse_json_string(Memory *memory, String source) 791 | { 792 | Json_Result result = parse_json_string_literal(source); 793 | if (result.is_error) return result; 794 | assert(result.value.type == JSON_STRING); 795 | 796 | const size_t buffer_capacity = result.value.string.len; 797 | source = result.value.string; 798 | String rest = result.rest; 799 | 800 | char *buffer = (char *)memory_alloc(memory, buffer_capacity); 801 | size_t buffer_size = 0; 802 | 803 | while (source.len) { 804 | if (*source.data == '\\') { 805 | result = parse_escape_sequence(memory, source); 806 | if (result.is_error) return result; 807 | assert(result.value.type == JSON_STRING); 808 | assert(buffer_size + result.value.string.len <= buffer_capacity); 809 | memcpy(buffer + buffer_size, 810 | result.value.string.data, 811 | result.value.string.len); 812 | buffer_size += result.value.string.len; 813 | 814 | source = result.rest; 815 | } else { 816 | // todo(#37): json parser is not aware of the input encoding 817 | assert(buffer_size < buffer_capacity); 818 | buffer[buffer_size++] = *source.data; 819 | chop(&source, 1); 820 | } 821 | } 822 | 823 | String result_string = {buffer_size, buffer}; 824 | return result_success(rest, json_string(result_string)); 825 | } 826 | 827 | TZOZENDEF Json_Result parse_json_array(Memory *memory, String source, int level) 828 | { 829 | assert(memory); 830 | 831 | if(source.len == 0 || *source.data != '[') { 832 | return result_failure(source, "Expected '['"); 833 | } 834 | 835 | chop(&source, 1); 836 | 837 | source = trim_begin(source); 838 | 839 | if (source.len == 0) { 840 | return result_failure(source, "Expected ']'"); 841 | } else if(*source.data == ']') { 842 | return result_success(drop(source, 1), json_array_empty()); 843 | } 844 | 845 | Json_Array array; 846 | memset(&array, 0, sizeof(array)); 847 | 848 | while(source.len > 0) { 849 | Json_Result item_result = parse_json_value_with_depth(memory, source, level + 1); 850 | if(item_result.is_error) { 851 | return item_result; 852 | } 853 | 854 | json_array_push(memory, &array, item_result.value); 855 | 856 | source = trim_begin(item_result.rest); 857 | 858 | if (source.len == 0) { 859 | return result_failure(source, "Expected ']' or ','"); 860 | } 861 | 862 | if (*source.data == ']') { 863 | return result_success(drop(source, 1), json_array(array)); 864 | } 865 | 866 | if (*source.data != ',') { 867 | return result_failure(source, "Expected ']' or ','"); 868 | } 869 | 870 | source = trim_begin(drop(source, 1)); 871 | } 872 | 873 | return result_failure(source, "EOF"); 874 | } 875 | 876 | TZOZENDEF Json_Result parse_json_object(Memory *memory, String source, int level) 877 | { 878 | assert(memory); 879 | 880 | if (source.len == 0 || *source.data != '{') { 881 | return result_failure(source, "Expected '{'");; 882 | } 883 | 884 | chop(&source, 1); 885 | 886 | source = trim_begin(source); 887 | 888 | if (source.len == 0) { 889 | return result_failure(source, "Expected '}'"); 890 | } else if (*source.data == '}') { 891 | return result_success(drop(source, 1), json_object_empty());; 892 | } 893 | 894 | Json_Object object; 895 | memset(&object, 0, sizeof(object)); 896 | 897 | while (source.len > 0) { 898 | source = trim_begin(source); 899 | 900 | Json_Result key_result = parse_json_string(memory, source); 901 | if (key_result.is_error) { 902 | return key_result; 903 | } 904 | source = trim_begin(key_result.rest); 905 | 906 | if (source.len == 0 || *source.data != ':') { 907 | return result_failure(source, "Expected ':'"); 908 | } 909 | 910 | chop(&source, 1); 911 | 912 | Json_Result value_result = parse_json_value_with_depth(memory, source, level + 1); 913 | if (value_result.is_error) { 914 | return value_result; 915 | } 916 | source = trim_begin(value_result.rest); 917 | 918 | assert(key_result.value.type == JSON_STRING); 919 | json_object_push(memory, &object, key_result.value.string, value_result.value); 920 | 921 | if (source.len == 0) { 922 | return result_failure(source, "Expected '}' or ','"); 923 | } 924 | 925 | if (*source.data == '}') { 926 | return result_success(drop(source, 1), json_object(object)); 927 | } 928 | 929 | if (*source.data != ',') { 930 | return result_failure(source, "Expected '}' or ','"); 931 | } 932 | 933 | source = drop(source, 1); 934 | } 935 | 936 | return result_failure(source, "EOF"); 937 | } 938 | 939 | TZOZENDEF Json_Result parse_json_value_with_depth(Memory *memory, String source, int level) 940 | { 941 | if (level >= JSON_DEPTH_MAX_LIMIT) { 942 | return result_failure(source, "Reached the max limit of depth"); 943 | } 944 | 945 | source = trim_begin(source); 946 | 947 | if (source.len == 0) { 948 | return result_failure(source, "EOF"); 949 | } 950 | 951 | switch (*source.data) { 952 | case 'n': return parse_token(source, SLT("null"), json_null(), "Expected `null`"); 953 | case 't': return parse_token(source, SLT("true"), json_true(), "Expected `true`"); 954 | case 'f': return parse_token(source, SLT("false"), json_false(), "Expected `false`"); 955 | case '"': return parse_json_string(memory, source); 956 | case '[': return parse_json_array(memory, source, level); 957 | case '{': return parse_json_object(memory, source, level); 958 | } 959 | 960 | return parse_json_number(memory, source); 961 | } 962 | 963 | // todo(#40): parse_json_value is not aware of input encoding 964 | TZOZENDEF Json_Result parse_json_value(Memory *memory, String source) 965 | { 966 | return parse_json_value_with_depth(memory, source, 0); 967 | } 968 | 969 | TZOZENDEF void print_json_null(FILE *stream) 970 | { 971 | fprintf(stream, "null"); 972 | } 973 | 974 | TZOZENDEF void print_json_boolean(FILE *stream, int boolean) 975 | { 976 | if (boolean) { 977 | fprintf(stream, "true"); 978 | } else { 979 | fprintf(stream, "false"); 980 | } 981 | } 982 | 983 | TZOZENDEF void print_json_number(FILE *stream, Json_Number number) 984 | { 985 | fwrite(number.integer.data, 1, number.integer.len, stream); 986 | 987 | if (number.fraction.len > 0) { 988 | fputc('.', stream); 989 | fwrite(number.fraction.data, 1, number.fraction.len, stream); 990 | } 991 | 992 | if (number.exponent.len > 0) { 993 | fputc('e', stream); 994 | fwrite(number.exponent.data, 1, number.exponent.len, stream); 995 | } 996 | } 997 | 998 | TZOZENDEF int json_get_utf8_char_len(unsigned char ch) 999 | { 1000 | if ((ch & 0x80) == 0) return 1; 1001 | switch (ch & 0xf0) { 1002 | case 0xf0: 1003 | return 4; 1004 | case 0xe0: 1005 | return 3; 1006 | default: 1007 | return 2; 1008 | } 1009 | } 1010 | 1011 | TZOZENDEF void print_json_string(FILE *stream, String string) 1012 | { 1013 | const char *hex_digits = "0123456789abcdef"; 1014 | const char *specials = "btnvfr"; 1015 | const char *p = string.data; 1016 | 1017 | fputc('"', stream); 1018 | size_t cl; 1019 | for (size_t i = 0; i < string.len; i++) { 1020 | unsigned char ch = ((unsigned char *) p)[i]; 1021 | if (ch == '"' || ch == '\\') { 1022 | fwrite("\\", 1, 1, stream); 1023 | fwrite(p + i, 1, 1, stream); 1024 | } else if (ch >= '\b' && ch <= '\r') { 1025 | fwrite("\\", 1, 1, stream); 1026 | fwrite(&specials[ch - '\b'], 1, 1, stream); 1027 | } else if (isprint(ch)) { 1028 | fwrite(p + i, 1, 1, stream); 1029 | } else if ((cl = json_get_utf8_char_len(ch)) == 1) { 1030 | fwrite("\\u00", 1, 4, stream); 1031 | fwrite(&hex_digits[(ch >> 4) % 0xf], 1, 1, stream); 1032 | fwrite(&hex_digits[ch % 0xf], 1, 1, stream); 1033 | } else { 1034 | fwrite(p + i, 1, cl, stream); 1035 | i += cl - 1; 1036 | } 1037 | } 1038 | fputc('"', stream); 1039 | } 1040 | 1041 | TZOZENDEF void print_json_array(FILE *stream, Json_Array array) 1042 | { 1043 | fprintf(stream, "["); 1044 | int t = 0; 1045 | FOR_JSON (Json_Array, elem, array) { 1046 | if (t) { 1047 | fprintf(stream, ","); 1048 | } else { 1049 | t = 1; 1050 | } 1051 | print_json_value(stream, elem->value); 1052 | } 1053 | fprintf(stream, "]"); 1054 | } 1055 | 1056 | TZOZENDEF void print_json_object(FILE *stream, Json_Object object) 1057 | { 1058 | fprintf(stream, "{"); 1059 | int t = 0; 1060 | FOR_JSON (Json_Object, elem, object) { 1061 | if (t) { 1062 | fprintf(stream, ","); 1063 | } else { 1064 | t = 1; 1065 | } 1066 | print_json_string(stream, elem->key); 1067 | fprintf(stream, ":"); 1068 | print_json_value(stream, elem->value); 1069 | } 1070 | fprintf(stream, "}"); 1071 | } 1072 | 1073 | TZOZENDEF void print_json_value(FILE *stream, Json_Value value) 1074 | { 1075 | switch (value.type) { 1076 | case JSON_NULL: { 1077 | print_json_null(stream); 1078 | } break; 1079 | case JSON_BOOLEAN: { 1080 | print_json_boolean(stream, value.boolean); 1081 | } break; 1082 | case JSON_NUMBER: { 1083 | print_json_number(stream, value.number); 1084 | } break; 1085 | case JSON_STRING: { 1086 | print_json_string(stream, value.string); 1087 | } break; 1088 | case JSON_ARRAY: { 1089 | print_json_array(stream, value.array); 1090 | } break; 1091 | case JSON_OBJECT: { 1092 | print_json_object(stream, value.object); 1093 | } break; 1094 | } 1095 | } 1096 | 1097 | TZOZENDEF void print_json_error(FILE *stream, Json_Result result, 1098 | String source, const char *prefix) 1099 | { 1100 | assert(stream); 1101 | assert(source.data <= result.rest.data); 1102 | 1103 | size_t n = result.rest.data - source.data; 1104 | 1105 | for (size_t line_number = 1; source.len; ++line_number) { 1106 | String line = chop_line(&source); 1107 | 1108 | if (n <= line.len) { 1109 | fprintf(stream, "%s:%ld: %s\n", prefix, line_number, result.message); 1110 | fwrite(line.data, 1, line.len, stream); 1111 | fputc('\n', stream); 1112 | 1113 | for (size_t j = 0; j < n; ++j) { 1114 | fputc(' ', stream); 1115 | } 1116 | fputc('^', stream); 1117 | fputc('\n', stream); 1118 | break; 1119 | } 1120 | 1121 | n -= line.len + 1; 1122 | } 1123 | 1124 | for (int i = 0; source.len && i < 3; ++i) { 1125 | String line = chop_line(&source); 1126 | fwrite(line.data, 1, line.len, stream); 1127 | fputc('\n', stream); 1128 | } 1129 | } 1130 | 1131 | TZOZENDEF Json_Value json_object_value_by_key(Json_Object object, String key) 1132 | { 1133 | FOR_JSON (Json_Object, element, object) { 1134 | if (string_equal(element->key, key)) { 1135 | return element->value; 1136 | } 1137 | } 1138 | return json_null(); 1139 | } 1140 | 1141 | #endif // TZOZEN_IMPLEMENTATION 1142 | -------------------------------------------------------------------------------- /src/vodus.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | #include FT_FREETYPE_H 12 | #include 13 | 14 | #ifdef VODUS_RELEASE 15 | # define STB_IMAGE_IMPLEMENTATION 16 | # define STB_IMAGE_RESIZE_IMPLEMENTATION 17 | # define STB_IMAGE_WRITE_IMPLEMENTATION 18 | #endif // VODUS_RELEASE 19 | 20 | #include "./stb_image.h" 21 | #include "./stb_image_resize.h" 22 | #include "./stb_image_write.h" 23 | 24 | extern "C" { 25 | #include 26 | #include 27 | #include 28 | } 29 | 30 | #include "./aids.hpp" 31 | using namespace aids; 32 | 33 | #ifdef VODUS_SSE 34 | #include 35 | #include 36 | #endif // VODUS_SSE 37 | 38 | #define GLFW_INCLUDE_GLCOREARB 39 | #define GL_GLEXT_PROTOTYPES 40 | #include 41 | 42 | // PLEASE READ THIS --> https://en.wikipedia.org/wiki/Single_Compilation_Unit 43 | #include "./vodus_error.cpp" 44 | #include "./vodus_queue.cpp" 45 | #include "./vodus_image32.cpp" 46 | #include "./vodus_video_params.cpp" 47 | #include "./vodus_emotes.cpp" 48 | #include "./vodus_message.cpp" 49 | #include "./vodus_encoder.cpp" 50 | #include "./vodus_main.cpp" 51 | -------------------------------------------------------------------------------- /src/vodus_emotes.cpp: -------------------------------------------------------------------------------- 1 | struct Gif_Animat 2 | { 3 | Animat32 animat; 4 | int duration; 5 | size_t index; 6 | 7 | bool is_null() const 8 | { 9 | return animat.count == 0; 10 | } 11 | 12 | void update_global_time(int global_time) 13 | { 14 | auto t = global_time % duration; 15 | index = 0; 16 | 17 | while (t > animat.frame_delays[index] && index < animat.count) { 18 | t -= animat.frame_delays[index]; 19 | index += 1; 20 | } 21 | } 22 | 23 | void slap_onto_image32(Image32 surface, int x, int y) 24 | { 25 | if (!is_null()) { 26 | slap_image32_onto_image32(surface, animat.frames[index], x, y); 27 | } 28 | } 29 | 30 | int width() const 31 | { 32 | return is_null() ? 0 : animat.frames[index].width; 33 | } 34 | 35 | int height() const 36 | { 37 | return is_null() ? 0 : animat.frames[index].height; 38 | } 39 | }; 40 | 41 | struct Emote 42 | { 43 | enum Type 44 | { 45 | Png = 0, 46 | Gif 47 | }; 48 | 49 | Type type; 50 | 51 | union 52 | { 53 | Image32 png; 54 | Gif_Animat gif; 55 | }; 56 | 57 | int width() const 58 | { 59 | switch (type) { 60 | case Png: return png.width; 61 | case Gif: return gif.width(); 62 | } 63 | assert(!"Incorrect Emote_Type value"); 64 | return 0; 65 | } 66 | 67 | int height() const 68 | { 69 | switch (type) { 70 | case Png: return png.height; 71 | case Gif: return gif.height(); 72 | } 73 | assert(!"Incorrect Emote_Type value"); 74 | return 0; 75 | } 76 | 77 | void slap_onto_image32(Image32 surface, int x, int y) 78 | { 79 | switch (type) { 80 | case Png: { 81 | slap_image32_onto_image32(surface, png, x, y); 82 | } return; 83 | 84 | case Gif: { 85 | gif.slap_onto_image32(surface, x, y); 86 | } return; 87 | } 88 | assert(!"Incorrect Emote_Type value"); 89 | } 90 | }; 91 | 92 | const size_t EMOTE_MAPPING_CAPACITY = 5000; 93 | const size_t EMOTE_GIFS_CAPACITY = EMOTE_MAPPING_CAPACITY; 94 | 95 | String_View file_extension(String_View filename) 96 | { 97 | String_View ext = {}; 98 | while (filename.count != 0) { 99 | ext = filename.chop_by_delim('.'); 100 | } 101 | return ext; 102 | } 103 | 104 | 105 | Emote load_gif_emote(String_View file_path, size_t size) 106 | { 107 | Emote emote = {Emote::Gif}; 108 | 109 | auto file_path_cstr = string_view_as_cstr(file_path); 110 | assert(file_path_cstr); 111 | defer(free((void*) file_path_cstr)); 112 | 113 | emote.gif.animat = load_animat32_from_gif(file_path_cstr, size); 114 | 115 | for (size_t i = 0; i < emote.gif.animat.count; ++i) { 116 | emote.gif.duration += emote.gif.animat.frame_delays[i]; 117 | } 118 | 119 | return emote; 120 | } 121 | 122 | Emote load_png_emote(String_View filepath, size_t size) 123 | { 124 | Emote emote = {Emote::Png}; 125 | auto filepath_cstr = string_view_as_cstr(filepath); 126 | 127 | assert(filepath_cstr); 128 | defer(free((void*) filepath_cstr)); 129 | 130 | emote.png = load_image32_from_png(filepath_cstr, size); 131 | return emote; 132 | } 133 | 134 | Emote load_emote(String_View filepath, size_t size) 135 | { 136 | auto ext = file_extension(filepath); 137 | if (ext == "gif"_sv) { 138 | return load_gif_emote(filepath, size); 139 | } else if (ext == "png"_sv) { 140 | return load_png_emote(filepath, size); 141 | } else { 142 | println(stderr, filepath, " has unsupported extension ", ext); 143 | abort(); 144 | } 145 | 146 | return {}; 147 | } 148 | 149 | struct Emote_Mapping 150 | { 151 | String_View name; 152 | Maybe emote; 153 | String_View filepath; 154 | }; 155 | 156 | // NOTE: stolen from http://www.cse.yorku.ca/~oz/hash.html 157 | unsigned long djb2(String_View str) 158 | { 159 | unsigned long hash = 5381; 160 | for (size_t i = 0; i < str.count; ++i) { 161 | hash = ((hash << 5) + hash) + str.data[i]; 162 | } 163 | return hash; 164 | } 165 | 166 | struct Emote_Cache 167 | { 168 | Maybe emote_by_name(String_View name, size_t size) 169 | { 170 | auto mapping = emote_mapping.get(name); 171 | if (mapping.has_value) { 172 | if (!mapping.unwrap->emote.has_value) { 173 | mapping.unwrap->emote = {true, load_emote(mapping.unwrap->filepath, size)}; 174 | if (mapping.unwrap->emote.unwrap.type == Emote::Gif) { 175 | // NOTE: storing the pointer to a bucket in the 176 | // Hash_Map is dangerous because it could be 177 | // invalidated when you insert into the 178 | // Hash_Map. The only reason this works is that we 179 | // don't insert into the Hash_Map after 180 | // populate_from_file(). 181 | gifs.push(&mapping.unwrap->emote.unwrap.gif); 182 | } 183 | } 184 | 185 | return mapping.unwrap->emote; 186 | } 187 | 188 | return {}; 189 | } 190 | 191 | void update_gifs(float delta_time) 192 | { 193 | const float GLOBAL_TIME_PERIOD = 1000000.0f; 194 | // NOTE: We are wrapping around the global_time_sec around 195 | // some big GLOBAL_TIME_PERIOD to prevent the global_time_sec 196 | // overflowing. This comes at the cost of slight GIF animation 197 | // glitch that happens every 11 days of the video (please 198 | // update this estimate if you update GLOBAL_TIME_SEC). 199 | global_time_sec = fmodf(global_time_sec + delta_time, GLOBAL_TIME_PERIOD); 200 | for (size_t i = 0; i < gifs.size; ++i) { 201 | gifs.data[i]->update_global_time((int) floorf(global_time_sec * 100.0f)); 202 | } 203 | } 204 | 205 | void populate_from_file(const char *mapping_filepath, size_t size) 206 | { 207 | auto mapping_csv = read_file_as_string_view(mapping_filepath); 208 | if (!mapping_csv.has_value) { 209 | println(stderr, "Could not read file `", mapping_filepath, "`"); 210 | abort(); 211 | } 212 | 213 | while (mapping_csv.unwrap.count > 0) { 214 | auto line = mapping_csv.unwrap.chop_by_delim('\n'); 215 | auto name = line.chop_by_delim(','); 216 | auto filename = line; 217 | 218 | Emote_Mapping mapping = {}; 219 | mapping.filepath = filename; 220 | mapping.emote = {}; 221 | mapping.name = name; 222 | emote_mapping.insert(name, mapping); 223 | } 224 | } 225 | 226 | Hash_Map emote_mapping; 227 | Dynamic_Array gifs; 228 | float global_time_sec = 0.0f; 229 | }; 230 | -------------------------------------------------------------------------------- /src/vodus_encoder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "vodus_encoder.hpp" 3 | 4 | void encode_avframe(AVCodecContext *context, AVFrame *frame, AVPacket *pkt, FILE *outfile) 5 | { 6 | avec(avcodec_send_frame(context, frame)); 7 | 8 | for (int ret = 0; ret >= 0; ) { 9 | ret = avcodec_receive_packet(context, pkt); 10 | if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { 11 | return; 12 | } else { 13 | avec(ret); 14 | } 15 | 16 | fwrite(pkt->data, 1, pkt->size, outfile); 17 | av_packet_unref(pkt); 18 | } 19 | } 20 | 21 | void slap_image32_onto_avframe(Image32 frame_image32, AVFrame *avframe) 22 | { 23 | assert(avframe->width == (int) frame_image32.width); 24 | assert(avframe->height == (int) frame_image32.height); 25 | 26 | for (int y = 0; y < avframe->height; ++y) { 27 | for (int x = 0; x < avframe->width; ++x) { 28 | Pixel32 p = frame_image32.pixels[y * frame_image32.width + x]; 29 | int Y = (0.257 * p.r) + (0.504 * p.g) + (0.098 * p.b) + 16; 30 | int V = (0.439 * p.r) - (0.368 * p.g) - (0.071 * p.b) + 128; 31 | int U = -(0.148 * p.r) - (0.291 * p.g) + (0.439 * p.b) + 128; 32 | avframe->data[0][y * avframe->linesize[0] + x] = Y; 33 | avframe->data[1][(y >> 1) * avframe->linesize[1] + (x >> 1)] = U; 34 | avframe->data[2][(y >> 1) * avframe->linesize[2] + (x >> 1)] = V; 35 | } 36 | } 37 | } 38 | 39 | void avencoder_encode(AVEncoder_Context *context, Video_Params, Image32 surface, int frame_index) 40 | { 41 | slap_image32_onto_avframe(surface, context->frame); 42 | context->frame->pts = frame_index; 43 | encode_avframe(context->context, context->frame, context->packet, context->output_stream); 44 | } 45 | 46 | void free_avencoder_context(AVEncoder_Context *context) 47 | { 48 | avcodec_free_context(&context->context); 49 | av_packet_free(&context->packet); 50 | fclose(context->output_stream); 51 | av_frame_free(&context->frame); 52 | delete context; 53 | } 54 | 55 | AVEncoder_Context *new_avencoder_context(Video_Params params, const char *output_filepath) 56 | { 57 | AVEncoder_Context *result = new AVEncoder_Context(); 58 | 59 | result->codec = fail_if_null( 60 | avcodec_find_encoder(AV_CODEC_ID_MPEG2VIDEO), 61 | "Codec not found"); 62 | 63 | result->context = fail_if_null( 64 | avcodec_alloc_context3(result->codec), 65 | "Could not allocate video codec context"); 66 | 67 | result->context->bit_rate = params.bitrate; 68 | result->context->width = params.width; 69 | result->context->height = params.height; 70 | result->context->time_base = (AVRational){1, (int) params.fps}; 71 | result->context->framerate = (AVRational){(int) params.fps, 1}; 72 | result->context->gop_size = 10; 73 | // result->context->max_b_frames = 1; 74 | result->context->pix_fmt = AV_PIX_FMT_YUV420P; 75 | 76 | result->packet = fail_if_null( 77 | av_packet_alloc(), 78 | "Could not allocate packet"); 79 | 80 | 81 | avec(avcodec_open2(result->context, result->codec, NULL)); 82 | 83 | if (output_filepath == nullptr) { 84 | println(stderr, "Error: Output filepath is not provided. Use `--output` flag."); 85 | usage(stderr); 86 | exit(1); 87 | } 88 | 89 | result->output_stream = fail_if_null( 90 | fopen(output_filepath, "wb"), 91 | "Could not open ", output_filepath); 92 | 93 | 94 | result->frame = fail_if_null( 95 | av_frame_alloc(), 96 | "Could not allocate video frame"); 97 | 98 | result->frame->format = result->context->pix_fmt; 99 | result->frame->width = result->context->width; 100 | result->frame->height = result->context->height; 101 | 102 | avec(av_frame_get_buffer(result->frame, 32)); 103 | 104 | return result; 105 | } 106 | 107 | void pngencoder_encode(PNGEncoder_Context *context, Video_Params, Image32 surface, int frame_index) 108 | { 109 | char buffer[1024]; 110 | String_Buffer path = {sizeof(buffer), buffer}; 111 | sprint(&path, context->output_folder_path, "/", frame_index, "-frame.png"); 112 | if (!stbi_write_png(buffer, surface.width, surface.height, 4, surface.pixels, surface.width * 4)) { 113 | println(stderr, "Could not save image: ", path); 114 | abort(); 115 | } 116 | } 117 | 118 | const char *vertex_shader_source = 119 | "#version 130\n" 120 | "out vec2 texcoord;" 121 | "void main(void)\n" 122 | "{\n" 123 | " int gray = gl_VertexID ^ (gl_VertexID >> 1);\n" 124 | " gl_Position = vec4(\n" 125 | " 2 * (gray / 2) - 1,\n" 126 | " 2 * (gray % 2) - 1,\n" 127 | " 0.0,\n" 128 | " 1.0);\n" 129 | " texcoord = vec2(gray / 2, 1 - gray % 2);\n" 130 | "}\n"; 131 | const char *fragment_shader_source = 132 | "#version 130\n" 133 | "in vec2 texcoord;\n" 134 | "uniform sampler2D frame;\n" 135 | "out vec4 color;\n" 136 | "void main(void) {\n" 137 | " color = texture(frame, texcoord);\n" 138 | " //color = vec4(0.5, 0.5, 0.5, 1.0);\n" 139 | "}\n"; 140 | 141 | struct Shader 142 | { 143 | GLuint unwrap; 144 | }; 145 | 146 | Shader compile_shader(const char *source_code, GLenum shader_type) 147 | { 148 | GLuint shader = {}; 149 | shader = glCreateShader(shader_type); 150 | if (shader == 0) { 151 | println(stderr, "Could not create a shader"); 152 | exit(1); 153 | } 154 | 155 | glShaderSource(shader, 1, &source_code, 0); 156 | glCompileShader(shader); 157 | 158 | GLint compiled = 0; 159 | glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); 160 | if (!compiled) { 161 | GLchar buffer[1024]; 162 | int length = 0; 163 | glGetShaderInfoLog(shader, sizeof(buffer), &length, buffer); 164 | println(stderr, "Could not compile shader: ", buffer); 165 | exit(1); 166 | } 167 | 168 | return {shader}; 169 | } 170 | 171 | struct Program 172 | { 173 | GLuint unwrap; 174 | }; 175 | 176 | Program link_program(Shader vertex_shader, Shader fragment_shader) 177 | { 178 | GLuint program = glCreateProgram(); 179 | 180 | if (program == 0) { 181 | println(stderr, "Could not create shader program"); 182 | exit(1); 183 | } 184 | 185 | glAttachShader(program, vertex_shader.unwrap); 186 | glAttachShader(program, fragment_shader.unwrap); 187 | glLinkProgram(program); 188 | 189 | GLint linked = 0; 190 | glGetProgramiv(program, GL_LINK_STATUS, &linked); 191 | if (!linked) { 192 | GLchar buffer[1024]; 193 | int length = 0; 194 | glGetProgramInfoLog(program, sizeof(buffer), &length, buffer); 195 | println(stdout, "Could not link the program: ", buffer); 196 | exit(1); 197 | } 198 | 199 | return {program}; 200 | } 201 | 202 | Preview_Context *new_preview_context(Video_Params params) 203 | { 204 | Preview_Context *context = new Preview_Context(); 205 | 206 | if (!glfwInit()) { 207 | println(stderr, "Could not initialize GLFW"); 208 | exit(1); 209 | } 210 | 211 | context->window = glfwCreateWindow(params.width, params.height, "Vodus", NULL, NULL); 212 | if (!context->window) { 213 | println(stderr, "Could not create GLFW window"); 214 | exit(1); 215 | } 216 | 217 | glfwMakeContextCurrent(context->window); 218 | 219 | // glfwSetFramebufferSizeCallback(window, framebuffer_resize); 220 | 221 | GLuint texture_id; 222 | 223 | glGenTextures(1, &texture_id); 224 | glActiveTexture(GL_TEXTURE0); 225 | glBindTexture(GL_TEXTURE_2D, texture_id); 226 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 227 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 228 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 229 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 230 | 231 | println(stdout, "Compiling vertex shader..."); 232 | Shader vertex_shader = compile_shader(vertex_shader_source, GL_VERTEX_SHADER); 233 | println(stdout, "Compiling fragment shader..."); 234 | Shader fragment_shader = compile_shader(fragment_shader_source, GL_FRAGMENT_SHADER); 235 | println(stdout, "Linking the program..."); 236 | Program program = link_program(vertex_shader, fragment_shader); 237 | 238 | glUseProgram(program.unwrap); 239 | 240 | GLint frame_sampler = glGetUniformLocation(program.unwrap, "frame"); 241 | glUniform1i(frame_sampler, 0); 242 | 243 | return context; 244 | } 245 | 246 | void delay(size_t milliseconds) 247 | { 248 | #ifdef _WIN32 249 | #error "TODO(#144): delay is not implemented for windows" 250 | #else 251 | usleep(milliseconds * 1000); 252 | #endif // _WIN32 253 | } 254 | 255 | void previewencoder_encode(Preview_Context *context, Video_Params params, Image32 surface, int frame_index) 256 | { 257 | if (glfwWindowShouldClose(context->window)) { 258 | // TODO(#140): encoder does not provide a mechanism to exit prematurely 259 | exit(0); 260 | } 261 | 262 | // Rebind the texture 263 | glTexImage2D(GL_TEXTURE_2D, 264 | 0, 265 | GL_RGBA, 266 | surface.width, 267 | surface.height, 268 | 0, 269 | GL_RGBA, 270 | GL_UNSIGNED_BYTE, 271 | surface.pixels); 272 | glGenerateMipmap(GL_TEXTURE_2D); 273 | 274 | // TODO(#142): Preview does not allow to pause and move backward/forward 275 | 276 | glDrawArrays(GL_QUADS, 0, 4); 277 | glfwSwapBuffers(context->window); 278 | glfwPollEvents(); 279 | 280 | clock_t end = clock(); 281 | const auto delta_time = 1000 / params.fps; 282 | assert(context->begin <= end); 283 | const auto frame_time = (size_t) (end - context->begin) / 1000; 284 | if (delta_time >= frame_time) { 285 | delay(delta_time - frame_time); 286 | } 287 | context->begin = end; 288 | } 289 | -------------------------------------------------------------------------------- /src/vodus_encoder.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VODUS_ENCODER_HPP_ 2 | #define VODUS_ENCODER_HPP_ 3 | 4 | typedef void (*Encode_Func)(void *context, Video_Params params, Image32 surface, int frame_index); 5 | 6 | struct Encoder 7 | { 8 | void *context; 9 | Encode_Func encode_func; 10 | 11 | void encode(Video_Params params, Image32 surface, int frame_index) 12 | { 13 | encode_func(context, params, surface, frame_index); 14 | } 15 | }; 16 | 17 | struct AVEncoder_Context 18 | { 19 | AVFrame *frame; 20 | AVCodecContext *context; 21 | AVPacket *packet; 22 | FILE *output_stream; 23 | AVCodec *codec; 24 | }; 25 | 26 | AVEncoder_Context *new_avencoder_context(Video_Params params, const char *output_filepath); 27 | void free_avencoder_context(AVEncoder_Context *context); 28 | void avencoder_encode(AVEncoder_Context *context, Video_Params params, Image32 surface, int frame_index); 29 | 30 | struct PNGEncoder_Context 31 | { 32 | String_View output_folder_path; 33 | }; 34 | 35 | void pngencoder_encode(PNGEncoder_Context *context, Video_Params params, Image32 surface, int frame_index); 36 | 37 | struct Preview_Context 38 | { 39 | GLFWwindow *window; 40 | clock_t begin; 41 | }; 42 | 43 | Preview_Context *new_preview_context(Video_Params parsm); 44 | 45 | void previewencoder_encode(Preview_Context *context, Video_Params params, Image32 surface, int frame_index); 46 | 47 | #endif // VODUS_ENCODER_HPP_ 48 | -------------------------------------------------------------------------------- /src/vodus_error.cpp: -------------------------------------------------------------------------------- 1 | void avec(int code) 2 | { 3 | if (code < 0) { 4 | const size_t buf_size = 256; 5 | char buf[buf_size]; 6 | av_strerror(code, buf, buf_size); 7 | println(stderr, "ffmpeg pooped itself: ", buf); 8 | exit(1); 9 | } 10 | } 11 | 12 | template 13 | T *fail_if_null(T *ptr, Args... args) 14 | { 15 | if (ptr == nullptr) { 16 | println(stderr, args...); 17 | exit(1); 18 | } 19 | 20 | return ptr; 21 | } 22 | -------------------------------------------------------------------------------- /src/vodus_image32.cpp: -------------------------------------------------------------------------------- 1 | struct Pixel32 2 | { 3 | uint8_t r, g, b, a; 4 | }; 5 | 6 | void print1(FILE *stream, Pixel32 pixel) 7 | { 8 | #define PRINT_BYTE(x) \ 9 | do { \ 10 | const auto __x = (x); \ 11 | const auto __u = __x / 0x10; \ 12 | const auto __l = __x % 0x10; \ 13 | print(stream, \ 14 | (char) (__u + (__u <= 9 ? '0' : 'a' - 10)), \ 15 | (char) (__l + (__l <= 9 ? '0' : 'a' - 10))); \ 16 | } while (0) 17 | 18 | PRINT_BYTE(pixel.r); 19 | PRINT_BYTE(pixel.g); 20 | PRINT_BYTE(pixel.b); 21 | PRINT_BYTE(pixel.a); 22 | } 23 | 24 | struct Image32 25 | { 26 | size_t width; 27 | size_t height; 28 | Pixel32 *pixels; 29 | 30 | bool is_null() const 31 | { 32 | return pixels == nullptr; 33 | } 34 | }; 35 | 36 | struct Animat32 37 | { 38 | Image32 *frames; 39 | int *frame_delays; 40 | size_t count; 41 | }; 42 | 43 | #ifdef VODUS_SSE 44 | // NOTE: Stolen from https://stackoverflow.com/a/53707227 45 | void mix_pixels_sse(Pixel32 *src, Pixel32 *dst, Pixel32 *c) 46 | { 47 | const __m128i _swap_mask = 48 | _mm_set_epi8(7, 6, 5, 4, 49 | 3, 2, 1, 0, 50 | 15, 14, 13, 12, 51 | 11, 10, 9, 8 52 | ); 53 | 54 | const __m128i _aa = 55 | _mm_set_epi8( 15,15,15,15, 56 | 11,11,11,11, 57 | 7,7,7,7, 58 | 3,3,3,3 ); 59 | 60 | const __m128i _mask1 = _mm_set_epi16(-1,0,0,0, -1,0,0,0); 61 | const __m128i _mask2 = _mm_set_epi16(0,-1,-1,-1, 0,-1,-1,-1); 62 | const __m128i _v1 = _mm_set1_epi16( 1 ); 63 | 64 | __m128i _src = _mm_loadu_si128((__m128i*)src); 65 | __m128i _src_a = _mm_shuffle_epi8(_src, _aa); 66 | 67 | __m128i _dst = _mm_loadu_si128((__m128i*)dst); 68 | __m128i _dst_a = _mm_shuffle_epi8(_dst, _aa); 69 | __m128i _one_minus_src_a = _mm_subs_epu8( 70 | _mm_set1_epi8(-1), _src_a); 71 | 72 | __m128i _out = {}; 73 | { 74 | __m128i _s_a = _mm_cvtepu8_epi16( _src_a ); 75 | __m128i _s = _mm_cvtepu8_epi16( _src ); 76 | __m128i _d = _mm_cvtepu8_epi16( _dst ); 77 | __m128i _d_a = _mm_cvtepu8_epi16( _one_minus_src_a ); 78 | _out = _mm_adds_epu16( 79 | _mm_mullo_epi16(_s, _s_a), 80 | _mm_mullo_epi16(_d, _d_a)); 81 | _out = _mm_srli_epi16( 82 | _mm_adds_epu16( 83 | _mm_adds_epu16( _v1, _out ), 84 | _mm_srli_epi16( _out, 8 ) ), 8 ); 85 | _out = _mm_or_si128( 86 | _mm_and_si128(_out,_mask2), 87 | _mm_and_si128( 88 | _mm_adds_epu16( 89 | _s_a, 90 | _mm_cvtepu8_epi16(_dst_a)), _mask1)); 91 | } 92 | 93 | // compute _out2 using high quadword of of the _src and _dst 94 | //... 95 | __m128i _out2 = {}; 96 | { 97 | __m128i _s_a = _mm_cvtepu8_epi16(_mm_shuffle_epi8(_src_a, _swap_mask)); 98 | __m128i _s = _mm_cvtepu8_epi16(_mm_shuffle_epi8(_src, _swap_mask)); 99 | __m128i _d = _mm_cvtepu8_epi16(_mm_shuffle_epi8(_dst, _swap_mask)); 100 | __m128i _d_a = _mm_cvtepu8_epi16(_mm_shuffle_epi8(_one_minus_src_a, _swap_mask)); 101 | _out2 = _mm_adds_epu16( 102 | _mm_mullo_epi16(_s, _s_a), 103 | _mm_mullo_epi16(_d, _d_a)); 104 | _out2 = _mm_srli_epi16( 105 | _mm_adds_epu16( 106 | _mm_adds_epu16( _v1, _out2 ), 107 | _mm_srli_epi16( _out2, 8 ) ), 8 ); 108 | _out2 = _mm_or_si128( 109 | _mm_and_si128(_out2,_mask2), 110 | _mm_and_si128( 111 | _mm_adds_epu16( 112 | _s_a, 113 | _mm_cvtepu8_epi16(_dst_a)), _mask1)); 114 | } 115 | 116 | __m128i _ret = _mm_packus_epi16( _out, _out2 ); 117 | 118 | _mm_storeu_si128( (__m128i_u*) c, _ret ); 119 | } 120 | #endif // VODUS_SSE 121 | 122 | Pixel32 mix_pixels(Pixel32 dst, Pixel32 src) 123 | { 124 | uint8_t rev_src_a = 255 - src.a; 125 | Pixel32 result; 126 | result.r = ((uint16_t) src.r * (uint16_t) src.a + (uint16_t) dst.r * rev_src_a) >> 8; 127 | result.g = ((uint16_t) src.g * (uint16_t) src.a + (uint16_t) dst.g * rev_src_a) >> 8; 128 | result.b = ((uint16_t) src.b * (uint16_t) src.a + (uint16_t) dst.b * rev_src_a) >> 8; 129 | result.a = dst.a; 130 | return result; 131 | } 132 | 133 | void fill_image32_with_color(Image32 image, Pixel32 color) 134 | { 135 | size_t n = image.width * image.height; 136 | for (size_t i = 0; i < n; ++i) 137 | image.pixels[i] = color; 138 | } 139 | 140 | void slap_ftbitmap_onto_image32(Image32 dest, FT_Bitmap *src, Pixel32 color, int x, int y) 141 | { 142 | assert(src->pixel_mode == FT_PIXEL_MODE_GRAY); 143 | assert(src->num_grays == 256); 144 | 145 | for (size_t row = 0; (row < src->rows); ++row) { 146 | if (row + y < dest.height) { 147 | for (size_t col = 0; (col < src->width); ++col) { 148 | if (col + x < dest.width) { 149 | color.a = src->buffer[row * src->pitch + col]; 150 | dest.pixels[(row + y) * dest.width + col + x] = 151 | mix_pixels( 152 | dest.pixels[(row + y) * dest.width + col + x], 153 | color); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | // TODO(#24): resizing version of slap_image32_onto_image32 requires some sort of interpolation 161 | void slap_image32_onto_image32(Image32 dst, Image32 src, 162 | int x0, int y0) 163 | { 164 | #ifdef VODUS_SSE 165 | const size_t SIMD_PIXEL_PACK_SIZE = 4; 166 | #else 167 | const size_t SIMD_PIXEL_PACK_SIZE = 1; 168 | #endif // VODUS_SSE 169 | 170 | size_t x1 = std::min(x0 + src.width, dst.width); 171 | size_t y1 = std::min(y0 + src.height, dst.height); 172 | 173 | for (size_t y = y0; y < y1; ++y) { 174 | for (size_t x = x0; 175 | x + SIMD_PIXEL_PACK_SIZE < x1; 176 | x += SIMD_PIXEL_PACK_SIZE) 177 | { 178 | assert(x >= 0); 179 | assert(y >= 0); 180 | assert(x < dst.width); 181 | assert(y < dst.height); 182 | 183 | assert(x - x0 >= 0); 184 | assert(y - y0 >= 0); 185 | assert(x - x0 < src.width); 186 | assert(y - y0 < src.height); 187 | 188 | #ifdef VODUS_SSE 189 | mix_pixels_sse( 190 | &src.pixels[(y - y0) * src.width + (x - x0)], 191 | &dst.pixels[y * dst.width + x], 192 | &dst.pixels[y * dst.width + x]); 193 | #else 194 | dst.pixels[y * dst.width + x] = 195 | mix_pixels( 196 | dst.pixels[y * dst.width + x], 197 | src.pixels[(y - y0) * src.width + (x - x0)]); 198 | #endif // VODUS_SSE 199 | } 200 | } 201 | } 202 | 203 | Maybe hexstr_as_pixel32(String_View hexstr) 204 | { 205 | if (hexstr.count != 8) return {}; 206 | 207 | Pixel32 result = {}; 208 | unwrap_into(result.r, hexstr.subview(0, 2).from_hex()); 209 | unwrap_into(result.g, hexstr.subview(2, 2).from_hex()); 210 | unwrap_into(result.b, hexstr.subview(4, 2).from_hex()); 211 | unwrap_into(result.a, hexstr.subview(6, 2).from_hex()); 212 | return {true, result}; 213 | } 214 | 215 | void slap_savedimage_to_image32(Image32 vscreen, 216 | ColorMapObject *screen_color_map, 217 | SavedImage *saved_image, 218 | GraphicsControlBlock gcb) 219 | { 220 | assert(screen_color_map); 221 | assert(screen_color_map->BitsPerPixel <= 8); 222 | assert(!screen_color_map->SortFlag); 223 | 224 | SavedImage *src = saved_image; 225 | 226 | assert(src->ImageDesc.Left >= 0); 227 | assert(src->ImageDesc.Top >= 0); 228 | 229 | for (size_t y = 0; (int) y < src->ImageDesc.Height; ++y) { 230 | for (size_t x = 0; (int) x < src->ImageDesc.Width; ++x) { 231 | auto src_color_index = src->RasterBits[y * src->ImageDesc.Width + x]; 232 | GifColorType pixel = {}; 233 | if (src->ImageDesc.ColorMap) { 234 | pixel = src->ImageDesc.ColorMap->Colors[src_color_index]; 235 | } else { 236 | pixel = screen_color_map->Colors[src_color_index]; 237 | } 238 | auto dst_pixel_index = (y + src->ImageDesc.Top) * vscreen.width + (x + src->ImageDesc.Left); 239 | if (src_color_index != gcb.TransparentColor) { 240 | vscreen.pixels[dst_pixel_index].r = pixel.Red; 241 | vscreen.pixels[dst_pixel_index].g = pixel.Green; 242 | vscreen.pixels[dst_pixel_index].b = pixel.Blue; 243 | vscreen.pixels[dst_pixel_index].a = 255; 244 | } 245 | } 246 | } 247 | } 248 | 249 | void save_image32_as_png(Image32 image, const char *filepath) 250 | { 251 | int stbi_ret = stbi_write_png( 252 | filepath, 253 | image.width, 254 | image.height, 255 | 4, 256 | image.pixels, 257 | image.width * sizeof(image.pixels[0])); 258 | assert(stbi_ret); 259 | } 260 | 261 | Animat32 load_animat32_from_gif(const char *filepath, size_t size) 262 | { 263 | int error = 0; 264 | GifFileType *gif_file = DGifOpenFileName(filepath, &error); 265 | if (error) { 266 | println(stderr, "[ERROR] Could not open gif file: ", filepath); 267 | abort(); 268 | } 269 | 270 | if (DGifSlurp(gif_file) == GIF_ERROR) { 271 | println(stderr, "[ERROR] Could not read gif file: ", filepath); 272 | abort(); 273 | } 274 | 275 | GraphicsControlBlock gcb = {}; 276 | Image32 vscreen = {}; 277 | vscreen.width = gif_file->SWidth; 278 | vscreen.height = gif_file->SHeight; 279 | vscreen.pixels = new Pixel32[vscreen.width * vscreen.height]; 280 | memset(vscreen.pixels, 0, sizeof(Pixel32) * vscreen.width * vscreen.height); 281 | defer(delete[] vscreen.pixels); 282 | 283 | Image32 prev_vscreen = {}; 284 | prev_vscreen.width = gif_file->SWidth; 285 | prev_vscreen.height = gif_file->SHeight; 286 | prev_vscreen.pixels = new Pixel32[prev_vscreen.width * prev_vscreen.height]; 287 | memset(prev_vscreen.pixels, 0, sizeof(Pixel32) * prev_vscreen.width * prev_vscreen.height); 288 | defer(delete[] prev_vscreen.pixels); 289 | 290 | Animat32 result = {}; 291 | result.count = gif_file->ImageCount; 292 | result.frames = new Image32[result.count]; 293 | result.frame_delays = new int[result.count]; 294 | 295 | const float emote_ratio = (float) vscreen.width / (float) vscreen.height; 296 | for (size_t index = 0; index < result.count; ++index) { 297 | memcpy(prev_vscreen.pixels, vscreen.pixels, sizeof(Pixel32) * vscreen.width * vscreen.height); 298 | 299 | int ok = DGifSavedExtensionToGCB(gif_file, index, &gcb); 300 | 301 | if (!ok) { 302 | println(stderr, "[ERROR] Could not retrieve Graphics Control Block from `", 303 | filepath, "` on index ", index); 304 | abort(); 305 | } 306 | 307 | result.frame_delays[index] = gcb.DelayTime; 308 | 309 | slap_savedimage_to_image32(vscreen, 310 | gif_file->SColorMap, 311 | &gif_file->SavedImages[index], 312 | gcb); 313 | 314 | result.frames[index].height = (int) size; 315 | result.frames[index].width = (int) floorf(result.frames[index].height * emote_ratio); 316 | result.frames[index].pixels = new Pixel32[result.frames[index].width * result.frames[index].height]; 317 | 318 | stbir_resize_uint8((unsigned char *) vscreen.pixels, vscreen.width, vscreen.height, 0, 319 | (unsigned char *) result.frames[index].pixels, 320 | result.frames[index].width, 321 | result.frames[index].height, 322 | 0, 323 | 4); 324 | 325 | switch (gcb.DisposalMode) { 326 | case DISPOSE_DO_NOT: 327 | break; 328 | case DISPOSE_BACKGROUND: 329 | // NOTE: This is a little bit of cheating. I think we are 330 | // supposed to fill up the virtual screen with 331 | // gif_file->SBackGroundColor. But the problem is that 332 | // it's unclear how to handle transparency in that case, 333 | // because transparency is defined per frame in 334 | // gif_file->SavedImages[index]. So in some frames 335 | // gif_file->SBackGroundColor is transparent, in some 336 | // frames it is not. So we decided to ignore 337 | // gif_file->SBackGroundColor and fill up the virtual 338 | // screen with transparency always. 339 | // 340 | // I have a feeling that majority of other software do the 341 | // same thing as well. (we probably wanna check that). 342 | memset(vscreen.pixels, 0, sizeof(Pixel32) * vscreen.width * vscreen.height); 343 | break; 344 | case DISPOSE_PREVIOUS: 345 | memcpy(vscreen.pixels, prev_vscreen.pixels, sizeof(Pixel32) * vscreen.width * vscreen.height); 346 | break; 347 | default: 348 | assert(0 && "Unsupported gif frame disposal mode"); 349 | } 350 | 351 | } 352 | 353 | DGifCloseFile(gif_file, &error); 354 | if (error) { 355 | println(stderr, "[ERROR] Could not close gif file: ", filepath); 356 | abort(); 357 | } 358 | 359 | return result; 360 | } 361 | 362 | Image32 load_image32_from_png(const char *filepath, size_t size) 363 | { 364 | Image32 result = {}; 365 | int w, h, n; 366 | 367 | unsigned char *origin = stbi_load(filepath, &w, &h, &n, 4); 368 | defer(stbi_image_free(origin)); 369 | 370 | const float emote_ratio = (float) w / (float) h; 371 | result.height = (int) size; 372 | result.width = (int) floorf(result.height * emote_ratio); 373 | result.pixels = new Pixel32[result.width * result.height]; 374 | 375 | stbir_resize_uint8(origin, w, h, 0, 376 | (unsigned char *) result.pixels, 377 | result.width, result.height, 0, 378 | 4); 379 | 380 | return result; 381 | } 382 | 383 | void advance_pen_for_text(FT_Face face, String_View text, int *pen_x, int *pen_y) 384 | { 385 | for (size_t i = 0; i < text.count; ++i) { 386 | auto error = FT_Load_Glyph( 387 | face, 388 | FT_Get_Char_Index(face, text.data[i]), 389 | FT_LOAD_DEFAULT); 390 | assert(!error); 391 | 392 | *pen_x += face->glyph->advance.x >> 6; 393 | } 394 | } 395 | 396 | void slap_text_onto_image32(Image32 surface, 397 | FT_Face face, 398 | String_View text, 399 | Pixel32 color, 400 | int *pen_x, int *pen_y) 401 | { 402 | String_View iter = text; 403 | 404 | while (iter.count > 0) { 405 | size_t size = 0; 406 | auto code = utf8_get_code(iter, &size); 407 | if (!code.has_value) { 408 | println(stderr, "`", text, "` is not a correct UTF-8 sequence"); 409 | abort(); 410 | } 411 | FT_UInt glyph_index = FT_Get_Char_Index(face, code.unwrap); 412 | iter.chop(size); 413 | 414 | auto error = FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT); 415 | assert(!error); 416 | 417 | error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL); 418 | assert(!error); 419 | 420 | slap_ftbitmap_onto_image32(surface, &face->glyph->bitmap, 421 | color, 422 | *pen_x + face->glyph->bitmap_left, 423 | *pen_y - face->glyph->bitmap_top); 424 | 425 | *pen_x += face->glyph->advance.x >> 6; 426 | } 427 | } 428 | 429 | /// returns true if wrapped around 430 | bool slap_text_onto_image32_wrapped(Image32 surface, 431 | FT_Face face, 432 | String_View text, 433 | Pixel32 color, 434 | int *pen_x, int *pen_y, 435 | size_t font_size) 436 | { 437 | bool wrapped = false; 438 | 439 | int copy_x = *pen_x; 440 | int copy_y = *pen_y; 441 | 442 | advance_pen_for_text(face, text, ©_x, ©_y); 443 | if (copy_x >= (int)surface.width) { 444 | *pen_x = 0; 445 | *pen_y += font_size; 446 | wrapped = true; 447 | } 448 | 449 | slap_text_onto_image32(surface, face, text, color, pen_x, pen_y); 450 | return wrapped; 451 | } 452 | 453 | /// returns true if wrapped around 454 | bool slap_text_onto_image32_wrapped(Image32 surface, 455 | FT_Face face, 456 | const char *cstr, 457 | Pixel32 color, 458 | int *pen_x, int *pen_y, 459 | size_t font_size) 460 | { 461 | return slap_text_onto_image32_wrapped(surface, 462 | face, 463 | cstr_as_string_view(cstr), 464 | color, 465 | pen_x, pen_y, 466 | font_size); 467 | } 468 | 469 | void slap_text_onto_image32(Image32 surface, 470 | FT_Face face, 471 | const char *text, 472 | Pixel32 color, 473 | int *pen_x, int *pen_y) 474 | { 475 | slap_text_onto_image32(surface, 476 | face, 477 | cstr_as_string_view(text), 478 | color, 479 | pen_x, pen_y); 480 | } 481 | -------------------------------------------------------------------------------- /src/vodus_main.cpp: -------------------------------------------------------------------------------- 1 | 2 | void sample_chat_log_animation(Message *messages, 3 | size_t messages_size, 4 | FT_Face face, 5 | Encoder encoder, 6 | Emote_Cache *emote_cache, 7 | Video_Params params) 8 | { 9 | auto message_buffer = new Message_Buffer<1024>(); 10 | defer(delete message_buffer); 11 | 12 | Image32 surface = { 13 | .width = params.width, 14 | .height = params.height, 15 | .pixels = new Pixel32[params.width * params.height] 16 | }; 17 | defer(delete[] surface.pixels); 18 | 19 | size_t message_end = 0; 20 | float message_cooldown = 0.0f; 21 | size_t frame_index = 0; 22 | float t = 0.0f; 23 | 24 | const float VODUS_DELTA_TIME_SEC = 1.0f / params.fps; 25 | const size_t TRAILING_BUFFER_SEC = 2; 26 | assert(messages_size > 0); 27 | const float total_t = (float) messages[messages_size - 1].timestamp / 1000.0f + TRAILING_BUFFER_SEC; 28 | for (; message_end < messages_size; ++frame_index) { 29 | if (message_cooldown <= 0.0f) { 30 | message_buffer->push(messages[message_end], face, emote_cache, params); 31 | 32 | message_end += 1; 33 | auto t1 = (float) messages[message_end - 1].timestamp / 1000.0f; 34 | auto t2 = (float) messages[message_end].timestamp / 1000.0f; 35 | message_cooldown = t2 - t1; 36 | } 37 | 38 | message_cooldown -= VODUS_DELTA_TIME_SEC; 39 | 40 | fill_image32_with_color(surface, params.background_color); 41 | message_buffer->render(surface, face, emote_cache, params); 42 | encoder.encode(params, surface, frame_index); 43 | 44 | t += VODUS_DELTA_TIME_SEC; 45 | emote_cache->update_gifs(VODUS_DELTA_TIME_SEC); 46 | message_buffer->update(VODUS_DELTA_TIME_SEC, face, emote_cache, params); 47 | 48 | print(stdout, "\rRendered ", (int) roundf(t), "/", (int) roundf(total_t), " seconds"); 49 | } 50 | 51 | for (size_t i = 0; i < TRAILING_BUFFER_SEC * params.fps; ++i, ++frame_index) { 52 | fill_image32_with_color(surface, params.background_color); 53 | message_buffer->render(surface, face, emote_cache, params); 54 | 55 | emote_cache->update_gifs(VODUS_DELTA_TIME_SEC); 56 | message_buffer->update(VODUS_DELTA_TIME_SEC, face, emote_cache, params); 57 | encoder.encode(params, surface, frame_index); 58 | 59 | t += VODUS_DELTA_TIME_SEC; 60 | print(stdout, "\rRendered ", (int) roundf(t), "/", (int) roundf(total_t), " seconds"); 61 | } 62 | 63 | print(stdout, "\rRendered ", (int) roundf(total_t), "/", (int) roundf(total_t), " seconds"); 64 | print(stdout, "\n"); 65 | } 66 | 67 | int main(int argc, char *argv[]) 68 | { 69 | Args args = {argc, argv}; 70 | args.shift(); // skip program name; 71 | 72 | if (args.empty()) { 73 | println(stderr, "[ERROR] Input filename is not provided"); 74 | usage(stderr); 75 | abort(); 76 | } 77 | const char *input_filepath = args.shift(); 78 | 79 | if (args.empty()) { 80 | println(stderr, "[ERROR] Output filename is not provided"); 81 | usage(stderr); 82 | abort(); 83 | } 84 | const char *output_filepath = args.shift(); 85 | 86 | Video_Params params = default_video_params(); 87 | patch_video_params_from_args(¶ms, &args); 88 | 89 | println(stdout, "params = ", params); 90 | 91 | if (params.width % 2 != 0 || params.height % 2 != 0) { 92 | println(stderr, "Error: resolution must be multiple of two!"); 93 | exit(1); 94 | } 95 | 96 | FT_Library library; 97 | auto error = FT_Init_FreeType(&library); 98 | if (error) { 99 | println(stderr, "Could not initialize FreeType2"); 100 | exit(1); 101 | } 102 | 103 | const char *face_filepath = string_view_as_cstr(params.font); 104 | defer(free((void*) face_filepath)); 105 | 106 | FT_Face face; 107 | error = FT_New_Face(library, 108 | face_filepath, 109 | 0, 110 | &face); 111 | if (error == FT_Err_Unknown_File_Format) { 112 | println(stderr, "`", face_filepath, "` appears to have an unsuppoted format"); 113 | exit(1); 114 | } else if (error) { 115 | println(stderr, "`", face_filepath, "` could not be opened\n"); 116 | exit(1); 117 | } 118 | 119 | println(stdout, "Loaded ", face_filepath); 120 | println(stdout, "\tnum_glyphs = ", face->num_glyphs); 121 | 122 | error = FT_Set_Pixel_Sizes(face, 0, params.font_size); 123 | if (error) { 124 | println(stderr, "Could not set font size in pixels"); 125 | exit(1); 126 | } 127 | 128 | Emote_Cache emote_cache = { }; 129 | emote_cache.populate_from_file("./mapping.csv", params.font_size); 130 | 131 | // TODO(#35): log is not retrived directly from the Twitch API 132 | // See https://github.com/PetterKraabol/Twitch-Chat-Downloader 133 | if (input_filepath == nullptr) { 134 | println(stderr, "Input log file is not provided"); 135 | usage(stderr); 136 | exit(1); 137 | } 138 | auto input = read_file_as_string_view(input_filepath); 139 | if (!input.has_value) { 140 | println(stderr, "Could not read file `", input_filepath, "`"); 141 | abort(); 142 | } 143 | 144 | Message *messages = nullptr; 145 | size_t messages_size = parse_messages_from_string_view(input.unwrap, &messages, params, input_filepath); 146 | defer(delete[] messages); 147 | 148 | Encoder encoder = {}; 149 | switch (params.output_type) { 150 | case Output_Type::Video: { 151 | encoder.context = new_avencoder_context(params, output_filepath); 152 | encoder.encode_func = (Encode_Func) &avencoder_encode; 153 | } break; 154 | 155 | case Output_Type::PNG: { 156 | encoder.context = new PNGEncoder_Context {cstr_as_string_view(output_filepath)}; 157 | encoder.encode_func = (Encode_Func) &pngencoder_encode; 158 | } break; 159 | 160 | case Output_Type::Preview: { 161 | encoder.context = new_preview_context(params); 162 | encoder.encode_func = (Encode_Func) &previewencoder_encode; 163 | } break; 164 | } 165 | 166 | { 167 | clock_t begin = clock(); 168 | sample_chat_log_animation(messages, messages_size, face, encoder, &emote_cache, params); 169 | println(stdout, "Rendering took ", (float) (clock() - begin) / (float) CLOCKS_PER_SEC, " seconds"); 170 | } 171 | 172 | switch (params.output_type) { 173 | case Output_Type::Video: { 174 | AVEncoder_Context *context = (AVEncoder_Context *) encoder.context; 175 | 176 | encode_avframe(context->context, NULL, context->packet, context->output_stream); 177 | 178 | uint8_t endcode[] = { 0, 0, 1, 0xb7 }; 179 | if (context->codec->id == AV_CODEC_ID_MPEG1VIDEO || context->codec->id == AV_CODEC_ID_MPEG2VIDEO) { 180 | fwrite(endcode, 1, sizeof(endcode), context->output_stream); 181 | } 182 | 183 | free_avencoder_context(context); 184 | } break; 185 | 186 | case Output_Type::PNG: { 187 | delete ((PNGEncoder_Context*) encoder.context); 188 | } break; 189 | 190 | case Output_Type::Preview: { 191 | glfwTerminate(); 192 | delete ((Preview_Context*)encoder.context); 193 | } break; 194 | } 195 | 196 | return 0; 197 | } 198 | -------------------------------------------------------------------------------- /src/vodus_message.cpp: -------------------------------------------------------------------------------- 1 | using Timestamp = uint64_t; 2 | 3 | struct Message 4 | { 5 | Timestamp timestamp; 6 | String_View nickname; 7 | String_View message; 8 | 9 | /// returns amount of rows the message took 10 | int height(FT_Face face, 11 | Emote_Cache *emote_cache, 12 | Video_Params params) 13 | { 14 | Image32 dummy = {}; 15 | dummy.width = params.width; 16 | return render(dummy, face, emote_cache, params, 0); 17 | } 18 | 19 | /// returns amount of rows the message took 20 | int render(Image32 surface, FT_Face face, 21 | Emote_Cache *emote_cache, 22 | Video_Params params, 23 | int row) 24 | { 25 | int result = 1; 26 | int pen_x = 0; 27 | int pen_y = (row + 1) * params.font_size; 28 | 29 | slap_text_onto_image32(surface, 30 | face, 31 | nickname, 32 | params.nickname_color, 33 | &pen_x, &pen_y); 34 | 35 | 36 | auto text = message.trim(); 37 | auto text_color = params.text_color; 38 | const char *nick_text_sep = ": "; 39 | 40 | const auto slash_me = "/me"_sv; 41 | 42 | if (text.has_prefix(slash_me)) { 43 | text.chop(slash_me.count); 44 | text_color = params.nickname_color; 45 | nick_text_sep = " "; 46 | } 47 | 48 | slap_text_onto_image32(surface, 49 | face, 50 | nick_text_sep, 51 | params.nickname_color, 52 | &pen_x, &pen_y); 53 | 54 | while (text.count > 0) { 55 | auto word = text.chop_word().trim(); 56 | auto maybe_emote = emote_cache->emote_by_name(word, params.font_size); 57 | 58 | // TODO(#23): Twitch emotes are not rendered 59 | if (maybe_emote.has_value) { 60 | auto emote = maybe_emote.unwrap; 61 | 62 | if (pen_x + emote.width() >= (int)surface.width) { 63 | pen_x = 0; 64 | pen_y += params.font_size; 65 | // WRAPPED! 66 | result += 1; 67 | } 68 | 69 | emote.slap_onto_image32(surface, pen_x, pen_y - emote.height()); 70 | pen_x += emote.width(); 71 | } else { 72 | if (slap_text_onto_image32_wrapped(surface, 73 | face, 74 | word, 75 | text_color, 76 | &pen_x, &pen_y, 77 | params.font_size)) { 78 | // WRAPPED! 79 | result += 1; 80 | } 81 | } 82 | 83 | // TODO(#112): no option to not separate consecutive emotes for better looking combos 84 | if (text.count > 0 && 85 | slap_text_onto_image32_wrapped(surface, 86 | face, 87 | " ", 88 | text_color, 89 | &pen_x, &pen_y, 90 | params.font_size)) { 91 | // WRAPPED! 92 | result += 1; 93 | } 94 | } 95 | 96 | return result; 97 | } 98 | }; 99 | 100 | void print1(FILE *stream, Message message) 101 | { 102 | print( 103 | stream, 104 | "Message { ", 105 | ".timestamp = ", message.timestamp, ", ", 106 | ".nickname = ", message.nickname, ", ", 107 | ".message = ", message.message, 108 | "}"); 109 | } 110 | 111 | template 112 | struct Message_Buffer 113 | { 114 | Queue message_queue; 115 | int height; 116 | int begin; 117 | 118 | // TODO(#74): message entering/leaving animation should be disablable 119 | // TODO(#75): entering/leaving animation should also animate alpha 120 | // TODO(#105): message animation is broken 121 | 122 | void normalize_queue(FT_Face face, 123 | Emote_Cache *emote_cache, 124 | Video_Params params) 125 | { 126 | while (message_queue.count > 0) { 127 | auto message_height = message_queue.first().height(face, emote_cache, params); 128 | if (message_height >= abs(begin)) return; 129 | message_queue.dequeue(); 130 | begin += message_height; 131 | height -= message_height; 132 | } 133 | } 134 | 135 | void push(Message message, 136 | FT_Face face, 137 | Emote_Cache *emote_cache, 138 | Video_Params params) 139 | { 140 | const auto message_height = message.height(face, emote_cache, params); 141 | const int rows_count = params.height / params.font_size; 142 | 143 | if (begin + height + message_height > rows_count) { 144 | begin -= message_height; 145 | } 146 | 147 | message_queue.enqueue(message); 148 | height += message_height; 149 | } 150 | 151 | void update(float dt, FT_Face face, Emote_Cache *emote_cache, Video_Params params) 152 | { 153 | normalize_queue(face, emote_cache, params); 154 | } 155 | 156 | void render(Image32 surface, 157 | FT_Face face, 158 | Emote_Cache *emote_cache, 159 | Video_Params params) 160 | { 161 | int row = begin; 162 | for (size_t i = 0; i < message_queue.count; ++i) { 163 | row += message_queue[i].render(surface, 164 | face, 165 | emote_cache, 166 | params, 167 | row); 168 | } 169 | } 170 | }; 171 | 172 | Timestamp chop_timestamp(const char *input_filepath, 173 | size_t line_number, 174 | String_View *input, 175 | String_View origin_input) 176 | { 177 | auto syntax_panic = [&] (auto ...args) { 178 | panic(input_filepath, ":", line_number, ":", input->data - origin_input.data + 1, ": ", 179 | args...); 180 | }; 181 | 182 | auto unwrap_or_syntax_panic = [&] (auto maybe, auto ...args) { 183 | if (!maybe.has_value) { 184 | syntax_panic(args...); 185 | } 186 | 187 | return maybe.unwrap; 188 | }; 189 | 190 | auto is_digit = [](char x) -> bool { 191 | return isdigit(x); 192 | }; 193 | 194 | auto expect_char = [&](char x) { 195 | if (input->count == 0 || *input->data != x) { 196 | syntax_panic("Expected `", x, "`"); 197 | } 198 | input->chop(1); 199 | }; 200 | 201 | auto expect_number = [&]() -> uint64_t { 202 | return unwrap_or_syntax_panic( 203 | input->chop_while(is_digit).as_integer(), 204 | "Expected number"); 205 | }; 206 | 207 | *input = input->trim_begin(); 208 | 209 | expect_char('['); 210 | auto hours = expect_number(); 211 | expect_char(':'); 212 | auto minutes = expect_number(); 213 | expect_char(':'); 214 | auto seconds = expect_number(); 215 | 216 | Timestamp mseconds = 0; 217 | if (input->count > 0 && *input->data == '.') { 218 | input->chop(1); 219 | auto mseconds_str = input->chop_while(is_digit); 220 | for (size_t i = 0; i < 3; ++i) { 221 | if (i < mseconds_str.count) { 222 | mseconds = mseconds * 10 + mseconds_str.data[i] - '0'; 223 | } else { 224 | mseconds *= 10; 225 | } 226 | } 227 | } 228 | 229 | expect_char(']'); 230 | 231 | return (hours * 60 * 60 + minutes * 60 + seconds) * 1000 + mseconds; 232 | } 233 | 234 | String_View chop_nickname(const char *input_filepath, 235 | size_t line_number, 236 | String_View *input, 237 | String_View origin_input) 238 | { 239 | auto syntax_panic = [&] (auto ...args) { 240 | panic(input_filepath, ":", line_number, ":", input->data - origin_input.data + 1, ": ", 241 | args...); 242 | }; 243 | 244 | auto expect_char = [&](char x) { 245 | if (input->count == 0 || *input->data != x) { 246 | syntax_panic("Expected `", x, "`"); 247 | } 248 | input->chop(1); 249 | }; 250 | 251 | *input = input->trim_begin(); 252 | expect_char('<'); 253 | auto nickname = input->chop_while([](char x) -> bool { 254 | return x != '>'; 255 | }); 256 | expect_char('>'); 257 | 258 | return nickname; 259 | } 260 | 261 | String_View chop_message(String_View *input) 262 | { 263 | return input->trim(); 264 | } 265 | 266 | size_t parse_messages_from_string_view(String_View input, 267 | Message **messages, 268 | Video_Params params, 269 | const char *input_filepath) 270 | { 271 | size_t expected_messages_size = input.count_chars('\n') + 1; 272 | if (params.messages_limit.has_value) { 273 | expected_messages_size = min(expected_messages_size, params.messages_limit.unwrap); 274 | } 275 | 276 | *messages = new Message[expected_messages_size]; 277 | 278 | size_t messages_size = 0; 279 | for (size_t line_number = 1; 280 | input.count > 0 && messages_size < expected_messages_size; 281 | ++line_number) 282 | { 283 | String_View origin_message = input.chop_by_delim('\n'); 284 | String_View message = origin_message; 285 | 286 | (*messages)[messages_size].timestamp = chop_timestamp(input_filepath, line_number, &message, origin_message); 287 | (*messages)[messages_size].nickname = chop_nickname(input_filepath, line_number, &message, origin_message); 288 | (*messages)[messages_size].message = chop_message(&message); 289 | messages_size++; 290 | } 291 | 292 | std::sort(*messages, *messages + messages_size, 293 | [](const Message &m1, const Message &m2) { 294 | return m1.timestamp < m2.timestamp; 295 | }); 296 | 297 | return messages_size; 298 | } 299 | -------------------------------------------------------------------------------- /src/vodus_queue.cpp: -------------------------------------------------------------------------------- 1 | template 2 | struct Queue 3 | { 4 | T items[Capacity]; 5 | size_t begin; 6 | size_t count; 7 | 8 | T &operator[](size_t index) 9 | { 10 | return items[(begin + index) % Capacity]; 11 | } 12 | 13 | const T &operator[](size_t index) const 14 | { 15 | return items[(begin + index) % Capacity]; 16 | } 17 | 18 | T &first() 19 | { 20 | assert(count > 0); 21 | return items[begin]; 22 | } 23 | 24 | void enqueue(T x) 25 | { 26 | assert(count < Capacity); 27 | items[(begin + count) % Capacity] = x; 28 | count++; 29 | } 30 | 31 | T dequeue() 32 | { 33 | assert(count > 0); 34 | T result = items[begin]; 35 | begin = (begin + 1) % Capacity; 36 | count--; 37 | return result; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/vodus_video_params.cpp: -------------------------------------------------------------------------------- 1 | #include "./vodus_video_params.hpp" 2 | 3 | const char *string_view_as_cstr(String_View sv) 4 | { 5 | char *cstr = (char *) malloc(sv.count + 1); 6 | if (!cstr) return cstr; 7 | memcpy(cstr, sv.data, sv.count); 8 | cstr[sv.count] = '\0'; 9 | return cstr; 10 | } 11 | 12 | void print1(FILE *stream, Output_Type output_type) 13 | { 14 | switch (output_type) { 15 | case Output_Type::Video: 16 | print1(stream, "video"); 17 | break; 18 | case Output_Type::PNG: 19 | print1(stream, "png"); 20 | break; 21 | case Output_Type::Preview: 22 | print1(stream, "preview"); 23 | break; 24 | } 25 | } 26 | 27 | void print1(FILE *stream, Video_Params params) 28 | { 29 | println(stream, "{"); 30 | println(stream, " .output_type = ", params.output_type); 31 | println(stream, " .fps = ", params.fps); 32 | println(stream, " .width = ", params.width); 33 | println(stream, " .height = ", params.height); 34 | println(stream, " .font_size = ", params.font_size); 35 | println(stream, " .background_color = ", params.background_color); 36 | println(stream, " .nickname_color = ", params.nickname_color); 37 | println(stream, " .text_color = ", params.text_color); 38 | println(stream, " .bitrate = ", params.bitrate); 39 | println(stream, " .font = ", params.font); 40 | println(stream, " .message_limit = ", params.messages_limit); 41 | print(stream, "}"); 42 | } 43 | 44 | Video_Params default_video_params() { 45 | Video_Params params = {}; 46 | params.output_type = Output_Type::Video; 47 | params.fps = 60; 48 | params.width = 1920; 49 | params.height = 1080; 50 | params.font_size = 128; 51 | params.background_color = {32, 32, 32, 255}; 52 | params.nickname_color = {255, 100, 100, 255}; 53 | params.text_color = {200, 200, 200, 255}; 54 | params.bitrate = 400'000; 55 | params.font = ""_sv; 56 | params.messages_limit = {}; 57 | return params; 58 | } 59 | 60 | void usage(FILE *stream) 61 | { 62 | println(stream, "Usage: vodus [OPTIONS] "); 63 | println(stream, " --help|-h Display this help and exit"); 64 | println(stream, " --output|-o Output path"); 65 | println(stream, " --font Path to the Font face file"); 66 | println(stream, " --font-size The size of the Font face"); 67 | println(stream, " --limit Limit the amount of messages to render"); 68 | println(stream, " --width Width of the output video"); 69 | println(stream, " --height Height of the output video"); 70 | println(stream, " --fps Frames per second of the output video"); 71 | println(stream, " --background-color Color of the background"); 72 | println(stream, " --nickname-color Color of the nickname part of the rendered message"); 73 | println(stream, " --text-color Color of the text part of the rendered message"); 74 | println(stream, " --bitrate The average bitrate in bits/s (probably, libavcodec\n" 75 | " didn't really tell me the exact units)"); 76 | println(stream, " Values:"); 77 | println(stream, " Path to a file according to the platform format"); 78 | println(stream, " Positive integer"); 79 | println(stream, " 8 hex characters digits describing RGBA.\n" 80 | " Case insensitive. No leading # character.\n" 81 | " Examples: ffffffff, 000000FF, A0eA0000"); 82 | } 83 | 84 | template 85 | Integer parse_integer_flag(String_View flag, String_View value) 86 | { 87 | auto integer = value.as_integer(); 88 | if (!integer.has_value) { 89 | println(stderr, "`", flag, "` expected a number, but `", value, "` is not a number! D:"); 90 | abort(); 91 | } 92 | return integer.unwrap; 93 | } 94 | 95 | Pixel32 parse_color_flag(String_View flag, String_View value) 96 | { 97 | auto color = hexstr_as_pixel32(value); 98 | if (!color.has_value) { 99 | println(stderr, "`", flag, "` expected a color, but `", value, "` is not a color! D:"); 100 | abort(); 101 | } 102 | return color.unwrap; 103 | } 104 | 105 | void patch_video_params_from_flag(Video_Params *params, String_View flag, String_View value); 106 | 107 | void patch_video_params_from_file(Video_Params *params, String_View filepath) 108 | { 109 | auto filepath_cstr = string_view_as_cstr(filepath); 110 | defer(free((void*) filepath_cstr)); 111 | 112 | auto content = read_file_as_string_view(filepath_cstr); 113 | if (!content.has_value) { 114 | println(stderr, "Could not read `", filepath, "`"); 115 | abort(); 116 | } 117 | 118 | while (content.unwrap.count > 0) { 119 | auto line = content.unwrap.chop_by_delim('\n').trim(); 120 | auto flag = line.chop_by_delim('=').trim(); 121 | auto value = line.trim(); 122 | 123 | patch_video_params_from_flag(params, flag, value); 124 | } 125 | } 126 | 127 | 128 | size_t levenshtein(String_View a, String_View b) 129 | { 130 | size_t *dp = new size_t[(a.count + 1) * (b.count + 1)]; 131 | #define DP(row, column) dp[(row) * (b.count + 1) + (column)] 132 | defer(delete[] dp); 133 | memset(dp, 0, sizeof(size_t) * (a.count + 1) * (b.count + 1)); 134 | 135 | for (size_t i = 0; i <= a.count; ++i) { 136 | for (size_t j = 0; j <= b.count; ++j) { 137 | if (min(i, j) == 0) { 138 | DP(i, j) = max(i, j); 139 | } else { 140 | DP(i, j) = min( 141 | DP(i - 1, j) + 1, 142 | DP(i, j - 1) + 1, 143 | DP(i - 1, j - 1) + (a.data[i - 1] != b.data[j - 1])); 144 | } 145 | } 146 | } 147 | 148 | return DP(a.count, b.count); 149 | #undef DP 150 | } 151 | 152 | void patch_video_params_from_flag(Video_Params *params, String_View flag, String_View value) 153 | { 154 | if (flag == "fps"_sv) { 155 | params->fps = parse_integer_flag(flag, value); 156 | } else if (flag == "width"_sv) { 157 | params->width = parse_integer_flag(flag, value); 158 | } else if (flag == "height"_sv) { 159 | params->height = parse_integer_flag(flag, value); 160 | } else if (flag == "font_size"_sv || flag == "font-size"_sv) { 161 | params->font_size = parse_integer_flag(flag, value); 162 | } else if (flag == "background_color"_sv || flag == "background-color"_sv) { 163 | params->background_color = parse_color_flag(flag, value); 164 | } else if (flag == "nickname_color"_sv || flag == "nickname-color"_sv) { 165 | params->nickname_color = parse_color_flag(flag, value); 166 | } else if (flag == "text_color"_sv || flag == "text-color"_sv) { 167 | params->text_color = parse_color_flag(flag, value); 168 | } else if (flag == "bitrate"_sv) { 169 | params->bitrate = parse_integer_flag(flag, value); 170 | } else if (flag == "font"_sv) { 171 | params->font = value; 172 | } else if (flag == "messages_limit"_sv || flag == "messages-limit"_sv) { 173 | params->messages_limit = {true, parse_integer_flag(flag, value)}; 174 | } else if (flag == "output_type"_sv || flag == "output-type"_sv) { 175 | if (value == "video"_sv) { 176 | params->output_type = Output_Type::Video; 177 | } else if (value == "png"_sv) { 178 | params->output_type = Output_Type::PNG; 179 | } else if (value == "preview"_sv) { 180 | params->output_type = Output_Type::Preview; 181 | } else { 182 | println(stderr, "Unknown output type `", value, "`"); 183 | abort(); 184 | } 185 | } else { 186 | size_t n = ULONG_MAX; 187 | String_View corrected_flag = {}; 188 | for (size_t i = 0; i < param_names_count; ++i) { 189 | auto m = levenshtein(flag, param_names[i]); 190 | if (m < n) { 191 | n = m; 192 | corrected_flag = param_names[i]; 193 | } 194 | } 195 | 196 | const size_t LEVENSHTEIN_CORRECTION_THRESHOLD = 5; 197 | if (n <= LEVENSHTEIN_CORRECTION_THRESHOLD) { 198 | println(stderr, "Unknown flag `", flag, "`. Maybe you meant `", corrected_flag, "`"); 199 | } else { 200 | println(stderr, "Unknown flag `", flag, "`."); 201 | } 202 | abort(); 203 | } 204 | } 205 | 206 | void patch_video_params_from_args(Video_Params *params, Args *args) 207 | { 208 | while (!args->empty()) { 209 | auto flag = cstr_as_string_view(args->shift()); 210 | flag.chop(2); 211 | 212 | if (args->empty()) { 213 | println(stderr, "[ERROR] no parameter for flag `", flag, "` found"); 214 | usage(stderr); 215 | abort(); 216 | } 217 | auto value = cstr_as_string_view(args->shift()); 218 | 219 | if (flag == "config"_sv) { 220 | patch_video_params_from_file(params, value); 221 | } else { 222 | patch_video_params_from_flag(params, flag, value); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/vodus_video_params.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VODUS_VIDEO_PARAMS_HPP_ 2 | #define VODUS_VIDEO_PARAMS_HPP_ 3 | 4 | enum class Output_Type 5 | { 6 | Video, 7 | PNG, 8 | Preview 9 | }; 10 | 11 | struct Video_Params 12 | { 13 | Output_Type output_type; 14 | size_t fps; 15 | size_t width; 16 | size_t height; 17 | size_t font_size; 18 | Pixel32 background_color; 19 | Pixel32 nickname_color; 20 | Pixel32 text_color; 21 | int bitrate; 22 | String_View font; 23 | Maybe messages_limit; 24 | }; 25 | 26 | String_View param_names[] = { 27 | "output_type"_sv, 28 | "fps"_sv, 29 | "width"_sv, 30 | "height"_sv, 31 | "font_size"_sv, 32 | "background_color"_sv, 33 | "nickname_color"_sv, 34 | "text_color"_sv, 35 | "bitrate"_sv, 36 | "font"_sv, 37 | "messages_limit"_sv, 38 | "message_regex"_sv, 39 | }; 40 | const size_t param_names_count = sizeof(param_names) / sizeof(param_names[0]); 41 | 42 | void print1(FILE *stream, Video_Params params); 43 | 44 | Video_Params default_video_params(); 45 | 46 | #endif // VODUS_VIDEO_PARAMS_HPP_ 47 | -------------------------------------------------------------------------------- /test/pajaWalk/.gitignore: -------------------------------------------------------------------------------- 1 | actual-frames/ -------------------------------------------------------------------------------- /test/pajaWalk/emote-107.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/emote-107.gif -------------------------------------------------------------------------------- /test/pajaWalk/emote-96.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/emote-96.gif -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/0-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/0-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/1-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/1-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/10-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/10-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/100-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/100-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/101-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/101-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/102-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/102-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/103-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/103-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/104-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/104-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/105-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/105-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/106-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/106-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/107-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/107-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/108-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/108-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/109-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/109-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/11-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/11-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/110-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/110-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/111-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/111-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/112-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/112-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/113-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/113-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/114-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/114-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/115-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/115-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/116-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/116-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/117-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/117-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/118-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/118-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/119-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/119-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/12-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/12-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/120-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/120-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/121-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/121-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/122-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/122-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/123-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/123-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/124-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/124-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/125-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/125-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/126-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/126-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/13-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/13-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/14-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/14-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/15-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/15-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/16-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/16-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/17-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/17-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/18-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/18-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/19-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/19-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/2-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/2-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/20-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/20-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/21-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/21-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/22-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/22-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/23-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/23-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/24-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/24-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/25-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/25-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/26-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/26-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/27-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/27-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/28-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/28-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/29-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/29-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/3-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/3-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/30-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/30-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/31-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/31-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/32-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/32-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/33-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/33-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/34-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/34-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/35-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/35-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/36-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/36-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/37-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/37-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/38-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/38-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/39-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/39-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/4-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/4-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/40-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/40-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/41-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/41-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/42-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/42-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/43-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/43-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/44-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/44-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/45-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/45-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/46-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/46-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/47-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/47-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/48-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/48-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/49-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/49-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/5-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/5-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/50-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/50-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/51-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/51-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/52-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/52-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/53-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/53-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/54-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/54-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/55-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/55-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/56-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/56-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/57-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/57-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/58-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/58-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/59-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/59-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/6-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/6-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/60-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/60-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/61-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/61-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/62-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/62-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/63-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/63-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/64-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/64-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/65-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/65-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/66-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/66-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/67-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/67-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/68-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/68-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/69-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/69-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/7-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/7-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/70-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/70-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/71-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/71-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/72-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/72-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/73-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/73-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/74-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/74-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/75-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/75-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/76-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/76-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/77-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/77-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/78-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/78-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/79-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/79-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/8-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/8-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/80-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/80-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/81-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/81-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/82-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/82-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/83-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/83-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/84-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/84-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/85-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/85-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/86-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/86-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/87-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/87-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/88-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/88-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/89-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/89-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/9-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/9-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/90-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/90-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/91-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/91-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/92-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/92-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/93-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/93-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/94-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/94-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/95-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/95-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/96-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/96-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/97-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/97-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/98-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/98-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/expected-frames/99-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/expected-frames/99-frame.png -------------------------------------------------------------------------------- /test/pajaWalk/mapping.csv: -------------------------------------------------------------------------------- 1 | pajaWalk1,pajaWalk1.gif 2 | pajaWalk2,pajaWalk2.gif 3 | pajaWalk3,pajaWalk3.gif 4 | WAYTOODANK,emote-96.gif 5 | monkaTOS,emote-107.gif 6 | -------------------------------------------------------------------------------- /test/pajaWalk/pajaWalk.txt: -------------------------------------------------------------------------------- 1 | [0:00:00] pajaWalk1 2 | [0:00:01] WAYTOODANK monkaTOS 3 | [0:00:01.5] WAYTOODANK monkaTOS 4 | [0:00:02] WAYTOODANK monkaTOS 5 | [0:00:02.500] WAYTOODANK monkaTOS 6 | [0:00:03] pajaWalk1 pajaWalk2 pajaWalk3 7 | [0:00:04] pajaWalk1 pajaWalk2 pajaWalk3 8 | [0:00:05] pajaWalk1 pajaWalk2 pajaWalk3 9 | [0:00:06] pajaWalk1 pajaWalk2 pajaWalk3 10 | -------------------------------------------------------------------------------- /test/pajaWalk/pajaWalk1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/pajaWalk1.gif -------------------------------------------------------------------------------- /test/pajaWalk/pajaWalk2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/pajaWalk2.gif -------------------------------------------------------------------------------- /test/pajaWalk/pajaWalk3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/pajaWalk/pajaWalk3.gif -------------------------------------------------------------------------------- /test/pajaWalk/renew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VODUS=../../vodus.release 4 | 5 | set -xe 6 | 7 | rm -rf ./expected-frames/ 8 | mkdir -p ./expected-frames/ 9 | $VODUS ./pajaWalk.txt expected-frames/ --config ./test.conf 10 | -------------------------------------------------------------------------------- /test/pajaWalk/test.conf: -------------------------------------------------------------------------------- 1 | output-type = png 2 | font = ../../assets/ComicNeue_Bold.otf 3 | font_size = 10 4 | fps = 15 5 | width = 200 6 | height = 100 7 | messages_limit = 20 8 | bitrate = 6000000 9 | -------------------------------------------------------------------------------- /test/pajaWalk/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | VODUS=../../vodus.release 4 | DIFFIMG=../../diffimg 5 | 6 | set -xe 7 | 8 | rm -rf ./actual-frames/ 9 | mkdir -p ./actual-frames/ 10 | $VODUS ./pajaWalk.txt actual-frames/ --config ./test.conf 11 | 12 | EXPECTED_COUNT=`ls ./expected-frames/ | wc -l` 13 | ACTUAL_COUNT=`ls ./actual-frames/ | wc -l` 14 | 15 | if [ "$EXPECTED_COUNT" != "$ACTUAL_COUNT" ]; then 16 | echo "UNEXPECTED AMOUNT OF FRAMES!" 17 | echo "Expected: $EXPECTED_COUNT" 18 | echo "Actual: $ACTUAL_COUNT" 19 | exit 1 20 | fi 21 | 22 | for frame in `ls ./expected-frames/`; do 23 | $DIFFIMG -e "./expected-frames/$frame" -a "./actual-frames/$frame" -t 5 24 | done 25 | -------------------------------------------------------------------------------- /test/utf8/.gitignore: -------------------------------------------------------------------------------- 1 | actual-frames/ -------------------------------------------------------------------------------- /test/utf8/expected-frames/0-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/0-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/1-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/1-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/10-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/10-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/11-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/11-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/12-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/12-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/13-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/13-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/14-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/14-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/15-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/15-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/16-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/16-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/17-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/17-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/18-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/18-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/19-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/19-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/2-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/2-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/20-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/20-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/21-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/21-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/22-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/22-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/23-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/23-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/24-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/24-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/25-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/25-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/26-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/26-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/27-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/27-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/28-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/28-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/29-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/29-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/3-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/3-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/30-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/30-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/31-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/31-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/32-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/32-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/33-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/33-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/34-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/34-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/35-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/35-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/36-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/36-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/37-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/37-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/38-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/38-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/39-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/39-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/4-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/4-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/40-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/40-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/41-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/41-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/42-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/42-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/43-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/43-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/44-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/44-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/45-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/45-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/46-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/46-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/47-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/47-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/48-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/48-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/49-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/49-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/5-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/5-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/50-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/50-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/51-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/51-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/52-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/52-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/53-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/53-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/54-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/54-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/55-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/55-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/56-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/56-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/57-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/57-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/58-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/58-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/59-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/59-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/6-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/6-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/60-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/60-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/61-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/61-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/62-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/62-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/63-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/63-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/64-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/64-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/65-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/65-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/66-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/66-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/67-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/67-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/68-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/68-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/69-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/69-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/7-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/7-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/70-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/70-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/71-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/71-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/72-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/72-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/73-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/73-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/74-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/74-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/75-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/75-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/76-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/76-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/77-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/77-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/78-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/78-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/8-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/8-frame.png -------------------------------------------------------------------------------- /test/utf8/expected-frames/9-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/expected-frames/9-frame.png -------------------------------------------------------------------------------- /test/utf8/fonts-japanese-gothic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/fonts-japanese-gothic.ttf -------------------------------------------------------------------------------- /test/utf8/mapping.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/vodus/02542318d295e25f7318f1f53b3705bc00ea8088/test/utf8/mapping.csv -------------------------------------------------------------------------------- /test/utf8/renew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VODUS=../../vodus.release 4 | 5 | set -xe 6 | 7 | rm -rf ./expected-frames/ 8 | mkdir -p ./expected-frames/ 9 | $VODUS ./utf-8.txt expected-frames/ --config ./test.conf 10 | -------------------------------------------------------------------------------- /test/utf8/test.conf: -------------------------------------------------------------------------------- 1 | output-type = png 2 | font = ./fonts-japanese-gothic.ttf 3 | font_size = 10 4 | fps = 15 5 | width = 200 6 | height = 100 7 | messages_limit = 20 8 | bitrate = 6000000 9 | -------------------------------------------------------------------------------- /test/utf8/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | VODUS=../../vodus.release 4 | DIFFIMG=../../diffimg 5 | 6 | set -xe 7 | 8 | rm -rf ./actual-frames/ 9 | mkdir -p ./actual-frames/ 10 | $VODUS ./utf-8.txt actual-frames/ --config ./test.conf 11 | 12 | EXPECTED_COUNT=`ls ./expected-frames/ | wc -l` 13 | ACTUAL_COUNT=`ls ./actual-frames/ | wc -l` 14 | 15 | if [ "$EXPECTED_COUNT" != "$ACTUAL_COUNT" ]; then 16 | echo "UNEXPECTED AMOUNT OF FRAMES!" 17 | echo "Expected: $EXPECTED_COUNT" 18 | echo "Actual: $ACTUAL_COUNT" 19 | exit 1 20 | fi 21 | 22 | for frame in `ls ./expected-frames/`; do 23 | $DIFFIMG -e "./expected-frames/$frame" -a "./actual-frames/$frame" -t 5 24 | done 25 | -------------------------------------------------------------------------------- /test/utf8/utf-8.txt: -------------------------------------------------------------------------------- 1 | [0:00:00] Hello, World! 2 | [0:00:01] Привет, Мир! 3 | [0:00:02] こんにちは世界! 4 | [0:00:03] <Цодинг> OPA DAVAI DAVAI 5 | -------------------------------------------------------------------------------- /third_party/.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.xz 2 | *.tar.gz 3 | *.zip 4 | *-dist/ 5 | ffmpeg-*/ 6 | giflib-*/ 7 | glfw-*/ -------------------------------------------------------------------------------- /third_party/Makefile.patch: -------------------------------------------------------------------------------- 1 | --- Makefile.orig 2019-06-26 12:08:33.000000000 +1000 2 | +++ Makefile 2019-06-26 12:48:05.000000000 +1000 3 | @@ -37,6 +37,8 @@ 4 | UHEADERS = getarg.h 5 | UOBJECTS = $(USOURCES:.c=.o) 6 | 7 | +UNAME:=$(shell uname) 8 | + 9 | # Some utilities are installed 10 | INSTALLABLE = \ 11 | gif2rgb \ 12 | @@ -61,27 +63,53 @@ 13 | 14 | LDLIBS=libgif.a -lm 15 | 16 | -all: libgif.so libgif.a libutil.so libutil.a $(UTILS) 17 | +SOEXTENION = so 18 | +LIBGIFSO = libgif.$(SOEXTENSION) 19 | +LIBGIFSOMAJOR = libgif.$(SOEXTENSION).$(LIBMAJOR) 20 | +LIBGIFSOVER = libgif.$(SOEXTENSION).$(LIBVER) 21 | +LIBUTILSO = libutil.$(SOEXTENSION) 22 | +LIBUTILSOMAJOR = libutil.$(SOEXTENSION).$(LIBMAJOR) 23 | +ifeq ($(UNAME), Darwin) 24 | +SOEXTENSION = dylib 25 | +LIBGIFSO = libgif.$(SOEXTENSION) 26 | +LIBGIFSOMAJOR = libgif.$(LIBMAJOR).$(SOEXTENSION) 27 | +LIBGIFSOVER = libgif.$(LIBVER).$(SOEXTENSION) 28 | +LIBUTILSO = libutil.$(SOEXTENSION) 29 | +LIBUTILSOMAJOR = libutil.$(LIBMAJOR).$(SOEXTENSION) 30 | +endif 31 | + 32 | +all: $(LIBGIFSO) libgif.a $(LIBUTILSO) libutil.a $(UTILS) 33 | +ifeq ($(UNAME), Darwin) 34 | +else 35 | $(MAKE) -C doc 36 | +endif 37 | 38 | $(UTILS):: libgif.a libutil.a 39 | 40 | -libgif.so: $(OBJECTS) $(HEADERS) 41 | - $(CC) $(CFLAGS) -shared $(LDFLAGS) -Wl,-soname -Wl,libgif.so.$(LIBMAJOR) -o libgif.so $(OBJECTS) 42 | +$(LIBGIFSO): $(OBJECTS) $(HEADERS) 43 | +ifeq ($(UNAME), Darwin) 44 | + $(CC) $(CFLAGS) -dynamiclib -current_version $(LIBVER) $(OBJECTS) -o $(LIBGIFSO) 45 | +else 46 | + $(CC) $(CFLAGS) -shared $(LDFLAGS) -Wl,-soname -Wl,$(LIBGIFSOMAJOR) -o $(LIBGIFSO) $(OBJECTS) 47 | +endif 48 | 49 | libgif.a: $(OBJECTS) $(HEADERS) 50 | $(AR) rcs libgif.a $(OBJECTS) 51 | 52 | -libutil.so: $(UOBJECTS) $(UHEADERS) 53 | - $(CC) $(CFLAGS) -shared $(LDFLAGS) -Wl,-soname -Wl,libutil.so.$(LIBMAJOR) -o libutil.so $(UOBJECTS) 54 | +$(LIBUTILSO): $(UOBJECTS) $(UHEADERS) 55 | +ifeq ($(UNAME), Darwin) 56 | + $(CC) $(CFLAGS) -dynamiclib -current_version $(LIBVER) $(OBJECTS) -o $(LIBUTILSO) 57 | +else 58 | + $(CC) $(CFLAGS) -shared $(LDFLAGS) -Wl,-soname -Wl,$(LIBUTILMAJOR) -o $(LIBUTILSO) $(UOBJECTS) 59 | +endif 60 | 61 | libutil.a: $(UOBJECTS) $(UHEADERS) 62 | $(AR) rcs libutil.a $(UOBJECTS) 63 | 64 | clean: 65 | - rm -f $(UTILS) $(TARGET) libgetarg.a libgif.a libgif.so libutil.a libutil.so *.o 66 | - rm -f libgif.so.$(LIBMAJOR).$(LIBMINOR).$(LIBPOINT) 67 | - rm -f libgif.so.$(LIBMAJOR) 68 | + rm -f $(UTILS) $(TARGET) libgetarg.a libgif.a $(LIBGIFSO) libutil.a $(LIBUTILSO) *.o 69 | + rm -f $(LIBGIFSOVER) 70 | + rm -f $(LIBGIFSOMAJOR) 71 | rm -fr doc/*.1 *.html doc/staging 72 | 73 | check: all 74 | @@ -89,7 +117,12 @@ 75 | 76 | # Installation/uninstallation 77 | 78 | +ifeq ($(UNAME), Darwin) 79 | +install: all install-bin install-include install-lib 80 | +else 81 | install: all install-bin install-include install-lib install-man 82 | +endif 83 | + 84 | install-bin: $(INSTALLABLE) 85 | $(INSTALL) -d "$(DESTDIR)$(BINDIR)" 86 | $(INSTALL) $^ "$(DESTDIR)$(BINDIR)" 87 | @@ -99,9 +132,9 @@ 88 | install-lib: 89 | $(INSTALL) -d "$(DESTDIR)$(LIBDIR)" 90 | $(INSTALL) -m 644 libgif.a "$(DESTDIR)$(LIBDIR)/libgif.a" 91 | - $(INSTALL) -m 755 libgif.so "$(DESTDIR)$(LIBDIR)/libgif.so.$(LIBVER)" 92 | - ln -sf libgif.so.$(LIBVER) "$(DESTDIR)$(LIBDIR)/libgif.so.$(LIBMAJOR)" 93 | - ln -sf libgif.so.$(LIBMAJOR) "$(DESTDIR)$(LIBDIR)/libgif.so" 94 | + $(INSTALL) -m 755 $(LIBGIFSO) "$(DESTDIR)$(LIBDIR)/$(LIBGIFSOVER)" 95 | + ln -sf $(LIBGIFSOVER) "$(DESTDIR)$(LIBDIR)/$(LIBGIFSOMAJOR)" 96 | + ln -sf $(LIBGIFSOMAJOR) "$(DESTDIR)$(LIBDIR)/$(LIBGIFSO)" 97 | install-man: 98 | $(INSTALL) -d "$(DESTDIR)$(MANDIR)/man1" 99 | $(INSTALL) -m 644 doc/*.1 "$(DESTDIR)$(MANDIR)/man1" 100 | @@ -112,7 +145,7 @@ 101 | rm -f "$(DESTDIR)$(INCDIR)/gif_lib.h" 102 | uninstall-lib: 103 | cd "$(DESTDIR)$(LIBDIR)" && \ 104 | - rm -f libgif.a libgif.so libgif.so.$(LIBMAJOR) libgif.so.$(LIBVER) 105 | + rm -f libgif.a $(LIBGIFSO) $(LIBGIFSOMAJOR) $(LIBGIFSOVER) 106 | uninstall-man: 107 | cd "$(DESTDIR)$(MANDIR)/man1" && rm -f $(shell cd doc >/dev/null && echo *.1) 108 | 109 | -------------------------------------------------------------------------------- /third_party/build_third_party.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # TODO(#89): Integrate ./build_third_party.sh into the main build process 6 | FFMPEG_VERSION=4.3 7 | GIFLIB_VERSION=5.2.1 8 | MAKE=make 9 | WGET='wget --no-dns-cache' 10 | 11 | # TODO(#102): ./build_third_party.sh does not respect other BSD's or Darwin that might not use GNU make 12 | # Testing is required and the conditions here may have to be changed accordingly. 13 | if [ `uname -s` = "FreeBSD" ]; then 14 | echo INFO : FreeBSD detected. Using gmake and fetch instead. 15 | if test -x "/usr/local/bin/gmake"; then 16 | MAKE=gmake 17 | WGET=fetch 18 | else 19 | echo "GNU Make is required to build the third-party dependencies." 20 | echo "Aborting..." 21 | exit 1 22 | fi 23 | fi 24 | 25 | if [ ! -d "ffmpeg-${FFMPEG_VERSION}-dist" ]; then 26 | $WGET "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" 27 | tar fvx "ffmpeg-${FFMPEG_VERSION}.tar.xz" 28 | mkdir "ffmpeg-${FFMPEG_VERSION}-dist" 29 | 30 | cd "ffmpeg-${FFMPEG_VERSION}" 31 | ./configure --disable-doc --disable-programs 32 | $MAKE -j5 33 | DESTDIR="../ffmpeg-${FFMPEG_VERSION}-dist" $MAKE install 34 | cd .. 35 | fi 36 | 37 | if [ ! -d "giflib-${GIFLIB_VERSION}-dist" ]; then 38 | $WGET "https://deac-riga.dl.sourceforge.net/project/giflib/giflib-${GIFLIB_VERSION}.tar.gz" 39 | tar fvx "giflib-${GIFLIB_VERSION}.tar.gz" 40 | # NOTE: Taken from here https://sourceforge.net/p/giflib/bugs/133/ 41 | patch "giflib-${GIFLIB_VERSION}/Makefile" < Makefile.patch 42 | mkdir "giflib-${GIFLIB_VERSION}-dist" 43 | 44 | cd "giflib-${GIFLIB_VERSION}" 45 | $MAKE -j5 46 | DESTDIR="../giflib-${GIFLIB_VERSION}-dist" $MAKE install 47 | cd .. 48 | fi 49 | -------------------------------------------------------------------------------- /video.conf: -------------------------------------------------------------------------------- 1 | font = assets/ComicNeue_Bold.otf 2 | font_size = 46 3 | fps = 30 4 | width = 704 5 | height = 576 6 | bitrate = 6000000 7 | -------------------------------------------------------------------------------- /video.params: -------------------------------------------------------------------------------- 1 | font = assets/ComicNeue_Bold.otf 2 | font_size = 46 3 | fps = 30 4 | width = 704 5 | height = 576 6 | limit = 20 7 | bitrate = 6000000 8 | --------------------------------------------------------------------------------