diff --git a/ass_transform b/ass_transform new file mode 100644 index 0000000..2c5efb7 --- /dev/null +++ b/ass_transform @@ -0,0 +1,229 @@ +!! Everything here is untested and best check for errors in the calculations !! + +Choose \fay, \fax, \fscx and \fscy to create a (almost) general 2D-transform-matrix +(Rotations are sometimes also used) + +(ASS Transform order: combined faxy, scale, frz, frx, fry) + +If shearOrigin == scaleOrigin == rotationOrigin == (0,0)^T +(which iirc is true for drawings with \org(0,0)?), +and there are no run splits, and if assuming \frz0 for now, +ASS will effectively use the following transform matrix + + | scale_x 0 | | 1 fax | + | | * | | + | 0 scale_y | | fay 1 | + + + | scale_x scale_x fax | + = | | + | scale_y fay scale_y | + + +Let's assume that's true. +Then given an arbitrary transform matrix M, +with known coefficients a, b, c, d \in \R + | a b | + M = | | + | c d | + +we get: + + I: scale_x = a + II: scale_y = d + III: scale_x fax = b + IV: scale_y fay = c + +If (a = b = 0) or (c = d = 0), then scale_x = 0 +or scale_y = 0 and all other values can be set to zero +as the event vanishes anyway. + +If (a = 0 and b != 0) or (c != 0 and d = 0), then +we can't solve this as is directly. Ignore this +case for now, we'll later describe how to adapt the solution for this. + +If scale_x = a != 0 != d = scale_y, it follows that + III => fax = b / scale_x + IV => fay = c / scale_y + +But even in this case, there's one issue: + scale_x, scale_y may be negative, + but ASS only allows positive scale values. + +To remedy this we'll use 3D rotations to emulate negative scale. +It is easy to see, that by themselves a +rotation around x by π, is equivalent to scale_y = -1 and +a rotation around y by π is equivalent to scale_x = -1. +If we use \fscx and \fscy to scale by the respective absolute values +and immediately follow this up by a “-1”-scale if needed the result +will be equivalent to a scale by the proper value. +In general this isn't so straightforward as ASS always applies rotations +in the order of z, x, y, but we chose rot_z = 0, resulting in it being a +dismissable identity operation. + +If scale_x is negative, pass the absolute value to \fscx and add \fry180. +If scale_y is negative, pass the absolute value to \fscy and add \frx180. + +ASS-3D-rotation matrices: + + | 1 0 0 | + | | + Rx = | 0 cos(rot_x) sin(rot_x) | + | | + | 0 sin(rot_x) -cos(rot_x) | + + + | cos(rot_y) 0 sin(rot_y) | + | | + Ry = | 0 1 0 | + | | + | sin(rot_y) 0 -cos(rot_y) | + + + | cos(rot_z) -sin(rot_z) 0 | + | | + Rz = | sin(rot_z) cos(rot_z) 0 | (for choice of rot_z=0 identity matrix) + | | + | 0 0 1 | + + (from: https://sourceforge.net/p/guliverkli2/code/HEAD/tree/src/subtitles/RTS.cpp#l148) + + + Using sgn_x, sgn_y \in {-1, 1} as the sign of the respective scales + and sc_x, sc_y \in { x >= 0 | x \in \R } as the absolute scale values, + all in all we get the following tags + + \fry(-360*(sgn_x-1)) \frx(-360/(sgn_y-1)) \fscx(sc_x) \fscy(sc_y) \fax(fax) \fay(fay) \org(0,0) + + which results in the ASS transform matrix: + + + | sgn_x 0 0 | | 1 0 0 | | sc_x sc_x fax 0 | + | | | | | | + | 0 1 0 | * | 0 sgn_y 0 | * | sc_y fay sc_y 0 | + | | | | | | + | 0 0 -sgn_x | | 0 0 -sgn_y | | 0 0 1 | + + + | sgn_x 0 0 | | sc_x sc_x fax 0 | + | | | | + = | 0 sgn_y 0 | * | sc_y fay sc_y 0 | + | | | | + | 0 0 sgn_x*sgn_y | | 0 0 1 | + + + | sgn_x*sc_x sgn_x*sc_x fax 0 | + | | + = | sgn_y*sc_y fay sgn_y*sc_y 0 | + | | + | 0 0 sgn_x*sgn_y | + + + using sgn_x*sc_x = scale_x and sgn_y*sc_y = scale_y + + + | scale_x scale_x fax 0 | + | | + = | scale_y fay scale_y 0 | + | | + | 0 0 sgn_x*sgn_y | + + + | a b 0 | + | | + = | c d 0 | + | | + | 0 0 sgn_x*sgn_y | + + + Apart from flipping the sign of the z-coordinates when sgn_x != sgn_y + this is exactly the matrix we wanted. + Since 2D-glyphs and -drawings are initially placed at z=0, + this additional sign-flip is of no concern. + +--- + +Now coming back to the case of (a = 0 and b != 0) or (c != 0 and d = 0): + + To be able to solve this we also need to apply a z-Rotation: + + 1) If in each column there's at least one non-zero entry or one row is all zero + (together with case condition the latter means only b xor c is non-zero): + Use \frz90, being a row swap with a sign flip: + + | 0 -1 | + \frz90 = | | + | 1 0 | + + So our transform-matrix equation becomes: + + | -scale_y fay -scale_y | | a b | + | | = | | + | scale_x scale_x fax | | c d | + + Which can be solved as before for signed scales. + The additon of a z-Rotation means our "scale sign"-roations + no longer immediately follow the actual scale. + To compensate, use \frx for sgn_x and \fry for sgn_y + (roles reversed from before) + + 2) Otherwise: + (If one column is zero and the other one has only non-zero entries) + + 2.1) a = 0 = c and b ≠ 0 ≠ d + + | scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | 0 b | + | | = | | + | scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(rz) + scale_y*cos(rz) | | 0 d | + + I: scale_x*cos(rz) - scale_y*fay*sin(rz) = 0 + II: scale_x*sin(rz) + scale_y*fay*cos(rz) = 0 + III: scale_x*fax*cos(rz) - scale_y*sin(rz) = b + IV: scale_x*fax*sin(rz) + scale_y*cos(rz) = d + + I² + II² ⇒ scale_x^2 = - scale_y^2 * fay^2 ⇒ scale_x = 0 and (scale_y = 0 or fay = 0) + + III' : -scale_y*sin(rz) = b + IV' : scale_y*cos(rz) = d + + ⇒ since b ≠ 0 ≠ d ⇒ scale_y ≠ 0 ⇒ fay = 0 + + ⇒ ||scale_y|| = sqrt(b^2 + d^2) + ⇒ rz = tan⁻¹(-b/d) + Note: The conventional tan⁻¹ definition always returns positive cosine. + Thus we can always coose the positive solution for scale_y + and instead add π to rot_z if d < 0 + + Since scale_y > 0 and scale_x = 0 we do not need \frx and \fry to simulate a negative scale. + + 2.2) b = 0 = d and a ≠ 0 ≠ c + + | scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | a 0 | + | | = | | + | scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(rz) + scale_y*cos(rz) | | c 0 | + + + I: scale_x*fax*cos(rz) - scale_y*sin(rz) = 0 + II: scale_x*fax*sin(rz) + scale_y*cos(rz) = 0 + III: scale_x*cos(rz) - scale_y*fay*sin(rz) = a + IV: scale_x*sin(rz) + scale_y*fay*cos(rz) = b + + I² + II² ⇒ scale_y^2 = - scale_x^2 * fax^2 ⇒ scale_y = 0 and (scale_x = 0 or fax = 0) + + III' : scale_x*cos(rz) = a + IV' : scale_x*sin(rz) = c + + ⇒ since a ≠ 0 ≠ c ⇒ scale_x ≠ 0 ⇒ fax = 0 + + ⇒ ||scale_x|| = sqrt(a^2 + c^2) + ⇒ rz = tan⁻¹(c/a) + Note: The conventional tan⁻¹ definition always returns positive cosine. + Thus we can always coose the positive solution for scale_x + and instead add π to rot_z if a < 0 + + Since scale_x > 0 and scale_y = 0 we do not need \frx and \fry to simulate a negative scale. + + In both 2.1) and 2.2) we get one dimension scaled to zero, and – if I didin't miss something – + no contradictions in the equations. Meaning the event actually vanishes and is visually identical + to choosing all parameters zero. If interpolations are involved having the mathematically accurate + transform matrix might still be good though. diff --git a/cimpl/Makefile b/cimpl/Makefile new file mode 100644 index 0000000..f135bc7 --- /dev/null +++ b/cimpl/Makefile @@ -0,0 +1,13 @@ +#!/usr/bin/gmake + +EXES := unstable stable + +all: $(EXES) + +$(EXES): %: %.c common.h + gcc -O2 -ftree-vectorize -fno-trapping-math -gdwarf \ + -Wall -Wextra -Wpedantic \ + -o $@ $^ -lm + +clean: + rm -f $(EXES) diff --git a/cimpl/common.h b/cimpl/common.h new file mode 100644 index 0000000..017099e --- /dev/null +++ b/cimpl/common.h @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include +#include +#include + +// Just a quick poc, assumes size_t >= int + +/* Matrices are represented as row-by-row, ltr, flattened matrix + * |a b| + * |c d| to {a, b, c, d} + */ + +#define C_PI 3.14159265358979323846 /* matches glibc and musl */ + +typedef struct { + double scale_x; // >= 0 + double scale_y; // >= 0 + double fax; + double fay; + double rot_z; + bool rot_x; // false: 0; true 180 + bool rot_y; // false: 0; true 180 +} Tags; + +void print_matrix(const char* name, const double* mat) +{ + int nlen = strlen(name); + #define FLP "%8.4f" + int llen = printf("%*s | "FLP" "FLP" |", nlen, "", mat[0], mat[1]); + assert(llen > nlen + 5); + printf("\n%s = |%.*s|\n" + "%*s | "FLP" "FLP" |\n", + name, llen-nlen-5, + " ", + nlen, "", mat[2], mat[3]); + #undef FLP +} + +void mult_matrices(const double* l, const double* r, double* res) +{ + #define I(row,col) (col + (row << 1)) + res[0] = l[I(0,0)] * r[I(0,0)] + l[I(0,1)] * r[I(1,0)]; + res[1] = l[I(0,0)] * r[I(0,1)] + l[I(0,1)] * r[I(1,1)]; + res[2] = l[I(1,0)] * r[I(0,0)] + l[I(1,1)] * r[I(1,0)]; + res[3] = l[I(1,0)] * r[I(0,1)] + l[I(1,1)] * r[I(1,1)]; + #undef I +} +void identity(double* mat) +{ + mat[0] = 1.0; mat[1] = 0.0; + mat[2] = 0.0; mat[3] = 1.0; +} + +#define COPY(src,dst) memcpy(dst, src, sizeof(double)*4) + +bool matrix_equal(const double* a, const double *b, const double eps) +{ + return fabs(a[0] - b[0]) <= eps + && fabs(a[1] - b[1]) <= eps + && fabs(a[2] - b[2]) <= eps + && fabs(a[3] - b[3]) <= eps; +} + +/** + * 0: same with default tolerance + * 1: same with 3 * default tolerance + * 2: same with 5 * default tolerance + * -1: not the same + */ +int matrix_compfuzzy(const double* a, const double* b) +{ + for(int i = 0; i < 3; ++i) { + if(matrix_equal(a, b, 2000 * (2*i + 1) * DBL_EPSILON)) + return i; + } + return -1; +} + + + + +/** + * Ignores z dimension + */ +void tags_to_matrix(const Tags* t, double* mat) +{ + // ASS applies operations in order: + // combined-faxy "shear", scale, rot_z, rot_x, rot_y + double* cur = mat; + double nextop[4] = {0}; + double tmp[4] = {0}; + + // Shear and Scale + cur[0] = t->scale_x; + cur[1] = t->scale_x * t->fax; + cur[2] = t->scale_y * t->fay; + cur[3] = t->scale_y; + + /*print_matrix("scaleshear", cur); + puts("");*/ + + // Rotation: Z + double c = cos(t->rot_z * C_PI / 180.0); + double s = sin(t->rot_z * C_PI / 180.0); + nextop[0] = c; nextop[1] = -s; + nextop[2] = s; nextop[3] = c; + mult_matrices(nextop, cur, tmp); + COPY(tmp, cur); + + /*print_matrix(" rotz", nextop); + print_matrix("rotz-scfa", cur); + puts("");*/ + + // Rotation: X + if (t->rot_x) { + identity(nextop); + nextop[3] = -1; + mult_matrices(nextop, cur, tmp); + COPY(tmp, cur); + /*print_matrix(" rotx", nextop); + print_matrix("rotx-zscfa", cur); + puts("");*/ + } + + + // Rotation: Y + if (t->rot_y) { + identity(nextop); + nextop[0] = -1; + mult_matrices(nextop, cur, tmp); + COPY(tmp, cur); + /*print_matrix(" roty", nextop); + print_matrix("roty-xzscfa", cur); + puts("");*/ + } +} + +/** + * Call with len=0 to get required size for untruncated output + */ +int tags_to_string(const Tags* t, char* str, size_t len) +{ + size_t p = 0; + if(t->rot_y) { + size_t tmp = snprintf(str, len, "\\fry180"); + p += tmp; + if(len <= tmp) { + len = 0; + str = NULL; + } else { + len -= tmp; + str += tmp; + } + } + if(t->rot_x) { + size_t tmp = snprintf(str, len, "\\frx180"); + p += tmp; + if(len <= tmp) { + len = 0; + str = NULL; + } else { + len -= tmp; + str += tmp; + } + } + if(t->rot_z) { + size_t tmp = snprintf(str, len, "\\frz%.5f ", t->rot_z); + p += tmp; + if(len <= tmp) { + len = 0; + str = NULL; + } else { + len -= tmp; + str += tmp; + } + } + + p += snprintf(str, len, "\\fscy%.5f\\fscx%.5f \\fay%.5f\\fax%.5f", + t->scale_y * 100.0, t->scale_x * 100.0, t->fay, t->fax); + return p; +} + + +//const double DEF_TARGET[4] = {-0.06203, 0.19083, -0.0758, 0.00354}; +const double DEF_TARGET[4] = {-0.12395, 0.42227, -0.17882, 0.04262}; + +bool parse_args(int argc, char** argv, double* target) +{ + if(argc <= 1) + memcpy(target, DEF_TARGET, sizeof(DEF_TARGET)); + else if(argc == 5) { + target[0] = strtod(argv[1], NULL); + target[1] = strtod(argv[2], NULL); + target[2] = strtod(argv[3], NULL); + target[3] = strtod(argv[4], NULL); + } else { + fprintf(stderr, "Usage: %s [ ]\n", argv[0]); + return false; + } + return true; +} + +int evaluate_result(const double* tar, const Tags* tags) +{ + double result[4] = {0}; + char tag_str[4096] = {0}; + tags_to_matrix(tags, result); + tags_to_string(tags, tag_str, 4096); + printf("\n %s\n", tag_str); + print_matrix("result", result); + puts(""); + + int score = matrix_compfuzzy(tar, result); + const char* score_name; + switch(score) { + case 0: score_name = "same(high)"; break; + case 1: score_name = "same(med)"; break; + case 2: score_name = "same"; break; + default: score_name = "FAIL"; break; + } + printf("Score: %s\n", score_name); + return score < 0; +} diff --git a/cimpl/test.sh b/cimpl/test.sh new file mode 100755 index 0000000..c1a57b4 --- /dev/null +++ b/cimpl/test.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +if [ "$#" -ne 1 ] ; then + echo "Usage $0 " >&2 + exit 2 +fi +NAME="$(basename $1)" + +cnt_fail=0 +cnt_succ=0 + +printf "" > failure_"$NAME".log +printf "" > success_"$NAME".log + +for sa in "" - ; do +for sb in "" - ; do +for sc in "" - ; do +for sd in "" - ; do + for a in 5 0 ; do + for b in 11 0 ; do + for c in 3 0 ; do + for d in 19 0 ; do + outp="$(./"$1" $sa$a $sb$b $sc$c $sd$d 2>/dev/null)" + if [ "$?" -ne 0 ] ; then + cnt_fail="$((cnt_fail+1))" + printf '\n\n' >> failure_"$NAME".log + printf "%s\n" "$outp" >> failure_"$NAME".log + else + cnt_succ="$((cnt_succ+1))" + printf '\n\n' >> success_"$SNAME".log + printf "%s\n" "$outp" >> success_"$NAME".log + fi + done ; done ; done ; done +done ; done ; done ; done + +printf " %3d SUCCESSES\n" "$cnt_succ" +printf " %3d FAILURES\n" "$cnt_fail" +printf " %3d total\n" "$((cnt_succ + cnt_fail))" + diff --git a/cimpl/unstable.c b/cimpl/unstable.c new file mode 100644 index 0000000..e2c33e6 --- /dev/null +++ b/cimpl/unstable.c @@ -0,0 +1,125 @@ +#include "common.h" + +inline bool zero(double d) +{ + return fabs(d) < 700 * DBL_EPSILON; + // For reference musl (64bit double) uses: + // DBL_EPSILON 2.22044604925031308085e-16 +} + +/* + * a == 0 \implies b == 0 + * and + * d == 0 \implies c == 0 + */ +void case_trivial(const double* m, Tags* t) +{ + t->scale_x = m[0]; + t->scale_y = m[3]; + t->fax = zero(m[0]) ? 0 : (m[1] / m[0]); // case condition + t->fay = zero(m[3]) ? 0 : (m[2] / m[3]); // case condition + if(t->scale_x < 0) { + t->rot_y = true; + t->scale_x = fabs(t->scale_x); + } + if(t->scale_y < 0) { + t->rot_x = true; + t->scale_y = fabs(t->scale_y); + } +} + +/** + * |0 b| |0 0| |0 b| |0 b| |a b| + * |0 0|, |c 0|, |c 0|, |c d| or |c 0| with letters non-zero + * (i.e. c==0 \implies d==0 and a==0 \implies d==0) + * + * apply rot_z of 90° to get + * |a b| |-scale_y*fay -scale_y | + * |c d| = | scale_x scale_x*fax| + */ +void case_rowswap(const double* m, Tags* t) +{ + t->rot_z = 90; + t->scale_x = m[2]; + t->scale_y = -m[1]; + t->fax = zero(m[2]) ? 0 : (m[3] / m[2]); // case condition + t->fay = zero(m[1]) ? 0 : (m[0] / m[1]); // case condition + if(t->scale_x < 0) { + t->rot_x = true; + t->scale_x = fabs(t->scale_x); + } + if(t->scale_y < 0) { + t->rot_y = true; + t->scale_y = fabs(t->scale_y); + } +} + +/** + * |a 0| + * |c 0| with letters non-zero + */ +void case_zerocol_r(const double* m, Tags* t) +{ + t->scale_y = 0; + t->fax = 0; + t->fay = 0; // Arbitrary + t->scale_x = sqrt(m[0] * m[0] + m[2] * m[2]); + t->rot_z = atan(m[2] / m[0]) * 180 / C_PI; + if (m[0] < 0) // atan always yields positive cos + t->rot_z += 180; +} + +/** + * |0 b| + * |0 d| with letters non-zero + */ +void case_zerocol_l(const double* m, Tags* t) +{ + t->scale_x = 0; + t->fay = 0; + t->fax = 0; // Arbitrary + t->scale_y = sqrt(m[1] * m[1] + m[3] * m[3]); + t->rot_z = atan(-m[1] / m[3]) * 180 / C_PI; + if (m[3] < 0) // atan always yields positive cos + t->rot_z += 180; +} + +int main(int argc, char** argv) +{ + double tar[4] = {0}; + + if(!parse_args(argc, argv, tar)) + exit(5); + + print_matrix("target", tar); + + Tags tags = {0}; + if( + !( (zero(tar[0]) && !zero(tar[1])) + || (zero(tar[3]) && !zero(tar[2])) ) + ) { + printf(" --trivial\n"); + case_trivial(tar, &tags); + } else if( + !( (zero(tar[1]) && !zero(tar[0])) + || (zero(tar[2]) && !zero(tar[3])) ) + ) { + printf(" --rowswap\n"); + case_rowswap(tar, &tags); + } else if( + zero(tar[0]) && zero(tar[2]) && !zero(tar[1]) && !zero(tar[3]) + ) { + printf(" --zerocol_l\n"); + case_zerocol_l(tar, &tags); + } else if( + !zero(tar[0]) && !zero(tar[2]) && zero(tar[1]) && zero(tar[3]) + ) { + printf(" --zerocol_r\n"); + case_zerocol_r(tar, &tags); + } else { + fprintf(stderr, "Oupsie, unknown matrix-case encountered. This shouldn't happen :P\n"); + exit(6); + } + + return evaluate_result(tar, &tags); +}