Skip to main content

record_sidecar/
lib.rs

1use anyhow::{bail, Context, Result};
2use base64::{engine::general_purpose, Engine as _};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::BTreeMap;
6use std::io::Read;
7
8pub const SIDECAR_MAGIC: &[u8; 4] = b"BTS1";
9pub const SIDECAR_CONTAINER_VERSION: u8 = 1;
10pub const SIDECAR_POINTER_VERSION: u8 = 1;
11pub const SIDECAR_POINTER_LENGTH: usize = 48;
12pub const SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2: u8 = 1;
13pub const SIDECAR_POINTER_CARRIER_LABEL: u8 = 0x01;
14pub const SIDECAR_POINTER_CARRIER_INTERGROOVE: u8 = 0x02;
15pub const SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX: u8 = 0x04;
16pub const SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2: &str = "pairsign-safe-luma-v2";
17pub const SIDECAR_DEFAULT_SEED: u32 = 0x4b50_4752;
18pub const SIDECAR_PAIR_SIGN_DELTA: i16 = 4;
19pub const SIDECAR_PAIR_MAGNITUDE_DELTA: i16 = 12;
20pub const SIDECAR_PAIR_MAGNITUDE_THRESHOLD: f64 = 16.0;
21pub const SIDECAR_SAFE_V2_MIN_SCORE: u16 = 20;
22pub const SIDECAR_TYPE_OPAQUE: u8 = 0;
23pub const SIDECAR_TYPE_UTF8_TEXT: u8 = 1;
24pub const SIDECAR_TYPE_IMAGE: u8 = 2;
25pub const SIDECAR_TYPE_JSON: u8 = 3;
26pub const SIDECAR_CODEC_RAW: u8 = 0;
27pub const SIDECAR_CODEC_BROTLI: u8 = 1;
28pub const SIDECAR_CODEC_ZSTD: u8 = 2;
29pub const SIDECAR_CODEC_AVIF: u8 = 3;
30pub const SIDECAR_RAW_LENGTH_ABSENT: u32 = u32::MAX;
31pub const DISPLAY_HEADER_MAGIC: &[u8; 4] = b"BDH1";
32pub const DISPLAY_HEADER_VERSION: u8 = 1;
33pub const DISPLAY_HEADER_LENGTH: usize = 128;
34pub const DISPLAY_HEADER_NAME: &str = "bitneedle-display-header.bin";
35pub const DISPLAY_HEADER_MIME: &str = "application/vnd.bitneedle.display-header";
36pub const PACKAGE_METADATA_ITEM_NAME: &str = "bitneedle-package-metadata.json";
37pub const PACKAGE_METADATA_MIME: &str = "application/vnd.bitneedle.package-metadata+json";
38pub const PACKAGE_PHOTO_MIME: &str = "image/avif";
39pub const PACKAGE_COVER_ITEM_NAME: &str = "album-cover.avif";
40pub const PACKAGE_PATTERN_SIDECAR_ITEM_NAME: &str = "bitneedle-pattern-map";
41pub const PACKAGE_PATTERN_SIDECAR_MIME: &str = "application/vnd.bitneedle.pattern-map";
42const DISPLAY_HEADER_FLAG_COVER_SHOWN: u16 = 1 << 0;
43const DISPLAY_HEADER_FLAG_COVER_EFFECTS: u16 = 1 << 1;
44const DISPLAY_HEADER_FLAG_COVER_RGB_GRAIN: u16 = 1 << 2;
45const DISPLAY_HEADER_FLAG_INNER_SLEEVE_SHOWN: u16 = 1 << 3;
46const DISPLAY_HEADER_FLAG_COVER_EMBEDDED: u16 = 1 << 4;
47const DEFAULT_COVER_POSTERIZE_LEVELS: f64 = 64.0;
48const DEFAULT_COVER_SATURATION: f64 = 2.0;
49const DEFAULT_COVER_CONTRAST: f64 = 1.3;
50const DEFAULT_COVER_RGB_GRAIN_OPACITY: f64 = 0.84;
51const COVER_RGB_GRAIN_OPACITY_MIN: f64 = 0.4;
52const COVER_CROP_ZOOM_MIN: f64 = 1.0;
53const COVER_CROP_ZOOM_MAX: f64 = 6.0;
54const COVER_PREVIEW_SIZE: f64 = 576.0;
55const PACKAGE_PHOTO_MAX_INNER: u32 = 576;
56const PACKAGE_PHOTO_MAX_QUANTIZER: u8 = 63;
57const PACKAGE_PHOTO_DEFAULT_QUANTIZER: u8 = 32;
58const PACKAGE_PHOTO_SEARCH_MIN_QUANTIZER: u8 = 35;
59const PACKAGE_PHOTO_SEARCH_START_QUANTIZER: u8 = 47;
60const PACKAGE_PHOTO_SEARCH_REFINEMENT_RADIUS: u8 = 2;
61const PACKAGE_COVER_MAX_BUDGET_DIVISOR: u64 = 3;
62const PACKAGE_SIDECAR_SCHEME: &str = "pairsign-safe-luma-v2";
63const PACKAGE_SIDECAR_CARRIERS: [&str; 2] = ["label", "intergroove"];
64const PACKAGE_COLOR_MODE_RGB: &str = "rgb";
65const PACKAGE_COLOR_MODE_GRAYSCALE: &str = "grayscale";
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct SidecarRenderSummary {
70    pub container: String,
71    pub scheme: String,
72    pub carriers: Vec<String>,
73    pub seed: u32,
74    pub bts1_bytes: usize,
75    pub sha256: String,
76    pub carrier_pixels: usize,
77    pub carrier_pairs: usize,
78    pub capacity_bytes: usize,
79    pub used_pairs: usize,
80    pub unused_pairs: usize,
81}
82
83#[derive(Debug, Clone, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct SidecarRenderOptions {
86    pub scheme: Option<String>,
87    pub label_tuning: Option<SidecarLabelTuningOptions>,
88    pub seed: Option<u32>,
89    pub carriers: Option<Vec<String>>,
90    pub bts1_base64: Option<String>,
91    pub items: Option<Vec<SidecarItemInput>>,
92}
93
94#[derive(Debug, Clone, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct SidecarLabelTuningOptions {
97    pub enabled: Option<bool>,
98    pub strength: Option<f64>,
99    pub target_luma: Option<f64>,
100    pub grain: Option<i16>,
101}
102
103#[derive(Debug, Clone)]
104pub struct PreparedSidecarLabelTuning {
105    pub strength: f64,
106    pub target_luma: f64,
107    pub grain: i16,
108}
109
110#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct SidecarItemInput {
113    #[serde(rename = "type", alias = "itemType")]
114    pub item_type: serde_json::Value,
115    pub codec: serde_json::Value,
116    pub name: Option<String>,
117    pub mime: Option<String>,
118    pub data_base64: Option<String>,
119    pub text: Option<String>,
120    pub json: Option<serde_json::Value>,
121    pub raw_byte_length: Option<u32>,
122    #[serde(default)]
123    pub flags: u8,
124}
125
126#[derive(Debug, Clone, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct SidecarContainerValidation {
129    pub ok: bool,
130    pub version: u8,
131    pub flags: u8,
132    pub item_count: usize,
133    pub total_length: usize,
134    pub items: Vec<SidecarItemValidation>,
135}
136
137#[derive(Debug, Clone, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct SidecarItemValidation {
140    pub item_type: u8,
141    pub item_type_name: String,
142    pub codec: u8,
143    pub codec_name: String,
144    pub flags: u8,
145    pub raw_byte_length: Option<u32>,
146    pub stored_byte_length: u32,
147    pub name: String,
148    pub mime: String,
149}
150
151#[derive(Debug, Clone, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct SidecarDecodedItems {
154    pub ok: bool,
155    pub validation: SidecarContainerValidation,
156    pub items: Vec<SidecarDecodedItem>,
157}
158
159#[derive(Debug, Clone, Serialize)]
160#[serde(rename_all = "camelCase")]
161pub struct SidecarDecodedItem {
162    pub item_type: u8,
163    pub item_type_name: String,
164    pub codec: u8,
165    pub codec_name: String,
166    pub flags: u8,
167    pub raw_byte_length: Option<u32>,
168    pub stored_byte_length: u32,
169    pub decoded_byte_length: usize,
170    pub name: String,
171    pub mime: String,
172    pub stored_data_base64: String,
173    pub data_base64: String,
174    pub text: Option<String>,
175    pub json: Option<serde_json::Value>,
176}
177
178#[derive(Debug, Clone, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct PackageDisplayHeaderInput {
181    pub cover_shown: Option<bool>,
182    pub cover_effects: Option<bool>,
183    pub cover_rgb_grain: Option<bool>,
184    pub inner_sleeve_shown: Option<bool>,
185    pub cover_embedded: Option<bool>,
186    pub design_label: Option<String>,
187    pub design_class_name: Option<String>,
188    pub posterize: Option<f64>,
189    pub saturation: Option<f64>,
190    pub contrast: Option<f64>,
191    pub rgb_grain_opacity: Option<f64>,
192    pub rgb_grain_blend_index: Option<f64>,
193    pub crop: Option<PackageDisplayHeaderCrop>,
194    pub quantizer: Option<f64>,
195    pub inner: Option<f64>,
196    pub sleeve_tone_color: Option<String>,
197    pub sleeve_sparkle_opacity: Option<f64>,
198}
199
200#[derive(Debug, Clone, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct PackageDisplayHeaderCrop {
203    pub x: Option<f64>,
204    pub y: Option<f64>,
205    pub zoom: Option<f64>,
206}
207
208#[derive(Debug, Clone, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct PackageMetadataInput {
211    #[serde(default)]
212    pub photos: Vec<PackageMetadataPhotoInput>,
213    pub cover: Option<PackageMetadataCoverInput>,
214}
215
216#[derive(Debug, Clone, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct PackageMetadataPhotoInput {
219    pub id: Option<String>,
220    pub name: Option<String>,
221    pub status: Option<String>,
222    pub has_avif: Option<bool>,
223    pub credit: Option<String>,
224}
225
226#[derive(Debug, Clone, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct PackageMetadataCoverInput {
229    pub has_file: Option<bool>,
230    pub has_preview: Option<bool>,
231    pub source_photo_id: Option<String>,
232    pub source_width: Option<f64>,
233    pub source_height: Option<f64>,
234    pub width: Option<f64>,
235    pub height: Option<f64>,
236    pub inner: Option<f64>,
237    pub crop: Option<PackageDisplayHeaderCrop>,
238    pub embedded: Option<bool>,
239    pub shown: Option<bool>,
240    pub design_label: Option<String>,
241    pub effects_enabled: Option<bool>,
242    pub name: Option<String>,
243}
244
245#[derive(Debug, Clone, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct PackagePhotoItemInput {
248    pub name: Option<String>,
249}
250
251#[derive(Debug, Clone, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct PackageImageCacheKeyInput {
254    pub kind: Option<String>,
255    pub file_name: Option<String>,
256    pub file_size: Option<serde_json::Value>,
257    pub file_last_modified: Option<serde_json::Value>,
258    pub quantizer: Option<serde_json::Value>,
259    pub inner: Option<serde_json::Value>,
260    pub crop: Option<serde_json::Value>,
261    pub color_mode: Option<String>,
262}
263
264#[derive(Debug, Clone, Serialize)]
265#[serde(rename_all = "camelCase")]
266pub struct PackageImageEncodeCacheKey {
267    pub key: String,
268    pub file_key: String,
269    pub crop_key: String,
270    pub quantizer: u8,
271    pub inner: u32,
272    pub crop: PackageNormalizedCrop,
273    pub color_mode: String,
274    pub monochrome: bool,
275}
276
277#[derive(Debug, Clone, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct PackageBestFitCacheKeyInput {
280    pub kind: Option<String>,
281    pub file_name: Option<String>,
282    pub file_size: Option<serde_json::Value>,
283    pub file_last_modified: Option<serde_json::Value>,
284    pub budget_bytes: Option<serde_json::Value>,
285    pub inner: Option<serde_json::Value>,
286    pub crop_key: Option<String>,
287    pub color_mode: Option<String>,
288}
289
290#[derive(Debug, Clone, Serialize)]
291#[serde(rename_all = "camelCase")]
292pub struct PackageBestFitCacheKey {
293    pub key: String,
294    pub file_key: String,
295    pub budget_bytes: u64,
296    pub inner: u32,
297    pub crop_key: String,
298    pub color_mode: String,
299}
300
301#[derive(Debug, Clone, Serialize)]
302#[serde(rename_all = "camelCase")]
303pub struct PackageNormalizedCrop {
304    pub x: f64,
305    pub y: f64,
306    pub zoom: f64,
307}
308
309#[derive(Debug, Clone, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct PackageQuantizerSearchPlanInput {
312    pub min_quantizer: Option<serde_json::Value>,
313    pub max_quantizer: Option<serde_json::Value>,
314    pub start_quantizer: Option<serde_json::Value>,
315    pub refinement_radius: Option<serde_json::Value>,
316    #[serde(default)]
317    pub trials: Vec<PackageQuantizerTrialInput>,
318}
319
320#[derive(Debug, Clone, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct PackageQuantizerTrialInput {
323    pub quantizer: Option<serde_json::Value>,
324    pub fits: Option<bool>,
325    pub budget_bytes: Option<serde_json::Value>,
326    pub contribution_bytes: Option<serde_json::Value>,
327    pub bts1_bytes: Option<serde_json::Value>,
328}
329
330#[derive(Debug, Clone, Serialize)]
331#[serde(rename_all = "camelCase")]
332pub struct PackageQuantizerSearchPlan {
333    pub min_quantizer: u8,
334    pub max_quantizer: u8,
335    pub start_quantizer: u8,
336    pub done: bool,
337    pub next_quantizer: Option<u8>,
338    pub note: String,
339    pub best_quantizer: Option<u8>,
340}
341
342#[derive(Debug, Clone, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct PackageFitBudgetInput {
345    pub capacity: Option<serde_json::Value>,
346    pub base_bts1_bytes: Option<serde_json::Value>,
347    pub photo_count: Option<serde_json::Value>,
348}
349
350#[derive(Debug, Clone, Serialize)]
351#[serde(rename_all = "camelCase")]
352pub struct PackageFitBudget {
353    pub capacity: u64,
354    pub base_bts1_bytes: u64,
355    pub available_bytes: u64,
356    pub remaining_bytes: u64,
357    pub cover_budget_bytes: u64,
358    pub photo_count: u64,
359    pub per_photo_budget_bytes: u64,
360}
361
362#[derive(Debug, Clone, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct PackageSidecarRenderOptionsInput {
365    pub seed: Option<serde_json::Value>,
366    pub capacity_bytes: Option<serde_json::Value>,
367    pub payload_bytes: Option<serde_json::Value>,
368    pub bts1_base64: Option<String>,
369    pub items: Option<serde_json::Value>,
370}
371
372#[derive(Debug, Clone, Serialize)]
373#[serde(rename_all = "camelCase")]
374pub struct SidecarDecodeResult {
375    pub ok: bool,
376    pub descriptor: serde_json::Value,
377    pub validation: SidecarContainerValidation,
378    pub bts1_byte_length: usize,
379    pub sha256: String,
380    pub carrier_pixels: usize,
381    pub carrier_pairs: usize,
382    pub capacity_bytes: usize,
383}
384
385#[derive(Debug, Clone, Serialize)]
386#[serde(rename_all = "camelCase")]
387pub struct SidecarCapacity {
388    pub scheme: String,
389    pub carriers: Vec<String>,
390    pub carrier_pixels: usize,
391    pub carrier_pairs: usize,
392    pub capacity_bits: usize,
393    pub capacity_bytes: usize,
394    pub bits_per_pair: f64,
395    pub two_bit_pairs: usize,
396}
397
398#[derive(Debug, Clone)]
399pub struct SidecarHeaderPointer {
400    pub scheme: String,
401    pub carriers: Vec<SidecarCarrier>,
402    pub seed: u32,
403    pub length: usize,
404    pub sha256: String,
405    pub sha256_bytes: [u8; 32],
406}
407
408#[derive(Debug, Clone)]
409pub struct PreparedSidecar {
410    pub bytes: Vec<u8>,
411    pub scheme: String,
412    pub label_tuning: Option<PreparedSidecarLabelTuning>,
413    pub carriers: Vec<SidecarCarrier>,
414    pub seed: u32,
415    pub sha256: String,
416    pub sha256_bytes: [u8; 32],
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub enum SidecarCarrier {
422    Label,
423    Intergroove,
424    LeadInDeadwax,
425}
426
427impl SidecarCarrier {
428    pub fn name(self) -> &'static str {
429        sidecar_carrier_name(self)
430    }
431}
432
433pub fn sha256_digest_bytes(bytes: &[u8]) -> [u8; 32] {
434    let digest = Sha256::digest(bytes);
435    let mut out = [0u8; 32];
436    out.copy_from_slice(&digest);
437    out
438}
439
440pub fn sha256_base64url(bytes: &[u8]) -> String {
441    general_purpose::URL_SAFE_NO_PAD.encode(sha256_digest_bytes(bytes))
442}
443
444pub fn decode_base64_text(value: &str, label: &str) -> Result<Vec<u8>> {
445    let trimmed = value.trim();
446    general_purpose::URL_SAFE_NO_PAD
447        .decode(trimmed)
448        .or_else(|_| general_purpose::URL_SAFE.decode(trimmed))
449        .or_else(|_| general_purpose::STANDARD.decode(trimmed))
450        .with_context(|| format!("{label} is not valid base64"))
451}
452
453pub fn sidecar_type_name(item_type: u8) -> String {
454    match item_type {
455        SIDECAR_TYPE_OPAQUE => "opaque".to_string(),
456        SIDECAR_TYPE_UTF8_TEXT => "utf8Text".to_string(),
457        SIDECAR_TYPE_IMAGE => "image".to_string(),
458        SIDECAR_TYPE_JSON => "json".to_string(),
459        value => format!("private:{value}"),
460    }
461}
462
463pub fn sidecar_codec_name(codec: u8) -> String {
464    match codec {
465        SIDECAR_CODEC_RAW => "raw".to_string(),
466        SIDECAR_CODEC_BROTLI => "brotli".to_string(),
467        SIDECAR_CODEC_ZSTD => "zstd".to_string(),
468        SIDECAR_CODEC_AVIF => "avif".to_string(),
469        value => format!("private:{value}"),
470    }
471}
472
473fn parse_sidecar_registry_value(
474    value: &serde_json::Value,
475    label: &str,
476    names: &[(&str, u8)],
477) -> Result<u8> {
478    if let Some(number) = value.as_u64() {
479        return u8::try_from(number).with_context(|| format!("{label} exceeds u8 range"));
480    }
481    let Some(raw) = value.as_str() else {
482        bail!("{label} must be a string or integer");
483    };
484    let normalized = raw.trim().to_ascii_lowercase().replace([' ', '-', '_'], "");
485    for (name, code) in names {
486        if normalized == *name {
487            return Ok(*code);
488        }
489    }
490    bail!("unknown {label}: {raw}");
491}
492
493pub fn parse_sidecar_item_type(value: &serde_json::Value) -> Result<u8> {
494    parse_sidecar_registry_value(
495        value,
496        "sidecar item type",
497        &[
498            ("opaque", SIDECAR_TYPE_OPAQUE),
499            ("bytes", SIDECAR_TYPE_OPAQUE),
500            ("binary", SIDECAR_TYPE_OPAQUE),
501            ("utf8text", SIDECAR_TYPE_UTF8_TEXT),
502            ("text", SIDECAR_TYPE_UTF8_TEXT),
503            ("utf8", SIDECAR_TYPE_UTF8_TEXT),
504            ("image", SIDECAR_TYPE_IMAGE),
505            ("photo", SIDECAR_TYPE_IMAGE),
506            ("json", SIDECAR_TYPE_JSON),
507        ],
508    )
509}
510
511pub fn parse_sidecar_codec(value: &serde_json::Value) -> Result<u8> {
512    parse_sidecar_registry_value(
513        value,
514        "sidecar codec",
515        &[
516            ("raw", SIDECAR_CODEC_RAW),
517            ("none", SIDECAR_CODEC_RAW),
518            ("brotli", SIDECAR_CODEC_BROTLI),
519            ("br", SIDECAR_CODEC_BROTLI),
520            ("zstd", SIDECAR_CODEC_ZSTD),
521            ("zstandard", SIDECAR_CODEC_ZSTD),
522            ("avif", SIDECAR_CODEC_AVIF),
523        ],
524    )
525}
526
527pub fn validate_sidecar_registry_ranges(item_type: u8, codec: u8) -> Result<()> {
528    if (4..=31).contains(&item_type) {
529        bail!("sidecar item type {item_type} is reserved");
530    }
531    if (4..=31).contains(&codec) {
532        bail!("sidecar codec {codec} is reserved");
533    }
534    Ok(())
535}
536
537pub fn validate_sidecar_name(value: &str, label: &str) -> Result<()> {
538    if value.chars().any(|ch| {
539        let code = ch as u32;
540        code <= 0x1f || code == 0x7f
541    }) {
542        bail!("sidecar {label} must not contain control characters");
543    }
544    Ok(())
545}
546
547pub fn looks_like_avif(bytes: &[u8]) -> bool {
548    if bytes.len() < 16 || &bytes[4..8] != b"ftyp" {
549        return false;
550    }
551    bytes[8..]
552        .chunks(4)
553        .any(|chunk| chunk == b"avif" || chunk == b"avis")
554}
555
556pub fn validate_sidecar_item_payload(
557    item_type: u8,
558    codec: u8,
559    raw_byte_length: u32,
560    stored: &[u8],
561    mime: &str,
562) -> Result<()> {
563    validate_sidecar_registry_ranges(item_type, codec)?;
564    if codec == SIDECAR_CODEC_AVIF && item_type != SIDECAR_TYPE_IMAGE {
565        bail!("AVIF sidecar codec is only valid for image items");
566    }
567    if item_type == SIDECAR_TYPE_IMAGE && codec != SIDECAR_CODEC_AVIF {
568        bail!("image sidecar items must use AVIF in this version");
569    }
570    if matches!(item_type, SIDECAR_TYPE_UTF8_TEXT | SIDECAR_TYPE_JSON)
571        && codec == SIDECAR_CODEC_AVIF
572    {
573        bail!("text and JSON sidecar items cannot use AVIF");
574    }
575    if codec == SIDECAR_CODEC_RAW && raw_byte_length != stored.len() as u32 {
576        bail!("raw sidecar item rawByteLength must equal stored length");
577    }
578    if item_type == SIDECAR_TYPE_UTF8_TEXT && codec == SIDECAR_CODEC_RAW {
579        std::str::from_utf8(stored).context("raw UTF-8 text sidecar item is not valid UTF-8")?;
580    }
581    if item_type == SIDECAR_TYPE_JSON && codec == SIDECAR_CODEC_RAW {
582        serde_json::from_slice::<serde_json::Value>(stored)
583            .context("raw JSON sidecar item is not valid JSON")?;
584    }
585    if item_type == SIDECAR_TYPE_IMAGE && codec == SIDECAR_CODEC_AVIF {
586        if !looks_like_avif(stored) {
587            bail!("AVIF sidecar image does not look like an AVIF file");
588        }
589        if !mime.is_empty() && !mime.eq_ignore_ascii_case("image/avif") {
590            bail!("AVIF sidecar image MIME type must be image/avif");
591        }
592    }
593    Ok(())
594}
595
596pub fn default_sidecar_mime(item_type: u8, codec: u8) -> &'static str {
597    match (item_type, codec) {
598        (SIDECAR_TYPE_UTF8_TEXT, _) => "text/plain;charset=utf-8",
599        (SIDECAR_TYPE_JSON, _) => "application/json",
600        (SIDECAR_TYPE_IMAGE, SIDECAR_CODEC_AVIF) => "image/avif",
601        _ => "",
602    }
603}
604
605fn write_ascii_padded(target: &mut [u8], offset: usize, length: usize, value: &str) {
606    let end = offset.saturating_add(length).min(target.len());
607    target[offset..end].fill(0);
608    let clean: String = value
609        .chars()
610        .map(|ch| {
611            let code = ch as u32;
612            if (0x20..=0x7e).contains(&code) {
613                ch
614            } else {
615                ' '
616            }
617        })
618        .collect();
619    for (index, byte) in clean
620        .as_bytes()
621        .iter()
622        .copied()
623        .take(end - offset)
624        .enumerate()
625    {
626        target[offset + index] = byte;
627    }
628}
629
630fn write_u16be(target: &mut [u8], offset: usize, value: u16) {
631    target[offset] = (value >> 8) as u8;
632    target[offset + 1] = value as u8;
633}
634
635fn write_u32be(target: &mut [u8], offset: usize, value: u32) {
636    target[offset] = (value >> 24) as u8;
637    target[offset + 1] = (value >> 16) as u8;
638    target[offset + 2] = (value >> 8) as u8;
639    target[offset + 3] = value as u8;
640}
641
642fn clamp_range(value: Option<f64>, min: f64, max: f64, fallback: f64) -> f64 {
643    let Some(number) = value else {
644        return fallback;
645    };
646    if !number.is_finite() {
647        return fallback;
648    }
649    number.clamp(min, max)
650}
651
652fn unit_to_u16(value: Option<f64>, fallback: f64) -> u16 {
653    (clamp_range(value, 0.0, 1.0, fallback) * u16::MAX as f64).round() as u16
654}
655
656fn ratio_to_permille(value: Option<f64>, min: f64, max: f64, fallback: f64) -> u16 {
657    (clamp_range(value, min, max, fallback) * 1000.0)
658        .round()
659        .clamp(0.0, u16::MAX as f64) as u16
660}
661
662fn display_header_flags(input: &PackageDisplayHeaderInput) -> u16 {
663    let mut flags = 0;
664    if input.cover_shown.unwrap_or(false) {
665        flags |= DISPLAY_HEADER_FLAG_COVER_SHOWN;
666    }
667    if input.cover_effects.unwrap_or(false) {
668        flags |= DISPLAY_HEADER_FLAG_COVER_EFFECTS;
669    }
670    if input.cover_rgb_grain.unwrap_or(false) {
671        flags |= DISPLAY_HEADER_FLAG_COVER_RGB_GRAIN;
672    }
673    if input.inner_sleeve_shown.unwrap_or(false) {
674        flags |= DISPLAY_HEADER_FLAG_INNER_SLEEVE_SHOWN;
675    }
676    if input.cover_embedded.unwrap_or(false) {
677        flags |= DISPLAY_HEADER_FLAG_COVER_EMBEDDED;
678    }
679    flags
680}
681
682use bytes2rgb::normalized_hex_color;
683
684pub fn build_package_display_header_bytes(input: &PackageDisplayHeaderInput) -> Vec<u8> {
685    let mut bytes = vec![0u8; DISPLAY_HEADER_LENGTH];
686    let crop = input.crop.as_ref();
687    write_ascii_padded(
688        &mut bytes,
689        0,
690        4,
691        &String::from_utf8_lossy(DISPLAY_HEADER_MAGIC),
692    );
693    bytes[4] = DISPLAY_HEADER_VERSION;
694    bytes[5] = DISPLAY_HEADER_LENGTH as u8;
695    write_u16be(&mut bytes, 6, display_header_flags(input));
696    write_ascii_padded(
697        &mut bytes,
698        16,
699        40,
700        input.design_label.as_deref().unwrap_or(""),
701    );
702    write_ascii_padded(
703        &mut bytes,
704        56,
705        8,
706        input.design_class_name.as_deref().unwrap_or(""),
707    );
708    bytes[64] =
709        clamp_range(input.posterize, 2.0, 64.0, DEFAULT_COVER_POSTERIZE_LEVELS).round() as u8;
710    write_u16be(
711        &mut bytes,
712        65,
713        ratio_to_permille(input.saturation, 0.0, 2.0, DEFAULT_COVER_SATURATION),
714    );
715    write_u16be(
716        &mut bytes,
717        67,
718        ratio_to_permille(input.contrast, 0.0, 2.0, DEFAULT_COVER_CONTRAST),
719    );
720    write_u16be(
721        &mut bytes,
722        69,
723        ratio_to_permille(
724            input.rgb_grain_opacity,
725            COVER_RGB_GRAIN_OPACITY_MIN,
726            1.0,
727            DEFAULT_COVER_RGB_GRAIN_OPACITY,
728        ),
729    );
730    bytes[71] = clamp_range(input.rgb_grain_blend_index, 0.0, u8::MAX as f64, 0.0).round() as u8;
731    write_u16be(
732        &mut bytes,
733        72,
734        unit_to_u16(crop.and_then(|item| item.x), 0.5),
735    );
736    write_u16be(
737        &mut bytes,
738        74,
739        unit_to_u16(crop.and_then(|item| item.y), 0.5),
740    );
741    write_u16be(
742        &mut bytes,
743        76,
744        ratio_to_permille(
745            crop.and_then(|item| item.zoom),
746            COVER_CROP_ZOOM_MIN,
747            COVER_CROP_ZOOM_MAX,
748            1.0,
749        ),
750    );
751    bytes[78] = input
752        .quantizer
753        .map(|value| clamp_range(Some(value), 0.0, 63.0, 32.0).round() as u8)
754        .unwrap_or(255);
755    bytes[79] = 0;
756    write_u16be(
757        &mut bytes,
758        80,
759        input
760            .inner
761            .map(|value| clamp_range(Some(value), 1.0, 576.0, 576.0).round() as u16)
762            .unwrap_or(0),
763    );
764    write_ascii_padded(
765        &mut bytes,
766        82,
767        7,
768        &normalized_hex_color(input.sleeve_tone_color.as_deref()),
769    );
770    write_u16be(
771        &mut bytes,
772        89,
773        ratio_to_permille(input.sleeve_sparkle_opacity, 0.0, 1.0, 1.0),
774    );
775
776    let payload_crc = record_core::crc32_ieee(&bytes[16..]);
777    write_u32be(&mut bytes, 8, payload_crc);
778    let mut header_for_crc = bytes.clone();
779    header_for_crc[12..16].fill(0);
780    let header_crc = record_core::crc32_ieee(&header_for_crc);
781    write_u32be(&mut bytes, 12, header_crc);
782    bytes
783}
784
785pub fn build_package_display_header_bytes_from_json(raw: &str) -> Result<Vec<u8>> {
786    let input: PackageDisplayHeaderInput =
787        serde_json::from_str(raw).context("display header options JSON is invalid")?;
788    Ok(build_package_display_header_bytes(&input))
789}
790
791pub fn build_package_display_header_item_json_from_input(
792    input: &PackageDisplayHeaderInput,
793) -> serde_json::Value {
794    let bytes = build_package_display_header_bytes(input);
795    serde_json::json!({
796        "type": "opaque",
797        "codec": "raw",
798        "name": DISPLAY_HEADER_NAME,
799        "mime": DISPLAY_HEADER_MIME,
800        "rawByteLength": bytes.len(),
801        "dataBase64": general_purpose::STANDARD.encode(bytes),
802    })
803}
804
805pub fn build_package_display_header_item_json(raw: &str) -> Result<String> {
806    let input: PackageDisplayHeaderInput =
807        serde_json::from_str(raw).context("display header options JSON is invalid")?;
808    Ok(build_package_display_header_item_json_from_input(&input).to_string())
809}
810
811fn clean_package_text(value: Option<&str>) -> String {
812    value
813        .unwrap_or("")
814        .split_whitespace()
815        .collect::<Vec<_>>()
816        .join(" ")
817}
818
819fn package_photo_item_name(value: Option<&str>) -> String {
820    let raw = value.unwrap_or("photo").trim();
821    let stem = raw
822        .rsplit_once('.')
823        .map(|(prefix, _)| prefix)
824        .unwrap_or(raw)
825        .trim();
826    format!("{}.avif", if stem.is_empty() { "photo" } else { stem })
827}
828
829fn package_photo_is_ready(photo: &PackageMetadataPhotoInput) -> bool {
830    matches!(photo.status.as_deref(), Some("ready") | Some("too-large"))
831        && photo.has_avif.unwrap_or(false)
832}
833
834fn rounded_positive_dimension(primary: Option<f64>, fallback: Option<f64>) -> f64 {
835    let value = primary.or(fallback).unwrap_or(COVER_PREVIEW_SIZE);
836    if value.is_finite() {
837        value.round().max(1.0)
838    } else {
839        COVER_PREVIEW_SIZE
840    }
841}
842
843fn cover_crop_metadata_json(cover: &PackageMetadataCoverInput) -> serde_json::Value {
844    let crop = cover.crop.as_ref();
845    let x = clamp_range(crop.and_then(|item| item.x), 0.0, 1.0, 0.5);
846    let y = clamp_range(crop.and_then(|item| item.y), 0.0, 1.0, 0.5);
847    let zoom = clamp_range(
848        crop.and_then(|item| item.zoom),
849        COVER_CROP_ZOOM_MIN,
850        COVER_CROP_ZOOM_MAX,
851        1.0,
852    );
853    let source_width = rounded_positive_dimension(cover.source_width, cover.width);
854    let source_height = rounded_positive_dimension(cover.source_height, cover.height);
855    let output_size = clamp_range(cover.inner, 1.0, 576.0, COVER_PREVIEW_SIZE).round();
856    let scale = (output_size / source_width).max(output_size / source_height) * zoom;
857    let sw = source_width.min(output_size / scale);
858    let sh = source_height.min(output_size / scale);
859    let max_sx = (source_width - sw).max(0.0);
860    let max_sy = (source_height - sh).max(0.0);
861    serde_json::json!({
862        "normalized": {
863            "x": (x * 1_000_000.0).round() / 1_000_000.0,
864            "y": (y * 1_000_000.0).round() / 1_000_000.0,
865            "zoom": (zoom * 1_000_000.0).round() / 1_000_000.0,
866        },
867        "source": {
868            "width": source_width as u32,
869            "height": source_height as u32,
870        },
871        "rectangle": {
872            "x": (max_sx * x).round() as u32,
873            "y": (max_sy * y).round() as u32,
874            "width": sw.round() as u32,
875            "height": sh.round() as u32,
876        },
877        "output": {
878            "width": output_size as u32,
879            "height": output_size as u32,
880        },
881    })
882}
883
884fn package_cover_metadata_json(input: &PackageMetadataInput) -> Option<serde_json::Value> {
885    let cover = input.cover.as_ref()?;
886    if !cover.has_file.unwrap_or(false) || !cover.has_preview.unwrap_or(false) {
887        return None;
888    }
889
890    let source_photo_id = clean_package_text(cover.source_photo_id.as_deref());
891    let source_photo = if source_photo_id.is_empty() {
892        None
893    } else {
894        input
895            .photos
896            .iter()
897            .enumerate()
898            .find(|(_, photo)| photo.id.as_deref() == Some(source_photo_id.as_str()))
899    };
900    let source = if let Some((index, photo)) = source_photo {
901        serde_json::json!({
902            "type": "photoInsert",
903            "position": index + 1,
904            "sourceName": photo.name.as_deref().unwrap_or(""),
905            "itemName": package_photo_item_name(photo.name.as_deref()),
906        })
907    } else {
908        serde_json::json!({
909            "type": if cover.embedded.unwrap_or(false) { "embeddedCover" } else { "coverUpload" },
910            "sourceName": cover.name.as_deref().unwrap_or(""),
911            "itemName": if cover.embedded.unwrap_or(false) { "album-cover.avif" } else { "" },
912        })
913    };
914
915    Some(serde_json::json!({
916        "role": "cover",
917        "source": source,
918        "crop": cover_crop_metadata_json(cover),
919        "display": {
920            "shown": cover.shown.unwrap_or(false),
921            "design": cover.design_label.as_deref().unwrap_or(""),
922            "effectsEnabled": cover.effects_enabled.unwrap_or(false),
923        },
924    }))
925}
926
927pub fn build_package_metadata_items_json_from_input(
928    input: &PackageMetadataInput,
929) -> serde_json::Value {
930    let photo_credits = input
931        .photos
932        .iter()
933        .filter(|photo| package_photo_is_ready(photo))
934        .enumerate()
935        .filter_map(|(index, photo)| {
936            let credit = clean_package_text(photo.credit.as_deref());
937            if credit.is_empty() {
938                return None;
939            }
940            Some(serde_json::json!({
941                "position": index + 1,
942                "sourceName": photo.name.as_deref().unwrap_or(""),
943                "itemName": package_photo_item_name(photo.name.as_deref()),
944                "credit": credit,
945            }))
946        })
947        .collect::<Vec<_>>();
948    let cover = package_cover_metadata_json(input);
949    if photo_credits.is_empty() && cover.is_none() {
950        return serde_json::Value::Array(Vec::new());
951    }
952
953    let mut metadata = serde_json::json!({
954        "kind": "bitneedle.packageMetadata",
955        "version": 1,
956    });
957    if let Some(cover) = cover {
958        metadata["cover"] = cover;
959    }
960    if !photo_credits.is_empty() {
961        metadata["photoCredits"] = serde_json::Value::Array(photo_credits);
962    }
963
964    serde_json::json!([{
965        "type": "json",
966        "codec": "raw",
967        "name": PACKAGE_METADATA_ITEM_NAME,
968        "mime": PACKAGE_METADATA_MIME,
969        "json": metadata,
970    }])
971}
972
973pub fn build_package_metadata_items_json(raw: &str) -> Result<String> {
974    let input: PackageMetadataInput =
975        serde_json::from_str(raw).context("package metadata options JSON is invalid")?;
976    Ok(build_package_metadata_items_json_from_input(&input).to_string())
977}
978
979pub fn build_package_photo_item_json_from_input(
980    input: &PackagePhotoItemInput,
981    avif_bytes: &[u8],
982) -> serde_json::Value {
983    serde_json::json!({
984        "type": "image",
985        "codec": "avif",
986        "name": package_photo_item_name(input.name.as_deref()),
987        "mime": PACKAGE_PHOTO_MIME,
988        "dataBase64": general_purpose::STANDARD.encode(avif_bytes),
989    })
990}
991
992pub fn build_package_photo_item_json(options_json: &str, avif_bytes: &[u8]) -> Result<String> {
993    let input: PackagePhotoItemInput =
994        serde_json::from_str(options_json).context("package photo item options JSON is invalid")?;
995    Ok(build_package_photo_item_json_from_input(&input, avif_bytes).to_string())
996}
997
998pub fn build_package_cover_item_json(avif_bytes: &[u8]) -> String {
999    serde_json::json!({
1000        "type": "image",
1001        "codec": "avif",
1002        "name": PACKAGE_COVER_ITEM_NAME,
1003        "mime": PACKAGE_PHOTO_MIME,
1004        "dataBase64": general_purpose::STANDARD.encode(avif_bytes),
1005    })
1006    .to_string()
1007}
1008
1009pub fn resolve_package_image_encode_cache_key_json(options_json: &str) -> Result<String> {
1010    let input: PackageImageCacheKeyInput = serde_json::from_str(options_json)
1011        .context("package image cache key options are invalid")?;
1012    let key = resolve_package_image_encode_cache_key(&input)?;
1013    serde_json::to_string(&key).context("failed to serialize package image cache key")
1014}
1015
1016pub fn resolve_package_image_encode_cache_key(
1017    input: &PackageImageCacheKeyInput,
1018) -> Result<PackageImageEncodeCacheKey> {
1019    let kind = input
1020        .kind
1021        .as_deref()
1022        .map(str::trim)
1023        .unwrap_or("photo")
1024        .to_ascii_lowercase();
1025    let file_key = package_file_cache_key(input);
1026    let quantizer = package_photo_quantizer(input.quantizer.as_ref());
1027    let inner = package_photo_inner(input.inner.as_ref());
1028    let crop = normalize_package_crop(input.crop.as_ref());
1029    let crop_key = package_crop_cache_key(&crop);
1030    let color_mode = normalize_package_color_mode(input.color_mode.as_deref());
1031    let key = if kind == "cover" {
1032        format!("{file_key}:cover:{quantizer}:{inner}:{crop_key}:{color_mode}")
1033    } else {
1034        format!("{file_key}:{quantizer}:{inner}:{crop_key}:{color_mode}")
1035    };
1036
1037    Ok(PackageImageEncodeCacheKey {
1038        key,
1039        file_key,
1040        crop_key,
1041        quantizer,
1042        inner,
1043        crop,
1044        monochrome: package_color_mode_is_monochrome(&color_mode),
1045        color_mode,
1046    })
1047}
1048
1049pub fn resolve_package_best_fit_cache_key_json(options_json: &str) -> Result<String> {
1050    let input: PackageBestFitCacheKeyInput = serde_json::from_str(options_json)
1051        .context("package best-fit cache key options are invalid")?;
1052    let key = resolve_package_best_fit_cache_key(&input)?;
1053    serde_json::to_string(&key).context("failed to serialize package best-fit cache key")
1054}
1055
1056pub fn resolve_package_best_fit_cache_key(
1057    input: &PackageBestFitCacheKeyInput,
1058) -> Result<PackageBestFitCacheKey> {
1059    let kind = input
1060        .kind
1061        .as_deref()
1062        .map(str::trim)
1063        .filter(|value| !value.is_empty())
1064        .unwrap_or("photo")
1065        .to_string();
1066    let file_key = package_file_cache_key_from_parts(
1067        input.file_name.as_deref(),
1068        input.file_size.as_ref(),
1069        input.file_last_modified.as_ref(),
1070    );
1071    let budget_bytes = package_u64(input.budget_bytes.as_ref());
1072    let inner = package_photo_inner(input.inner.as_ref());
1073    let crop_key = input
1074        .crop_key
1075        .as_deref()
1076        .filter(|value| !value.is_empty())
1077        .unwrap_or("fit")
1078        .to_string();
1079    let color_mode = normalize_package_color_mode(input.color_mode.as_deref());
1080    let key = [
1081        kind.as_str(),
1082        &format!(
1083            "fit-v7-square-crop-qbest{}-{}-{}",
1084            PACKAGE_PHOTO_SEARCH_START_QUANTIZER,
1085            PACKAGE_PHOTO_SEARCH_MIN_QUANTIZER,
1086            PACKAGE_PHOTO_MAX_QUANTIZER
1087        ),
1088        file_key.as_str(),
1089        &budget_bytes.to_string(),
1090        &inner.to_string(),
1091        crop_key.as_str(),
1092        color_mode.as_str(),
1093    ]
1094    .join(":");
1095
1096    Ok(PackageBestFitCacheKey {
1097        key,
1098        file_key,
1099        budget_bytes,
1100        inner,
1101        crop_key,
1102        color_mode,
1103    })
1104}
1105
1106pub fn package_quantizer_search_plan_json(options_json: &str) -> Result<String> {
1107    let input: PackageQuantizerSearchPlanInput = serde_json::from_str(options_json)
1108        .context("package quantizer search options are invalid")?;
1109    let plan = package_quantizer_search_plan(&input);
1110    serde_json::to_string(&plan).context("failed to serialize package quantizer search plan")
1111}
1112
1113pub fn package_quantizer_search_plan(
1114    input: &PackageQuantizerSearchPlanInput,
1115) -> PackageQuantizerSearchPlan {
1116    let min_q = package_effective_min_quantizer(input.min_quantizer.as_ref());
1117    let max_q = package_photo_quantizer(input.max_quantizer.as_ref()).max(min_q);
1118    let start_q = package_photo_quantizer(input.start_quantizer.as_ref()).clamp(min_q, max_q);
1119    let radius = package_photo_quantizer(input.refinement_radius.as_ref())
1120        .min(PACKAGE_PHOTO_MAX_QUANTIZER)
1121        .max(0);
1122    let radius = if input.refinement_radius.is_some() {
1123        radius
1124    } else {
1125        PACKAGE_PHOTO_SEARCH_REFINEMENT_RADIUS
1126    };
1127
1128    let trials = package_quantizer_trials_by_quantizer(input, min_q, max_q);
1129    let best_q = best_package_quantizer_fit(&trials);
1130    let done = |best_q: Option<u8>| PackageQuantizerSearchPlan {
1131        min_quantizer: min_q,
1132        max_quantizer: max_q,
1133        start_quantizer: start_q,
1134        done: true,
1135        next_quantizer: None,
1136        note: String::new(),
1137        best_quantizer: best_q,
1138    };
1139    let next = |quantizer: u8, note: &str, best_q: Option<u8>| PackageQuantizerSearchPlan {
1140        min_quantizer: min_q,
1141        max_quantizer: max_q,
1142        start_quantizer: start_q,
1143        done: false,
1144        next_quantizer: Some(quantizer),
1145        note: note.to_string(),
1146        best_quantizer: best_q,
1147    };
1148
1149    if !trials.contains_key(&start_q) {
1150        return next(start_q, "start", best_q);
1151    }
1152    let first_fits = trials
1153        .get(&start_q)
1154        .is_some_and(|trial| trial.fits.unwrap_or(false));
1155    if first_fits {
1156        if start_q != min_q && !trials.contains_key(&min_q) {
1157            return next(min_q, "floor", best_q);
1158        }
1159        if start_q != min_q
1160            && !trials
1161                .get(&min_q)
1162                .is_some_and(|trial| trial.fits.unwrap_or(false))
1163        {
1164            if let Some(mid) = next_missing_binary_quantizer(min_q, start_q, &trials) {
1165                return next(mid, "bisect", best_q);
1166            }
1167        }
1168        if let Some(refine_q) = next_missing_refinement_quantizer(best_q, min_q, radius, &trials) {
1169            return next(refine_q, "refine", best_q);
1170        }
1171        return done(best_q);
1172    }
1173
1174    if start_q == max_q {
1175        return done(best_q);
1176    }
1177    if !trials.contains_key(&max_q) {
1178        return next(max_q, "ceiling", best_q);
1179    }
1180    if !trials
1181        .get(&max_q)
1182        .is_some_and(|trial| trial.fits.unwrap_or(false))
1183    {
1184        return done(best_q);
1185    }
1186    if let Some(mid) = next_missing_binary_quantizer(start_q, max_q, &trials) {
1187        return next(mid, "bisect", best_q);
1188    }
1189    if let Some(refine_q) = next_missing_refinement_quantizer(best_q, min_q, radius, &trials) {
1190        return next(refine_q, "refine", best_q);
1191    }
1192    done(best_q)
1193}
1194
1195pub fn package_fit_budget_json(options_json: &str) -> Result<String> {
1196    let input: PackageFitBudgetInput =
1197        serde_json::from_str(options_json).context("package fit budget options are invalid")?;
1198    let budget = package_fit_budget(&input);
1199    serde_json::to_string(&budget).context("failed to serialize package fit budget")
1200}
1201
1202pub fn package_fit_budget(input: &PackageFitBudgetInput) -> PackageFitBudget {
1203    let capacity = package_u64(input.capacity.as_ref());
1204    let base_bts1_bytes = package_u64(input.base_bts1_bytes.as_ref());
1205    let photo_count = package_u64(input.photo_count.as_ref());
1206    let available_bytes = capacity.saturating_sub(base_bts1_bytes);
1207    let cover_budget_bytes = if capacity > 0 {
1208        available_bytes / PACKAGE_COVER_MAX_BUDGET_DIVISOR
1209    } else {
1210        0
1211    };
1212    let remaining_bytes = capacity.saturating_sub(base_bts1_bytes);
1213    let per_photo_budget_bytes = if capacity > 0 && photo_count > 0 {
1214        remaining_bytes / photo_count
1215    } else {
1216        0
1217    };
1218    PackageFitBudget {
1219        capacity,
1220        base_bts1_bytes,
1221        available_bytes,
1222        remaining_bytes,
1223        cover_budget_bytes,
1224        photo_count,
1225        per_photo_budget_bytes,
1226    }
1227}
1228
1229pub fn build_package_sidecar_render_options_json(options_json: &str) -> Result<String> {
1230    let input: PackageSidecarRenderOptionsInput =
1231        serde_json::from_str(options_json).context("package sidecar render options are invalid")?;
1232    Ok(build_package_sidecar_render_options(&input).to_string())
1233}
1234
1235pub fn build_package_sidecar_render_options(
1236    input: &PackageSidecarRenderOptionsInput,
1237) -> serde_json::Value {
1238    let payload_bytes = package_u64(input.payload_bytes.as_ref());
1239    let capacity_bytes = package_u64(input.capacity_bytes.as_ref());
1240    let seed = package_u64(input.seed.as_ref());
1241    let utilization = if capacity_bytes > 0 && payload_bytes > 0 {
1242        ((payload_bytes as f64) / (capacity_bytes as f64)).clamp(0.0, 1.0)
1243    } else {
1244        0.0
1245    };
1246    let strength = if capacity_bytes > 0 {
1247        0.12 + utilization * 0.36
1248    } else {
1249        0.22
1250    };
1251    let strength = ((strength.clamp(0.0, 0.5) * 100.0).round()) / 100.0;
1252    let grain = if utilization > 0.85 {
1253        5
1254    } else if utilization > 0.55 {
1255        4
1256    } else {
1257        3
1258    };
1259
1260    let mut sidecar = serde_json::Map::new();
1261    sidecar.insert("seed".to_string(), serde_json::json!(seed));
1262    sidecar.insert(
1263        "carriers".to_string(),
1264        serde_json::json!(PACKAGE_SIDECAR_CARRIERS.to_vec()),
1265    );
1266    sidecar.insert(
1267        "scheme".to_string(),
1268        serde_json::json!(PACKAGE_SIDECAR_SCHEME),
1269    );
1270    sidecar.insert(
1271        "labelTuning".to_string(),
1272        serde_json::json!({
1273            "enabled": true,
1274            "targetLuma": 128,
1275            "strength": strength,
1276            "grain": grain,
1277        }),
1278    );
1279
1280    if let Some(bts1_base64) = input
1281        .bts1_base64
1282        .as_deref()
1283        .map(str::trim)
1284        .filter(|value| !value.is_empty())
1285    {
1286        sidecar.insert("bts1Base64".to_string(), serde_json::json!(bts1_base64));
1287    } else {
1288        sidecar.insert(
1289            "items".to_string(),
1290            input
1291                .items
1292                .clone()
1293                .unwrap_or_else(|| serde_json::Value::Array(Vec::new())),
1294        );
1295    }
1296
1297    serde_json::json!({ "sidecar": serde_json::Value::Object(sidecar) })
1298}
1299
1300pub fn package_preserved_pattern_items_json(decoded_json: &str) -> Result<String> {
1301    let decoded: serde_json::Value =
1302        serde_json::from_str(decoded_json).context("decoded sidecar JSON is invalid")?;
1303    let items = package_preserved_pattern_items(&decoded)?;
1304    serde_json::to_string(&items).context("failed to serialize preserved pattern items")
1305}
1306
1307pub fn package_preserved_pattern_items(
1308    decoded: &serde_json::Value,
1309) -> Result<Vec<serde_json::Value>> {
1310    let Some(items) = decoded.get("items").and_then(serde_json::Value::as_array) else {
1311        return Ok(Vec::new());
1312    };
1313    let mut preserved = Vec::new();
1314    for item in items {
1315        if package_decoded_item_is_pattern_map(item) {
1316            preserved.push(package_preserved_pattern_item(item)?);
1317        }
1318    }
1319    Ok(preserved)
1320}
1321
1322fn package_decoded_item_is_pattern_map(item: &serde_json::Value) -> bool {
1323    decoded_item_text_field(item, "name")
1324        .is_some_and(|value| value == PACKAGE_PATTERN_SIDECAR_ITEM_NAME)
1325        || decoded_item_text_field(item, "mime")
1326            .is_some_and(|value| value == PACKAGE_PATTERN_SIDECAR_MIME)
1327}
1328
1329fn package_preserved_pattern_item(item: &serde_json::Value) -> Result<serde_json::Value> {
1330    let item_type = decoded_item_u8_field(item, "itemType")?;
1331    let codec = decoded_item_u8_field(item, "codec")?;
1332    let mut preserved = serde_json::Map::new();
1333    preserved.insert(
1334        "type".to_string(),
1335        package_sidecar_input_type_value(item_type),
1336    );
1337    preserved.insert(
1338        "codec".to_string(),
1339        package_sidecar_input_codec_value(codec),
1340    );
1341    preserved.insert(
1342        "name".to_string(),
1343        serde_json::Value::String(
1344            decoded_item_text_field(item, "name")
1345                .filter(|value| !value.is_empty())
1346                .unwrap_or(PACKAGE_PATTERN_SIDECAR_ITEM_NAME)
1347                .to_string(),
1348        ),
1349    );
1350    preserved.insert(
1351        "mime".to_string(),
1352        serde_json::Value::String(
1353            decoded_item_text_field(item, "mime")
1354                .filter(|value| !value.is_empty())
1355                .unwrap_or(PACKAGE_PATTERN_SIDECAR_MIME)
1356                .to_string(),
1357        ),
1358    );
1359    preserved.insert(
1360        "dataBase64".to_string(),
1361        serde_json::Value::String(
1362            decoded_item_text_field(item, "storedDataBase64")
1363                .context("pattern sidecar item is missing storedDataBase64")?
1364                .to_string(),
1365        ),
1366    );
1367    if let Some(raw_byte_length) = decoded_item_optional_u64_field(item, "rawByteLength") {
1368        preserved.insert(
1369            "rawByteLength".to_string(),
1370            serde_json::json!(raw_byte_length),
1371        );
1372    }
1373    if let Some(flags) = decoded_item_optional_u64_field(item, "flags").filter(|value| *value != 0)
1374    {
1375        preserved.insert("flags".to_string(), serde_json::json!(flags));
1376    }
1377    Ok(serde_json::Value::Object(preserved))
1378}
1379
1380fn package_sidecar_input_type_value(item_type: u8) -> serde_json::Value {
1381    match item_type {
1382        SIDECAR_TYPE_OPAQUE => serde_json::json!("opaque"),
1383        SIDECAR_TYPE_UTF8_TEXT => serde_json::json!("text"),
1384        SIDECAR_TYPE_IMAGE => serde_json::json!("image"),
1385        SIDECAR_TYPE_JSON => serde_json::json!("json"),
1386        value => serde_json::json!(value),
1387    }
1388}
1389
1390fn package_sidecar_input_codec_value(codec: u8) -> serde_json::Value {
1391    match codec {
1392        SIDECAR_CODEC_RAW => serde_json::json!("raw"),
1393        SIDECAR_CODEC_BROTLI => serde_json::json!("brotli"),
1394        SIDECAR_CODEC_ZSTD => serde_json::json!("zstd"),
1395        SIDECAR_CODEC_AVIF => serde_json::json!("avif"),
1396        value => serde_json::json!(value),
1397    }
1398}
1399
1400fn decoded_item_u8_field(item: &serde_json::Value, field: &str) -> Result<u8> {
1401    let value = item
1402        .get(field)
1403        .and_then(serde_json::Value::as_u64)
1404        .with_context(|| format!("decoded sidecar item is missing numeric {field}"))?;
1405    u8::try_from(value).with_context(|| format!("decoded sidecar item {field} exceeds u8 range"))
1406}
1407
1408fn decoded_item_optional_u64_field(item: &serde_json::Value, field: &str) -> Option<u64> {
1409    item.get(field).and_then(serde_json::Value::as_u64)
1410}
1411
1412fn decoded_item_text_field<'a>(item: &'a serde_json::Value, field: &str) -> Option<&'a str> {
1413    item.get(field)
1414        .and_then(serde_json::Value::as_str)
1415        .map(str::trim)
1416        .filter(|value| !value.is_empty())
1417}
1418
1419fn package_quantizer_trials_by_quantizer(
1420    input: &PackageQuantizerSearchPlanInput,
1421    min_q: u8,
1422    max_q: u8,
1423) -> BTreeMap<u8, PackageQuantizerTrialInput> {
1424    let mut trials = BTreeMap::new();
1425    for trial in &input.trials {
1426        let q = package_photo_quantizer(trial.quantizer.as_ref()).clamp(min_q, max_q);
1427        trials.insert(q, trial.clone());
1428    }
1429    trials
1430}
1431
1432fn best_package_quantizer_fit(trials: &BTreeMap<u8, PackageQuantizerTrialInput>) -> Option<u8> {
1433    let mut best = None;
1434    for (q, trial) in trials {
1435        if !trial.fits.unwrap_or(false) {
1436            continue;
1437        }
1438        let Some(current_q) = best else {
1439            best = Some(*q);
1440            continue;
1441        };
1442        if *q < current_q
1443            || (*q == current_q
1444                && package_trial_budget_ratio(trial)
1445                    > trials
1446                        .get(&current_q)
1447                        .map(package_trial_budget_ratio)
1448                        .unwrap_or(1.0))
1449        {
1450            best = Some(*q);
1451        }
1452    }
1453    best
1454}
1455
1456fn next_missing_binary_quantizer(
1457    mut low: u8,
1458    mut high: u8,
1459    trials: &BTreeMap<u8, PackageQuantizerTrialInput>,
1460) -> Option<u8> {
1461    while high.saturating_sub(low) > 1 {
1462        let mid = low + (high - low) / 2;
1463        let Some(trial) = trials.get(&mid) else {
1464            return Some(mid);
1465        };
1466        if trial.fits.unwrap_or(false) {
1467            high = mid;
1468        } else {
1469            low = mid;
1470        }
1471    }
1472    None
1473}
1474
1475fn next_missing_refinement_quantizer(
1476    best_q: Option<u8>,
1477    min_q: u8,
1478    radius: u8,
1479    trials: &BTreeMap<u8, PackageQuantizerTrialInput>,
1480) -> Option<u8> {
1481    let best_q = best_q?;
1482    let lower = best_q.saturating_sub(radius).max(min_q);
1483    (lower..best_q).find(|q| !trials.contains_key(q))
1484}
1485
1486fn package_trial_budget_ratio(trial: &PackageQuantizerTrialInput) -> f64 {
1487    let budget = package_number(trial.budget_bytes.as_ref()).unwrap_or(0.0);
1488    let contribution = package_number(trial.contribution_bytes.as_ref())
1489        .or_else(|| package_number(trial.bts1_bytes.as_ref()))
1490        .unwrap_or(0.0);
1491    if budget > 0.0 {
1492        contribution / budget
1493    } else {
1494        1.0
1495    }
1496}
1497
1498fn package_file_cache_key(input: &PackageImageCacheKeyInput) -> String {
1499    package_file_cache_key_from_parts(
1500        input.file_name.as_deref(),
1501        input.file_size.as_ref(),
1502        input.file_last_modified.as_ref(),
1503    )
1504}
1505
1506fn package_file_cache_key_from_parts(
1507    file_name: Option<&str>,
1508    file_size: Option<&serde_json::Value>,
1509    file_last_modified: Option<&serde_json::Value>,
1510) -> String {
1511    format!(
1512        "{}:{}:{}",
1513        file_name.unwrap_or(""),
1514        package_u64(file_size),
1515        package_u64(file_last_modified)
1516    )
1517}
1518
1519fn package_photo_quantizer(value: Option<&serde_json::Value>) -> u8 {
1520    let Some(number) = package_number(value) else {
1521        return PACKAGE_PHOTO_DEFAULT_QUANTIZER;
1522    };
1523    number
1524        .round()
1525        .clamp(0.0, f64::from(PACKAGE_PHOTO_MAX_QUANTIZER)) as u8
1526}
1527
1528fn package_effective_min_quantizer(value: Option<&serde_json::Value>) -> u8 {
1529    package_photo_quantizer(value).max(PACKAGE_PHOTO_SEARCH_MIN_QUANTIZER)
1530}
1531
1532fn package_photo_inner(value: Option<&serde_json::Value>) -> u32 {
1533    let Some(number) = package_number(value) else {
1534        return PACKAGE_PHOTO_MAX_INNER;
1535    };
1536    number
1537        .round()
1538        .clamp(1.0, f64::from(PACKAGE_PHOTO_MAX_INNER)) as u32
1539}
1540
1541fn normalize_package_crop(value: Option<&serde_json::Value>) -> PackageNormalizedCrop {
1542    let object = value.and_then(serde_json::Value::as_object);
1543    PackageNormalizedCrop {
1544        x: package_range(object.and_then(|object| object.get("x")), 0.0, 1.0, 0.5),
1545        y: package_range(object.and_then(|object| object.get("y")), 0.0, 1.0, 0.5),
1546        zoom: package_range(
1547            object.and_then(|object| object.get("zoom")),
1548            COVER_CROP_ZOOM_MIN,
1549            COVER_CROP_ZOOM_MAX,
1550            1.0,
1551        ),
1552    }
1553}
1554
1555fn package_crop_cache_key(crop: &PackageNormalizedCrop) -> String {
1556    format!("crop-{:.4}-{:.4}-{:.4}", crop.x, crop.y, crop.zoom)
1557}
1558
1559fn normalize_package_color_mode(value: Option<&str>) -> String {
1560    match value.unwrap_or("").to_ascii_lowercase().as_str() {
1561        PACKAGE_COLOR_MODE_RGB => PACKAGE_COLOR_MODE_RGB.to_string(),
1562        PACKAGE_COLOR_MODE_GRAYSCALE => PACKAGE_COLOR_MODE_GRAYSCALE.to_string(),
1563        _ => PACKAGE_COLOR_MODE_RGB.to_string(),
1564    }
1565}
1566
1567fn package_color_mode_is_monochrome(value: &str) -> bool {
1568    value == PACKAGE_COLOR_MODE_GRAYSCALE
1569}
1570
1571fn package_range(value: Option<&serde_json::Value>, min: f64, max: f64, fallback: f64) -> f64 {
1572    package_number(value)
1573        .filter(|number| number.is_finite())
1574        .unwrap_or(fallback)
1575        .clamp(min, max)
1576}
1577
1578fn package_u64(value: Option<&serde_json::Value>) -> u64 {
1579    match value {
1580        Some(serde_json::Value::Number(number)) => number.as_u64().unwrap_or_else(|| {
1581            number
1582                .as_f64()
1583                .filter(|number| number.is_finite() && *number > 0.0)
1584                .map(|number| number.floor() as u64)
1585                .unwrap_or(0)
1586        }),
1587        Some(serde_json::Value::String(text)) => text
1588            .trim()
1589            .parse::<f64>()
1590            .ok()
1591            .filter(|number| number.is_finite() && *number > 0.0)
1592            .map(|number| number.floor() as u64)
1593            .unwrap_or(0),
1594        _ => 0,
1595    }
1596}
1597
1598fn package_number(value: Option<&serde_json::Value>) -> Option<f64> {
1599    match value {
1600        Some(serde_json::Value::Number(number)) => number.as_f64(),
1601        Some(serde_json::Value::String(text)) => text.trim().parse::<f64>().ok(),
1602        _ => None,
1603    }
1604    .filter(|number| number.is_finite())
1605}
1606
1607fn sidecar_item_stored_bytes(
1608    input: &SidecarItemInput,
1609    item_type: u8,
1610    codec: u8,
1611) -> Result<Vec<u8>> {
1612    if let Some(data_base64) = input.data_base64.as_deref() {
1613        return decode_base64_text(data_base64, "sidecar item dataBase64");
1614    }
1615    if codec == SIDECAR_CODEC_RAW && item_type == SIDECAR_TYPE_UTF8_TEXT {
1616        if let Some(text) = input.text.as_deref() {
1617            return Ok(text.as_bytes().to_vec());
1618        }
1619    }
1620    if codec == SIDECAR_CODEC_RAW && item_type == SIDECAR_TYPE_JSON {
1621        if let Some(json) = input.json.as_ref() {
1622            return serde_json::to_vec(json).context("failed to serialize sidecar JSON item");
1623        }
1624    }
1625    bail!("sidecar item requires dataBase64, except raw text/json items may use text/json")
1626}
1627
1628pub fn build_sidecar_container_from_items(items: &[SidecarItemInput]) -> Result<Vec<u8>> {
1629    if items.len() > u16::MAX as usize {
1630        bail!("sidecar item count exceeds u16 limit");
1631    }
1632    let mut item_bytes = Vec::new();
1633    for input in items {
1634        let item_type = parse_sidecar_item_type(&input.item_type)?;
1635        let codec = parse_sidecar_codec(&input.codec)?;
1636        if input.flags != 0 {
1637            bail!("sidecar item flags must be 0 in this version");
1638        }
1639        let stored = sidecar_item_stored_bytes(input, item_type, codec)?;
1640        if stored.len() > u32::MAX as usize {
1641            bail!("sidecar item stored data exceeds u32 length limit");
1642        }
1643        let raw_byte_length = if codec == SIDECAR_CODEC_RAW {
1644            u32::try_from(stored.len()).context("sidecar raw item exceeds u32 length limit")?
1645        } else {
1646            input.raw_byte_length.unwrap_or(SIDECAR_RAW_LENGTH_ABSENT)
1647        };
1648        let name = input.name.as_deref().unwrap_or("");
1649        let mime = input
1650            .mime
1651            .as_deref()
1652            .unwrap_or(default_sidecar_mime(item_type, codec));
1653        validate_sidecar_name(name, "item name")?;
1654        validate_sidecar_name(mime, "MIME type")?;
1655        validate_sidecar_item_payload(item_type, codec, raw_byte_length, &stored, mime)?;
1656        let name_bytes = name.as_bytes();
1657        let mime_bytes = mime.as_bytes();
1658        if name_bytes.len() > u16::MAX as usize {
1659            bail!("sidecar item name exceeds u16 length limit");
1660        }
1661        if mime_bytes.len() > u16::MAX as usize {
1662            bail!("sidecar item MIME type exceeds u16 length limit");
1663        }
1664        item_bytes.push(item_type);
1665        item_bytes.push(codec);
1666        item_bytes.push(input.flags);
1667        item_bytes.push(0);
1668        item_bytes.extend_from_slice(&raw_byte_length.to_be_bytes());
1669        item_bytes.extend_from_slice(&(stored.len() as u32).to_be_bytes());
1670        item_bytes.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
1671        item_bytes.extend_from_slice(&(mime_bytes.len() as u16).to_be_bytes());
1672        item_bytes.extend_from_slice(name_bytes);
1673        item_bytes.extend_from_slice(mime_bytes);
1674        item_bytes.extend_from_slice(&stored);
1675    }
1676
1677    let total_length = 12usize
1678        .checked_add(item_bytes.len())
1679        .context("sidecar container length overflow")?;
1680    if total_length > u32::MAX as usize {
1681        bail!("sidecar container exceeds u32 length limit");
1682    }
1683    let mut out = Vec::with_capacity(total_length);
1684    out.extend_from_slice(SIDECAR_MAGIC);
1685    out.push(SIDECAR_CONTAINER_VERSION);
1686    out.push(0);
1687    out.extend_from_slice(&(items.len() as u16).to_be_bytes());
1688    out.extend_from_slice(&(total_length as u32).to_be_bytes());
1689    out.extend_from_slice(&item_bytes);
1690    validate_sidecar_container(&out)?;
1691    Ok(out)
1692}
1693
1694pub fn build_sidecar_container_from_items_json(raw: &str) -> Result<Vec<u8>> {
1695    #[derive(Deserialize)]
1696    #[serde(rename_all = "camelCase")]
1697    struct SidecarItemsWrapper {
1698        items: Vec<SidecarItemInput>,
1699    }
1700
1701    let trimmed = raw.trim();
1702    if trimmed.is_empty() {
1703        bail!("sidecar items JSON is empty");
1704    }
1705    let items = if trimmed.starts_with('[') {
1706        serde_json::from_str::<Vec<SidecarItemInput>>(trimmed)
1707            .context("sidecar items JSON must be an item array")?
1708    } else {
1709        serde_json::from_str::<SidecarItemsWrapper>(trimmed)
1710            .context("sidecar items JSON must be an item array or {\"items\": [...] }")?
1711            .items
1712    };
1713    build_sidecar_container_from_items(&items)
1714}
1715
1716fn read_u16be(bytes: &[u8], offset: usize, label: &str) -> Result<u16> {
1717    if offset + 2 > bytes.len() {
1718        bail!("{label} is truncated");
1719    }
1720    Ok(u16::from_be_bytes(
1721        bytes[offset..offset + 2].try_into().expect("slice length"),
1722    ))
1723}
1724
1725fn read_u32be(bytes: &[u8], offset: usize, label: &str) -> Result<u32> {
1726    if offset + 4 > bytes.len() {
1727        bail!("{label} is truncated");
1728    }
1729    Ok(u32::from_be_bytes(
1730        bytes[offset..offset + 4].try_into().expect("slice length"),
1731    ))
1732}
1733
1734pub fn validate_sidecar_container(bytes: &[u8]) -> Result<SidecarContainerValidation> {
1735    if bytes.len() < 12 {
1736        bail!("sidecar container is too short");
1737    }
1738    if &bytes[..4] != SIDECAR_MAGIC {
1739        bail!("sidecar container magic is unsupported");
1740    }
1741    let version = bytes[4];
1742    if version != SIDECAR_CONTAINER_VERSION {
1743        bail!("unsupported sidecar container version {version}");
1744    }
1745    let flags = bytes[5];
1746    if flags != 0 {
1747        bail!("sidecar container flags must be 0 in this version");
1748    }
1749    let item_count = read_u16be(bytes, 6, "sidecar item count")? as usize;
1750    let total_length = read_u32be(bytes, 8, "sidecar total length")? as usize;
1751    if total_length != bytes.len() {
1752        bail!("sidecar total length does not match byte stream length");
1753    }
1754
1755    let mut offset = 12usize;
1756    let mut items = Vec::with_capacity(item_count);
1757    for _ in 0..item_count {
1758        if offset + 16 > bytes.len() {
1759            bail!("sidecar item header is truncated");
1760        }
1761        let item_type = bytes[offset];
1762        let codec = bytes[offset + 1];
1763        let item_flags = bytes[offset + 2];
1764        let reserved = bytes[offset + 3];
1765        if item_flags != 0 {
1766            bail!("sidecar item flags must be 0 in this version");
1767        }
1768        if reserved != 0 {
1769            bail!("sidecar item reserved byte must be 0");
1770        }
1771        let raw_byte_length = read_u32be(bytes, offset + 4, "sidecar item raw length")?;
1772        let stored_byte_length = read_u32be(bytes, offset + 8, "sidecar item stored length")?;
1773        let name_len = read_u16be(bytes, offset + 12, "sidecar item name length")? as usize;
1774        let mime_len = read_u16be(bytes, offset + 14, "sidecar item MIME length")? as usize;
1775        offset += 16;
1776        let item_end = offset
1777            .checked_add(name_len)
1778            .and_then(|value| value.checked_add(mime_len))
1779            .and_then(|value| value.checked_add(stored_byte_length as usize))
1780            .context("sidecar item length overflow")?;
1781        if item_end > bytes.len() {
1782            bail!("sidecar item payload is truncated");
1783        }
1784        let name = std::str::from_utf8(&bytes[offset..offset + name_len])
1785            .context("sidecar item name is not valid UTF-8")?
1786            .to_string();
1787        offset += name_len;
1788        let mime = std::str::from_utf8(&bytes[offset..offset + mime_len])
1789            .context("sidecar item MIME type is not valid ASCII/UTF-8")?
1790            .to_string();
1791        offset += mime_len;
1792        let stored = &bytes[offset..offset + stored_byte_length as usize];
1793        offset += stored_byte_length as usize;
1794
1795        validate_sidecar_name(&name, "item name")?;
1796        validate_sidecar_name(&mime, "MIME type")?;
1797        validate_sidecar_item_payload(item_type, codec, raw_byte_length, stored, &mime)?;
1798        items.push(SidecarItemValidation {
1799            item_type,
1800            item_type_name: sidecar_type_name(item_type),
1801            codec,
1802            codec_name: sidecar_codec_name(codec),
1803            flags: item_flags,
1804            raw_byte_length: (raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT)
1805                .then_some(raw_byte_length),
1806            stored_byte_length,
1807            name,
1808            mime,
1809        });
1810    }
1811    if offset != bytes.len() {
1812        bail!("sidecar container has trailing bytes");
1813    }
1814    Ok(SidecarContainerValidation {
1815        ok: true,
1816        version,
1817        flags,
1818        item_count,
1819        total_length,
1820        items,
1821    })
1822}
1823
1824fn decode_sidecar_item_payload(
1825    item_type: u8,
1826    codec: u8,
1827    raw_byte_length: u32,
1828    stored: &[u8],
1829) -> Result<Vec<u8>> {
1830    let decoded = match codec {
1831        SIDECAR_CODEC_RAW | SIDECAR_CODEC_AVIF => stored.to_vec(),
1832        SIDECAR_CODEC_BROTLI => {
1833            let mut reader = brotli::Decompressor::new(stored, 4096);
1834            let mut out = Vec::new();
1835            reader
1836                .read_to_end(&mut out)
1837                .context("failed to decompress Brotli sidecar item")?;
1838            out
1839        }
1840        SIDECAR_CODEC_ZSTD => bail!("zstd sidecar item extraction is not implemented"),
1841        _ => stored.to_vec(),
1842    };
1843    if raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT && decoded.len() != raw_byte_length as usize {
1844        bail!(
1845            "decoded sidecar item length {} does not match declared raw length {}",
1846            decoded.len(),
1847            raw_byte_length
1848        );
1849    }
1850    if item_type == SIDECAR_TYPE_UTF8_TEXT {
1851        std::str::from_utf8(&decoded)
1852            .context("decoded UTF-8 text sidecar item is not valid UTF-8")?;
1853    }
1854    if item_type == SIDECAR_TYPE_JSON {
1855        serde_json::from_slice::<serde_json::Value>(&decoded)
1856            .context("decoded JSON sidecar item is not valid JSON")?;
1857    }
1858    Ok(decoded)
1859}
1860
1861pub fn decode_sidecar_container_items(bytes: &[u8]) -> Result<SidecarDecodedItems> {
1862    let validation = validate_sidecar_container(bytes)?;
1863    let item_count = read_u16be(bytes, 6, "sidecar item count")? as usize;
1864    let mut offset = 12usize;
1865    let mut items = Vec::with_capacity(item_count);
1866    for _ in 0..item_count {
1867        if offset + 16 > bytes.len() {
1868            bail!("sidecar item header is truncated");
1869        }
1870        let item_type = bytes[offset];
1871        let codec = bytes[offset + 1];
1872        let flags = bytes[offset + 2];
1873        let raw_byte_length = read_u32be(bytes, offset + 4, "sidecar item raw length")?;
1874        let stored_byte_length = read_u32be(bytes, offset + 8, "sidecar item stored length")?;
1875        let name_len = read_u16be(bytes, offset + 12, "sidecar item name length")? as usize;
1876        let mime_len = read_u16be(bytes, offset + 14, "sidecar item MIME length")? as usize;
1877        offset += 16;
1878        let item_end = offset
1879            .checked_add(name_len)
1880            .and_then(|value| value.checked_add(mime_len))
1881            .and_then(|value| value.checked_add(stored_byte_length as usize))
1882            .context("sidecar item length overflow")?;
1883        if item_end > bytes.len() {
1884            bail!("sidecar item payload is truncated");
1885        }
1886        let name = std::str::from_utf8(&bytes[offset..offset + name_len])
1887            .context("sidecar item name is not valid UTF-8")?
1888            .to_string();
1889        offset += name_len;
1890        let mime = std::str::from_utf8(&bytes[offset..offset + mime_len])
1891            .context("sidecar item MIME type is not valid ASCII/UTF-8")?
1892            .to_string();
1893        offset += mime_len;
1894        let stored = &bytes[offset..offset + stored_byte_length as usize];
1895        offset += stored_byte_length as usize;
1896        let decoded = decode_sidecar_item_payload(item_type, codec, raw_byte_length, stored)?;
1897        let text = if item_type == SIDECAR_TYPE_UTF8_TEXT {
1898            Some(
1899                std::str::from_utf8(&decoded)
1900                    .context("decoded UTF-8 text sidecar item is not valid UTF-8")?
1901                    .to_string(),
1902            )
1903        } else {
1904            None
1905        };
1906        let json = if item_type == SIDECAR_TYPE_JSON {
1907            Some(
1908                serde_json::from_slice::<serde_json::Value>(&decoded)
1909                    .context("decoded JSON sidecar item is not valid JSON")?,
1910            )
1911        } else {
1912            None
1913        };
1914        items.push(SidecarDecodedItem {
1915            item_type,
1916            item_type_name: sidecar_type_name(item_type),
1917            codec,
1918            codec_name: sidecar_codec_name(codec),
1919            flags,
1920            raw_byte_length: (raw_byte_length != SIDECAR_RAW_LENGTH_ABSENT)
1921                .then_some(raw_byte_length),
1922            stored_byte_length,
1923            decoded_byte_length: decoded.len(),
1924            name,
1925            mime,
1926            stored_data_base64: general_purpose::STANDARD.encode(stored),
1927            data_base64: general_purpose::STANDARD.encode(&decoded),
1928            text,
1929            json,
1930        });
1931    }
1932    if offset != bytes.len() {
1933        bail!("sidecar container has trailing bytes");
1934    }
1935    Ok(SidecarDecodedItems {
1936        ok: true,
1937        validation,
1938        items,
1939    })
1940}
1941
1942pub fn parse_sidecar_carrier(raw: &str) -> Result<SidecarCarrier> {
1943    match raw
1944        .trim()
1945        .to_ascii_lowercase()
1946        .replace([' ', '-', '_'], "")
1947        .as_str()
1948    {
1949        "label" => Ok(SidecarCarrier::Label),
1950        "intergroove" | "intragroove" | "groove" => Ok(SidecarCarrier::Intergroove),
1951        "leadindeadwax" | "leaddeadwax" | "leadin" | "leadout" | "deadwax" | "runout" => {
1952            Ok(SidecarCarrier::LeadInDeadwax)
1953        }
1954        _ => bail!("unknown sidecar carrier: {raw}"),
1955    }
1956}
1957
1958pub fn sidecar_carrier_name(carrier: SidecarCarrier) -> &'static str {
1959    match carrier {
1960        SidecarCarrier::Label => "label",
1961        SidecarCarrier::Intergroove => "intergroove",
1962        SidecarCarrier::LeadInDeadwax => "leadInDeadwax",
1963    }
1964}
1965
1966pub fn normalize_sidecar_carriers(raw: Option<&[String]>) -> Result<Vec<SidecarCarrier>> {
1967    let mut carriers = Vec::new();
1968    if let Some(raw) = raw {
1969        for value in raw {
1970            let carrier = parse_sidecar_carrier(value)?;
1971            if !carriers.contains(&carrier) {
1972                carriers.push(carrier);
1973            }
1974        }
1975    } else {
1976        carriers.push(SidecarCarrier::Label);
1977        carriers.push(SidecarCarrier::Intergroove);
1978    }
1979    if carriers.is_empty() {
1980        bail!("sidecar carriers must not be empty");
1981    }
1982    Ok(carriers)
1983}
1984
1985pub fn default_sidecar_carriers() -> Vec<SidecarCarrier> {
1986    vec![SidecarCarrier::Label, SidecarCarrier::Intergroove]
1987}
1988
1989pub fn normalize_sidecar_scheme(raw: Option<&str>) -> Result<String> {
1990    let Some(raw) = raw else {
1991        return Ok(SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2.to_string());
1992    };
1993    let normalized = raw.trim().to_ascii_lowercase();
1994    match normalized.as_str() {
1995        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(normalized),
1996        _ => bail!("unsupported sidecar carrier scheme {raw}"),
1997    }
1998}
1999
2000pub fn sidecar_pointer_scheme_id(scheme: &str) -> Result<u8> {
2001    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
2002        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2),
2003        _ => unreachable!("scheme normalized"),
2004    }
2005}
2006
2007pub fn sidecar_pointer_scheme_name(scheme_id: u8) -> Result<String> {
2008    match scheme_id {
2009        SIDECAR_POINTER_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => {
2010            Ok(SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2.to_string())
2011        }
2012        _ => bail!("unsupported sidecar pointer scheme id {scheme_id}"),
2013    }
2014}
2015
2016pub fn sidecar_pointer_carrier_flags(carriers: &[SidecarCarrier]) -> u8 {
2017    let mut flags = 0u8;
2018    if carriers.contains(&SidecarCarrier::Label) {
2019        flags |= SIDECAR_POINTER_CARRIER_LABEL;
2020    }
2021    if carriers.contains(&SidecarCarrier::Intergroove) {
2022        flags |= SIDECAR_POINTER_CARRIER_INTERGROOVE;
2023    }
2024    if carriers.contains(&SidecarCarrier::LeadInDeadwax) {
2025        flags |= SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX;
2026    }
2027    flags
2028}
2029
2030pub fn sidecar_pointer_carriers(flags: u8) -> Result<Vec<SidecarCarrier>> {
2031    if flags
2032        & !(SIDECAR_POINTER_CARRIER_LABEL
2033            | SIDECAR_POINTER_CARRIER_INTERGROOVE
2034            | SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX)
2035        != 0
2036    {
2037        bail!("sidecar pointer has unsupported carrier flags {flags:#04x}");
2038    }
2039    let mut carriers = Vec::new();
2040    if flags & SIDECAR_POINTER_CARRIER_LABEL != 0 {
2041        carriers.push(SidecarCarrier::Label);
2042    }
2043    if flags & SIDECAR_POINTER_CARRIER_INTERGROOVE != 0 {
2044        carriers.push(SidecarCarrier::Intergroove);
2045    }
2046    if flags & SIDECAR_POINTER_CARRIER_LEAD_IN_DEADWAX != 0 {
2047        carriers.push(SidecarCarrier::LeadInDeadwax);
2048    }
2049    if carriers.is_empty() {
2050        bail!("sidecar pointer has no carriers");
2051    }
2052    Ok(carriers)
2053}
2054
2055pub fn encode_sidecar_header_pointer(pointer: &SidecarHeaderPointer) -> Result<Vec<u8>> {
2056    let length = u32::try_from(pointer.length).context("sidecar length exceeds u32")?;
2057    let mut out = Vec::with_capacity(SIDECAR_POINTER_LENGTH);
2058    out.extend_from_slice(SIDECAR_MAGIC);
2059    out.push(SIDECAR_POINTER_VERSION);
2060    out.push(sidecar_pointer_scheme_id(&pointer.scheme)?);
2061    out.push(sidecar_pointer_carrier_flags(&pointer.carriers));
2062    out.push(0);
2063    out.extend_from_slice(&pointer.seed.to_be_bytes());
2064    out.extend_from_slice(&length.to_be_bytes());
2065    out.extend_from_slice(&pointer.sha256_bytes);
2066    debug_assert_eq!(out.len(), SIDECAR_POINTER_LENGTH);
2067    Ok(out)
2068}
2069
2070pub fn decode_sidecar_header_pointer(payload: &[u8]) -> Result<SidecarHeaderPointer> {
2071    if payload.len() != SIDECAR_POINTER_LENGTH {
2072        bail!("sidecar pointer has invalid length {}", payload.len());
2073    }
2074    if &payload[..4] != SIDECAR_MAGIC {
2075        bail!("sidecar pointer magic is unsupported");
2076    }
2077    let version = payload[4];
2078    if version != SIDECAR_POINTER_VERSION {
2079        bail!("unsupported sidecar pointer version {version}");
2080    }
2081    let scheme = sidecar_pointer_scheme_name(payload[5])?;
2082    let carriers = sidecar_pointer_carriers(payload[6])?;
2083    if payload[7] != 0 {
2084        bail!("sidecar pointer reserved byte must be 0");
2085    }
2086    let seed = u32::from_be_bytes(payload[8..12].try_into().expect("slice length"));
2087    if seed == 0 {
2088        bail!("sidecar seed must be nonzero");
2089    }
2090    let length = u32::from_be_bytes(payload[12..16].try_into().expect("slice length")) as usize;
2091    if length == 0 {
2092        bail!("sidecar length must be nonzero");
2093    }
2094    let mut sha256_bytes = [0u8; 32];
2095    sha256_bytes.copy_from_slice(&payload[16..48]);
2096    let sha256 = general_purpose::URL_SAFE_NO_PAD.encode(sha256_bytes);
2097    Ok(SidecarHeaderPointer {
2098        scheme,
2099        carriers,
2100        seed,
2101        length,
2102        sha256,
2103        sha256_bytes,
2104    })
2105}
2106
2107pub fn sidecar_header_pointer_json(pointer: &SidecarHeaderPointer) -> serde_json::Value {
2108    serde_json::json!({
2109        "v": SIDECAR_POINTER_VERSION,
2110        "c": "BTS1",
2111        "s": pointer.scheme.as_str(),
2112        "r": pointer.carriers.iter().map(|carrier| sidecar_carrier_name(*carrier)).collect::<Vec<_>>(),
2113        "n": pointer.seed,
2114        "l": pointer.length,
2115        "h": pointer.sha256.as_str(),
2116    })
2117}
2118
2119pub fn sidecar_header_pointer_from_prepared(sidecar: &PreparedSidecar) -> SidecarHeaderPointer {
2120    SidecarHeaderPointer {
2121        scheme: sidecar.scheme.clone(),
2122        carriers: sidecar.carriers.clone(),
2123        seed: sidecar.seed,
2124        length: sidecar.bytes.len(),
2125        sha256: sidecar.sha256.clone(),
2126        sha256_bytes: sidecar.sha256_bytes,
2127    }
2128}
2129
2130pub fn prepare_sidecar_label_tuning(
2131    raw: Option<&SidecarLabelTuningOptions>,
2132) -> Option<PreparedSidecarLabelTuning> {
2133    let raw = raw?;
2134    if !raw.enabled.unwrap_or(true) {
2135        return None;
2136    }
2137    Some(PreparedSidecarLabelTuning {
2138        strength: raw.strength.unwrap_or(0.22).clamp(0.0, 1.0),
2139        target_luma: raw.target_luma.unwrap_or(128.0).clamp(0.0, 255.0),
2140        grain: raw.grain.unwrap_or(3).clamp(0, 31),
2141    })
2142}
2143
2144pub fn prepare_sidecar_render(
2145    options: Option<&SidecarRenderOptions>,
2146) -> Result<Option<PreparedSidecar>> {
2147    let Some(options) = options else {
2148        return Ok(None);
2149    };
2150    let has_raw = options
2151        .bts1_base64
2152        .as_deref()
2153        .map(|value| !value.trim().is_empty())
2154        .unwrap_or(false);
2155    let has_items = options
2156        .items
2157        .as_ref()
2158        .map(|items| !items.is_empty())
2159        .unwrap_or(false);
2160    if has_raw == has_items {
2161        bail!("sidecar requires exactly one of bts1Base64 or non-empty items");
2162    }
2163    let bytes = if has_raw {
2164        let raw = options.bts1_base64.as_deref().expect("checked has_raw");
2165        let bytes = decode_base64_text(raw, "sidecar bts1Base64")?;
2166        validate_sidecar_container(&bytes)?;
2167        bytes
2168    } else {
2169        build_sidecar_container_from_items(options.items.as_deref().expect("checked has_items"))?
2170    };
2171    let carriers = normalize_sidecar_carriers(options.carriers.as_deref())?;
2172    let scheme = normalize_sidecar_scheme(options.scheme.as_deref())?;
2173    let label_tuning = prepare_sidecar_label_tuning(options.label_tuning.as_ref());
2174    let seed = options.seed.unwrap_or(SIDECAR_DEFAULT_SEED);
2175    if seed == 0 {
2176        bail!("sidecar seed must be nonzero");
2177    }
2178    if scheme != SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 {
2179        bail!("sidecar stream uses a fixed carrier scheme in this version");
2180    }
2181    let sha256_bytes = sha256_digest_bytes(&bytes);
2182    let sha256 = general_purpose::URL_SAFE_NO_PAD.encode(sha256_bytes);
2183    Ok(Some(PreparedSidecar {
2184        bytes,
2185        scheme,
2186        label_tuning,
2187        carriers,
2188        seed,
2189        sha256,
2190        sha256_bytes,
2191    }))
2192}
2193
2194pub fn mulberry32_next(state: &mut u32) -> u32 {
2195    *state = state.wrapping_add(0x6d2b_79f5);
2196    let mut t = *state;
2197    t = (t ^ (t >> 15)).wrapping_mul(t | 1);
2198    t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61));
2199    t ^ (t >> 14)
2200}
2201
2202pub fn shuffle_pairs_mulberry32(pairs: &mut [(usize, usize)], seed: u32) {
2203    let mut state = seed;
2204    for i in (1..pairs.len()).rev() {
2205        let j = (mulberry32_next(&mut state) as usize) % (i + 1);
2206        pairs.swap(i, j);
2207    }
2208}
2209
2210pub use bytes2rgb::luma_rec709 as sidecar_luma_rec709;
2211
2212pub fn metadata_dither(pixel_index: usize, sequence_index: usize, salt: usize) -> u8 {
2213    let mut value = pixel_index as u64;
2214    value ^= (sequence_index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
2215    value ^= (salt as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9);
2216    value ^= value >> 30;
2217    value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
2218    value ^= value >> 27;
2219    value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
2220    value ^= value >> 31;
2221    (value & 0xff) as u8
2222}
2223
2224fn sidecar_transparent_pair_fallback_base(first_pixel: usize, second_pixel: usize) -> i16 {
2225    128i16 + (metadata_dither(first_pixel, second_pixel, 181) % 31) as i16 - 15i16
2226}
2227
2228pub fn sidecar_capacity_bytes_for_scheme(scheme: &str, carrier_pairs: usize) -> Result<usize> {
2229    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
2230        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(carrier_pairs / 4),
2231        _ => unreachable!("scheme normalized"),
2232    }
2233}
2234
2235pub fn sidecar_pair_bit_width_for_scheme(
2236    scheme: &str,
2237    _rgba: &[u8],
2238    _first_pixel: usize,
2239    _second_pixel: usize,
2240) -> Result<usize> {
2241    match normalize_sidecar_scheme(Some(scheme))?.as_str() {
2242        SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2 => Ok(2),
2243        _ => unreachable!("scheme normalized"),
2244    }
2245}
2246
2247pub fn sidecar_bit_capacity_for_pairs(
2248    scheme: &str,
2249    pairs: &[(usize, usize)],
2250    rgba: &[u8],
2251) -> Result<usize> {
2252    let mut bits = 0usize;
2253    for &(first, second) in pairs {
2254        bits = bits
2255            .checked_add(sidecar_pair_bit_width_for_scheme(
2256                scheme, rgba, first, second,
2257            )?)
2258            .context("sidecar pair bit capacity overflow")?;
2259    }
2260    Ok(bits)
2261}
2262
2263pub fn sidecar_capacity_for_pairs(
2264    scheme: &str,
2265    carriers: &[SidecarCarrier],
2266    pairs: &[(usize, usize)],
2267    rgba: &[u8],
2268) -> Result<SidecarCapacity> {
2269    let bit_capacity = sidecar_bit_capacity_for_pairs(scheme, pairs, rgba)?;
2270    let two_bit_pairs = pairs
2271        .iter()
2272        .filter_map(|&(first, second)| {
2273            sidecar_pair_bit_width_for_scheme(scheme, rgba, first, second).ok()
2274        })
2275        .filter(|width| *width == 2)
2276        .count();
2277    Ok(SidecarCapacity {
2278        scheme: normalize_sidecar_scheme(Some(scheme))?,
2279        carriers: carriers
2280            .iter()
2281            .map(|carrier| sidecar_carrier_name(*carrier).to_string())
2282            .collect(),
2283        carrier_pixels: pairs.len() * 2,
2284        carrier_pairs: pairs.len(),
2285        capacity_bits: bit_capacity,
2286        capacity_bytes: bit_capacity / 8,
2287        bits_per_pair: if pairs.is_empty() {
2288            0.0
2289        } else {
2290            bit_capacity as f64 / pairs.len() as f64
2291        },
2292        two_bit_pairs,
2293    })
2294}
2295
2296fn clamp_sidecar_byte(value: i16) -> u8 {
2297    value.clamp(0, 255) as u8
2298}
2299
2300pub fn tune_sidecar_pixels(
2301    data: &mut [u8],
2302    pixel_indices: &[usize],
2303    tuning: &PreparedSidecarLabelTuning,
2304) {
2305    for &pixel in pixel_indices {
2306        let offset = pixel * 4;
2307        if offset + 3 >= data.len() || data[offset + 3] == 0 {
2308            continue;
2309        }
2310        let grain = if tuning.grain > 0 {
2311            let span = (tuning.grain as u8).saturating_mul(2).saturating_add(1);
2312            (metadata_dither(pixel, pixel, 223) % span) as i16 - tuning.grain
2313        } else {
2314            0
2315        };
2316        let target = (tuning.target_luma + grain as f64).clamp(0.0, 255.0);
2317        let luma = sidecar_luma_rec709(data, pixel);
2318        let delta = ((target - luma) * tuning.strength).round() as i16;
2319        for channel in 0..3 {
2320            data[offset + channel] = clamp_sidecar_byte(data[offset + channel] as i16 + delta);
2321        }
2322        data[offset + 3] = 255;
2323    }
2324}
2325
2326fn sidecar_payload_bit(bytes: &[u8], bit_index: usize) -> u8 {
2327    let byte = bytes[bit_index / 8];
2328    let shift = 7 - (bit_index % 8);
2329    (byte >> shift) & 0x01
2330}
2331
2332fn push_sidecar_decoded_bit(out: &mut [u8], bit_index: usize, bit: u8) {
2333    if bit != 0 {
2334        let byte_index = bit_index / 8;
2335        let shift = 7 - (bit_index % 8);
2336        out[byte_index] |= 1 << shift;
2337    }
2338}
2339
2340fn paint_sidecar_pair_bit(
2341    data: &mut [u8],
2342    first_pixel: usize,
2343    second_pixel: usize,
2344    bit: u8,
2345    magnitude_bit: Option<u8>,
2346) {
2347    let first_offset = first_pixel * 4;
2348    let second_offset = second_pixel * 4;
2349    let delta = if magnitude_bit == Some(1) {
2350        SIDECAR_PAIR_MAGNITUDE_DELTA
2351    } else {
2352        SIDECAR_PAIR_SIGN_DELTA
2353    };
2354    let first_delta = if bit == 1 { delta } else { -delta };
2355    let second_delta = -first_delta;
2356    let transparent = data[first_offset + 3] == 0 || data[second_offset + 3] == 0;
2357    let fallback_base = sidecar_transparent_pair_fallback_base(first_pixel, second_pixel);
2358    for channel in 0..3 {
2359        let average = if transparent {
2360            fallback_base
2361        } else {
2362            (data[first_offset + channel] as i16 + data[second_offset + channel] as i16 + 1) / 2
2363        }
2364        .clamp(delta, 255 - delta);
2365        data[first_offset + channel] = clamp_sidecar_byte(average + first_delta);
2366        data[second_offset + channel] = clamp_sidecar_byte(average + second_delta);
2367    }
2368    if data[first_offset + 3] == 0 {
2369        data[first_offset + 3] = 255;
2370    }
2371    if data[second_offset + 3] == 0 {
2372        data[second_offset + 3] = 255;
2373    }
2374}
2375
2376pub fn decode_pairsign_sidecar_bytes_from_pairs(
2377    rgba: &[u8],
2378    pairs: &[(usize, usize)],
2379    scheme: &str,
2380    byte_length: usize,
2381) -> Result<Vec<u8>> {
2382    let bit_capacity = sidecar_bit_capacity_for_pairs(scheme, pairs, rgba)?;
2383    if byte_length.saturating_mul(8) > bit_capacity {
2384        bail!(
2385            "sidecar descriptor length {} exceeds pair-sign carrier capacity {}",
2386            byte_length,
2387            bit_capacity / 8
2388        );
2389    }
2390    let target_bits = byte_length
2391        .checked_mul(8)
2392        .context("sidecar decode bit length overflow")?;
2393    let mut out = vec![0u8; byte_length];
2394    let mut bit_index = 0usize;
2395    for &(first, second) in pairs {
2396        if bit_index >= target_bits {
2397            break;
2398        }
2399        let first_luma = sidecar_luma_rec709(rgba, first);
2400        let second_luma = sidecar_luma_rec709(rgba, second);
2401        let sign_bit = if first_luma > second_luma { 1u8 } else { 0u8 };
2402        push_sidecar_decoded_bit(&mut out, bit_index, sign_bit);
2403        bit_index += 1;
2404        if bit_index < target_bits
2405            && sidecar_pair_bit_width_for_scheme(scheme, rgba, first, second)? == 2
2406        {
2407            let magnitude_bit =
2408                if (first_luma - second_luma).abs() >= SIDECAR_PAIR_MAGNITUDE_THRESHOLD {
2409                    1u8
2410                } else {
2411                    0u8
2412                };
2413            push_sidecar_decoded_bit(&mut out, bit_index, magnitude_bit);
2414            bit_index += 1;
2415        }
2416    }
2417    if bit_index < target_bits {
2418        bail!("sidecar carrier did not provide enough bits to decode descriptor length");
2419    }
2420    Ok(out)
2421}
2422
2423pub fn paint_sidecar_bytes_into_pairs(
2424    data: &mut [u8],
2425    pairs: &[(usize, usize)],
2426    sidecar: &PreparedSidecar,
2427) -> Result<SidecarRenderSummary> {
2428    let carrier_pair_count = pairs.len();
2429    let bit_capacity = sidecar_bit_capacity_for_pairs(&sidecar.scheme, pairs, data)?;
2430    let capacity_bytes = bit_capacity / 8;
2431    let payload_bits = sidecar
2432        .bytes
2433        .len()
2434        .checked_mul(8)
2435        .context("sidecar payload bit length overflow")?;
2436    if payload_bits > bit_capacity {
2437        bail!(
2438            "sidecar stream is {} bytes but selected carriers only fit {} bytes",
2439            sidecar.bytes.len(),
2440            capacity_bytes
2441        );
2442    }
2443
2444    let mut pair_index = 0usize;
2445    let mut bit_index = 0usize;
2446    while bit_index < payload_bits {
2447        let (first_pixel, second_pixel) = pairs[pair_index];
2448        let sign_bit = sidecar_payload_bit(&sidecar.bytes, bit_index);
2449        bit_index += 1;
2450        let pair_bit_width =
2451            sidecar_pair_bit_width_for_scheme(&sidecar.scheme, data, first_pixel, second_pixel)?;
2452        let magnitude_bit = if pair_bit_width == 2 {
2453            let bit = if bit_index < payload_bits {
2454                let bit = sidecar_payload_bit(&sidecar.bytes, bit_index);
2455                bit_index += 1;
2456                bit
2457            } else {
2458                0
2459            };
2460            Some(bit)
2461        } else {
2462            None
2463        };
2464        paint_sidecar_pair_bit(data, first_pixel, second_pixel, sign_bit, magnitude_bit);
2465        pair_index += 1;
2466    }
2467    let carriers = sidecar
2468        .carriers
2469        .iter()
2470        .map(|carrier| sidecar_carrier_name(*carrier).to_string())
2471        .collect::<Vec<_>>();
2472    Ok(SidecarRenderSummary {
2473        container: "BTS1".to_string(),
2474        scheme: sidecar.scheme.clone(),
2475        carriers,
2476        seed: sidecar.seed,
2477        bts1_bytes: sidecar.bytes.len(),
2478        sha256: sidecar.sha256.clone(),
2479        carrier_pixels: carrier_pair_count * 2,
2480        carrier_pairs: carrier_pair_count,
2481        capacity_bytes,
2482        used_pairs: pair_index,
2483        unused_pairs: carrier_pair_count.saturating_sub(pair_index),
2484    })
2485}
2486
2487pub fn decode_sidecar_from_pairs(
2488    rgba: &[u8],
2489    pairs: &[(usize, usize)],
2490    scheme: &str,
2491    byte_length: usize,
2492) -> Result<(Vec<u8>, SidecarDecodeResult)> {
2493    let bts1 = decode_pairsign_sidecar_bytes_from_pairs(rgba, pairs, scheme, byte_length)?;
2494    let validation = validate_sidecar_container(&bts1)?;
2495    let sha256 = sha256_base64url(&bts1);
2496    let capacity = sidecar_capacity_bytes_for_scheme(scheme, pairs.len())?;
2497    let descriptor = serde_json::json!({
2498        "container": "BTS1",
2499        "scheme": normalize_sidecar_scheme(Some(scheme))?,
2500        "length": byte_length,
2501    });
2502    let result = SidecarDecodeResult {
2503        ok: true,
2504        descriptor,
2505        validation,
2506        bts1_byte_length: bts1.len(),
2507        sha256,
2508        carrier_pixels: pairs.len() * 2,
2509        carrier_pairs: pairs.len(),
2510        capacity_bytes: capacity,
2511    };
2512    Ok((bts1, result))
2513}
2514
2515#[cfg(test)]
2516mod tests {
2517    use super::*;
2518
2519    #[test]
2520    fn raw_text_container_round_trips() {
2521        let json = r#"[{"type":"text","codec":"raw","name":"liner.txt","text":"hello"}]"#;
2522        let bts1 = build_sidecar_container_from_items_json(json).unwrap();
2523        let validation = validate_sidecar_container(&bts1).unwrap();
2524        assert_eq!(validation.item_count, 1);
2525        let decoded = decode_sidecar_container_items(&bts1).unwrap();
2526        assert_eq!(decoded.items[0].text.as_deref(), Some("hello"));
2527    }
2528
2529    #[test]
2530    fn pointer_round_trips() {
2531        let options: SidecarRenderOptions = serde_json::from_str(
2532            r#"{"seed":324508639,"items":[{"type":"text","codec":"raw","text":"hello"}]}"#,
2533        )
2534        .unwrap();
2535        let sidecar = prepare_sidecar_render(Some(&options)).unwrap().unwrap();
2536        let pointer = sidecar_header_pointer_from_prepared(&sidecar);
2537        let encoded = encode_sidecar_header_pointer(&pointer).unwrap();
2538        let decoded = decode_sidecar_header_pointer(&encoded).unwrap();
2539        assert_eq!(decoded.seed, 324508639);
2540        assert_eq!(decoded.length, sidecar.bytes.len());
2541        assert_eq!(decoded.sha256, sidecar.sha256);
2542    }
2543
2544    #[test]
2545    fn pointer_round_trips_expanded_carriers() {
2546        let options: SidecarRenderOptions = serde_json::from_str(
2547            r#"{"carriers":["label","intergroove","leadInDeadwax"],"items":[{"type":"text","codec":"raw","text":"hello"}]}"#,
2548        )
2549        .unwrap();
2550        let sidecar = prepare_sidecar_render(Some(&options)).unwrap().unwrap();
2551        let pointer = sidecar_header_pointer_from_prepared(&sidecar);
2552        let encoded = encode_sidecar_header_pointer(&pointer).unwrap();
2553        let decoded = decode_sidecar_header_pointer(&encoded).unwrap();
2554        assert_eq!(
2555            decoded.carriers,
2556            vec![
2557                SidecarCarrier::Label,
2558                SidecarCarrier::Intergroove,
2559                SidecarCarrier::LeadInDeadwax,
2560            ]
2561        );
2562    }
2563
2564    #[test]
2565    fn package_display_header_builder_owns_layout_and_checksums() {
2566        let json = r##"{
2567            "coverShown": true,
2568            "coverEffects": true,
2569            "coverRgbGrain": true,
2570            "innerSleeveShown": true,
2571            "coverEmbedded": true,
2572            "designLabel": "Design Ω",
2573            "designClassName": "posterized",
2574            "posterize": 18,
2575            "saturation": 0.72,
2576            "contrast": 0.64,
2577            "rgbGrainOpacity": 0.86,
2578            "rgbGrainBlendIndex": 3,
2579            "crop": { "x": 0.5, "y": 0.25, "zoom": 1.25 },
2580            "quantizer": null,
2581            "inner": null,
2582            "sleeveToneColor": "#abc",
2583            "sleeveSparkleOpacity": 0.5
2584        }"##;
2585        let bytes = build_package_display_header_bytes_from_json(json).unwrap();
2586        assert_eq!(bytes.len(), DISPLAY_HEADER_LENGTH);
2587        assert_eq!(&bytes[0..4], DISPLAY_HEADER_MAGIC);
2588        assert_eq!(bytes[4], DISPLAY_HEADER_VERSION);
2589        assert_eq!(bytes[5], DISPLAY_HEADER_LENGTH as u8);
2590        assert_eq!(read_u16be(&bytes, 6, "display header flags").unwrap(), 0x1f);
2591        assert_eq!(String::from_utf8_lossy(&bytes[16..22]), "Design");
2592        assert_eq!(bytes[22], b' ');
2593        assert_eq!(String::from_utf8_lossy(&bytes[56..64]), "posteriz");
2594        assert_eq!(bytes[64], 18);
2595        assert_eq!(read_u16be(&bytes, 65, "saturation").unwrap(), 720);
2596        assert_eq!(read_u16be(&bytes, 67, "contrast").unwrap(), 640);
2597        assert_eq!(read_u16be(&bytes, 69, "grain").unwrap(), 860);
2598        assert_eq!(bytes[71], 3);
2599        assert_eq!(read_u16be(&bytes, 72, "crop x").unwrap(), 32768);
2600        assert_eq!(read_u16be(&bytes, 74, "crop y").unwrap(), 16384);
2601        assert_eq!(read_u16be(&bytes, 76, "crop zoom").unwrap(), 1250);
2602        assert_eq!(bytes[78], 255);
2603        assert_eq!(read_u16be(&bytes, 80, "inner").unwrap(), 0);
2604        assert_eq!(String::from_utf8_lossy(&bytes[82..89]), "#AABBCC");
2605        assert_eq!(read_u16be(&bytes, 89, "sparkle opacity").unwrap(), 500);
2606
2607        let payload_crc = read_u32be(&bytes, 8, "payload CRC").unwrap();
2608        assert_eq!(payload_crc, record_core::crc32_ieee(&bytes[16..]));
2609        let mut header_for_crc = bytes.clone();
2610        header_for_crc[12..16].fill(0);
2611        let header_crc = read_u32be(&bytes, 12, "header CRC").unwrap();
2612        assert_eq!(header_crc, record_core::crc32_ieee(&header_for_crc));
2613
2614        let item: serde_json::Value =
2615            serde_json::from_str(&build_package_display_header_item_json(json).unwrap()).unwrap();
2616        assert_eq!(item["type"], "opaque");
2617        assert_eq!(item["codec"], "raw");
2618        assert_eq!(item["name"], DISPLAY_HEADER_NAME);
2619        assert_eq!(item["mime"], DISPLAY_HEADER_MIME);
2620        assert_eq!(item["rawByteLength"], DISPLAY_HEADER_LENGTH);
2621        let item_bytes = general_purpose::STANDARD
2622            .decode(item["dataBase64"].as_str().unwrap())
2623            .unwrap();
2624        assert_eq!(item_bytes, bytes);
2625    }
2626
2627    #[test]
2628    fn package_metadata_item_builder_owns_cover_and_credit_metadata() {
2629        let json = r#"{
2630            "photos": [
2631                {
2632                    "id": "insert-1",
2633                    "name": "Insert One.jpg",
2634                    "status": "ready",
2635                    "hasAvif": true,
2636                    "credit": "  Alice   B.  "
2637                },
2638                {
2639                    "id": "insert-2",
2640                    "name": "Insert Two.png",
2641                    "status": "ready",
2642                    "hasAvif": true,
2643                    "credit": ""
2644                },
2645                {
2646                    "id": "insert-3",
2647                    "name": "Draft.png",
2648                    "status": "queued",
2649                    "hasAvif": false,
2650                    "credit": "Skipped"
2651                }
2652            ],
2653            "cover": {
2654                "hasFile": true,
2655                "hasPreview": true,
2656                "sourcePhotoId": "insert-1",
2657                "sourceWidth": 1200,
2658                "sourceHeight": 800,
2659                "inner": 576,
2660                "crop": { "x": 0.25, "y": 0.5, "zoom": 2 },
2661                "embedded": false,
2662                "shown": true,
2663                "designLabel": "Poster",
2664                "effectsEnabled": true,
2665                "name": "cover.png"
2666            }
2667        }"#;
2668        let items = build_package_metadata_items_json_from_input(
2669            &serde_json::from_str::<PackageMetadataInput>(json).unwrap(),
2670        );
2671        let item = items.as_array().unwrap().first().unwrap();
2672        assert_eq!(item["type"], "json");
2673        assert_eq!(item["codec"], "raw");
2674        assert_eq!(item["name"], PACKAGE_METADATA_ITEM_NAME);
2675        assert_eq!(item["mime"], PACKAGE_METADATA_MIME);
2676
2677        let metadata = &item["json"];
2678        assert_eq!(metadata["kind"], "bitneedle.packageMetadata");
2679        assert_eq!(metadata["version"], 1);
2680        assert_eq!(metadata["photoCredits"].as_array().unwrap().len(), 1);
2681        assert_eq!(metadata["photoCredits"][0]["position"], 1);
2682        assert_eq!(metadata["photoCredits"][0]["sourceName"], "Insert One.jpg");
2683        assert_eq!(metadata["photoCredits"][0]["itemName"], "Insert One.avif");
2684        assert_eq!(metadata["photoCredits"][0]["credit"], "Alice B.");
2685
2686        assert_eq!(metadata["cover"]["role"], "cover");
2687        assert_eq!(metadata["cover"]["source"]["type"], "photoInsert");
2688        assert_eq!(metadata["cover"]["source"]["position"], 1);
2689        assert_eq!(metadata["cover"]["source"]["itemName"], "Insert One.avif");
2690        assert_eq!(metadata["cover"]["display"]["shown"], true);
2691        assert_eq!(metadata["cover"]["display"]["design"], "Poster");
2692        assert_eq!(metadata["cover"]["display"]["effectsEnabled"], true);
2693        assert_eq!(metadata["cover"]["crop"]["normalized"]["x"], 0.25);
2694        assert_eq!(metadata["cover"]["crop"]["normalized"]["y"], 0.5);
2695        assert_eq!(metadata["cover"]["crop"]["normalized"]["zoom"], 2.0);
2696        assert_eq!(metadata["cover"]["crop"]["source"]["width"], 1200);
2697        assert_eq!(metadata["cover"]["crop"]["source"]["height"], 800);
2698        assert_eq!(metadata["cover"]["crop"]["rectangle"]["x"], 200);
2699        assert_eq!(metadata["cover"]["crop"]["rectangle"]["y"], 200);
2700        assert_eq!(metadata["cover"]["crop"]["rectangle"]["width"], 400);
2701        assert_eq!(metadata["cover"]["crop"]["rectangle"]["height"], 400);
2702        assert_eq!(metadata["cover"]["crop"]["output"]["width"], 576);
2703        assert_eq!(metadata["cover"]["crop"]["output"]["height"], 576);
2704    }
2705
2706    #[test]
2707    fn package_metadata_item_builder_omits_empty_metadata() {
2708        let items = build_package_metadata_items_json(r#"{"photos":[]}"#).unwrap();
2709        assert_eq!(items, "[]");
2710    }
2711
2712    #[test]
2713    fn package_image_item_builders_own_names_and_payload_shape() {
2714        let photo_item = build_package_photo_item_json(
2715            r#"{"name":" Insert One.final.png "}"#,
2716            &[0xde, 0xad, 0xbe, 0xef],
2717        )
2718        .unwrap();
2719        let photo: serde_json::Value = serde_json::from_str(&photo_item).unwrap();
2720        assert_eq!(photo["type"], "image");
2721        assert_eq!(photo["codec"], "avif");
2722        assert_eq!(photo["name"], "Insert One.final.avif");
2723        assert_eq!(photo["mime"], PACKAGE_PHOTO_MIME);
2724        assert_eq!(photo["dataBase64"], "3q2+7w==");
2725
2726        let cover_item = build_package_cover_item_json(&[1, 2, 3]);
2727        let cover: serde_json::Value = serde_json::from_str(&cover_item).unwrap();
2728        assert_eq!(cover["type"], "image");
2729        assert_eq!(cover["codec"], "avif");
2730        assert_eq!(cover["name"], PACKAGE_COVER_ITEM_NAME);
2731        assert_eq!(cover["mime"], PACKAGE_PHOTO_MIME);
2732        assert_eq!(cover["dataBase64"], "AQID");
2733    }
2734
2735    #[test]
2736    fn package_encode_cache_key_resolver_owns_normalization() {
2737        let key = resolve_package_image_encode_cache_key_json(
2738            r#"{
2739                "kind": "cover",
2740                "fileName": "cover.png",
2741                "fileSize": 12,
2742                "fileLastModified": "34",
2743                "quantizer": 47.4,
2744                "inner": 999,
2745                "crop": { "x": 1.5, "y": -1, "zoom": 9 },
2746                "colorMode": "sepia"
2747            }"#,
2748        )
2749        .unwrap();
2750        let parsed: serde_json::Value = serde_json::from_str(&key).unwrap();
2751        assert_eq!(
2752            parsed["key"],
2753            "cover.png:12:34:cover:47:576:crop-1.0000-0.0000-6.0000:rgb"
2754        );
2755        assert_eq!(parsed["fileKey"], "cover.png:12:34");
2756        assert_eq!(parsed["cropKey"], "crop-1.0000-0.0000-6.0000");
2757        assert_eq!(parsed["quantizer"], 47);
2758        assert_eq!(parsed["inner"], 576);
2759        assert_eq!(parsed["crop"]["x"], 1.0);
2760        assert_eq!(parsed["crop"]["y"], 0.0);
2761        assert_eq!(parsed["crop"]["zoom"], 6.0);
2762        assert_eq!(parsed["colorMode"], "rgb");
2763        assert_eq!(parsed["monochrome"], false);
2764
2765        let grayscale_key = resolve_package_image_encode_cache_key_json(
2766            r#"{
2767                "kind": "photo",
2768                "fileName": "insert.jpg",
2769                "colorMode": "grayscale"
2770            }"#,
2771        )
2772        .unwrap();
2773        let grayscale: serde_json::Value = serde_json::from_str(&grayscale_key).unwrap();
2774        assert_eq!(grayscale["colorMode"], "grayscale");
2775        assert_eq!(grayscale["monochrome"], true);
2776    }
2777
2778    #[test]
2779    fn package_best_fit_cache_key_resolver_owns_cache_protocol() {
2780        let key = resolve_package_best_fit_cache_key_json(
2781            r#"{
2782                "kind": "photo",
2783                "fileName": "insert.jpg",
2784                "fileSize": 100,
2785                "fileLastModified": 200,
2786                "budgetBytes": "12.9",
2787                "inner": 128,
2788                "cropKey": "crop-0.2500-0.5000-2.0000",
2789                "colorMode": "grayscale"
2790            }"#,
2791        )
2792        .unwrap();
2793        let parsed: serde_json::Value = serde_json::from_str(&key).unwrap();
2794        assert_eq!(
2795            parsed["key"],
2796            "photo:fit-v7-square-crop-qbest47-35-63:insert.jpg:100:200:12:128:crop-0.2500-0.5000-2.0000:grayscale"
2797        );
2798        assert_eq!(parsed["fileKey"], "insert.jpg:100:200");
2799        assert_eq!(parsed["budgetBytes"], 12);
2800        assert_eq!(parsed["inner"], 128);
2801        assert_eq!(parsed["cropKey"], "crop-0.2500-0.5000-2.0000");
2802        assert_eq!(parsed["colorMode"], "grayscale");
2803    }
2804
2805    #[test]
2806    fn package_quantizer_search_planner_drives_floor_and_binary_search() {
2807        let plan = package_quantizer_search_plan(
2808            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2809                r#"{"minQuantizer":0,"maxQuantizer":63,"startQuantizer":47,"trials":[]}"#,
2810            )
2811            .unwrap(),
2812        );
2813        assert_eq!(plan.next_quantizer, Some(47));
2814        assert_eq!(plan.note, "start");
2815        assert!(!plan.done);
2816
2817        let plan = package_quantizer_search_plan(
2818            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2819                r#"{
2820                    "minQuantizer":0,
2821                    "maxQuantizer":63,
2822                    "startQuantizer":47,
2823                    "trials":[{"quantizer":47,"fits":true}]
2824                }"#,
2825            )
2826            .unwrap(),
2827        );
2828        assert_eq!(plan.next_quantizer, Some(35));
2829        assert_eq!(plan.note, "floor");
2830
2831        let plan = package_quantizer_search_plan(
2832            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2833                r#"{
2834                    "minQuantizer":0,
2835                    "maxQuantizer":63,
2836                    "startQuantizer":47,
2837                    "trials":[
2838                        {"quantizer":47,"fits":true},
2839                        {"quantizer":35,"fits":false}
2840                    ]
2841                }"#,
2842            )
2843            .unwrap(),
2844        );
2845        assert_eq!(plan.next_quantizer, Some(41));
2846        assert_eq!(plan.note, "bisect");
2847        assert_eq!(plan.best_quantizer, Some(47));
2848    }
2849
2850    #[test]
2851    fn package_quantizer_search_planner_drives_ceiling_and_refinement() {
2852        let plan = package_quantizer_search_plan(
2853            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2854                r#"{
2855                    "minQuantizer":0,
2856                    "maxQuantizer":63,
2857                    "startQuantizer":47,
2858                    "trials":[{"quantizer":47,"fits":false}]
2859                }"#,
2860            )
2861            .unwrap(),
2862        );
2863        assert_eq!(plan.next_quantizer, Some(63));
2864        assert_eq!(plan.note, "ceiling");
2865        assert_eq!(plan.best_quantizer, None);
2866
2867        let plan = package_quantizer_search_plan(
2868            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2869                r#"{
2870                    "minQuantizer":0,
2871                    "maxQuantizer":63,
2872                    "startQuantizer":47,
2873                    "trials":[
2874                        {"quantizer":47,"fits":false},
2875                        {"quantizer":63,"fits":true}
2876                    ]
2877                }"#,
2878            )
2879            .unwrap(),
2880        );
2881        assert_eq!(plan.next_quantizer, Some(55));
2882        assert_eq!(plan.note, "bisect");
2883
2884        let plan = package_quantizer_search_plan(
2885            &serde_json::from_str::<PackageQuantizerSearchPlanInput>(
2886                r#"{
2887                    "minQuantizer":0,
2888                    "maxQuantizer":63,
2889                    "startQuantizer":47,
2890                    "trials":[
2891                        {"quantizer":47,"fits":false},
2892                        {"quantizer":63,"fits":true},
2893                        {"quantizer":55,"fits":true},
2894                        {"quantizer":51,"fits":true},
2895                        {"quantizer":49,"fits":true},
2896                        {"quantizer":48,"fits":true}
2897                    ]
2898                }"#,
2899            )
2900            .unwrap(),
2901        );
2902        assert_eq!(plan.next_quantizer, Some(46));
2903        assert_eq!(plan.note, "refine");
2904        assert_eq!(plan.best_quantizer, Some(48));
2905    }
2906
2907    #[test]
2908    fn package_fit_budget_owns_cover_and_photo_budget_policy() {
2909        let budget = package_fit_budget(
2910            &serde_json::from_str::<PackageFitBudgetInput>(
2911                r#"{"capacity":9000,"baseBts1Bytes":3000,"photoCount":4}"#,
2912            )
2913            .unwrap(),
2914        );
2915        assert_eq!(budget.capacity, 9000);
2916        assert_eq!(budget.base_bts1_bytes, 3000);
2917        assert_eq!(budget.available_bytes, 6000);
2918        assert_eq!(budget.cover_budget_bytes, 2000);
2919        assert_eq!(budget.remaining_bytes, 6000);
2920        assert_eq!(budget.photo_count, 4);
2921        assert_eq!(budget.per_photo_budget_bytes, 1500);
2922
2923        let over_budget = package_fit_budget(
2924            &serde_json::from_str::<PackageFitBudgetInput>(
2925                r#"{"capacity":100,"baseBts1Bytes":250,"photoCount":2}"#,
2926            )
2927            .unwrap(),
2928        );
2929        assert_eq!(over_budget.available_bytes, 0);
2930        assert_eq!(over_budget.cover_budget_bytes, 0);
2931        assert_eq!(over_budget.remaining_bytes, 0);
2932        assert_eq!(over_budget.per_photo_budget_bytes, 0);
2933    }
2934
2935    #[test]
2936    fn package_sidecar_render_options_own_protocol_and_label_tuning() {
2937        let options = build_package_sidecar_render_options(
2938            &serde_json::from_str::<PackageSidecarRenderOptionsInput>(
2939                r#"{"seed":42,"capacityBytes":1000,"payloadBytes":600,"items":[{"type":"text"}]}"#,
2940            )
2941            .unwrap(),
2942        );
2943        assert_eq!(options["sidecar"]["seed"], 42);
2944        assert_eq!(
2945            options["sidecar"]["carriers"],
2946            serde_json::json!(["label", "intergroove"])
2947        );
2948        assert_eq!(options["sidecar"]["scheme"], "pairsign-safe-luma-v2");
2949        assert_eq!(options["sidecar"]["labelTuning"]["enabled"], true);
2950        assert_eq!(options["sidecar"]["labelTuning"]["targetLuma"], 128);
2951        assert_eq!(options["sidecar"]["labelTuning"]["strength"], 0.34);
2952        assert_eq!(options["sidecar"]["labelTuning"]["grain"], 4);
2953        assert_eq!(options["sidecar"]["items"][0]["type"], "text");
2954        assert!(options["sidecar"]["bts1Base64"].is_null());
2955
2956        let bts1_options = build_package_sidecar_render_options(
2957            &serde_json::from_str::<PackageSidecarRenderOptionsInput>(
2958                r#"{"seed":7,"capacityBytes":0,"payloadBytes":1200,"bts1Base64":"AAAA"}"#,
2959            )
2960            .unwrap(),
2961        );
2962        assert_eq!(bts1_options["sidecar"]["labelTuning"]["strength"], 0.22);
2963        assert_eq!(bts1_options["sidecar"]["labelTuning"]["grain"], 3);
2964        assert_eq!(bts1_options["sidecar"]["bts1Base64"], "AAAA");
2965        assert!(bts1_options["sidecar"]["items"].is_null());
2966    }
2967
2968    #[test]
2969    fn package_pattern_preservation_owns_decoded_item_protocol_mapping() {
2970        let decoded = serde_json::json!({
2971            "items": [
2972                {
2973                    "itemType": 3,
2974                    "codec": 0,
2975                    "flags": 2,
2976                    "rawByteLength": 123,
2977                    "name": PACKAGE_PATTERN_SIDECAR_ITEM_NAME,
2978                    "mime": PACKAGE_PATTERN_SIDECAR_MIME,
2979                    "storedDataBase64": "stored-bytes",
2980                    "dataBase64": "decoded-bytes"
2981                },
2982                {
2983                    "itemType": 2,
2984                    "codec": 3,
2985                    "name": "album-cover.avif",
2986                    "mime": "image/avif",
2987                    "storedDataBase64": "cover"
2988                }
2989            ]
2990        });
2991        let items = package_preserved_pattern_items(&decoded).unwrap();
2992        assert_eq!(items.len(), 1);
2993        assert_eq!(items[0]["type"], "json");
2994        assert_eq!(items[0]["codec"], "raw");
2995        assert_eq!(items[0]["name"], PACKAGE_PATTERN_SIDECAR_ITEM_NAME);
2996        assert_eq!(items[0]["mime"], PACKAGE_PATTERN_SIDECAR_MIME);
2997        assert_eq!(items[0]["dataBase64"], "stored-bytes");
2998        assert_eq!(items[0]["rawByteLength"], 123);
2999        assert_eq!(items[0]["flags"], 2);
3000    }
3001
3002    #[test]
3003    fn pair_sign_round_trips_bytes() {
3004        let options: SidecarRenderOptions =
3005            serde_json::from_str(r#"{"items":[{"type":"text","codec":"raw","text":"hello"}]}"#)
3006                .unwrap();
3007        let sidecar = prepare_sidecar_render(Some(&options)).unwrap().unwrap();
3008        let mut rgba = vec![128u8; 512 * 4];
3009        for pixel in 0..512 {
3010            rgba[pixel * 4 + 3] = 255;
3011        }
3012        let pairs = (0..256)
3013            .map(|index| (index * 2, index * 2 + 1))
3014            .collect::<Vec<_>>();
3015        paint_sidecar_bytes_into_pairs(&mut rgba, &pairs, &sidecar).unwrap();
3016        let decoded = decode_pairsign_sidecar_bytes_from_pairs(
3017            &rgba,
3018            &pairs,
3019            SIDECAR_SCHEME_PAIRSIGN_SAFE_LUMA_V2,
3020            sidecar.bytes.len(),
3021        )
3022        .unwrap();
3023        assert_eq!(decoded, sidecar.bytes);
3024    }
3025}