ltmarx / core /embedder.ts
harelcain's picture
Upload 37 files
f2f99a3 verified
/**
* 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 };
}