1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! Container fixtures shared by the test suites of this workspace.
//!
//! Compiled only under the `test-utils` feature, which no release build turns
//! on. It exists because building a container that passes every gate is not a
//! two-line job — see [`stable_cover`] — and both crates of the workspace need
//! one: the core suite to drive the pipeline, and the front-end suite to have
//! something for `scan` to accept.
//!
//! # What a usable container has to satisfy
//!
//! The gates of layer 1 and layer 3 pull in different directions, and the cover
//! below is shaped by that:
//!
//! - **Perceptual stability.** [`compute_stable_phash`] hashes a 32x32
//! thumbnail and refuses any image with more than one DCT coefficient within
//! `5.0` of the median. Gaussian grain on a flat background cannot satisfy
//! this: a 2000x2000 image collapses to 32x32 by averaging some four thousand
//! pixels per output sample, which drives the grain's contribution to well
//! under one coefficient unit and leaves all 64 AC coefficients piled up
//! around a near-zero median. The cover therefore carries a low-frequency
//! random field *underneath* the grain — structure coarse enough to survive
//! the downscale is the only thing that spreads those coefficients out.
//! - **Texture.** The HILL gate refuses an image whose 95th-percentile cost
//! falls below `0.10`, i.e. one that is high-energy everywhere. Grain of
//! standard deviation two sits comfortably on the accepting side.
//! - **No JPEG grid.** The block-boundary detector of layer 1 rejects anything
//! scoring above `1.3`. The field is interpolated smoothly rather than drawn
//! as blocks, so the cover introduces no periodic step of its own.
//!
//! [`compute_stable_phash`]: crate::image_io::phash::compute_stable_phash
// This module is the one place in `src/` that may panic. It builds fixtures for
// a test to consume, and a fixture that cannot be built is a test that cannot
// run: propagating an error would only move the failure to a caller whose only
// sensible reaction is the same one. The lints stay in force everywhere else.
use TAU;
use Path;
use ;
use StdRng;
use ;
use crateHillCostProvider;
use crateCostProvider;
use crate;
use cratecompute_stable_phash;
/// Side length of the covers, in pixels.
///
/// Exactly the minimum layer 1 accepts, so a fixture built here also pins that
/// boundary: a container one pixel smaller would be refused.
pub const COVER_SIDE: u32 = 2000;
/// Side length of the control grid of the low-frequency field.
///
/// Deliberately the side of the perceptual-hash thumbnail. One control point
/// per thumbnail sample is what puts energy in every coefficient the hash
/// reads, including the highest horizontal frequency it looks at.
const FIELD_GRID: usize = 32;
/// Peak deviation of the low-frequency field from mid grey, in levels.
///
/// Large enough that the AC coefficients of the thumbnail land two orders of
/// magnitude above the `5.0` stability margin, small enough that grain never
/// pushes a sample out of range and gets clipped.
const FIELD_AMPLITUDE: f32 = 95.0;
/// Share of a field cell spent easing from one control value to the next.
///
/// See [`sample_field`]: the remaining two thirds of the cell are flat, which
/// is what lets the perceptual-hash thumbnail see the control values themselves
/// rather than a heavily smoothed version of them.
const FIELD_EDGE_WIDTH: f32 = 0.25;
/// Standard deviation of the grain added on top of the field, in levels.
///
/// The quantity the HILL cost map actually measures. Two levels keeps the
/// 95th-percentile cost well above the `0.10` texture floor; raising it towards
/// four would start pressing against that gate from the other side.
const GRAIN_SIGMA: f32 = 2.0;
/// Candidate seeds [`stable_cover`] will try before giving up.
const MAX_CANDIDATES: u64 = 64;
/// Builds a container that passes every gate, and reports the seed it took.
///
/// # Why this is a search and not a constant
///
/// The stability gate compares each of the 64 AC coefficients against their
/// median, and the median is the midpoint of the two central ones — so those
/// two are always equidistant from it, and the image is accepted precisely when
/// the gap between them exceeds twice the `5.0` threshold. For a field of this
/// amplitude the gap averages a few tens of units, which makes acceptance close
/// to a coin toss on an arbitrary seed. Pinning a literal would work until the
/// day a dependency changed a rounding somewhere; searching upwards from a seed
/// costs one candidate when nothing has drifted and repairs itself when
/// something has.
///
/// Candidates are judged in memory, against the same two providers the pipeline
/// uses. Nothing is written until one passes.
///
/// # Panics
///
/// Panics when no candidate in [`MAX_CANDIDATES`] passes, which would mean the
/// generator no longer produces containers this crate accepts at all.
/// Writes the container [`stable_cover`] found to `path`, as a PNG.
///
/// # Panics
///
/// Panics when the file cannot be written; see [`stable_cover`] for the other
/// way this can fail.
/// A deterministic byte string that Zstandard cannot shrink.
///
/// The capacity gate measures the payload *after* compression, so a test that
/// wants to fill or overflow a container has to hand it something
/// incompressible. Anything with structure — a run of zeros, a counter, a byte
/// pattern with a short period — is compressed to a few dozen bytes at level 19
/// and sails through the very check it was meant to exercise.
/// Seed of the incompressible payload.
const PAYLOAD_SEED: u64 = 0x5041_594C_4F41_4421;
/// The textured cover: a mottled random field with photographic grain on top.
///
/// Parameterised by seed and side length because choosing a good seed means
/// searching for one; see [`stable_cover`].
/// Draws the `FIELD_GRID + 1` square control grid of the low-frequency field.
///
/// One extra row and column so that [`sample_field`] always has a cell to the
/// right and below the sample it is interpolating, including on the last pixel
/// of the image.
///
/// Control values are two-valued rather than drawn from the whole range. A
/// uniform draw has a standard deviation of only `amplitude / sqrt(3)`, and
/// every unit of that spread is a unit of margin the stability gate measures;
/// putting all the mass at the extremes buys a factor of `sqrt(3)` for free,
/// against a clipping budget that is what limits the amplitude in the first
/// place.
/// Interpolates the control grid at an image coordinate.
///
/// Neither a hard mosaic nor a smooth blend, but a plateau with an eased
/// shoulder, and the width of that shoulder is the whole design of this
/// fixture. It sets where the cover lands between two gates that want opposite
/// things:
///
/// - Interpolating across the full cell attenuates the thumbnail's high
/// frequencies badly. Measured that way the 64 hash coefficients had a
/// standard deviation of some 250 units, four of them fell inside the `5.0`
/// margin, and the cover was refused as perceptually unstable.
/// - Not interpolating at all leaves a step at every cell edge. Cells are 62.5
/// pixels wide against a JPEG grid of 8, so those steps would walk through
/// every phase of the grid and inflate the boundary population the artifact
/// detector measures.
///
/// A shoulder of [`FIELD_EDGE_WIDTH`] resolves both: each cell keeps a flat
/// interior, so the 32x32 thumbnail reproduces the control value almost
/// unattenuated, while the transition is spread over some twenty pixels and no
/// pair of adjacent pixels anywhere in the image carries a step.
/// Maps a position within a cell onto the eased shoulder of [`sample_field`].
///
/// Everything outside the middle [`FIELD_EDGE_WIDTH`] of the cell is clamped to
/// one endpoint or the other; the band in between runs through the classic
/// `3t^2 - 2t^3` ease, whose derivative vanishes at both ends and therefore
/// joins the two plateaus without a crease.
/// One normally distributed sample of standard deviation `sigma`, by
/// Box-Muller.
///
/// Only the cosine half of the transform is kept and the sine half discarded.
/// Storing the spare would make the function stateful, and the grain is drawn
/// four million times per channel from a seeded generator — the sequence has to
/// depend on the seed alone, not on how the caller happens to interleave its
/// calls.