Skip to content

Commit d638bcf

Browse files
refactor: standardize number parsing, adjust animation timing test: Add mocks and new suite for grid rendering.
1 parent 3ab98c1 commit d638bcf

6 files changed

Lines changed: 294 additions & 54 deletions

File tree

core/src/wavelet.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ static const double INV_SQRT2 = 0.70710678118654752440; // 1.0 / sqrt(2)
3030
* det[k] = (data[2k] - data[2k+1]) / sqrt(2)
3131
*/
3232
static void haar1d_fwd(double* data, int n) {
33-
double tmp[8];
33+
if (n > 8) return; // Basic safety check
34+
double tmp[8] = {0};
3435
int half = n / 2;
3536
for (int k = 0; k < half; ++k) {
3637
tmp[k] = (data[2*k] + data[2*k+1]) * INV_SQRT2;
@@ -47,7 +48,8 @@ static void haar1d_fwd(double* data, int n) {
4748
* x[2k+1] = (avg[k] - det[k]) / sqrt(2)
4849
*/
4950
static void haar1d_inv(double* data, int n) {
50-
double tmp[8];
51+
if (n > 8) return; // Basic safety check
52+
double tmp[8] = {0};
5153
int half = n / 2;
5254
for (int k = 0; k < half; ++k) {
5355
tmp[2*k] = (data[k] + data[k + half]) * INV_SQRT2;
@@ -279,9 +281,13 @@ void idwtImage(double* data, int width, int height, int levels) {
279281
}
280282

281283
double dwtQuantStep(int x, int y, int width, int height, int levels, double baseStep) {
282-
// Replay the forward pass dimensions (levels capped at 6, so fixed arrays suffice).
283-
// fwdW[lev] / fwdH[lev] = even dimension of the subimage transformed at forward level `lev`.
284-
int fwdW[6], fwdH[6];
284+
if (levels <= 0) return baseStep;
285+
286+
// Safety cap to prevent buffer overflow and integer shift overflow.
287+
// 16 levels is enough for any image up to 65536x65536.
288+
if (levels > 16) levels = 16;
289+
290+
int fwdW[16], fwdH[16];
285291
{
286292
int fw = width, fh = height;
287293
for (int lev = 0; lev < levels; ++lev) {

readme.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@
44

55
An interactive codec laboratory to visualize how transform-based image compression works. The C++ core is compiled to WebAssembly, allowing for real-time, in-browser experimentation.
66

7-
This project demonstrates:
7+
## ✨ Key Features
88

9-
- 8x8 block splitting
10-
- Color space transformation (RGB → YCbCr)
11-
- Discrete Cosine Transform (DCT)
12-
- Quantization
13-
- Inverse DCT (reconstruction)
14-
- PSNR computation
15-
- Visual difference and artifact analysis
16-
- Block-level coefficient inspection
9+
- Real-time transform-based compression in the browser
10+
- C++ codec core compiled to WebAssembly
11+
- DCT and Wavelet transform comparison
12+
- Interactive block inspector
13+
- Zig-zag and zero-run visualization
14+
- Artifact analysis maps
15+
- PSNR-based quality metrics
1716

18-
---
17+
## 🛠 Technologies
18+
19+
- C++17
20+
- WebAssembly (Emscripten)
21+
- Svelte 5
22+
- TypeScript
23+
- Vite
24+
- OpenCV (native testing)
1925

2026
## 🏗 Project Structure
2127

@@ -128,16 +134,18 @@ Run `./build/codec_app --help` to see available options.
128134
- [x] DWT-specific artifact analysis in Inspector
129135
- [x] Multi-transform bitrate estimation & comparison
130136

131-
### Phase 4: Motion Estimation
137+
### Phase 4: Future Work
132138

133139
- [ ] Two-frame video input
134140
- [ ] Block matching (SAD/MSE)
135141
- [ ] Motion vector visualization
136142
- [ ] Temporal bitrate comparison vs. static frames
137143

138-
## ▶️ Why This Project Exists
144+
## ▶️ Motivation
145+
146+
Modern image and video codecs rely heavily on transform-based compression, but the internal mechanisms are often hidden behind opaque implementations.
139147

140-
Transform-based compression is often treated as a black box.
148+
Codec Explorer makes these algorithms visible and interactive.
141149

142150
This project aims to:
143151
- Build intuition for energy compaction

web/public/codec.wasm

-2.25 KB
Binary file not shown.

web/src/lib/grid-renderer.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ export function renderGrid(
143143
const cell = target.closest('.grid-cell') as HTMLElement;
144144
if (!cell) return;
145145

146-
const row = parseInt(cell.dataset.row || '0');
147-
const col = parseInt(cell.dataset.col || '0');
146+
const row = Number.parseInt(cell.dataset.row || '0');
147+
const col = Number.parseInt(cell.dataset.col || '0');
148148
const valStr = cell.dataset.val || '';
149149
const desc = cell.dataset.desc || '';
150150
const isBasis = cell.dataset.isBasis === 'true';
@@ -177,8 +177,8 @@ export function renderGrid(
177177
const isBasis = cell.dataset.isBasis === 'true';
178178
if (!isBasis) return;
179179

180-
const row = parseInt(cell.dataset.row || '0');
181-
const col = parseInt(cell.dataset.col || '0');
180+
const row = Number.parseInt(cell.dataset.row || '0');
181+
const col = Number.parseInt(cell.dataset.col || '0');
182182
const targetGridId = el.dataset.target || '';
183183

184184
window.dispatchEvent(new CustomEvent('animate-basis', {
@@ -295,7 +295,7 @@ export function renderGrid(
295295
badge.style.cursor = 'help';
296296
badge.addEventListener('mouseenter', (e) => {
297297
const target = e.currentTarget as HTMLElement;
298-
const count = parseInt(target.textContent || '0');
298+
const count = Number.parseInt(target.textContent || '0');
299299
showTooltip(e, `${count}/64`, '-', '-', 'Non-zero coefficients (|value| ≥ 0.5)');
300300
});
301301
badge.addEventListener('mouseleave', () => hideTooltip());
@@ -527,10 +527,10 @@ function applyPartialReconToGrid(row: number, col: number): void {
527527
const t = Math.min(1, Math.abs(contrib) / maxContrib) * 0.75;
528528
if (contrib > 0) {
529529
r = Math.round(r + (239 - r) * t);
530-
g = Math.round(g + (68 - g) * t);
531-
b = Math.round(b + (68 - b) * t);
530+
g = Math.round(g + (68 - g) * t);
531+
b = Math.round(b + (68 - b) * t);
532532
} else if (contrib < 0) {
533-
r = Math.round(r + (59 - r) * t);
533+
r = Math.round(r + (59 - r) * t);
534534
g = Math.round(g + (130 - g) * t);
535535
b = Math.round(b + (246 - b) * t);
536536
}
@@ -580,7 +580,7 @@ export function startReconstructionAnimation(): void {
580580

581581
// Resume if paused mid-animation
582582
if (reconState !== null) {
583-
reconState.startTime = performance.now() - reconState.currentIndex * RECON_STEP_MS;
583+
reconState.startTime = Date.now() - reconState.currentIndex * RECON_STEP_MS;
584584
setReconstructionButtonIcon(true);
585585
reconstructionAnimationId = requestAnimationFrame(runReconFrame);
586586
return;
@@ -603,7 +603,7 @@ export function startReconstructionAnimation(): void {
603603
currentIndex: 0,
604604
lastBasisPattern: null,
605605
lastBasisCoeff: 0,
606-
startTime: performance.now(),
606+
startTime: Date.now(),
607607
lastZigzagIdx: 0,
608608
lastRawCoeff: 0,
609609
};
@@ -705,7 +705,7 @@ function updateReconstructionProgress(index: number): void {
705705
if (!el) return;
706706

707707
if (index < 0) { el.style.display = 'none'; return; }
708-
if (index === 0) { el.textContent = ''; el.style.display = 'none'; return; }
708+
if (index === 0) { el.textContent = '0/64'; el.style.display = ''; return; }
709709

710710
if (index >= 64) {
711711
el.textContent = '64/64';
@@ -744,16 +744,16 @@ function showBannerForCell(row: number, col: number): void {
744744
}
745745

746746
const FREQ_DESCRIPTIONS: Record<string, string> = {
747-
'DC': 'Sets the average brightness across the whole block',
748-
'Low': 'Broad shapes and gentle gradients',
749-
'Mid': 'Edges and moderate detail',
747+
'DC': 'Sets the average brightness across the whole block',
748+
'Low': 'Broad shapes and gentle gradients',
749+
'Mid': 'Edges and moderate detail',
750750
'High': 'Sharp edges and fine texture',
751751
};
752752

753753
const FREQ_COLORS: Record<string, string> = {
754-
'DC': '#8b5cf6',
755-
'Low': '#10b981',
756-
'Mid': '#f59e0b',
754+
'DC': '#8b5cf6',
755+
'Low': '#10b981',
756+
'Mid': '#f59e0b',
757757
'High': '#ef4444',
758758
};
759759

@@ -768,16 +768,16 @@ function updateReconAnimBanner(
768768

769769
if (processedCount >= 64) {
770770
// Done state
771-
const stepEl = document.getElementById('reconBannerStep');
772-
const fillEl = document.getElementById('reconBannerFill') as HTMLElement | null;
773-
const freqEl = document.getElementById('reconBannerFreq');
771+
const stepEl = document.getElementById('reconBannerStep');
772+
const fillEl = document.getElementById('reconBannerFill') as HTMLElement | null;
773+
const freqEl = document.getElementById('reconBannerFreq');
774774
const coeffEl = document.getElementById('reconBannerCoeff');
775-
const descEl = document.getElementById('reconBannerDesc');
776-
if (stepEl) stepEl.textContent = '64';
777-
if (fillEl) fillEl.style.width = '100%';
778-
if (freqEl) { freqEl.textContent = 'Complete'; freqEl.style.background = '#10b981'; freqEl.style.color = 'white'; }
775+
const descEl = document.getElementById('reconBannerDesc');
776+
if (stepEl) stepEl.textContent = '64';
777+
if (fillEl) fillEl.style.width = '100%';
778+
if (freqEl) { freqEl.textContent = 'Complete'; freqEl.style.background = '#10b981'; freqEl.style.color = 'white'; }
779779
if (coeffEl) { coeffEl.textContent = ''; coeffEl.style.color = ''; }
780-
if (descEl) descEl.textContent = 'All 64 coefficients applied — reconstruction complete';
780+
if (descEl) descEl.textContent = 'All 64 coefficients applied — reconstruction complete';
781781
// Clear the canvas
782782
const canvas = document.getElementById('reconBasisCanvas') as HTMLCanvasElement | null;
783783
if (canvas) { const ctx = canvas.getContext('2d'); if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); }
@@ -787,19 +787,19 @@ function updateReconAnimBanner(
787787

788788
banner.style.display = '';
789789

790-
const row = Math.floor(zigzagIdx / 8);
791-
const col = zigzagIdx % 8;
790+
const row = Math.floor(zigzagIdx / 8);
791+
const col = zigzagIdx % 8;
792792
const label = getTransformFreqLabel(row, col);
793793
const isZero = Math.abs(coeff) < 0.0001;
794794

795-
const stepEl = document.getElementById('reconBannerStep');
796-
const fillEl = document.getElementById('reconBannerFill') as HTMLElement | null;
797-
const freqEl = document.getElementById('reconBannerFreq');
795+
const stepEl = document.getElementById('reconBannerStep');
796+
const fillEl = document.getElementById('reconBannerFill') as HTMLElement | null;
797+
const freqEl = document.getElementById('reconBannerFreq');
798798
const coeffEl = document.getElementById('reconBannerCoeff');
799-
const descEl = document.getElementById('reconBannerDesc');
799+
const descEl = document.getElementById('reconBannerDesc');
800800

801-
if (stepEl) stepEl.textContent = String(processedCount);
802-
if (fillEl) fillEl.style.width = `${(processedCount / 64) * 100}%`;
801+
if (stepEl) stepEl.textContent = String(processedCount);
802+
if (fillEl) fillEl.style.width = `${(processedCount / 64) * 100}%`;
803803

804804
if (freqEl) {
805805
freqEl.textContent = label;

web/src/lib/inspection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export function inspectBlock(blockX: number, blockY: number): void {
2626
const qualitySlider = document.getElementById('qualitySlider') as HTMLInputElement | null;
2727

2828
const quality = (appState.appMode === 'inspector' && inspQualitySlider)
29-
? parseInt(inspQualitySlider.value)
30-
: (qualitySlider ? parseInt(qualitySlider.value) : appState.quality);
29+
? Number.parseInt(inspQualitySlider.value)
30+
: (qualitySlider ? Number.parseInt(qualitySlider.value) : appState.quality);
3131

3232
const ptr = inspectBlockData(blockX, blockY, channelIndex, quality);
3333
if (!ptr) {

0 commit comments

Comments
 (0)