Kiosk: apply schedule engine in playback and add System Settings toggle

ScrollerPage now resolves the active Block from ScheduleManager (reusing
the existing stream-switch path), honoring dynamicAudio true/false and
falling back to local selection for gaps, no schedule, or toggle off.
Adds a 'Schedule' On/Off entry (default On) in System Settings persisted
in config.yaml, and config plumbing (proclamation.schedule_enabled). All
on-device schedule times remain 12h AM/PM.
This commit is contained in:
NeilClack
2026-09-29 10:34:45 -05:00
parent 509c925480
commit da0b6ffd5e
15 changed files with 959 additions and 41 deletions
+502
View File
@@ -0,0 +1,502 @@
**Ben Williams** February 9, 2007 MATH491 Summer Research Project 2006–2007 Department of Mathematics and Statistics, University of Canterbury
## Abstract
An analysis of some recently proposed methods of digital image scrambling that use Fibonacci numbers. This report describes the mathematics required to understand why these methods work.
## Contents
1. Introduction
2. Properties of the Fibonacci numbers
3. Affine transformations
4. Applications to digital image scrambling
5. Conclusion Appendix: Methods to calculate $f_n$ in $\mathbb{Z}_m$
---
## 1. Introduction
Digital image scrambling is a method to hide the content of an image. One application is in satellite television broadcasting systems to prevent unauthorized viewing. The scrambling algorithm needs to be fast so that each frame can be de-scrambled in real-time. A method used in the past has been to permute the rows of the image. A key is then required to describe the inverse permutation.
Other methods of scrambling include scrambling pixel positions or scrambling the color values of each pixel. Without knowledge of the key it should be difficult to recover the scrambled image given time constraints.
Digital image scrambling methods have specific requirements that general data encryption algorithms may not provide. The algorithms need to be simple and fast so that they can be used by low cost equipment. The scrambled image should not adversely affect data compression algorithms, and information about the image such as dimension needs to be available without decrypting the image.
In order to understand the new scrambling methods proposed in [2], [3], [4], some properties of the Fibonacci numbers are described. Affine transformations are then presented because these will be used to scramble pixels to new positions within an image. Some affine transformations that use Fibonacci numbers for scrambling digital images are described and analysed.
---
## 2. Properties of the Fibonacci numbers
**Definition 2.1.** The _Fibonacci numbers_ are the numbers in the sequence $0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, \dots$, where the $n$th Fibonacci number $f_n$ is defined by the linear recurrence relation
$$f_n = f_{n-1} + f_{n-2} \quad \text{for } n \ge 2 \text{ and } f_0 = 0,\ f_1 = 1.$$
**Theorem 2.1.** $f_n$ is even $\iff$ 3 divides $n$.
_Proof._ Consider the Fibonacci sequence where each number is reduced modulo 2.
$$s_n = f_n \bmod 2$$ $$s_n = (f_{n-1} + f_{n-2}) \bmod 2$$ $$s_n = (s_{n-1} + s_{n-2}) \bmod 2$$
The sequence generated is
$$0, 1, 1, 0, 1, 1, 0, 1, 1, 0, \dots$$
The period of the sequence is 3 because $s_i = s_{i+3}$ for all $i \in \mathbb{N}$. It follows that
$$s_{3k} = 0, \qquad s_{3k+1} = 1, \qquad s_{3k+2} = 1 \quad \text{for all } k \in \mathbb{N}.$$
Therefore $f_n \equiv 0 \pmod 2 \iff n = 3k$. $\blacksquare$
**Definition 2.2.** The _Fibonacci matrix_ is the $2 \times 2$ matrix
$$F = \begin{bmatrix} 0 & 1 \ 1 & 1 \end{bmatrix}$$
It is used to represent the Fibonacci sequence as a matrix equation. Let $\mathbf{u}_n = \begin{bmatrix} f_{n-1} \ f_n \end{bmatrix}$. Then for $n \ge 1$, $\mathbf{u}_{n+1} = F\mathbf{u}_n$.
**Theorem 2.2.**
$$f_n = \frac{(1+\sqrt5)^n - (1-\sqrt5)^n}{2^n \sqrt5}$$
**Table 1: The Fibonacci numbers**
|$n$|$f_n$|$f_{n+1}$|$f_n^2 \bmod f_{n+1}$|
|---|---|---|---|
|0 |0 |1 |0 |
|1|1|1|0|
|2|1|2|-1|
|3|2|3|1|
|4|3|5|-1|
|5|5|8|1|
|6|8|13|-1|
|7|13|21|1|
|8|21|34|-1|
|9|34|55|1|
|10|55|89|-1|
|11|89|144|1|
|12|144|233|-1|
|13|233|377|1|
|14|377|610|-1|
|15|610|987|1|
|16|987|1597|-1|
_Proof of Theorem 2.2._ [5] p. 355.
$$\mathbf{u}_{n+1} = F\mathbf{u}_n = F(F\mathbf{u}_{n-1}) = F^2 \mathbf{u}_{n-1} = \cdots = F^n \mathbf{u}_{n-(n-1)} = F^n \mathbf{u}_1$$
$\lambda$ is an eigenvalue of $F$ if $\det(F - \lambda I) = 0$. Solving the characteristic equation for $F$ gives eigenvalues
$$\lambda_1 = \frac{1+\sqrt5}{2}, \qquad \lambda_2 = \frac{1-\sqrt5}{2}$$
Let $\mathbf{v}_1$ and $\mathbf{v}_2$ be corresponding eigenvectors. These eigenvectors are linearly independent because $\lambda_1 \ne \lambda_2$, therefore they span $\mathbb{R}^2$.
Let $\mathbf{u}_1 = c_1 \mathbf{v}_1 + c_2 \mathbf{v}_2$.
$$\mathbf{u}_{n+1} = F^n \mathbf{u}_1 = F^n(c_1 \mathbf{v}_1 + c_2 \mathbf{v}_2) = c_1 F^n \mathbf{v}_1 + c_2 F^n \mathbf{v}_2$$
$$\begin{bmatrix} f_n \ f_{n+1} \end{bmatrix} = c_1 \lambda_1^n \mathbf{v}_1 + c_2 \lambda_2^n \mathbf{v}_2$$
Therefore $f_n = d_1 \lambda_1^n + d_2 \lambda_2^n$ for coefficients yet to be determined. Solving the system of equations
$$f_0 = d_1 \lambda_1^0 + d_2 \lambda_2^0 = d_1 + d_2 = 0$$
$$f_1 = d_1 \lambda_1^1 + d_2 \lambda_2^1 = d_1\left(\frac{1+\sqrt5}{2}\right) + d_2\left(\frac{1-\sqrt5}{2}\right) = 1$$
gives $d_1 = \dfrac{1}{\sqrt5}$, $d_2 = \dfrac{-1}{\sqrt5}$.
$$f_n = \frac{1}{\sqrt5}\left(\left(\frac{1+\sqrt5}{2}\right)^n - \left(\frac{1-\sqrt5}{2}\right)^n\right) = \frac{(1+\sqrt5)^n - (1-\sqrt5)^n}{2^n\sqrt5} \qquad \blacksquare$$
**Lemma 2.3.** For $n \ge 1$
$$F^n = \begin{bmatrix} f_{n-1} & f_n \ f_n & f_{n+1} \end{bmatrix}$$
_Proof._ By induction. Let $P(n)$ be the proposition that $F^n = \begin{bmatrix} f_{n-1} & f_n \ f_n & f_{n+1} \end{bmatrix}$.
$P(1)$ is the proposition that $F = \begin{bmatrix} 0 & 1 \ 1 & 1 \end{bmatrix}$, which is true.
If $P(k)$ is true for some positive integer $k$ then
$$F^k F = \begin{bmatrix} f_{k-1} & f_k \ f_k & f_{k+1} \end{bmatrix}\begin{bmatrix} 0 & 1 \ 1 & 1 \end{bmatrix} = \begin{bmatrix} f_k & f_{k-1}+f_k \ f_{k+1} & f_k+f_{k+1} \end{bmatrix} = \begin{bmatrix} f_k & f_{k+1} \ f_{k+1} & f_{k+2} \end{bmatrix} = \begin{bmatrix} f_{(k+1)-1} & f_{k+1} \ f_{k+1} & f_{(k+1)+1} \end{bmatrix} = F^{k+1}$$
Therefore $P(k) \Rightarrow P(k+1)$ and by induction, $P(n)$ is true for all $n \ge 1$. $\blacksquare$
**Theorem 2.4.** Let $f_n$ denote the $n$th Fibonacci number. Then for $n \ge 1$
$$f_{n-1} \cdot f_{n+1} - f_n^2 = (-1)^n$$
_Proof._ [7] Taking determinants on both sides of the equation in Lemma 2.3 gives
$$\begin{vmatrix} f_{n-1} & f_n \ f_n & f_{n+1} \end{vmatrix} = \det(F^n) = \det(F)^n = (-1)^n \qquad \blacksquare$$
**Theorem 2.5.** Any two consecutive Fibonacci numbers $f_n$ and $f_{n+1}$ are relatively prime.
_Proof._ By induction. Let $P(n)$ be the proposition that $\gcd(f_n, f_{n+1}) = 1$.
$P(0)$ is the proposition that $\gcd(0,1)=1$, which is true.
If $P(k)$ is true for some positive integer $k$ then
$$\gcd(f_k, f_{k+1}) = 1 = \gcd(f_k+f_{k+1}, f_{k+1}) = \gcd(f_{k+1}, f_{k+2})$$
because $f_k + f_{k+1} = f_{k+2}$ by Definition 2.1.
Therefore $P(k) \Rightarrow P(k+1)$ and by induction, $P(n)$ is true for all $n \ge 0$. $\blacksquare$
---
## 3. Affine transformations
Let $\mathbb{Z}_m = {0,1,2,\dots,m-1}$. Denote $\mathbb{Z}_m^n$ as the set of vectors of dimension $n$ with each component an element in $\mathbb{Z}_m$. Denote $M_n(\mathbb{Z}_m)$ as the set of all $n \times n$ matrices with each entry an element in $\mathbb{Z}_m$. Matrix operations addition and multiplication over $\mathbb{Z}_m$ are defined the same as addition and multiplication over $\mathbb{Z}$ with each matrix entry reduced modulo $m$.
For example: Let $A = (a_{i,j})$, $B = (b_{i,j}) \in M_n(\mathbb{Z}_m)$ and $m \in \mathbb{N}$. We write
$$A \equiv B \pmod m$$
if $a_{i,j} \equiv b_{i,j} \pmod m$ for all $i,j \in {0,1,\dots,n}$.
**Definition 3.1.** If there exists $A' \in M_n(\mathbb{Z}_m)$ such that $AA' \equiv I_n \pmod m$ then $A$ is _invertible_mod $m$.
**Theorem 3.1.** Let $A \in M_n(\mathbb{Z}_m)$ and $m \in \mathbb{N}$. $A$ is invertible $\iff \det(A)$ and $m$ are relatively prime.
_Proof._ [4] p. 94. Suppose $A$ is invertible. Then $AB \equiv I \pmod m$ for some $B \in M_n(\mathbb{Z}_m)$.
$$\det(AB) \equiv 1 \pmod m$$ $$\det(A) \cdot \det(B) \equiv 1 \pmod m$$
Let $a = \det(A)$ and $b = \det(B)$.
$$ab + km = 1 \text{ for some } k \in \mathbb{Z}$$
Suppose $c \mid a$ and $c \mid m$. Then $c \mid (ab+km)$, i.e. $c \mid 1$. Therefore $\gcd(a,m) = 1$.
Conversely suppose $\gcd(a,m) = 1$. Then
$$ab + km = 1 \text{ for some } b,k \in \mathbb{Z}$$ $$ab \equiv 1 \pmod m$$
A well known equation for square matrices is
$$A , \mathrm{adj}(A) = \det(A), I$$
where $\mathrm{adj}(A)$ is the adjoint matrix of $A$. This equation gives a formula to calculate $A^{-1}$.
$$A,\mathrm{adj}(A) \equiv \det(A), I \pmod m$$ $$bA,\mathrm{adj}(A) \equiv ba, I \pmod m$$ $$A, b,\mathrm{adj}(A) \equiv I \pmod m$$ $$A^{-1} \equiv b,\mathrm{adj}(A) \pmod m \qquad \blacksquare$$
**Remark 1.** If $\det(A) = 0$ then $A$ is not invertible, since $\gcd(0,m) = m$. An example of a non-invertible matrix with a non-zero determinant is $\begin{bmatrix} 2 & 0 \ 0 & 1 \end{bmatrix}$ over $M_2(\mathbb{Z}_4)$.
**Definition 3.2.** A transformation $T: \mathbb{Z}_m^n \to \mathbb{Z}_m^n$ defined by $T(\mathbf{x}) = A\mathbf{x} + \mathbf{b} \pmod m$ for some $\mathbf{b} \in \mathbb{Z}_m^n$, $A \in M_n(\mathbb{Z}_m)$ is called _affine_. If $\mathbf{b} = 0$, then $T$ is a _linear_ transformation.
**Corollary 3.2.** The affine linear map from Definition 3.2 is bijective if and only if $\det(A)$ and $m$ are relatively prime.
**Definition 3.3.** Let $A \in M_n(\mathbb{Z}_m)$ and $m \in \mathbb{N}$. $A$ is _periodic_ if there exists $t \in \mathbb{N}$ such that $t > 0$ and $A^t = I_n$. The _period_ of $A$ is the smallest positive integer $t$ such that $A^t = I_n$.
An affine transformation $T: \mathbb{Z}_m^n \to \mathbb{Z}_m^n$ is _periodic_ if $T^t(\mathbf{x}) = \mathbf{x}$ for some positive integer $t$ and all $\mathbf{x} \in \mathbb{Z}_m^n$.
**Theorem 3.3.** Let $A \in M_n(\mathbb{Z}_m)$ and $m \in \mathbb{N}$. $A$ is periodic $\iff$ $A$ is invertible.
_Proof._ Suppose $A$ is periodic. Then for some positive integer $t$
$$A^t \equiv I \pmod m$$ $$A, A^{t-1} \equiv I \pmod m$$
Therefore $A$ is invertible.
Conversely, suppose $A$ is invertible. There are exactly $m^{n^2}$ distinct matrices in $M_n(\mathbb{Z}_m)$. Define the multiset $S = {A^i \bmod m : 0 \le i \le m^{n^2}}$. There exists $A^k, A^l \in S$ where $k \le l$ and $A^k = A^l$.
$$A^k \equiv A^l \pmod m$$ $$A^k(A^{-1})^k \equiv A^l(A^{-1})^k \pmod m$$ $$I \equiv A^{l-k} \pmod m$$
Therefore $A$ is periodic. $\blacksquare$
**Theorem 3.4.** The affine transformation $T: \mathbf{x} \to A\mathbf{x} + c\mathbf{b}$ defined in 3.2 is periodic if and only if $A$ is invertible.
_Proof._ Define affine transformation $S: \mathbb{Z}_m^{n+1} \to \mathbb{Z}_m^{n+1}$ by
$$S(\mathbf{v}) = \begin{bmatrix} A & \mathbf{b} \ \mathbf{0}^T & 1 \end{bmatrix} = A'\mathbf{v}$$
Let $\mathbf{y} = \begin{bmatrix} \mathbf{x} \ c \end{bmatrix}$ for some $c \in \mathbb{Z}_m$, $\mathbf{x} \in \mathbb{Z}_m^n$. Then
$$S(\mathbf{y}) = \begin{bmatrix} A & \mathbf{b} \ \mathbf{0}^T & 1 \end{bmatrix}\begin{bmatrix} \mathbf{x} \ c \end{bmatrix} = \begin{bmatrix} A\mathbf{x} + c\mathbf{b} \ c \end{bmatrix} = \begin{bmatrix} T(\mathbf{x}) \ c \end{bmatrix}$$
Let $P(n)$ be the proposition that
$$S^n\left(\begin{bmatrix} \mathbf{x} \ c \end{bmatrix}\right) = \begin{bmatrix} T^n(\mathbf{x}) \ c \end{bmatrix}$$
$P(1)$ has been proved. If $P(k)$ is true, then
$$S^{k+1}(\mathbf{y}) = S(S^k(\mathbf{y})) = \begin{bmatrix} A & \mathbf{b} \ \mathbf{0}^T & 1 \end{bmatrix}\begin{bmatrix} T^k(\mathbf{x}) \ c \end{bmatrix} = \begin{bmatrix} A,T^k(\mathbf{x}) + c\mathbf{b} \ c \end{bmatrix} = \begin{bmatrix} T^{k+1}(\mathbf{x}) \ c \end{bmatrix}$$
Therefore $P(k) \Rightarrow P(k+1)$ and by induction, $P(n)$ is true for all $n \ge 1$.
If there exists positive integer $t$ such that
$$S^t\left(\begin{bmatrix} \mathbf{x} \ c \end{bmatrix}\right) = \begin{bmatrix} T^t(\mathbf{x}) \ c \end{bmatrix} = \begin{bmatrix} \mathbf{x} \ c \end{bmatrix}$$
then $S$ and $T$ are both periodic, otherwise $S$ and $T$ are both non-periodic.
$$T \text{ is periodic} \iff S \text{ is periodic}$$ $$T \text{ is periodic} \iff A' \text{ is invertible (Theorem 3.3)}$$ $$T \text{ is periodic} \iff \text{there exists } A'^{-1} = \begin{bmatrix} A^{-1} & A^{-1}(-\mathbf{b}) \ \mathbf{0}^T & 1 \end{bmatrix}$$ $$T \text{ is periodic} \iff A \text{ is invertible} \qquad \blacksquare$$
**Remark 2.** Any scalar multiple of $\mathbf{b}$ can be used in $T$ and the period will remain the same.
---
## 4. Applications to digital image scrambling
**Definition 4.1.** The _Fibonacci affine transformation_ $T: \mathbb{Z}_m^2 \to \mathbb{Z}_m^2$ is defined by
$$T(\mathbf{x}) = F\mathbf{x} + \mathbf{b} \pmod m$$
for some $\mathbf{b} \in \mathbb{Z}_m^2$, $m \in \mathbb{N}$ and $F$ from Definition 2.2.
**Proposition 4.1.** The Fibonacci affine transformation is periodic.
_Proof._ $\gcd(\det(F), m) = 1$ for all $m$, so by Theorems 3.1 and 3.4, the transformation is periodic. $\blacksquare$
The Fibonacci matrix transformation is described in [3] where the authors use it to scramble images of dimension $m \times m$ pixels. Each pixel has a coordinate in $\mathbb{Z}_m^2$ that can be scrambled to a new coordinate by $T$. The period of the transformation varies for different $m$. The suggested scrambling method is to apply $T$ on every pixel of the image. Then to decrypt, repeatedly apply $T$ to the scrambled image until the original image is recovered. It is unclear why the inverse transformation of $T$ is not used to make the decryption quicker. The vector $\mathbf{b}$ can be used as a key. Different values of $\mathbf{b}$ will affect the period of $T$.
**Definition 4.2.** The _Arnold transformation_ $T: \mathbb{Z}_m^2 \to \mathbb{Z}_m^2$ is defined by $T(\mathbf{x}) = A\mathbf{x} \pmod m$ for $m \in \mathbb{N}$ and
$$A = \begin{bmatrix} 1 & 1 \ 1 & 2 \end{bmatrix}$$
The Arnold transformation works the same way as the Fibonacci matrix transformation. The Arnold transformation is also periodic for all $m$.
**Remark 3.** $F^2 = A$.
Another transformation described in [4] works as follows.
**Definition 4.3.** $T: \mathbb{Z}_m^2 \to \mathbb{Z}_m^2$ defined by $T(\mathbf{x}) = B\mathbf{x} + \mathbf{b} \pmod{f_{n+1}}$ for some $\mathbf{b} \in \mathbb{Z}_m^2$, $m \in \mathbb{N}$ with
$$B = \begin{bmatrix} f_n & 0 \ 0 & f_n \end{bmatrix}$$
**Proposition 4.2.** $B$ is invertible, hence the transformation is periodic.
_Proof._ $\det(B) = f_n^2 = f_{n-1}f_{n+1} + (-1)^{n+1}$ by Theorem 2.4.
If $c \mid \det(B)$ and $c \mid f_{n+1}$ then $c \mid 1$, therefore $\det(B)$ and $f_{n+1}$ are coprime. $\blacksquare$
**Proposition 4.3.** When $n$ is odd the period of $B$ is 2. When $n$ is even the period of $B$ is 4.
_Proof._ From Theorem 2.4 we have
$$f_n^2 \equiv (-1)^{n+1} \pmod{f_{n+1}}$$
so $f_n^4 \equiv 1 \pmod{f_{n+1}}$ and $B^4 \equiv I \pmod{f_{n+1}}$.
If $B$ has period $t$ then $t \mid 4$.
When $n$ is even, $B \ne I$ and $B^2 \equiv -I \pmod{f_{n+1}}$. Therefore when $n$ is even the period of $B$ is 4.
When $n$ is odd, $f_n^2 \equiv 1 \pmod{f_{n+1}}$, so $B^2 \equiv I \pmod{f_{n+1}}$. Since $B \ne I \pmod{f_{n+1}}$, when $n$ is odd the period of $B$ is 2. $\blacksquare$
**Remark 4.** Although $B$ has period 2 or 4, the period of the transformation may not be 2 or 4 when $\mathbf{b}$ is non-zero.
The transformation permutes the columns and rows of the image by the permutation $R: k \to kf_n + b \pmod{f_{n+1}}$ where $k$ is a column or row number in ${1,2,\dots,f_{n+1}-1}$. The authors in [4] also include zero in this set but zero is a fixed point when $b = 0$. The authors state that having a period of 2 or 4 is efficient because encryption and decryption are the same functions and decryption can be achieved by reapplying the transformation.
The case where $b = 0$ will be examined. Let $S: k \to kf_n \pmod{f_{n+1}}$ for $k$ in ${1,2,\dots,f_{n+1}-1}$. It follows from Proposition 4.3 that $S$ has period 4 if $n$ is even and period 2 if $n$ is odd.
**Definition 4.4.** A _uniform permutation_ is a permutation in which all disjoint cycles are of the same length. This means there are no fixed points unless the permutation is the identity.
**Remark 5.** If a permutation has period $t$, then the length of each cycle divides $t$, since $t$ is the lowest common multiple of all cycle lengths.
Suppose the columns or rows of an image are scrambled using a uniform permutation. Then every row or column is moved, so the image content may be hidden more effectively than a permutation that leaves some rows or columns unchanged.
In general the permutation $S$ is not uniform. Fixed points can be found by solving $x$ in the equation
$$xf_n \equiv x \pmod{f_{n+1}} \tag{1}$$
**Proposition 4.4.** For $n > 3$, if $n$ is odd then $S$ is not uniform.
_Proof._ $x = k(f_n+1)$, $k \in \mathbb{N}$, gives solutions to equation (1).
$$k(f_n+1) \cdot f_n = k(f_n^2+f_n) \equiv k(1+f_n) \pmod{f_{n+1}}$$
**Remark 6.** When $n=3$, $S$ is uniform. $\blacksquare$
**Proposition 4.5.** If $n$ is even, then $S$ has no transpositions.
_Proof._ A transposition exists if
$$x \equiv f_n^2 x \pmod{f_{n+1}} \tag{2}$$
and
$$x \not\equiv f_n x \pmod{f_{n+1}} \tag{3}$$
for some $x \in {1,2,\dots,f_{n+1}-1}$. This proof will show that if equation (2) has a solution, then $x \equiv f_n x \pmod{f_{n+1}}$ has the same solution.
Solutions are found by solving for $x$:
$$x \equiv f_n^2 x \pmod{f_{n+1}} \tag{4}$$ $$x \equiv -x \pmod{f_{n+1}} \tag{5}$$ $$2x \equiv 0 \pmod{f_{n+1}} \tag{6}$$
A solution exists $\iff$ 2 divides $f_{n+1}$, then $x = \dfrac{f_{n+1}}{2}$.
$f_{n+1}$ is even, so by Theorem 2.1, $f_n$ is odd.
$$f_n x = (2m+1)x \equiv (2m)\frac{f_{n+1}}{2} + \frac{f_{n+1}}{2} \equiv m f_{n+1} + x \pmod{f_{n+1}}$$ $$f_n x \equiv x \pmod{f_{n+1}}$$
This is a contradiction to equation (3). Therefore, no transpositions exist. $\blacksquare$
**Proposition 4.6.** For even $n$, $S$ is uniform $\iff f_{n+1}$ is odd.
_Proof._ Suppose $S$ is uniform, then no fixed points exist. If $x \equiv f_n x \pmod{f_{n+1}}$ has no solutions then by Proposition 4.5
$$x \equiv f_n^2 x \pmod{f_{n+1}} \tag{7}$$ $$2x \equiv 0 \pmod{f_{n+1}} \tag{8}$$
has no solutions so $f_{n+1}$ is odd.
Conversely, suppose $f_{n+1}$ is odd and a fixed point $x$ exists. Then equation (8) has no solution, thus
$$x \equiv f_n^2 x \pmod{f_{n+1}}$$
has no solution. But a fixed point exists so
$$x \equiv f_n x \equiv f_n^2 x \pmod{f_{n+1}}$$
This is a contradiction. $\blacksquare$
**Proposition 4.7.** For even $n$, $f_{n+1}$ is odd $\iff n \equiv 0$ or $4 \pmod 6$.
_Proof._ Let $f_{n+1}$ be odd, then by Theorem 2.1 either
$$n+1 = 3k+1 \implies n = 3k$$
or
$$n+1 = 3l+2 \implies n = 3l+1$$
Equivalently, $f_{n+1}$ is odd $\iff n = 3k$ or $n = 3l+1$. Now restrict $n$ to even values, so let $k = 2m$ and $l = 2m+1$. Then
$$n = 6m$$
or
$$n = 3(2m+1)+1 = 6m+4$$
Therefore, for even $n$, $f_{n+1}$ is odd $\iff n = 6m$ or $n = 6m+4$. $\blacksquare$
---
## 5. Conclusion
The Fibonacci transformation and the Arnold transformation can be used on square images of any size. The period of the transformation will vary for different size images. The period of $F \bmod m$ can be as large as $6m$ so the decryption process described could be very inefficient.
The transformation in Definition 4.3 transforms pixels of an $f_{n+1} \times f_{n+1}$ image. In most practical cases, images are unlikely to be square and of this dimension. In this case the image can be cropped to $f_{n+1} \times f_{n+1}$ so that the transformation can be applied. Alternatively, a large Fibonacci number can be chosen that exceeds the dimensions of the image. But this would generate an encrypted image larger than the unencrypted image. Due to these limitations, the transformations do not appear to be very practical.
---
## Appendix: Methods to calculate $f_n$ in $\mathbb{Z}_m$
To calculate the $n$th Fibonacci number mod $m$, the equation in Theorem 2.2 can be used and then $f_n$ can be reduced modulo $m$. For very large $n$, this is infeasible. If $F$ can be diagonalized over $\mathbb{Z}_m$ then calculating $F^n$ is fast.
Eigenvalues of $F$ exist if for non-zero vector $\mathbf{x}$,
$$F\mathbf{x} \equiv \lambda \mathbf{x} \pmod m$$ $$F\mathbf{x} - \lambda \mathbf{x} \equiv \mathbf{0} \pmod m$$ $$(F - \lambda I)\mathbf{x} \equiv \mathbf{0} \pmod m$$
Therefore $\lambda$ is an eigenvalue of $F$ if
$$\gcd(\det(F-\lambda I), m) > 1$$
The characteristic polynomial of $F$ is $p(\lambda) = \det(F - \lambda I) = \lambda^2 - \lambda - 1$.
For example: $\lambda_1 = 3$ and $\lambda_2 = 8$ are eigenvalues of $F$ in $\mathbb{Z}_{10}$ because $p(3) = 5$ and $p(8) = 5$ and $\gcd(5,10) \ne 1$.
$$E_3 = \mathrm{null}\begin{bmatrix} -3 & 1 \ 1 & -2 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 7 & 1 \ 1 & 8 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 8 \ 7 & 1 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 8 \ 0 & 5 \end{bmatrix} = \mathrm{span}\begin{bmatrix} 6 \ -2 \end{bmatrix}$$
$$E_8 = \mathrm{null}\begin{bmatrix} -8 & 1 \ 1 & -7 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 2 & 1 \ 1 & 3 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 3 \ 2 & 1 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 3 \ 0 & 5 \end{bmatrix} = \mathrm{span}\begin{bmatrix} 6 \ -2 \end{bmatrix}$$
Although $\lambda_1$ and $\lambda_2$ are distinct, they have the same eigenspace so we do not have two linearly independent eigenvectors to diagonalize $F$.
**Proposition .1.** If $m \mid p(\lambda_1)$ and $m \mid p(\lambda_2)$ for distinct eigenvalues $\lambda_1$ and $\lambda_2$, and $\gcd(\lambda_1 - \lambda_2, m) = 1$, then
$$f_n \equiv (\lambda_1 - \lambda_2)^{-1}(\lambda_1^n - \lambda_2^n) \pmod m$$
_Proof._ If $m \mid p(\lambda)$ then $\lambda(\lambda - 1) \equiv 1 \pmod m$ and $\lambda(1-\lambda) \equiv -1 \pmod m$.
$$E_\lambda = \mathrm{null}\begin{bmatrix} -\lambda & 1 \ 1 & 1-\lambda \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 1-\lambda \ -\lambda & 1 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 1-\lambda \ 0 & \lambda(1-\lambda)+1 \end{bmatrix} = \mathrm{null}\begin{bmatrix} 1 & 1-\lambda \ 0 & 0 \end{bmatrix} = \mathrm{span}\begin{bmatrix} \lambda-1 \ 1 \end{bmatrix}$$
Let $\mathbf{v}_1$ and $\mathbf{v}_2$ be eigenvectors corresponding to $\lambda_1$ and $\lambda_2$ and let $P = [\mathbf{v}_1\ \mathbf{v}_2]$.
$$F = PDP^{-1}$$ $$F^n = PD^nP^{-1} = \begin{bmatrix} \lambda_1-1 & \lambda_2-1 \ 1 & 1 \end{bmatrix}\begin{bmatrix} \lambda_1^n & 0 \ 0 & \lambda_2^n \end{bmatrix}\frac{1}{\det(P)}\begin{bmatrix} 1 & 1-\lambda_2 \ -1 & \lambda_1-1 \end{bmatrix}$$
$$= \frac{1}{\det(P)}\begin{bmatrix} \lambda_1-1 & \lambda_2-1 \ 1 & 1 \end{bmatrix}\begin{bmatrix} \lambda_1^n & (1-\lambda_2)\lambda_1^n \ -\lambda_2^n & (\lambda_1-1)\lambda_2^n \end{bmatrix}$$
$$= \frac{1}{\det(P)}\begin{bmatrix} (\lambda_1-1)\lambda_1^n+(1-\lambda_2)\lambda_2^n & (\lambda_1-1)(1-\lambda_2)\lambda_1^n+(\lambda_1-1)(\lambda_2-1)\lambda_2^n \ \lambda_1^n-\lambda_2^n & (1-\lambda_2)\lambda_1^n+(\lambda_1-1)\lambda_2^n \end{bmatrix}$$
$$F^n = \begin{bmatrix} f_{n-1} & f_n \ f_n & f_{n+1} \end{bmatrix}$$
$$f_n = \frac{1}{\det(P)}\lambda_1^n - \lambda_2^n \pmod m$$
$$f_n = (\lambda_1-\lambda_2)^{-1}(\lambda_1^n - \lambda_2^n) \pmod m \qquad \blacksquare$$
**Remark 7.** If $m \mid p(\lambda_1)$ and $m \mid p(\lambda_2)$, this does not imply $\gcd(\lambda_1-\lambda_2, m) = 1$. E.g. in $\mathbb{Z}_{55}$, $p(48) \equiv 0 \pmod{55}$ and $p(8) \equiv 0 \pmod{55}$ but $\gcd(48-8, 55) = 5$.
The eigenvalues in these examples have been found by trying each element in $\mathbb{Z}_m$. Eigenvalues can be found directly if the equation from Theorem 2.2 is used. There are some advantages to using prime $m$. When $m$ is prime, 2 is invertible and a method to calculate $\sqrt5$ is possible for some primes. Also $\gcd(\lambda_1-\lambda_2, m) = 1$ is always true, so the equation from Proposition .1 can be used once the eigenvalues have been found.
**Definition .1.** Given integers $a$ and $p$, if $x^2 \equiv a \pmod p$ for some $x \in {1,2,\dots,p-1}$ then $a$ is a _quadratic residue_ mod $p$.
**Definition .2.** Given odd prime $p$ and integer $a$, the _Legendre symbol_ is
$$(a/p) = a^{\frac{p-1}{2}} \pmod p$$
**Theorem .2.** For $p$ an odd prime and $a \in \mathbb{Z}_p^*$
$$a \text{ is a quadratic residue} \pmod p \iff (a/p) \equiv 1 \pmod p$$
_Proof._ [9] Suppose $x^2 \equiv a \pmod p$ for some $x \in \mathbb{Z}_p^*$. Then by using Fermat's Theorem
$$a^{\frac{p-1}{2}} = (x^2)^{\frac{p-1}{2}} = x^{p-1} \equiv 1 \pmod p$$
Suppose $(a/p) = 1$ and let $a = g^k$ for primitive $g \in \mathbb{Z}_p^*$ and $k \in {1,2,\dots,p-1}$. Note: this is always possible for prime $p$.
$$a^{\frac{p-1}{2}} = (g^k)^{\frac{p-1}{2}} = g^{k(\frac{p-1}{2})} \equiv 1 \pmod p \quad \text{(by assumption)}$$ $$g^{k(\frac{p-1}{2})} = g^{p-1} \pmod p \quad \text{(because primitive)}$$
Therefore $(p-1) \mid k(\tfrac{p-1}{2})$, so $k$ is even. Then $a = g^k = g^{2m} = (g^m)^2$ for some $m \in \mathbb{N}$. Thus $a$ is a quadratic residue. $\blacksquare$
**Theorem .3 (Law of Quadratic Reciprocity).** For distinct odd primes $p$ and $q$,
1. If both $p$ and $q \equiv 3 \pmod 4$, then either $(q/p) = 1$ or $(p/q) = 1$ but not both.
2. If $p$ or $q \equiv 1 \pmod 4$, then $(q/p) = 1 \iff (p/q) = 1$.
**Proposition .4.** For odd prime $p$, 5 is a quadratic residue $\iff p \equiv 1$ or $-1 \pmod{10}$.
_Proof._ By Quadratic reciprocity,
$$(5/p) = 1 \iff (p/5) = 1$$ $$(5/p) = 1 \iff p^{\frac{5-1}{2}} \equiv 1 \pmod 5$$ $$(5/p) = 1 \iff p^2 \equiv 1 \pmod 5$$ $$(5/p) = 1 \iff p \equiv 1 \text{ or } p \equiv -1 \pmod 5$$ $$(5/p) = 1 \iff p = 5k \pm 1$$ $$(5/p) = 1 \iff p = 5(2m) \pm 1 \quad \text{(odd } k \text{ gives even } p\text{)}$$ $$(5/p) = 1 \iff p \equiv 1 \text{ or } p \equiv -1 \pmod{10} \qquad \blacksquare$$
Now that we can identify which primes have 5 as a quadratic residue, there still remains the problem of calculating $\sqrt5$. In general, calculating square roots of quadratic residues has the same level of complexity as integer factorization [12].
**Proposition .5.** For given integer $a$, if $p \equiv 3 \pmod 4$ then $\sqrt{a} \equiv a^{\frac{p+1}{4}} \pmod p$.
_Proof._ Assume $a$ is a quadratic residue. Then
$$a^{\frac{p-1}{2}} \equiv 1 \pmod p$$ $$a \cdot a^{\frac{p-1}{2}} \equiv a \pmod p$$ $$a^{\frac{p-1}{2}+1} \equiv a \pmod p$$ $$a^{\frac{p+1}{2}} \equiv a \pmod p$$ $$\sqrt{a^{\frac{p+1}{2}}} \equiv \sqrt{a} \pmod p$$ $$a^{\frac{p+1}{4}} \equiv \sqrt{a} \pmod p$$
There is always a solution because $4 \mid (p+1)$. $\blacksquare$
**Remark 8.** In the first 1000 primes the proportion that are of the form $10k \pm 1$ is 0.491. The proportion that are of the form $4k+3$ is 0.504. The proportion that satisfy both forms is 0.251.
---
## References
1. Buchmann, J.A. _Introduction to Cryptography_, Springer-Verlag New York Inc, 2001.
2. J. Zou, R. K. Ward, D. Qi. _A new image scrambling method based on Fibonacci numbers_, Proceedings of the 2004 International Symposium on Circuits and Systems ISCAS 2004, May 23–26, 2004, Vancouver, Canada, Vol 3. pg 965.
3. J. Zou, X. Tie, R. K. Ward, D. Qi. _Some Novel Image Scrambling Methods Based on Affine Modular Matrix Transformation_, Journal of Information & Computational Science 2:1 (2005) 223–227.
4. J. Zou, R. K. Ward, D. Qi. _The Generalized Fibonacci Transformations and Application to Image Scrambling_, Acoustics, Speech, and Signal Processing, 2004. Proceedings. (ICASSP '04). IEEE International Conference on, Vol 3. pg 385.
5. Poole, D. _Linear Algebra: A Modern Introduction_, 2nd Ed, Australia: Belmont, CA: Thomson Brooks/Cole, 2006.
6. W. Zeng, H. Yu, and C. Lin. _Multimedia Security Technologies for Digital Rights Management_, Amsterdam; Boston: Academic Press, 2006.
7. http://en.wikipedia.org/wiki/Fibonacci_number
8. http://mcraeclan.com/MathHelp/BasicNumberSquareQuadraticResidues.htm (Read 30/11/2006)
9. http://rooster.stanford.edu/~ben/maths/numbertheory/qr.html (Read 30/11/2006)
10. http://mathworld.wolfram.com/QuadraticResidue.html (Read 30/11/2006)
11. http://en.wikipedia.org/wiki/Quadratic_reciprocity (Read 5/12/2006)
12. http://en.wikipedia.org/wiki/Quadratic_residue (Read 5/12/2006)
---
I asked if this algorithm could be modified to handle images of any size instead of pinned Fibonacci numbered sizes. This was the response I received, but I did not receive any examples, code or math:
Good news: the paper's own math already gives us what we need — the restriction to specific sizes (like fn+1×fn+1fn+1​×fn+1​) only applied to Zou's _scaled_ transformation (Definition 4.3). The plain **Fibonacci affine transform**(Definition 4.1, T(x)=Fx+b  mT(x)=Fx+bmodm) is periodic for _every_ modulus mm, because det⁡(F)=−1det(F)=−1 is coprime to every integer (Proposition 4.1). The only real constraint is that FF acts on a **square** Zm2Zm2​grid.
So to handle _any_ image size, we just need two small extensions beyond the paper:
1. **Pad to a square.** Any W×HW×H image can be embedded in a square canvas of side N=max⁡(W,H)N=max(W,H) (no need for NN to be a Fibonacci number, or a power of two, or anything special — any NNworks because of the fact above).
2. **Use the exact inverse instead of relying on the period.** The paper decrypts by re-applying TT until the period comes back around, which the authors themselves call inefficient (up to 6N6N applications). Since F−1≡[−1110](modN)F−1≡[−11​10​](modN) always exists, we can decrypt in exactly as many steps as we encrypted — no searching for the period.
That turns the "encryption key" into: a **round count** tt (how many times FF is applied — more rounds = more scrambled) and a **sequence of offset vectors** b1,…,btb1​,…,bt​ (derived from a passphrase), applied in order to encrypt and in reverse with F−1F−1 to decrypt.
I built this as a working tool below: type text, it's rendered to an image, padded to a square, and scrambled with this generalized Fibonacci map — fully reversible with the same key.
+2
View File
@@ -8,6 +8,7 @@
#include "network/network_manager.h"
#include "network/license_status.h"
#include "network/system_name_sync.h"
#include "schedule/schedule_manager.h"
#include "audio/audio_manager.h"
#include "audio/audio_downloader.h"
@@ -21,6 +22,7 @@ struct AppState {
radiantwave::network::NetworkManager network;
radiantwave::license::LicenseStatusTracker licenseTracker;
radiantwave::systemname::SystemNameSync systemNameSync;
radiantwave::schedule::ScheduleManager scheduleManager;
radiantwave::audio::AudioManager audio;
radiantwave::audio::AudioDownloader audioDownloader;
bool quitRequested = false;
+1
View File
@@ -17,6 +17,7 @@ struct ProclamationConfig {
int font_size = 32; // font size in pt
std::string current_id = ""; // currently active proclamation ID
bool schedule_active = false; // follow the portal's listening schedule
bool schedule_enabled = true; // local Schedule on/off toggle (default on)
};
struct AppConfig {
+3
View File
@@ -253,6 +253,9 @@ void ConfigManager::loadAndMergeDefaults() {
if (proc_node.hasChild("schedule_active")) {
merged.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(defaults.proclamation.schedule_active);
}
if (proc_node.hasChild("schedule_enabled")) {
merged.proclamation.schedule_enabled = proc_node.getChild("schedule_enabled").asBool(defaults.proclamation.schedule_enabled);
}
}
{
+4
View File
@@ -234,6 +234,9 @@ AppConfig parseConfigFromYaml(const std::string& yamlContent) {
if (proc_node.hasChild("schedule_active")) {
config.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(config.proclamation.schedule_active);
}
if (proc_node.hasChild("schedule_enabled")) {
config.proclamation.schedule_enabled = proc_node.getChild("schedule_enabled").asBool(config.proclamation.schedule_enabled);
}
}
return config;
@@ -260,6 +263,7 @@ std::string serializeConfigToYaml(const AppConfig& config) {
ss << " font_size: " << config.proclamation.font_size << "\n";
ss << " current_id: \"" << config.proclamation.current_id << "\"\n";
ss << " schedule_active: " << (config.proclamation.schedule_active ? "true" : "false") << "\n";
ss << " schedule_enabled: " << (config.proclamation.schedule_enabled ? "true" : "false") << "\n";
return ss.str();
}
+6
View File
@@ -166,6 +166,11 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
// timer; license validation also applies the name (see license_service).
state->systemNameSync.start(state->db);
// Background Schedule sync. Loads the on-disk cache immediately, then pulls
// on boot, on a 5-minute timer, and on demand. Playback reads it locally.
state->scheduleManager.setEnabled(state->config.get().proclamation.schedule_enabled);
state->scheduleManager.start(state->db, state->config.get().timezone);
state->config.onChanged(
[state](const radiantwave::config::AppConfig &new_config) {
SDL_Log("Live config update received: volume=%d, dynamic_audio=%d",
@@ -275,6 +280,7 @@ void SDL_AppQuit(void *appstate, SDL_AppResult result) {
auto *state = static_cast<AppState *>(appstate);
if (state) {
state->audioDownloader.shutdown();
state->scheduleManager.shutdown();
state->systemNameSync.shutdown();
state->licenseTracker.shutdown();
state->audio.shutdown();
+152
View File
@@ -0,0 +1,152 @@
#include "schedule_toggle_page.h"
#include "app_state.h"
#include "config/app_config.h"
#include "logical_size.h"
#include <SDL3/SDL.h>
void ScheduleTogglePage::onPush(AppState& state)
{
state_ = &state;
TTF_Init();
createFonts();
enabled_ = state.config.get().proclamation.schedule_enabled;
statusMessage_ = "Press ENTER to turn the Schedule on or off";
}
void ScheduleTogglePage::onResume(AppState& state)
{
state_ = &state;
createFonts();
enabled_ = state.config.get().proclamation.schedule_enabled;
}
void ScheduleTogglePage::onPause(AppState& state)
{
state.keybinds.unbindAll();
destroyFonts();
destroyTextures();
}
void ScheduleTogglePage::onPop(AppState& state)
{
destroyFonts();
destroyTextures();
TTF_Quit();
}
void ScheduleTogglePage::createFonts()
{
destroyFonts();
fontTitle_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 52);
fontHeader_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 28);
fontBody_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 32);
fontSmall_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 22);
}
void ScheduleTogglePage::destroyFonts()
{
if (fontTitle_) { TTF_CloseFont(fontTitle_); fontTitle_ = nullptr; }
if (fontHeader_) { TTF_CloseFont(fontHeader_); fontHeader_ = nullptr; }
if (fontBody_) { TTF_CloseFont(fontBody_); fontBody_ = nullptr; }
if (fontSmall_) { TTF_CloseFont(fontSmall_); fontSmall_ = nullptr; }
}
void ScheduleTogglePage::destroyTextures()
{
titleText_.destroy();
stateText_.destroy();
statusText_.destroy();
hintText_.destroy();
}
void ScheduleTogglePage::toggle()
{
if (!state_) return;
const bool next = !enabled_;
const bool ok = state_->config.update([next](radiantwave::config::AppConfig& cfg) {
cfg.proclamation.schedule_enabled = next;
});
if (ok) {
enabled_ = next;
statusMessage_ = enabled_ ? "Schedule enabled" : "Schedule disabled";
// Push the new state to the engine and nudge a refresh. While off the
// engine takes no action but keeps its cache updating.
state_->scheduleManager.setEnabled(enabled_);
state_->scheduleManager.requestRefresh();
} else {
statusMessage_ = "Failed to save";
}
}
void ScheduleTogglePage::handleEvent(const SDL_Event& event)
{
if (!state_) return;
if (event.type == SDL_EVENT_KEY_DOWN) {
SDL_Keycode key = event.key.key;
if (key == SDLK_RETURN || key == SDLK_KP_ENTER) {
toggle();
} else if (key == SDLK_ESCAPE) {
state_->pages.pop(*state_);
}
}
}
void ScheduleTogglePage::update(float dt)
{
}
void ScheduleTogglePage::render(SDL_Renderer* renderer)
{
int winW, winH;
radiantwave::getLayoutSize(renderer, &winW, &winH);
float fw = static_cast<float>(winW);
float fh = static_cast<float>(winH);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
SDL_FRect bgRect{0, 0, fw, fh};
SDL_RenderFillRect(renderer, &bgRect);
if (!fontTitle_ || !fontHeader_ || !fontBody_ || !fontSmall_) return;
SDL_Color white = {255, 255, 255, 255};
SDL_Color green = {110, 220, 130, 255};
SDL_Color gray = {180, 180, 180, 255};
float currentY = 40.0f;
titleText_.ensure(renderer, fontTitle_, "Schedule", white);
if (titleText_.texture) {
float x = (fw - titleText_.width) / 2.0f;
SDL_FRect dst{x, currentY, titleText_.width, titleText_.height};
SDL_RenderTexture(renderer, titleText_.texture, nullptr, &dst);
currentY += titleText_.height + 50.0f;
}
std::string value = enabled_ ? "On" : "Off";
stateText_.ensure(renderer, fontBody_, "Schedule: " + value,
enabled_ ? green : gray);
if (stateText_.texture) {
float x = (fw - stateText_.width) / 2.0f;
SDL_FRect dst{x, currentY, stateText_.width, stateText_.height};
SDL_RenderTexture(renderer, stateText_.texture, nullptr, &dst);
currentY += stateText_.height + 40.0f;
}
if (!statusMessage_.empty()) {
statusText_.ensure(renderer, fontHeader_, statusMessage_, gray);
if (statusText_.texture) {
float x = (fw - statusText_.width) / 2.0f;
SDL_FRect dst{x, currentY, statusText_.width, statusText_.height};
SDL_RenderTexture(renderer, statusText_.texture, nullptr, &dst);
}
}
hintText_.ensure(renderer, fontSmall_,
"[ENTER] Toggle [ESC] Back", gray);
if (hintText_.texture) {
float x = (fw - hintText_.width) / 2.0f;
SDL_FRect dst{x, fh - 40.0f, hintText_.width, hintText_.height};
SDL_RenderTexture(renderer, hintText_.texture, nullptr, &dst);
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "page.h"
#include "text_cache.h"
#include <SDL3_ttf/SDL_ttf.h>
#include <string>
// System Settings: local Schedule on/off toggle. Default On. When Off the
// schedule engine takes no action, but the cache keeps updating. Persisted
// locally in config.yaml (proclamation.schedule_enabled).
class ScheduleTogglePage : public Page {
public:
void onPush(AppState& state) override;
void onResume(AppState& state) override;
void onPause(AppState& state) override;
void onPop(AppState& state) override;
void handleEvent(const SDL_Event& event) override;
void update(float dt) override;
void render(SDL_Renderer* renderer) override;
private:
void createFonts();
void destroyFonts();
void destroyTextures();
void toggle();
AppState* state_ = nullptr;
bool enabled_ = true;
std::string statusMessage_;
TTF_Font* fontTitle_ = nullptr;
TTF_Font* fontHeader_ = nullptr;
TTF_Font* fontBody_ = nullptr;
TTF_Font* fontSmall_ = nullptr;
CachedText titleText_;
CachedText stateText_;
CachedText statusText_;
CachedText hintText_;
};
+185
View File
@@ -0,0 +1,185 @@
#pragma once
// ---------------------------------------------------------------------------
// Fibonacci affine image scrambling (RadiantWave scroller text obfuscation)
//
// This module implements the Fibonacci affine transformation as a reversible
// image permutation. The RadiantWave scroller uses it to render proclamation
// text as scrambled glyphs: every code point's glyph is permuted by the map
// below so letterforms are destroyed and the text is never readable on screen,
// while the exact inverse recovers the original glyph given the same key.
//
// ---------------------------------------------------------------------------
// [Published: Williams 2007] -- concepts from the published research
//
// Ben Williams, "Some properties of digital image scrambling methods using
// Fibonacci numbers", MATH491 Summer Research Project 2006-2007, Department
// of Mathematics and Statistics, University of Canterbury, 2007 (mirrored in
// this repository as algorithm.md).
//
// Mathematical foundations:
// - The Fibonacci matrix (Definition 2.2) is
// F = [ 0 1 ]
// [ 1 1 ]
// so consecutive Fibonacci numbers satisfy u(n+1) = F.u(n). It squares to
// the Arnold matrix F^2 = [[1,1],[1,2]] (Remark 3).
// - The Fibonacci affine transformation (Definition 4.1) acts on the square
// grid Z_m^2 = {0..m-1}^2 with modular arithmetic (Section 3):
// T(x) = F.x + b (mod m), b in Z_m^2.
// - An affine map x -> A.x + b is a bijection of Z_m^2 iff gcd(det A, m) = 1
// (Theorem 3.1 / Corollary 3.2), and it is periodic under the same
// condition (Theorem 3.4).
// - det F = -1, which is coprime to every m. By Proposition 4.1 the
// Fibonacci affine transformation is therefore a permutation of Z_m^2 for
// EVERY modulus m: no constraint whatsoever on the grid size.
// - The paper decrypts by re-applying T until the period recurs. Section 5
// notes the period of F mod m can reach 6m, so that route is slow; the
// paper never uses the inverse transform.
//
// ---------------------------------------------------------------------------
// [Adapted beyond the paper] -- generalization from algorithm.md's appendix
// (lines 491-500)
//
// Two extensions turn the published square-only transform into an
// arbitrary-size scrambler with a fast, exact round trip:
//
// 1. Arbitrary image sizes. Any W x H image is embedded in a square canvas
// of side
// N = canvasSize(W, H) = max(W, H)
// and T acts on Z_N^2. Because det F = -1 is invertible for every N, no
// Fibonacci-number, power-of-two, or other size constraint applies. (The
// size restriction in the paper was specific to Zou's scaled transform,
// Definition 4.3, not to the plain Fibonacci affine map.)
//
// 2. Exact inverse instead of period iteration. Since
// F^-1 = [ -1 1 ]
// [ 1 0 ]
// exists for every modulus, decryption applies the inverse directly and
// costs exactly as many rounds as encryption -- not up to 6N applications
// of T. This also turns the key into (round count, offset sequence):
// scramble applies the per-round offsets b1..bt in order, and unscramble
// applies the inverse in reverse order.
//
// ---------------------------------------------------------------------------
// [This implementation] -- project-specific engineering decisions
//
// Gather-form permutation. Each round is a single gather pass:
// scramble: dst[y] = src[preimage(y)], preimage(y) = T^-1(y)
// unscramble: dst[y] = src[image(y)], image(y) = T(y)
// Every output pixel is produced from exactly one input pixel, so there are
// never holes or collisions, and the padding convention is filled directly
// where a preimage falls outside the source. (The paper describes the
// forward scatter -- source pixel at x moves to T(x); gather is the
// equivalent output-directed formulation.)
//
// Composition semantics. Scrambling applies the inverse maps in round order
// (T1^-1, T2^-1, ..., Tt^-1); unscrambling applies the forward maps in
// reverse order (Tt, ..., T1). The composition telescopes:
// T1^-1( T2^-1( ... Tt^-1( Tt( ... T2( T1(y) ) ... ) ) ... ) ) = y
// so the round trip is exact in exactly `rounds` passes, regardless of the
// transform's period. Padding is created in round 1 and then permuted like
// any other pixel; because the composition is a bijection, each W x H output
// position of the final reverse pass maps to a canvas position holding a real
// source pixel, so no padding value ever reaches the recovered W x H region.
// (Intermediate reverse passes do read padding pixels -- they move the full
// N x N canvas -- but they are routed out of the W x H output.)
//
// Key derivation. The key is the (seed, rounds) pair. Each round derives its
// offset vector b = (b0, b1) from `seed` via splitmix64 -- two consecutive
// pure-integer draws reduced mod N:
// b0 = splitmix64(seed) mod N, b1 = splitmix64(seed) mod N.
// Pure integer arithmetic makes the offsets bit-identical on every platform,
// so a compile-time seed produces identical scrambling everywhere. The
// scroller fixes the seed at compile time (kDefaultSeed), meaning the same
// glyph scrambles identically on every device and every run, and no key
// state is ever persisted or transmitted.
//
// Padding convention. During scramble the W x H source sits in the natural
// top-left embedding of the N x N canvas. Canvas positions whose preimage
// falls outside [0, W) x [0, H) are written as transparent black
// (0, 0, 0, 0), which is invisible on a black background.
// Because the map is a bijection, the W x H source pixels land on W x H
// distinct canvas positions, and the remaining N^2 - W*H positions are
// padding. unscramble() reads the whole N x N canvas and writes only the
// W x H region, reproducing the source exactly. Display uses the matching
// crop-window convention: render the top-left W x H sub-rectangle of the
// scrambled canvas.
//
// Whole pixels. Each RGBA8888 pixel, alpha included, is permuted as a unit,
// so a round trip preserves alpha exactly.
//
// In-place hazard / scratch. A gather pass is not safe in place: within one
// pass, dst[y] = src[p(y)] reads source position p(y) after an earlier write
// at that same position may have replaced it (whenever p(y) < y in iteration
// order). Every pass therefore reads and writes distinct buffers. Two
// ping-pong scratch buffers of N*N*4 bytes are reused across rounds, so read
// and write never alias while memory stays bounded.
//
// Numerics. All coordinate products are accumulated in int64_t and reduced
// mod N, which is exact for any N whose squares fit comfortably in int64_t
// (N <= 2^30). Rounds below 1 are clamped to 1; N == 1 (a 1x1 image) yields
// the identity map, which is correct. In practice N is the side of a glyph
// tile, at most a few hundred pixels.
//
// Format & portability. The module is SDL-free and pure C++17: it operates
// on tightly packed RGBA8888 pixels (4 bytes per pixel, row stride = width *
// 4) and keeps no state between calls. Any caller can adopt it; the scroller
// uses it per glyph (see scroller_page.cpp).
//
// Why CPU. Glyph tiles are tiny and every distinct code point's tile is
// scrambled at most once per font size and then cached, so the total cost is
// a one-time, amortized O(N^2 * rounds) integer permute per character. A GPU
// path would need per-texture compute passes or shader support that buys
// nothing here.
// ---------------------------------------------------------------------------
#include <cstdint>
namespace radiantwave {
namespace scramble {
// Fixed compile-time seed so every device renders the same glyph scrambling.
// The value is splitmix64's golden-ratio constant; any constant would do, the
// requirement is only that it never changes across runs and platforms.
inline constexpr uint64_t kDefaultSeed = 0x9E3779B97F4A7C15ULL;
// Default round count. Each round applies the affine map once; more rounds mix
// pixels across a larger fraction of the canvas. Values below 1 are clamped to
// 1 by the API.
inline constexpr int kDefaultRounds = 4;
// Per-call configuration: how many rounds and which key (seed).
struct ScrambleConfig
{
int rounds = kDefaultRounds;
uint64_t seed = kDefaultSeed;
};
// Square canvas side N for a W x H image: N = max(W, H).
// Returns 0 when width or height is < 1 (the scramble/unscramble entry points
// reject N == 0).
int canvasSize(int width, int height);
// Scramble a W x H RGBA8888 image (tightly packed, stride = width * 4) into
// an N x N RGBA8888 canvas where N = canvasSize(width, height). `dst` must
// hold at least N*N*4 bytes. Out-of-canvas source coordinates are written as
// transparent black per the padding convention above. `src` and `dst` must not
// alias.
//
// Returns false (writing nothing to `dst`) when width/height < 1, when either
// pointer is null, or when N exceeds the int64-safe bound (2^30).
bool scramble(const uint8_t* src, uint8_t* dst, int width, int height,
const ScrambleConfig& config);
// Exact inverse of scramble(): reads the N x N RGBA8888 canvas (stride =
// N * 4) and writes back the top-left W x H RGBA8888 region (stride = width *
// 4). `dst` must hold at least W*H*4 bytes. Must be called with the same
// (width, height, config) as the matching scramble() -- the key is the same
// tuple. `src` and `dst` must not alias.
//
// Returns false under the same conditions as scramble().
bool unscramble(const uint8_t* src, uint8_t* dst, int width, int height,
const ScrambleConfig& config);
} // namespace scramble
} // namespace radiantwave
+39 -26
View File
@@ -214,15 +214,32 @@ void ScrollerPage::buildStreams(AppState &state) {
streamIndex_ = 0;
scheduleChangePending_ = false;
// Schedule state: only an active schedule with a currently-matching block
// changes the play list. The block boundary is handled in advanceStream(),
// so rebuilding mid-playback never truncates the current proclamation.
// Schedule state: resolve the active Block from the schedule manager (which
// owns the fetched/cached payload and the local on/off toggle). The block
// boundary is handled in advanceStream(), so rebuilding mid-playback never
// truncates the current proclamation.
const radiantwave::config::AppConfig cfg = state.config.get();
scheduleActive_ = cfg.proclamation.schedule_active;
timezone_ = cfg.timezone;
scheduleBlocks_ = scheduleActive_ ? state.db.getScheduleBlocks()
: std::vector<radiantwave::db::ScheduleBlockRecord>{};
activeBlockIndex_ = currentScheduleBlockIndex();
// Keep the engine's local toggle in step with config (it may have been
// changed in System Settings or hot-reloaded).
state.scheduleManager.setEnabled(cfg.proclamation.schedule_enabled);
const int now = radiantwave::timezone::currentLocalMinutes(timezone_);
const radiantwave::schedule::Weekday weekday =
radiantwave::schedule::weekdayFromIndex(
radiantwave::timezone::currentLocalWeekday(timezone_));
const radiantwave::schedule::Decision decision =
state.scheduleManager.activeBlock(now, weekday);
scheduleActive_ = state.scheduleManager.enabled();
if (decision.hasBlock) {
activeBlockProclamationIds_ = decision.block.proclamation_ids;
activeBlockDynamicAudio_ = decision.block.dynamic_audio;
activeBlockIndex_ = 1; // any non-negative marker: a block is active
} else {
activeBlockProclamationIds_.clear();
activeBlockDynamicAudio_ = false;
activeBlockIndex_ = -1;
}
auto proclamations = state.db.getActiveProclamations();
if (proclamations.empty()) {
@@ -264,9 +281,8 @@ void ScrollerPage::buildStreams(AppState &state) {
const std::vector<std::string>* scheduledIds = nullptr;
std::vector<std::string> scheduledOrder;
if (activeBlockIndex_ >= 0 &&
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size())) {
scheduledOrder = scheduleBlocks_[activeBlockIndex_].proclamation_ids;
if (activeBlockIndex_ >= 0 && !activeBlockProclamationIds_.empty()) {
scheduledOrder = activeBlockProclamationIds_;
scheduledIds = &scheduledOrder;
}
@@ -275,26 +291,25 @@ void ScrollerPage::buildStreams(AppState &state) {
DEFAULT_PROPLEAMATION_ID, scheduledIds);
}
int ScrollerPage::currentScheduleBlockIndex() const {
if (!scheduleActive_ || scheduleBlocks_.empty()) {
return -1;
bool ScrollerPage::scheduleBlockActiveNow() const {
if (!state_) {
return false;
}
const int now = radiantwave::timezone::currentLocalMinutes(timezone_);
for (size_t i = 0; i < scheduleBlocks_.size(); ++i) {
const radiantwave::db::ScheduleBlockRecord &block = scheduleBlocks_[i];
if (radiantwave::scroller::minuteInScheduleBlock(
now, block.start_minutes, block.end_minutes, block.overnight)) {
return static_cast<int>(i);
}
}
return -1;
const radiantwave::schedule::Weekday weekday =
radiantwave::schedule::weekdayFromIndex(
radiantwave::timezone::currentLocalWeekday(timezone_));
return state_->scheduleManager.activeBlock(now, weekday).hasBlock;
}
void ScrollerPage::pollScheduleChange() {
if (!scheduleActive_) {
// The manager owns the local toggle; when off, there is never an active block.
if (!state_) {
return;
}
if (currentScheduleBlockIndex() != activeBlockIndex_) {
const bool activeNow = scheduleBlockActiveNow();
const bool hadBlock = activeBlockIndex_ >= 0;
if (activeNow != hadBlock) {
scheduleChangePending_ = true;
}
}
@@ -303,9 +318,7 @@ bool ScrollerPage::effectiveDynamicAudio() const {
// dynamicAudio=true on the active block forces dynamic audio, overriding
// whatever the device selected. false means "don't override": the device's
// own selection (dynamic or a fixed track) applies as usual.
if (scheduleActive_ && activeBlockIndex_ >= 0 &&
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size()) &&
scheduleBlocks_[activeBlockIndex_].dynamic_audio) {
if (scheduleActive_ && activeBlockIndex_ >= 0 && activeBlockDynamicAudio_) {
return true;
}
return state_ ? state_->config.get().audio.dynamic_audio : false;
+9 -8
View File
@@ -83,9 +83,8 @@ private:
bool addLineAt(float baseY);
void advanceStream();
void switchAudioForCurrentStream();
// Re-reads the schedule from the DB/config and records which block (if any)
// matches the clock right now. Returns the matching index, or -1.
int currentScheduleBlockIndex() const;
// Re-reads the schedule and records whether a block is active right now.
bool scheduleBlockActiveNow() const;
// If the active block changed, latch a pending switch that is applied at the
// next proclamation boundary so the current proclamation is never cut off.
void pollScheduleChange();
@@ -108,12 +107,14 @@ private:
int streamIndex_ = 0;
bool prefilling_ = false;
// Schedule playback. `scheduleActive_` mirrors config; `scheduleBlocks_` is
// the cached schedule; `activeBlockIndex_` is the block currently playing
// (-1 = normal mode / no matching block). A change is latched in
// `scheduleChangePending_` and applied when the current proclamation ends.
// Schedule playback. `scheduleActive_` mirrors config; the active Block is
// resolved from ScheduleManager each poll; `activeBlockIndex_` is the block
// currently playing (-1 = normal mode / no matching block). A change is
// latched in `scheduleChangePending_` and applied when the current
// proclamation ends.
std::string timezone_;
std::vector<radiantwave::db::ScheduleBlockRecord> scheduleBlocks_;
std::vector<std::string> activeBlockProclamationIds_;
bool activeBlockDynamicAudio_ = false;
bool scheduleActive_ = false;
int activeBlockIndex_ = -1;
bool scheduleChangePending_ = false;
+1
View File
@@ -7,6 +7,7 @@ std::vector<SystemSettingEntry> systemSettingEntries() {
{"Email Address", SystemSettingId::EmailAddress},
{"License Key", SystemSettingId::LicenseKey},
{"System Name", SystemSettingId::SystemName},
{"Schedule", SystemSettingId::Schedule},
{"WiFi Setup", SystemSettingId::WiFiSetup},
{"Time Zone Setup", SystemSettingId::Timezone},
{"Date Format", SystemSettingId::DateFormat},
+1
View File
@@ -10,6 +10,7 @@ enum class SystemSettingId {
EmailAddress,
LicenseKey,
SystemName,
Schedule,
WiFiSetup,
Timezone,
DateFormat,
+4
View File
@@ -7,6 +7,7 @@
#include "timezone_page.h"
#include "wifi_setup_page.h"
#include "system_name_page.h"
#include "schedule_toggle_page.h"
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <algorithm>
@@ -80,6 +81,9 @@ void SystemSettingsPage::activateSelected(AppState &state)
case radiantwave::settings::SystemSettingId::SystemName:
state.pages.push(state, std::make_unique<SystemNamePage>());
break;
case radiantwave::settings::SystemSettingId::Schedule:
state.pages.push(state, std::make_unique<ScheduleTogglePage>());
break;
case radiantwave::settings::SystemSettingId::WiFiSetup:
state.pages.push(state, std::make_unique<WiFiSetupPage>());
break;
+10 -7
View File
@@ -7,19 +7,21 @@ namespace settings = radiantwave::settings;
TEST(SystemSettingsMenuTest, ListsTheSystemSettingsInOrder) {
const auto entries = settings::systemSettingEntries();
ASSERT_EQ(entries.size(), 6u);
ASSERT_EQ(entries.size(), 7u);
EXPECT_EQ(entries[0].label, "Email Address");
EXPECT_EQ(entries[0].id, settings::SystemSettingId::EmailAddress);
EXPECT_EQ(entries[1].label, "License Key");
EXPECT_EQ(entries[1].id, settings::SystemSettingId::LicenseKey);
EXPECT_EQ(entries[2].label, "System Name");
EXPECT_EQ(entries[2].id, settings::SystemSettingId::SystemName);
EXPECT_EQ(entries[3].label, "WiFi Setup");
EXPECT_EQ(entries[3].id, settings::SystemSettingId::WiFiSetup);
EXPECT_EQ(entries[4].label, "Time Zone Setup");
EXPECT_EQ(entries[4].id, settings::SystemSettingId::Timezone);
EXPECT_EQ(entries[5].label, "Date Format");
EXPECT_EQ(entries[5].id, settings::SystemSettingId::DateFormat);
EXPECT_EQ(entries[3].label, "Schedule");
EXPECT_EQ(entries[3].id, settings::SystemSettingId::Schedule);
EXPECT_EQ(entries[4].label, "WiFi Setup");
EXPECT_EQ(entries[4].id, settings::SystemSettingId::WiFiSetup);
EXPECT_EQ(entries[5].label, "Time Zone Setup");
EXPECT_EQ(entries[5].id, settings::SystemSettingId::Timezone);
EXPECT_EQ(entries[6].label, "Date Format");
EXPECT_EQ(entries[6].id, settings::SystemSettingId::DateFormat);
}
TEST(SystemSettingsMenuTest, LabelsAreNonEmptyAndUnique) {
@@ -41,6 +43,7 @@ TEST(SystemSettingsMenuTest, EveryEntryMapsToAKnownPage) {
case settings::SystemSettingId::EmailAddress:
case settings::SystemSettingId::LicenseKey:
case settings::SystemSettingId::SystemName:
case settings::SystemSettingId::Schedule:
case settings::SystemSettingId::WiFiSetup:
case settings::SystemSettingId::Timezone:
case settings::SystemSettingId::DateFormat: