File size: 6,642 Bytes
f2f99a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/**
 * High-level watermark embedder
 *
 * Takes a Y plane + payload + key + config → returns watermarked Y plane
 */

import type { WatermarkConfig, EmbedResult, Buffer2D } from './types.js';
import { yPlaneToBuffer, bufferToYPlane, dwtForward, dwtInverse, extractSubband, writeSubband } from './dwt.js';
import { dctForward8x8, dctInverse8x8, extractBlock, writeBlock, ZIGZAG_ORDER } from './dct.js';
import { dmqimEmbed } from './dmqim.js';
import { crcAppend } from './crc.js';
import { BchCodec } from './bch.js';
import { generateDithers, generatePermutation } from './keygen.js';
import { computeTileGrid, getTileOrigin, getTileBlocks } from './tiling.js';
import { blockAcEnergy, computeMaskingFactors } from './masking.js';

/**
 * Convert a 32-bit payload (4 bytes) to a bit array
 */
export function payloadToBits(payload: Uint8Array): Uint8Array {
  const bits = new Uint8Array(payload.length * 8);
  for (let i = 0; i < payload.length; i++) {
    for (let b = 0; b < 8; b++) {
      bits[i * 8 + b] = (payload[i] >> (7 - b)) & 1;
    }
  }
  return bits;
}

/**
 * Convert a bit array back to bytes
 */
export function bitsToPayload(bits: Uint8Array): Uint8Array {
  const bytes = new Uint8Array(Math.ceil(bits.length / 8));
  for (let i = 0; i < bits.length; i++) {
    if (bits[i]) {
      bytes[Math.floor(i / 8)] |= 1 << (7 - (i % 8));
    }
  }
  return bytes;
}

/**
 * Embed a watermark into a Y plane
 */
export function embedWatermark(
  yPlane: Uint8Array,
  width: number,
  height: number,
  payload: Uint8Array,
  key: string,
  config: WatermarkConfig
): EmbedResult {
  // Step 1: Encode payload
  // Always use exactly 32 bits of payload, zero-pad the BCH message to fill k
  const PAYLOAD_BITS = 32;
  let payloadBits = payloadToBits(payload);
  // Truncate or pad to exactly 32 bits
  if (payloadBits.length < PAYLOAD_BITS) {
    const padded = new Uint8Array(PAYLOAD_BITS);
    padded.set(payloadBits);
    payloadBits = padded;
  } else if (payloadBits.length > PAYLOAD_BITS) {
    payloadBits = payloadBits.subarray(0, PAYLOAD_BITS);
  }
  // CRC protects the 32-bit payload
  const withCrc = crcAppend(payloadBits, config.crc.bits);
  // Pad to fill BCH message length k
  const bchMessage = new Uint8Array(config.bch.k);
  bchMessage.set(withCrc);
  const bch = new BchCodec(config.bch);
  const encoded = bch.encode(bchMessage);
  const codedLength = encoded.length;

  // Generate interleaving permutation
  const perm = generatePermutation(key, codedLength);
  const interleaved = new Uint8Array(codedLength);
  for (let i = 0; i < codedLength; i++) {
    interleaved[perm[i]] = encoded[i];
  }

  // Step 2: Forward DWT
  const buf = yPlaneToBuffer(yPlane, width, height);
  const { buf: dwtBuf, dims } = dwtForward(buf, config.dwtLevels);

  // Step 3: Extract HL subband at deepest level
  // After N levels, the HL subband is in the bottom-left of the level-N region
  let w = dims[dims.length - 1].w;
  let h = dims[dims.length - 1].h;
  for (let l = 0; l < config.dwtLevels - 1; l++) {
    w >>= 1;
    h >>= 1;
  }
  const hlSubband = extractSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL');

  // Step 4: Set up tile grid
  // tilePeriod is in spatial pixels; in subband it's tilePeriod / (2^levels)
  const subbandTilePeriod = Math.floor(config.tilePeriod / (1 << config.dwtLevels));
  const tileGrid = computeTileGrid(hlSubband.width, hlSubband.height, subbandTilePeriod);

  if (tileGrid.totalTiles === 0) {
    // Frame too small for any tiles — return unchanged
    return { yPlane: new Uint8Array(yPlane), psnr: Infinity };
  }

  // Step 5: Generate dithers — same sequence reused per tile so each tile
  // is independently decodable (required for crop robustness)
  const coeffsPerBlock = config.zigzagPositions.length;
  const maxCoeffsPerTile = 1024; // Upper bound
  const dithers = generateDithers(key, maxCoeffsPerTile, config.delta);

  // Step 6: For each tile, embed the coded bits
  // Reusable block buffer to avoid allocation per block
  const blockBuf = new Float64Array(64);

  // Precompute zigzag → coefficient index mapping
  const zigCoeffIdx = new Int32Array(config.zigzagPositions.length);
  for (let z = 0; z < config.zigzagPositions.length; z++) {
    const [r, c] = ZIGZAG_ORDER[config.zigzagPositions[z]];
    zigCoeffIdx[z] = r * 8 + c;
  }

  for (let tileIdx = 0; tileIdx < tileGrid.totalTiles; tileIdx++) {
    let ditherIdx = 0; // Reset per tile — each tile uses same dither sequence
    const origin = getTileOrigin(tileGrid, tileIdx);
    const blocks = getTileBlocks(origin.x, origin.y, subbandTilePeriod, hlSubband.width, hlSubband.height);

    // Compute masking factors if enabled
    let maskingFactors: Float64Array | null = null;
    if (config.perceptualMasking && blocks.length > 0) {
      const energies = new Float64Array(blocks.length);
      for (let bi = 0; bi < blocks.length; bi++) {
        extractBlock(hlSubband.data, hlSubband.width, blocks[bi].row, blocks[bi].col, blockBuf);
        dctForward8x8(blockBuf);
        energies[bi] = blockAcEnergy(blockBuf);
      }
      maskingFactors = computeMaskingFactors(energies);
    }

    let bitIdx = 0;
    for (let bi = 0; bi < blocks.length; bi++) {
      const { row, col } = blocks[bi];
      extractBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);

      // Forward DCT
      dctForward8x8(blockBuf);

      const maskFactor = maskingFactors ? maskingFactors[bi] : 1.0;
      const effectiveDelta = config.delta * maskFactor;

      // Embed bits into selected zigzag positions
      for (let z = 0; z < zigCoeffIdx.length; z++) {
        if (bitIdx >= codedLength) bitIdx = 0; // Repeat pattern

        const coeffIdx = zigCoeffIdx[z];
        const bit = interleaved[bitIdx];
        const dither = dithers[ditherIdx++];

        blockBuf[coeffIdx] = dmqimEmbed(blockBuf[coeffIdx], bit, effectiveDelta, dither);

        bitIdx++;
      }

      // Inverse DCT and write back
      dctInverse8x8(blockBuf);
      writeBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
    }
  }

  // Step 7: Write modified HL subband back and inverse DWT
  writeSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL', hlSubband);
  dwtInverse(dwtBuf, dims);

  // Convert back to Y plane
  const watermarkedY = bufferToYPlane(dwtBuf);

  // Compute PSNR
  let mse = 0;
  for (let i = 0; i < yPlane.length; i++) {
    const diff = yPlane[i] - watermarkedY[i];
    mse += diff * diff;
  }
  mse /= yPlane.length;
  const psnr = mse > 0 ? 10 * Math.log10(255 * 255 / mse) : Infinity;

  return { yPlane: watermarkedY, psnr };
}