Spaces:
Running
Running
| /** | |
| * 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 }; | |
| } | |