From cc97f3d131609cf1b195cf96fcf688331ef786c7 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 17 Feb 2026 21:15:45 +0530 Subject: [PATCH 1/3] add codec and image tests --- .github/workflows/ci.yml | 13 ++++- core/tests/CMakeLists.txt | 2 + core/tests/test_codecanalysis.cpp | 72 ++++++++++++++++++++++++ core/tests/test_imagecodec.cpp | 93 +++++++++++++++++++++++++++++++ sonar-project.properties | 2 +- 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 core/tests/test_codecanalysis.cpp create mode 100644 core/tests/test_imagecodec.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcc8952..f824584 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: args: > - -Dsonar.cfamily.build-wrapper-output=bw-output + -Dsonar.cfamily.compile-commands=bw-output/compile_commands.json -Dsonar.coverageReportPaths=coverage.xml build-web: @@ -78,6 +78,12 @@ jobs: needs: build-web # Only deploy on pushes to main, not PRs if: github.ref == 'refs/heads/main' && github.event_name == 'push' + permissions: + deployments: write + contents: read + environment: + name: production + url: ${{ steps.deploy.outputs.deployment_url }} steps: - uses: actions/checkout@v4 @@ -97,7 +103,10 @@ jobs: run: vercel build --prod --token=$VERCEL_TOKEN - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN + id: deploy + run: | + DEPLOY_URL=$(vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN --yes) + echo "deployment_url=$DEPLOY_URL" >> $GITHUB_OUTPUT env: VERCEL_ORG_ID: ${{ secrets.ORGID }} VERCEL_PROJECT_ID: ${{ secrets.PROJECTID }} diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index fb30c29..b0ef3fe 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -20,6 +20,8 @@ add_executable(codec_core_tests image_test.cpp colorspace_test.cpp transform_test.cpp + test_imagecodec.cpp + test_codecanalysis.cpp ) target_link_libraries(codec_core_tests diff --git a/core/tests/test_codecanalysis.cpp b/core/tests/test_codecanalysis.cpp new file mode 100644 index 0000000..0e6556d --- /dev/null +++ b/core/tests/test_codecanalysis.cpp @@ -0,0 +1,72 @@ +#include +#include "CodecAnalysis.h" +#include "Image.h" +#include + +// Helper to create a flat color image +Image createFlatImage(int width, int height, double r, double g, double b) { + Image img(width, height, 3); + double* data = img.data(); + for (int i = 0; i < width * height; ++i) { + data[i * 3 + 0] = b; + data[i * 3 + 1] = g; + data[i * 3 + 2] = r; + } + return img; +} + +TEST(CodecAnalysisTest, PSNR_Identical) { + Image img = createFlatImage(16, 16, 100.0, 100.0, 100.0); + double psnr = CodecAnalysis::computePSNR(img, img); + // Expect very high PSNR (capped at 100.0 in implementation for perfect match) + EXPECT_GE(psnr, 99.0); +} + +TEST(CodecAnalysisTest, PSNR_Different) { + Image img1 = createFlatImage(16, 16, 100.0, 100.0, 100.0); + Image img2 = createFlatImage(16, 16, 110.0, 110.0, 110.0); // Slightly different + + double psnr = CodecAnalysis::computePSNR(img1, img2); + // Should be finite and less than perfect + EXPECT_LT(psnr, 99.0); + EXPECT_GT(psnr, 0.0); +} + +TEST(CodecAnalysisTest, SSIM_Identical) { + Image img = createFlatImage(16, 16, 100.0, 100.0, 100.0); + double ssim = CodecAnalysis::computeSSIM(img, img); + EXPECT_DOUBLE_EQ(ssim, 1.0); +} + +TEST(CodecAnalysisTest, SSIM_Different) { + Image img1 = createFlatImage(16, 16, 0.0, 0.0, 0.0); + Image img2 = createFlatImage(16, 16, 255.0, 255.0, 255.0); + + double ssim = CodecAnalysis::computeSSIM(img1, img2); + EXPECT_LT(ssim, 1.0); + EXPECT_GE(ssim, 0.0); +} + +TEST(CodecAnalysisTest, ArtifactMap_Dimensions) { + Image img1 = createFlatImage(20, 20, 100.0, 100.0, 100.0); + Image img2 = createFlatImage(20, 20, 105.0, 105.0, 105.0); + + Image artifact = CodecAnalysis::computeArtifactMap(img1, img2); + EXPECT_EQ(artifact.width(), 20); + EXPECT_EQ(artifact.height(), 20); + EXPECT_EQ(artifact.channels(), 3); +} + +TEST(CodecAnalysisTest, ComputeMetrics_Structure) { + Image img1 = createFlatImage(16, 16, 100.0, 50.0, 25.0); + Image img2 = createFlatImage(16, 16, 100.0, 50.0, 25.0); + + CodecMetrics metrics = CodecAnalysis::computeMetrics(img1, img2); + + EXPECT_GE(metrics.psnrY, 99.0); + EXPECT_GE(metrics.psnrCr, 99.0); + EXPECT_GE(metrics.psnrCb, 99.0); + EXPECT_DOUBLE_EQ(metrics.ssimY, 1.0); + EXPECT_DOUBLE_EQ(metrics.ssimCr, 1.0); + EXPECT_DOUBLE_EQ(metrics.ssimCb, 1.0); +} diff --git a/core/tests/test_imagecodec.cpp b/core/tests/test_imagecodec.cpp new file mode 100644 index 0000000..dbaba4a --- /dev/null +++ b/core/tests/test_imagecodec.cpp @@ -0,0 +1,93 @@ +#include +#include "ImageCodec.h" +#include "CodecAnalysis.h" +#include "Image.h" +#include + +// Helper to create a simple test image (gradient) +Image createTestImage(int width, int height) { + Image img(width, height, 3); + double* data = img.data(); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int idx = (y * width + x) * 3; + data[idx + 0] = static_cast(x % 256); // B + data[idx + 1] = static_cast(y % 256); // G + data[idx + 2] = static_cast((x + y) % 256); // R + } + } + return img; +} + +TEST(ImageCodecTest, ProcessDimensions) { + int w = 64; + int h = 64; + Image input = createTestImage(w, h); + + // Default 4:4:4 + ImageCodec codec(90.0); + Image output = codec.process(input); + + EXPECT_EQ(output.width(), w); + EXPECT_EQ(output.height(), h); + EXPECT_EQ(output.channels(), 3); +} + +TEST(ImageCodecTest, QualityImpact) { + int w = 32; + int h = 32; + Image input = createTestImage(w, h); + + // Low quality + ImageCodec lowQualCodec(10.0); + Image lowQualOutput = lowQualCodec.process(input); + + // High quality + ImageCodec highQualCodec(90.0); + Image highQualOutput = highQualCodec.process(input); + + // Measure PSNR + CodecMetrics lowMetrics = CodecAnalysis::computeMetrics(input, lowQualOutput); + CodecMetrics highMetrics = CodecAnalysis::computeMetrics(input, highQualOutput); + + // High quality should have better (higher) PSNR than low quality + // We check Y channel specifically as it's the most significant + EXPECT_GT(highMetrics.psnrY, lowMetrics.psnrY); + EXPECT_GT(highMetrics.psnrY, 20.0); // Basic sanity check +} + +TEST(ImageCodecTest, SubsamplingModes) { + int w = 32; + int h = 32; + Image input = createTestImage(w, h); // Multiple of 2 for clean 4:2:0 subsampling + + // 4:4:4 + { + ImageCodec codec(80.0, true, ImageCodec::ChromaSubsampling::CS_444); + Image output = codec.process(input); + EXPECT_EQ(output.width(), w); + EXPECT_EQ(output.height(), h); + CodecMetrics metrics = CodecAnalysis::computeMetrics(input, output); + EXPECT_GT(metrics.psnrY, 30.0); + } + + // 4:2:2 + { + ImageCodec codec(80.0, true, ImageCodec::ChromaSubsampling::CS_422); + Image output = codec.process(input); + EXPECT_EQ(output.width(), w); + EXPECT_EQ(output.height(), h); + CodecMetrics metrics = CodecAnalysis::computeMetrics(input, output); + EXPECT_GT(metrics.psnrY, 30.0); + } + + // 4:2:0 + { + ImageCodec codec(80.0, true, ImageCodec::ChromaSubsampling::CS_420); + Image output = codec.process(input); + EXPECT_EQ(output.width(), w); + EXPECT_EQ(output.height(), h); + CodecMetrics metrics = CodecAnalysis::computeMetrics(input, output); + EXPECT_GT(metrics.psnrY, 30.0); + } +} diff --git a/sonar-project.properties b/sonar-project.properties index 88d51e0..a65e08d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,7 +7,7 @@ sonar.tests=core/tests sonar.sourceEncoding=UTF-8 # C++ Analysis Requirements -sonar.cfamily.build-wrapper-output=bw-output +sonar.cfamily.compile-commands=bw-output/compile_commands.json # Use the XML report generated by gcovr in your GitHub Action sonar.coverageReportPaths=coverage.xml From 3b2f5554790e951ea7a55a4857cf9a681dd3cf22 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 17 Feb 2026 21:34:08 +0530 Subject: [PATCH 2/3] address and undefined behaviour sanitizer for local --- CMakeLists.txt | 10 ++++++++++ Makefile | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index ba2d2a1..8b93c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,16 @@ if(ENABLE_COVERAGE) endif() endif() +# --- Sanitizers --- +option(ENABLE_SANITIZERS "Enable ASan and UBSan" OFF) +if(ENABLE_SANITIZERS) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + message(STATUS "Enabling AddressSanitizer and UndefinedBehaviorSanitizer") + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) + endif() +endif() + # --- Subdirectories --- add_subdirectory(core) add_subdirectory(apps/native) \ No newline at end of file diff --git a/Makefile b/Makefile index e409344..0834db8 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,14 @@ coverage: # In gcovr 8.6, we use the search path at the end instead of --build-root gcovr --sonarqube -o coverage.xml -r . --filter core/src/ -e build/ --gcov-ignore-parse-errors=all build +# 6. Sanitize (ASan + UBSan) +sanitize: + @echo "๐Ÿ›ก๏ธ Running with Sanitizers..." + @mkdir -p build + @if [ -d build/CMakeCache.txt ]; then rm build/CMakeCache.txt; fi + @cd build && cmake .. -DENABLE_SANITIZERS=ON && make -j$(NPROCS) + @cd build && ctest --output-on-failure + # ------------------------------------ # ๐Ÿงน CLEANUP # ------------------------------------ From 1bce92aee88a10d435e22897130853f23bb38801 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 18 Feb 2026 00:03:04 +0530 Subject: [PATCH 3/3] Block inspector and ui improvements --- Makefile | 2 +- apps/native/CodecExplorerApp.cpp | 114 ++++++ apps/native/CodecExplorerApp.h | 11 + core/inc/ImageCodec.h | 11 + core/src/ImageCodec.cpp | 82 ++++ web/public/codec.js | 2 +- web/public/codec.wasm | Bin 34276 -> 40253 bytes web/public/css/base.css | 29 ++ web/public/css/components.css | 261 ++++++++++++ web/public/css/header.css | 84 ++++ web/public/css/inspection.css | 666 +++++++++++++++++++++++++++++++ web/public/css/layout.css | 24 ++ web/public/css/onboarding.css | 64 +++ web/public/css/style.css | 9 + web/public/css/utilities.css | 72 ++++ web/public/css/variables.css | 70 ++++ web/public/css/viewer.css | 115 ++++++ web/public/index.html | 453 ++++++++++++++++++++- web/public/js/image-manager.js | 44 ++ web/public/js/inspection.js | 408 +++++++++++++++++++ web/public/js/main.js | 151 +++++++ web/public/js/state.js | 21 + web/public/js/ui-controls.js | 336 ++++++++++++++++ web/public/js/wasm-bridge.js | 64 +++ web/public/main.js | 294 -------------- web/public/style.css | 468 ---------------------- web/src/codec_web.cpp | 34 ++ 27 files changed, 3110 insertions(+), 779 deletions(-) create mode 100644 web/public/css/base.css create mode 100644 web/public/css/components.css create mode 100644 web/public/css/header.css create mode 100644 web/public/css/inspection.css create mode 100644 web/public/css/layout.css create mode 100644 web/public/css/onboarding.css create mode 100644 web/public/css/style.css create mode 100644 web/public/css/utilities.css create mode 100644 web/public/css/variables.css create mode 100644 web/public/css/viewer.css create mode 100644 web/public/js/image-manager.js create mode 100644 web/public/js/inspection.js create mode 100644 web/public/js/main.js create mode 100644 web/public/js/state.js create mode 100644 web/public/js/ui-controls.js create mode 100644 web/public/js/wasm-bridge.js delete mode 100644 web/public/main.js delete mode 100644 web/public/style.css diff --git a/Makefile b/Makefile index 0834db8..50864a8 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ WEB_COMPILER = emcc WEB_FLAGS = -Icore/inc -O3 \ -s WASM=1 \ -s ALLOW_MEMORY_GROWTH=1 \ - -s EXPORTED_FUNCTIONS='["_init_session", "_process_image", "_get_view_ptr", "_set_view_tint", "_get_psnr_y", "_get_psnr_cr", "_get_psnr_cb", "_get_ssim_y", "_get_ssim_cr", "_get_ssim_cb", "_malloc", "_free"]' \ + -s EXPORTED_FUNCTIONS='["_init_session", "_process_image", "_get_view_ptr", "_set_view_tint", "_get_psnr_y", "_get_psnr_cr", "_get_psnr_cb", "_get_ssim_y", "_get_ssim_cr", "_get_ssim_cb", "_inspect_block_data", "_malloc", "_free"]' \ -s EXPORTED_RUNTIME_METHODS='["cwrap", "ccall", "HEAPU8"]' # Source: Core C++ + Web Glue C++ (in src folder) diff --git a/apps/native/CodecExplorerApp.cpp b/apps/native/CodecExplorerApp.cpp index 9d36e04..4d4ee63 100644 --- a/apps/native/CodecExplorerApp.cpp +++ b/apps/native/CodecExplorerApp.cpp @@ -78,6 +78,7 @@ CodecExplorerApp::CodecExplorerApp(const std::string& imagePath, ImageCodec::Chr // Per OpenCV warning, use nullptr for the value pointer and set the position manually. cv::createTrackbar("Quality", m_state.windowName, nullptr, 100, onQualityChangeStatic, this); cv::setTrackbarPos("Quality", m_state.windowName, m_quality); + cv::setMouseCallback(m_state.windowName, onMouseStatic, this); } void CodecExplorerApp::run() { @@ -105,7 +106,9 @@ void CodecExplorerApp::handleKey(int key) { else if (key == 't') { m_useTint = !m_useTint; viewChanged = true; } else if (key == '4') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_444; codecChanged = true; } else if (key == '2') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_422; codecChanged = true; } + else if (key == '2') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_422; codecChanged = true; } else if (key == '0') { m_chromaSubsampling = ImageCodec::ChromaSubsampling::CS_420; codecChanged = true; } + else if (key == 'c') { m_showInspection = false; viewChanged = true; } if (codecChanged) { @@ -126,6 +129,72 @@ void CodecExplorerApp::onQualityChangeStatic(int quality, void* userdata) { void CodecExplorerApp::onQualityChange(int quality) { m_quality = std::max(1, quality); updateCodecOutput(); + updateCodecOutput(); + render(); +} + +void CodecExplorerApp::onMouseStatic(int event, int x, int y, int flags, void* userdata) { + auto* app = static_cast(userdata); + if (app) { + app->onMouse(event, x, y, flags); + } +} + +void CodecExplorerApp::onMouse(int event, int x, int y, int flags) { + if (event == cv::EVENT_LBUTTONDOWN) { + // Check if click is within the original image area (top-left) + if (x < m_state.originalImage.width() && y < m_state.originalImage.height()) { + inspectBlockAt(x, y); + } else { + // Hide inspection if clicked elsewhere + m_showInspection = false; + render(); + } + } +} + +void CodecExplorerApp::inspectBlockAt(int x, int y) { + m_selectedBlockX = x / 8; + m_selectedBlockY = y / 8; + m_showInspection = true; + + ImageCodec codec(m_quality, true, m_chromaSubsampling); + + // Determine which channel we are inspecting based on view mode + if (m_state.mode == AppState::ViewMode::Cr) { + // Need to pass the Cr channel. BUT wait, inspectBlock expects the input channel. + // We need to extract the channel from the ORIGINAL image first, as per logic + // But `inspectBlock` does internally convert if we passed the whole image? No. + // `inspectBlock` takes a channel Image. + + // We need the original YCrCb image to pass the correct channel + Image ycrcb = bgrToYCrCb(m_state.originalImage); + Image cr(ycrcb.width(), ycrcb.height(), 1); + const double* src = ycrcb.data(); + double* dst = cr.data(); + for(size_t i=0; i 50 for X, 20 -> 25 for Y + int px = offsetX + j * 45; + int py = offsetY + i * 25; + cv::putText(inspectionView, valStr, {px, py}, cv::FONT_HERSHEY_SIMPLEX, 0.35, color, 1); + } + } + }; + + // Adjusted positions + drawGrid(20, 50, "Original", m_inspectionData.original, true); + drawGrid(420, 50, "DCT", m_inspectionData.dct, false); + drawGrid(20, 300, "Quantized", m_inspectionData.quantized, true); + drawGrid(420, 300, "Reconstructed", m_inspectionData.reconstructed, true); + + cv::imshow("Block Inspection", inspectionView); + } else { + try { + cv::destroyWindow("Block Inspection"); + } catch(...) {} + } } \ No newline at end of file diff --git a/apps/native/CodecExplorerApp.h b/apps/native/CodecExplorerApp.h index 191cbc4..4dad452 100644 --- a/apps/native/CodecExplorerApp.h +++ b/apps/native/CodecExplorerApp.h @@ -48,8 +48,19 @@ class CodecExplorerApp { static void onQualityChangeStatic(int quality, void* userdata); void onQualityChange(int quality); + // Mouse Handling + static void onMouseStatic(int event, int x, int y, int flags, void* userdata); + void onMouse(int event, int x, int y, int flags); + void inspectBlockAt(int x, int y); + AppState m_state; int m_quality = 50; ImageCodec::ChromaSubsampling m_chromaSubsampling; bool m_useTint = true; + + // Inspection State + bool m_showInspection = false; + int m_selectedBlockX = 0; + int m_selectedBlockY = 0; + ImageCodec::BlockDebugData m_inspectionData; }; \ No newline at end of file diff --git a/core/inc/ImageCodec.h b/core/inc/ImageCodec.h index 08ab565..fb40aef 100644 --- a/core/inc/ImageCodec.h +++ b/core/inc/ImageCodec.h @@ -59,6 +59,17 @@ class ImageCodec { const double quantTable[8][8]); Image downsampleChannel(const Image& channel, ChromaSubsampling cs) const; Image upsampleChannel(const Image& channel, int targetWidth, int targetHeight, ChromaSubsampling cs) const; + +public: + struct BlockDebugData { + double original[8][8]; + double dct[8][8]; + double quantTable[8][8]; + double quantized[8][8]; + double reconstructed[8][8]; + }; + + BlockDebugData inspectBlock(const Image& channel, int blockX, int blockY, bool isChroma = false); }; #endif diff --git a/core/src/ImageCodec.cpp b/core/src/ImageCodec.cpp index 7711ad1..0e660fb 100644 --- a/core/src/ImageCodec.cpp +++ b/core/src/ImageCodec.cpp @@ -303,4 +303,86 @@ Image ImageCodec::process(const Image& bgrImage) } return ycrcbToBgr(merged); +} + +ImageCodec::BlockDebugData ImageCodec::inspectBlock(const Image& channel, int blockX, int blockY, bool isChroma) { + BlockDebugData data; + const double (*quantTable)[8] = isChroma ? m_chromaQuantTable : m_lumaQuantTable; + + // 1. Copy Quantization Table + for(int i=0; i<8; ++i) { + for(int j=0; j<8; ++j) { + data.quantTable[i][j] = quantTable[i][j]; + } + } + + // 2. Extract Original Block + // Ensure we don't go out of bounds + int startX = blockX * 8; + int startY = blockY * 8; + + // Fill with zeros first + for(int i=0; i<8; ++i) + for(int j=0; j<8; ++j) + data.original[i][j] = 0.0; + + const double* channelData = channel.data(); + int width = channel.width(); + int height = channel.height(); + + for(int i=0; i<8; ++i) { + for(int j=0; j<8; ++j) { + int y = startY + i; + int x = startX + j; + if (x < width && y < height) { + data.original[i][j] = channelData[y * width + x]; + } + } + } + + // 3. DCT + double blockCentered[8][8]; + for(int i=0; i<8; ++i) + for(int j=0; j<8; ++j) + blockCentered[i][j] = data.original[i][j] - 128.0; + + dct8x8(blockCentered, data.dct); + + // 4. Quantization + if (m_enableQuantization) { + for(int i=0; i<8; ++i) { + for(int j=0; j<8; ++j) { + double coeff = data.dct[i][j] / quantTable[i][j]; + data.quantized[i][j] = std::round(coeff); // Store integer index + // But for display in "Quantized" view, we might want the actual integer value + // In processChannel we do: dctBlock[i][j] = std::round(coeff) * quantTable[i][j]; + // effectively dequantizing immediately. + // Let's store the quantized integer index in `quantized` + } + } + } else { + for(int i=0; i<8; ++i) + for(int j=0; j<8; ++j) + data.quantized[i][j] = data.dct[i][j]; + } + + // 5. Dequantization & IDCT (Reconstruction) + double dequantized[8][8]; + for(int i=0; i<8; ++i) { + for(int j=0; j<8; ++j) { + if (m_enableQuantization) + dequantized[i][j] = data.quantized[i][j] * quantTable[i][j]; + else + dequantized[i][j] = data.quantized[i][j]; + } + } + + double reconBlock[8][8]; + idct8x8(dequantized, reconBlock); + + for(int i=0; i<8; ++i) + for(int j=0; j<8; ++j) + data.reconstructed[i][j] = reconBlock[i][j] + 128.0; + + return data; } \ No newline at end of file diff --git a/web/public/codec.js b/web/public/codec.js index 7e2697e..bccff8d 100644 --- a/web/public/codec.js +++ b/web/public/codec.js @@ -1 +1 @@ -var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=globalThis.document?.currentScript?.src;if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("codec.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _init_session,_process_image,_get_view_ptr,_malloc,_set_view_tint,_get_psnr_y,_get_psnr_cr,_get_psnr_cb,_get_ssim_y,_get_ssim_cr,_get_ssim_cb,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_init_session=Module["_init_session"]=wasmExports["f"];_process_image=Module["_process_image"]=wasmExports["g"];_get_view_ptr=Module["_get_view_ptr"]=wasmExports["h"];_malloc=Module["_malloc"]=wasmExports["i"];_set_view_tint=Module["_set_view_tint"]=wasmExports["j"];_get_psnr_y=Module["_get_psnr_y"]=wasmExports["k"];_get_psnr_cr=Module["_get_psnr_cr"]=wasmExports["l"];_get_psnr_cb=Module["_get_psnr_cb"]=wasmExports["m"];_get_ssim_y=Module["_get_ssim_y"]=wasmExports["n"];_get_ssim_cr=Module["_get_ssim_cr"]=wasmExports["o"];_get_ssim_cb=Module["_get_ssim_cb"]=wasmExports["p"];_free=Module["_free"]=wasmExports["q"];__emscripten_stack_restore=wasmExports["r"];__emscripten_stack_alloc=wasmExports["s"];_emscripten_stack_get_current=wasmExports["t"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,b:__abort_js,c:_emscripten_resize_heap};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;createWasm();run(); +var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=globalThis.document?.currentScript?.src;if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("codec.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _init_session,_process_image,_get_view_ptr,_malloc,_set_view_tint,_get_psnr_y,_get_psnr_cr,_get_psnr_cb,_get_ssim_y,_get_ssim_cr,_get_ssim_cb,_inspect_block_data,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_init_session=Module["_init_session"]=wasmExports["f"];_process_image=Module["_process_image"]=wasmExports["g"];_get_view_ptr=Module["_get_view_ptr"]=wasmExports["h"];_malloc=Module["_malloc"]=wasmExports["i"];_set_view_tint=Module["_set_view_tint"]=wasmExports["j"];_get_psnr_y=Module["_get_psnr_y"]=wasmExports["k"];_get_psnr_cr=Module["_get_psnr_cr"]=wasmExports["l"];_get_psnr_cb=Module["_get_psnr_cb"]=wasmExports["m"];_get_ssim_y=Module["_get_ssim_y"]=wasmExports["n"];_get_ssim_cr=Module["_get_ssim_cr"]=wasmExports["o"];_get_ssim_cb=Module["_get_ssim_cb"]=wasmExports["p"];_inspect_block_data=Module["_inspect_block_data"]=wasmExports["q"];_free=Module["_free"]=wasmExports["r"];__emscripten_stack_restore=wasmExports["s"];__emscripten_stack_alloc=wasmExports["t"];_emscripten_stack_get_current=wasmExports["u"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,b:__abort_js,c:_emscripten_resize_heap};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;createWasm();run(); diff --git a/web/public/codec.wasm b/web/public/codec.wasm index 99f37a48bb16569c46f0303ed21671f4f6c5b1e4..80121e27b86b359d5b2d5610cb792f0b2fd39381 100755 GIT binary patch delta 14249 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 delta 8412 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 - + + + + + + + + +

WASM Codec Explorer

-
- - - - - Processed locally on your device +
+
+ + + + + Processed locally on your device +
+ + GitHub Logo + View Source on GitHub + +
- - GitHub Logo - View Source on GitHub -
@@ -68,8 +107,16 @@

WASM Codec Explorer

-
- + +
+ + + + + +
Drop an image here
+
or browse to upload
@@ -77,6 +124,20 @@

WASM Codec Explorer

+
+ + + +
@@ -126,6 +187,21 @@

WASM Codec Explorer

+
+ +
+ + +
+
+
@@ -168,11 +244,358 @@

WASM Codec Explorer

+ + +
+ + +
- + diff --git a/web/public/js/image-manager.js b/web/public/js/image-manager.js new file mode 100644 index 0000000..dd32e7b --- /dev/null +++ b/web/public/js/image-manager.js @@ -0,0 +1,44 @@ +import { state } from './state.js'; +import { initSession } from './wasm-bridge.js'; + +export function handleFileSelect(file, onImageLoaded) { + if (!file) return; + + const img = new Image(); + img.onload = () => { + let scale = 1.0; + if (img.width > state.maxDim || img.height > state.maxDim) { + scale = Math.min(state.maxDim / img.width, state.maxDim / img.height); + } + + state.imgWidth = Math.floor(img.width * scale); + state.imgHeight = Math.floor(img.height * scale); + + const originalCanvas = document.getElementById('originalCanvas'); + const processedCanvas = document.getElementById('processedCanvas'); + + // Resize canvases + originalCanvas.width = state.imgWidth; + originalCanvas.height = state.imgHeight; + processedCanvas.width = state.imgWidth; + processedCanvas.height = state.imgHeight; + + const ctx = originalCanvas.getContext('2d'); + ctx.drawImage(img, 0, 0, state.imgWidth, state.imgHeight); + state.originalImageData = ctx.getImageData(0, 0, state.imgWidth, state.imgHeight); + + // Initialize WASM session with new image + initSession(); + + if (onImageLoaded) onImageLoaded(); + }; + img.src = URL.createObjectURL(file); +} + +export function getOriginalContext() { + return document.getElementById('originalCanvas').getContext('2d'); +} + +export function getProcessedContext() { + return document.getElementById('processedCanvas').getContext('2d', { willReadFrequently: true }); +} diff --git a/web/public/js/inspection.js b/web/public/js/inspection.js new file mode 100644 index 0000000..6e66dbe --- /dev/null +++ b/web/public/js/inspection.js @@ -0,0 +1,408 @@ +import { state, ViewMode } from './state.js'; +import { inspectBlockData } from './wasm-bridge.js'; + +// Store grid data globally for cross-grid highlighting and basis viewer +let cachedGridData = {}; +const ALL_GRID_IDS = [ + 'gridOriginal', 'gridDCT', 'gridQuantized', + 'gridQuantized2', 'gridDequantized', 'gridReconstructed', + 'gridQuantTable', 'gridError' +]; + +export function inspectBlock(blockX, blockY) { + // Track which block is being inspected + state.inspectedBlock = { x: blockX, y: blockY }; + + // Show inspector content, hide placeholder + const content = document.getElementById('inspectorContent'); + const placeholder = document.getElementById('inspectorPlaceholder'); + if (content) content.style.display = 'block'; + if (placeholder) placeholder.style.display = 'none'; + + // Hide basis viewer when inspecting a new block + const basisViewer = document.getElementById('basisViewer'); + if (basisViewer) basisViewer.style.display = 'none'; + const basisPlaceholder = document.getElementById('basisPlaceholder'); + if (basisPlaceholder) basisPlaceholder.style.display = 'flex'; + + const coordsSpan = document.getElementById('blockCoords'); + const qTableType = document.getElementById('qTableType'); + const qTableType2 = document.getElementById('qTableType2'); + + if (coordsSpan) coordsSpan.innerText = `${blockX * 8}, ${blockY * 8} (Block ${blockX},${blockY})`; + + let channelIndex = 0; // Default Y + if (state.currentViewMode === ViewMode.Cr) channelIndex = 1; + if (state.currentViewMode === ViewMode.Cb) channelIndex = 2; + + const tableLabel = (channelIndex === 0) ? "Luma" : "Chroma"; + if (qTableType) qTableType.innerText = tableLabel; + if (qTableType2) qTableType2.innerText = tableLabel; + + const qualitySlider = document.getElementById('qualitySlider'); + const quality = qualitySlider ? parseInt(qualitySlider.value) : 50; + + const ptr = inspectBlockData(blockX, blockY, channelIndex, quality); + if (!ptr) { + console.error("Failed to inspect block: Ptr is null"); + return; + } + + const blockSize = 64; + const readGrid = (offsetIdx) => { + const startBytes = ptr + (offsetIdx * blockSize * 8); + const data = []; + const dataView = new DataView(Module.HEAPU8.buffer); + + try { + for (let i = 0; i < blockSize; ++i) { + const val = dataView.getFloat64(startBytes + (i * 8), true); + data.push(val); + } + } catch (e) { + console.error("Error reading grid data:", e); + } + return data; + }; + + const originalData = readGrid(0); + const dctData = readGrid(1); + const qtData = readGrid(2); + const quantData = readGrid(3); + const reconData = readGrid(4); + + // Compute derived data + const errorData = new Float64Array(64); + for (let i = 0; i < 64; i++) { + errorData[i] = originalData[i] - reconData[i]; + } + + const dequantizedData = []; + for (let i = 0; i < 64; i++) { + dequantizedData.push(quantData[i] * qtData[i]); + } + + // Cache the data for basis viewer + cachedGridData = { dctData, qtData, quantData, dequantizedData }; + + // Compute summary stats + let mse = 0; + let peakError = 0; + let zeroCount = 0; + + for (let i = 0; i < 64; i++) { + mse += errorData[i] * errorData[i]; + peakError = Math.max(peakError, Math.abs(errorData[i])); + if (Math.abs(quantData[i]) < 0.5) zeroCount++; + } + mse /= 64; + + // Update stats display + const setStatText = (id, text) => { + const el = document.getElementById(id); + if (el) el.innerText = text; + }; + + setStatText('statMSE', mse.toFixed(2)); + setStatText('statPeakError', peakError.toFixed(1)); + setStatText('statZeros', `${zeroCount} / 64`); + setStatText('statCompression', `${Math.round((zeroCount / 64) * 100)}%`); + + // Render grids in pipeline order + renderGrid('gridOriginal', originalData, 'intensity', 'original'); + renderGrid('gridDCT', dctData, 'frequency', 'dct'); + renderGrid('gridQuantized', quantData, 'frequency', 'quantized'); + + renderGrid('gridQuantized2', quantData, 'frequency', 'quantized'); + renderGrid('gridDequantized', dequantizedData, 'frequency', 'dequantized'); + renderGrid('gridReconstructed', reconData, 'intensity', 'reconstructed'); + + renderGrid('gridQuantTable', qtData, 'qtable', 'qtable'); + renderGrid('gridError', errorData, 'error', 'error'); + + // Render zoom canvases (pixel-art enlarged previews) + renderZoomCanvas('zoomOriginal', originalData); + renderZoomCanvas('zoomReconstructed', reconData); + + // Setup basis viewer close button + const basisClose = document.querySelector('.basis-close'); + if (basisClose) { + basisClose.onclick = () => { + if (basisViewer) basisViewer.style.display = 'none'; + clearAllHighlights(); + }; + } +} + +function getCellDescription(row, col, gridType) { + if (gridType === 'dct' || gridType === 'dequantized' || gridType === 'quantized') { + if (row === 0 && col === 0) return 'DC coefficient (average brightness)'; + const freqLevel = row + col; + if (freqLevel <= 2) return 'Low frequency'; + if (freqLevel <= 5) return 'Mid frequency'; + return 'High frequency'; + } + if (gridType === 'original' || gridType === 'reconstructed') { + return 'Pixel intensity'; + } + if (gridType === 'error') { + return 'Error value'; + } + if (gridType === 'qtable') { + return 'Divisor'; + } + return ''; +} + +function getFreqLabel(row, col) { + if (row === 0 && col === 0) return 'DC'; + const level = row + col; + if (level <= 2) return 'Low Freq'; + if (level <= 5) return 'Mid Freq'; + return 'High Freq'; +} + +// ===== Cross-Grid Highlighting ===== +function clearAllHighlights() { + document.querySelectorAll('.grid-cell.cell-highlight').forEach(c => { + c.classList.remove('cell-highlight'); + }); +} + +function highlightAcrossGrids(row, col) { + clearAllHighlights(); + ALL_GRID_IDS.forEach(gridId => { + const grid = document.getElementById(gridId); + if (!grid) return; + const idx = row * 8 + col; + const cell = grid.children[idx]; + if (cell) cell.classList.add('cell-highlight'); + }); +} + +// ===== DCT Basis Pattern Computation ===== +function computeBasisPattern(u, v) { + const N = 8; + const pattern = new Float64Array(64); + const cu = (u === 0) ? 1 / Math.sqrt(2) : 1; + const cv = (v === 0) ? 1 / Math.sqrt(2) : 1; + + for (let y = 0; y < N; y++) { + for (let x = 0; x < N; x++) { + pattern[y * N + x] = (cu * cv / 4) * + Math.cos((2 * x + 1) * u * Math.PI / (2 * N)) * + Math.cos((2 * y + 1) * v * Math.PI / (2 * N)); + } + } + return pattern; +} + +function drawPatternOnCanvas(canvasId, data, mode) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const size = canvas.width; + const cellSize = size / 8; + + ctx.clearRect(0, 0, size, size); + + // Find min/max for normalization + let min = Infinity, max = -Infinity; + for (let i = 0; i < 64; i++) { + min = Math.min(min, data[i]); + max = Math.max(max, data[i]); + } + const range = Math.max(Math.abs(min), Math.abs(max)) || 1; + + for (let y = 0; y < 8; y++) { + for (let x = 0; x < 8; x++) { + const val = data[y * 8 + x]; + + if (mode === 'diverging') { + // Red-white-blue diverging colormap + const t = val / range; // -1 to 1 + let r, g, b; + if (t >= 0) { + // White to Red + r = 255; + g = Math.round(255 * (1 - t)); + b = Math.round(255 * (1 - t)); + } else { + // White to Blue + r = Math.round(255 * (1 + t)); + g = Math.round(255 * (1 + t)); + b = 255; + } + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + } else { + // Grayscale + const norm = Math.round(((val - min) / (max - min || 1)) * 255); + ctx.fillStyle = `rgb(${norm}, ${norm}, ${norm})`; + } + + ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } + + // Draw grid lines + ctx.strokeStyle = 'rgba(0,0,0,0.08)'; + ctx.lineWidth = 0.5; + for (let i = 1; i < 8; i++) { + ctx.beginPath(); + ctx.moveTo(i * cellSize, 0); + ctx.lineTo(i * cellSize, size); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i * cellSize); + ctx.lineTo(size, i * cellSize); + ctx.stroke(); + } +} + +function showBasisViewer(row, col) { + const viewer = document.getElementById('basisViewer'); + if (!viewer || !cachedGridData.dctData) return; + + const idx = row * 8 + col; + const dctVal = cachedGridData.dctData[idx]; + const quantVal = cachedGridData.quantData[idx]; + const qtVal = cachedGridData.qtData[idx]; + + // Update labels + const setEl = (id, text) => { + const el = document.getElementById(id); + if (el) el.innerText = text; + }; + + setEl('basisCoord', `(${row}, ${col})`); + setEl('basisFreqLabel', getFreqLabel(row, col)); + setEl('basisValue', dctVal.toFixed(2)); + setEl('basisQuantized', Math.round(quantVal).toString()); + setEl('basisDivisor', Math.round(qtVal).toString()); + + // Compute basis pattern for position (row, col) + // In DCT, row = v (vertical freq), col = u (horizontal freq) + const basisPattern = computeBasisPattern(col, row); + + // Contribution = coefficient * basis + const contribution = new Float64Array(64); + for (let i = 0; i < 64; i++) { + contribution[i] = dctVal * basisPattern[i]; + } + + // Draw canvases + drawPatternOnCanvas('basisCanvas', Array.from(basisPattern), 'diverging'); + drawPatternOnCanvas('contributionCanvas', Array.from(contribution), 'diverging'); + + // Show viewer, hide placeholder + viewer.style.display = 'block'; + const basisPlaceholder = document.getElementById('basisPlaceholder'); + if (basisPlaceholder) basisPlaceholder.style.display = 'none'; + + // Scroll viewer into view smoothly + viewer.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +} + +function renderGrid(elementId, data, type = 'number', gridType = '') { + const el = document.getElementById(elementId); + if (!el) return; + el.innerHTML = ''; + + for (let i = 0; i < 64; ++i) { + const val = data[i]; + const row = Math.floor(i / 8); + const col = i % 8; + const cell = document.createElement('div'); + cell.className = 'grid-cell'; + cell.dataset.row = row; + cell.dataset.col = col; + + let displayVal = val; + if (type === 'intensity' || type === 'qtable') { + displayVal = Math.round(val); + } else { + displayVal = val.toFixed(1); + if (val === 0) displayVal = "0"; + if (displayVal === "-0.0") displayVal = "0"; + } + + cell.innerText = displayVal; + + // Build tooltip + const desc = getCellDescription(row, col, gridType); + cell.title = `(${row}, ${col}) = ${val.toFixed(4)}${desc ? '\n' + desc : ''}`; + + // Cross-grid highlighting on hover + cell.addEventListener('mouseenter', () => { + highlightAcrossGrids(row, col); + }); + + // Click on frequency-domain cells opens basis viewer + if (gridType === 'dct' || gridType === 'quantized' || gridType === 'dequantized') { + cell.style.cursor = 'pointer'; + cell.addEventListener('click', (e) => { + e.stopPropagation(); + highlightAcrossGrids(row, col); + showBasisViewer(row, col); + }); + } + + // Apply cell coloring + if (type === 'intensity') { + const norm = Math.max(0, Math.min(255, val)); + cell.style.backgroundColor = `rgb(${norm}, ${norm}, ${norm})`; + cell.style.color = norm > 128 ? '#1e293b' : '#f1f5f9'; + } else if (type === 'qtable') { + const maxQt = 200; + const t = Math.min(1, val / maxQt); + const r = Math.round(255); + const g = Math.round(255 - t * 110); + const b = Math.round(255 - t * 200); + cell.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; + cell.style.color = t > 0.5 ? '#7c2d12' : '#78350f'; + } else if (type === 'frequency' || type === 'error') { + if (Math.abs(val) < 0.5) { + cell.classList.add('cell-zero'); + } else { + const isPos = val > 0; + const visualMax = (type === 'error') ? 30 : 100; + let opacity = Math.min(1, Math.abs(val) / visualMax); + opacity = Math.max(0.1, opacity); + + if (isPos) { + cell.style.backgroundColor = `rgba(239, 68, 68, ${opacity})`; + cell.style.color = opacity > 0.5 ? '#fff' : 'var(--text)'; + } else { + cell.style.backgroundColor = `rgba(59, 130, 246, ${opacity})`; + cell.style.color = opacity > 0.5 ? '#fff' : 'var(--text)'; + } + } + } + el.appendChild(cell); + } + + // Clear highlights when mouse leaves a grid + el.addEventListener('mouseleave', () => { + clearAllHighlights(); + }); +} + +function renderZoomCanvas(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + + // Canvas is 8ร—8 native, CSS scales it up with pixelated rendering + ctx.clearRect(0, 0, 8, 8); + const imgData = ctx.createImageData(8, 8); + + for (let i = 0; i < 64; i++) { + const v = Math.max(0, Math.min(255, Math.round(data[i]))); + imgData.data[i * 4 + 0] = v; // R + imgData.data[i * 4 + 1] = v; // G + imgData.data[i * 4 + 2] = v; // B + imgData.data[i * 4 + 3] = 255; // A + } + + ctx.putImageData(imgData, 0, 0); +} diff --git a/web/public/js/main.js b/web/public/js/main.js new file mode 100644 index 0000000..9d01ff3 --- /dev/null +++ b/web/public/js/main.js @@ -0,0 +1,151 @@ +import { state } from './state.js'; +import { setupWasm, getViewPtr, getStats, free, processImage } from './wasm-bridge.js'; +import { handleFileSelect, getProcessedContext, getOriginalContext } from './image-manager.js'; +import { setupControls, updateComparisonView, updateFileSizeEstimate } from './ui-controls.js'; + +// DOM Elements +const statusDiv = document.getElementById('status'); +const fileInput = document.getElementById('fileInput'); + +function onWasmReady() { + state.wasmReady = true; + if (statusDiv) { + statusDiv.innerText = "WASM Module Ready!"; + statusDiv.classList.add('ready'); + } + + // Enable controls + const controls = document.querySelectorAll('input:disabled, button:disabled'); + controls.forEach(el => el.disabled = false); + + console.log("WASM Runtime Initialized (Module)"); +} + +function render() { + if (!state.wasmReady || !state.originalImageData) return; + + const rgbaSize = state.imgWidth * state.imgHeight * 4; + let outputPtr = 0; + try { + outputPtr = getViewPtr(state.currentViewMode); + if (!outputPtr) throw new Error("WASM get_view_ptr returned null"); + + // Create ImageData from WASM memory + // outputPtr is simple offset in HEAPU8 + const dataView = new Uint8ClampedArray(Module.HEAPU8.buffer, outputPtr, rgbaSize); + const imageData = new ImageData(dataView, state.imgWidth, state.imgHeight); + + const ctx = getProcessedContext(); + ctx.putImageData(imageData, 0, 0); + + // Update Stats + const stats = getStats(); + + const setText = (id, val) => { + const el = document.getElementById(id); + if (el) el.textContent = val; + }; + + setText('psnrY', stats.psnr.y.toFixed(2)); + setText('psnrCr', stats.psnr.cr.toFixed(2)); + setText('psnrCb', stats.psnr.cb.toFixed(2)); + + setText('ssimY', stats.ssim.y.toFixed(4)); + setText('ssimCr', stats.ssim.cr.toFixed(4)); + setText('ssimCb', stats.ssim.cb.toFixed(4)); + + // Highlight Logic + const origCtx = getOriginalContext(); + if (origCtx) { + // Always redraw original to clear artifacts/highlights + origCtx.putImageData(state.originalImageData, 0, 0); + + if (state.isInspectMode && state.highlightBlock) { + const bx = state.highlightBlock.x * 8; + const by = state.highlightBlock.y * 8; + origCtx.strokeStyle = "#ff0000"; + origCtx.lineWidth = 1; + origCtx.strokeRect(bx, by, 8, 8); + } + } + + } catch (err) { + console.error("Render error:", err); + } finally { + if (outputPtr) free(outputPtr); + } +} + +// Initial Setup +setupWasm(onWasmReady); + +// ===== Theme Toggle ===== +const themeToggle = document.getElementById('themeToggle'); +function updateThemeIcons() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const sunIcon = themeToggle?.querySelector('.icon-sun'); + const moonIcon = themeToggle?.querySelector('.icon-moon'); + if (sunIcon) sunIcon.style.display = isDark ? 'none' : 'block'; + if (moonIcon) moonIcon.style.display = isDark ? 'block' : 'none'; +} +updateThemeIcons(); // sync on load + +if (themeToggle) { + themeToggle.addEventListener('click', () => { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + if (isDark) { + document.documentElement.removeAttribute('data-theme'); + localStorage.setItem('theme', 'light'); + } else { + document.documentElement.setAttribute('data-theme', 'dark'); + localStorage.setItem('theme', 'dark'); + } + updateThemeIcons(); + }); +} + +// ===== Drop Zone / File Input ===== +const dropZone = document.getElementById('dropZone'); + +function loadFile(file) { + if (!file || !file.type.startsWith('image/')) return; + handleFileSelect(file, () => { + processImage(50, state.currentCsMode); + updateComparisonView(50); + render(); + updateFileSizeEstimate(); + // Hide drop zone after successful load + if (dropZone) dropZone.style.display = 'none'; + }); +} + +if (fileInput) { + fileInput.addEventListener('change', (e) => { + loadFile(e.target.files[0]); + }); +} + +if (dropZone) { + // Click drop zone to trigger file input + dropZone.addEventListener('click', () => { + if (fileInput) fileInput.click(); + }); + + // Drag events + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('drag-over'); + }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + const file = e.dataTransfer?.files[0]; + loadFile(file); + }); +} + +// Setup UI Controls +setupControls(render); diff --git a/web/public/js/state.js b/web/public/js/state.js new file mode 100644 index 0000000..271b193 --- /dev/null +++ b/web/public/js/state.js @@ -0,0 +1,21 @@ +export const ViewMode = { + RGB: 0, + Artifacts: 1, + Y: 2, + Cr: 3, + Cb: 4 +}; + +export const state = { + originalImageData: null, + imgWidth: 0, + imgHeight: 0, + currentViewMode: ViewMode.RGB, + currentCsMode: 444, // 4:4:4 + maxDim: 1024, + wasmReady: false, + isInspectMode: false, + highlightBlock: null, + inspectedBlock: null, // { x, y } of currently inspected block + isDragging: false +}; diff --git a/web/public/js/ui-controls.js b/web/public/js/ui-controls.js new file mode 100644 index 0000000..2dfcfbe --- /dev/null +++ b/web/public/js/ui-controls.js @@ -0,0 +1,336 @@ +import { state, ViewMode } from './state.js'; +import { processImage, setViewTint, inspectBlockData } from './wasm-bridge.js'; +import { inspectBlock } from './inspection.js'; +import { getOriginalContext } from './image-manager.js'; + +export function setupControls(renderCallback) { + const qualitySlider = document.getElementById('qualitySlider'); + const qualityValue = document.getElementById('qualityValue'); + const viewModeRadios = document.querySelectorAll('input[name="view_mode"]'); + const csRadios = document.querySelectorAll('input[name="chroma_subsampling"]'); + const tintToggle = document.getElementById('tint_toggle'); + const comparisonSlider = document.getElementById('comparisonSlider'); + const comparisonViewer = document.querySelector('.comparison-viewer'); + const inspectToggle = document.getElementById('inspect_mode'); + const sliderContainer = document.querySelector('.slider-container'); + const inspectionPanel = document.getElementById('inspectionPanel'); + const inspectorContent = document.getElementById('inspectorContent'); + const inspectorPlaceholder = document.getElementById('inspectorPlaceholder'); + + // Debounce helper + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // Re-inspect the current block after settings change + function reinspectIfNeeded() { + if (state.isInspectMode && state.inspectedBlock && state.wasmReady) { + inspectBlock(state.inspectedBlock.x, state.inspectedBlock.y); + } + } + + const debouncedUpdate = debounce(() => { + if (!state.wasmReady || !state.originalImageData) return; + processImage(parseInt(qualitySlider.value), state.currentCsMode); + renderCallback(); + reinspectIfNeeded(); + updateFileSizeEstimate(); + }, 150); + + if (qualitySlider) { + qualitySlider.addEventListener('input', () => { + qualityValue.textContent = qualitySlider.value; + updatePresetHighlight(parseInt(qualitySlider.value)); + if (state.originalImageData) debouncedUpdate(); + }); + } + + // Quality Presets + const presetBtns = document.querySelectorAll('.preset-btn'); + presetBtns.forEach(btn => { + btn.addEventListener('click', () => { + const q = parseInt(btn.dataset.quality); + if (qualitySlider) { + qualitySlider.value = q; + qualityValue.textContent = q; + } + updatePresetHighlight(q); + if (state.originalImageData) debouncedUpdate(); + }); + }); + + function updatePresetHighlight(quality) { + presetBtns.forEach(btn => { + const bq = parseInt(btn.dataset.quality); + btn.classList.toggle('active', bq === quality); + }); + } + + viewModeRadios.forEach(radio => { + radio.addEventListener('change', (e) => { + switch (e.target.value) { + case 'artifacts': state.currentViewMode = ViewMode.Artifacts; break; + case 'y': state.currentViewMode = ViewMode.Y; break; + case 'cr': state.currentViewMode = ViewMode.Cr; break; + case 'cb': state.currentViewMode = ViewMode.Cb; break; + default: state.currentViewMode = ViewMode.RGB; break; + } + if (state.originalImageData) { + renderCallback(); + reinspectIfNeeded(); + } + }); + }); + + csRadios.forEach(radio => { + radio.addEventListener('change', (e) => { + state.currentCsMode = parseInt(e.target.value, 10); + if (state.originalImageData) debouncedUpdate(); + }); + }); + + if (tintToggle) { + tintToggle.addEventListener('change', (e) => { + if (state.wasmReady) { + setViewTint(e.target.checked ? 1 : 0); + if (state.originalImageData) { + renderCallback(); + reinspectIfNeeded(); + } + } + }); + } + + if (inspectToggle) { + inspectToggle.addEventListener('change', (e) => { + state.isInspectMode = e.target.checked; + if (comparisonViewer) { + comparisonViewer.style.cursor = state.isInspectMode ? 'crosshair' : 'col-resize'; + } + + if (sliderContainer) { + if (state.isInspectMode) { + sliderContainer.classList.add('disabled'); + if (comparisonSlider) comparisonSlider.disabled = true; + } else { + sliderContainer.classList.remove('disabled'); + if (comparisonSlider) comparisonSlider.disabled = false; + } + } + + // Show/hide inline inspector panel + if (inspectionPanel) { + if (state.isInspectMode) { + inspectionPanel.style.display = 'block'; + // Reset to placeholder until a block is clicked + if (inspectorContent) inspectorContent.style.display = 'none'; + if (inspectorPlaceholder) inspectorPlaceholder.style.display = 'flex'; + } else { + inspectionPanel.style.display = 'none'; + } + } + + if (!state.isInspectMode) { + state.highlightBlock = null; + state.inspectedBlock = null; + } + renderCallback(); + }); + } + + // Comparison Viewer Interaction + if (comparisonViewer && comparisonSlider) { + // Slider update + comparisonSlider.addEventListener('input', (e) => { + updateComparisonView(e.target.value); + }); + + // Mouse/Touch interaction + const handleInteraction = (clientX) => { + const rect = comparisonViewer.getBoundingClientRect(); + const x = clientX - rect.left; + const percent = (x / rect.width) * 100; + updateComparisonView(percent); + }; + + comparisonViewer.addEventListener('mousedown', (e) => { + if (state.isInspectMode) return; + state.isDragging = true; + comparisonViewer.style.cursor = 'grabbing'; + handleInteraction(e.clientX); + }); + + document.addEventListener('mouseup', () => { + state.isDragging = false; + if (comparisonViewer && !state.isInspectMode) comparisonViewer.style.cursor = 'col-resize'; + }); + + document.addEventListener('mouseleave', () => { state.isDragging = false; }); + + document.addEventListener('mousemove', (e) => { + if (state.isDragging) handleInteraction(e.clientX); + + // Highlight Logic + if (state.isInspectMode && state.originalImageData && !state.isDragging) { + const rect = comparisonViewer.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Scale to image coords + const originalCanvas = document.getElementById('originalCanvas'); // use dimensions from here + const scaleX = state.imgWidth / rect.width; + const scaleY = state.imgHeight / rect.height; + + const imgX = Math.floor(x * scaleX); + const imgY = Math.floor(y * scaleY); + + if (imgX < 0 || imgX >= state.imgWidth || imgY < 0 || imgY >= state.imgHeight) { + state.highlightBlock = null; + } else { + const blockX = Math.floor(imgX / 8); + const blockY = Math.floor(imgY / 8); + state.highlightBlock = { x: blockX, y: blockY }; + } + renderCallback(); // Re-render to show highlight + } + }); + + comparisonViewer.addEventListener('mouseleave', () => { + state.highlightBlock = null; + if (state.isInspectMode) renderCallback(); + }); + + comparisonViewer.addEventListener('click', (e) => { + if (state.isInspectMode && state.highlightBlock && state.wasmReady) { + inspectBlock(state.highlightBlock.x, state.highlightBlock.y); + } + }); + } + + // No close button needed for inline inspector +} + +export function updateComparisonView(percent) { + const comparisonSlider = document.getElementById('comparisonSlider'); + const processedCanvas = document.getElementById('processedCanvas'); + + const clampedPercent = Math.max(0, Math.min(100, percent)); + + if (comparisonSlider) comparisonSlider.value = clampedPercent; + if (processedCanvas) { + processedCanvas.style.clipPath = `polygon(${clampedPercent}% 0, 100% 0, 100% 100%, ${clampedPercent}% 100%)`; + } +} + +// ===== File Size Estimation ===== +export function updateFileSizeEstimate() { + if (!state.wasmReady || !state.originalImageData) return; + + const container = document.getElementById('fileSizeContainer'); + if (!container) return; + container.style.display = 'block'; + + const w = state.imgWidth; + const h = state.imgHeight; + const totalPixels = w * h; + + // Original: uncompressed RGB bytes + const originalBytes = totalPixels * 3; + + // Sample random blocks to estimate zero ratio + const blocksX = Math.floor(w / 8); + const blocksY = Math.floor(h / 8); + const totalBlocks = blocksX * blocksY; + + if (totalBlocks === 0) return; + + const qualitySlider = document.getElementById('qualitySlider'); + const quality = qualitySlider ? parseInt(qualitySlider.value) : 50; + + // Sample up to 16 blocks + const sampleCount = Math.min(16, totalBlocks); + let totalZeros = 0; + const sampledIndices = new Set(); + + // Use deterministic sampling (evenly spaced) for consistency + for (let i = 0; i < sampleCount; i++) { + const idx = Math.floor((i / sampleCount) * totalBlocks); + if (sampledIndices.has(idx)) continue; + sampledIndices.add(idx); + + const bx = idx % blocksX; + const by = Math.floor(idx / blocksX); + + try { + const ptr = inspectBlockData(bx, by, 0, quality); // Y channel + if (!ptr) continue; + + // Read quantized data (offset index 3 = quantized coefficients) + const blockSize = 64; + const startBytes = ptr + (3 * blockSize * 8); + const dataView = new DataView(Module.HEAPU8.buffer); + + let zeros = 0; + for (let j = 0; j < blockSize; j++) { + const val = dataView.getFloat64(startBytes + (j * 8), true); + if (Math.abs(val) < 0.5) zeros++; + } + totalZeros += zeros; + } catch (e) { + // Skip problematic blocks + } + } + + const actualSampled = sampledIndices.size; + if (actualSampled === 0) return; + + const avgZeroRatio = totalZeros / (actualSampled * 64); + + // Estimate: JPEG uses ~0.5-2 bits per non-zero coefficient, plus overhead + // With Huffman + RLE, zeros are nearly free + const bitsPerNonZero = 4.5; // average bits per non-zero DCT coefficient + const nonZeroRatio = 1 - avgZeroRatio; + const totalCoefficients = totalBlocks * 64 * 3; // Y + Cr + Cb channels + const estimatedBits = totalCoefficients * nonZeroRatio * bitsPerNonZero; + const headerOverhead = 600; // JPEG header bytes + let estimatedBytes = Math.round(estimatedBits / 8) + headerOverhead; + + // Apply chroma subsampling factor + if (state.currentCsMode === 422) { + estimatedBytes = Math.round(estimatedBytes * 0.75); + } else if (state.currentCsMode === 420) { + estimatedBytes = Math.round(estimatedBytes * 0.6); + } + + // Clamp minimum + estimatedBytes = Math.max(estimatedBytes, headerOverhead + 100); + + // Format sizes + const formatSize = (bytes) => { + if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return bytes + ' B'; + }; + + const reduction = Math.max(0, Math.round((1 - estimatedBytes / originalBytes) * 100)); + const ratio = (originalBytes / estimatedBytes).toFixed(1); + + // Update DOM + const values = document.getElementById('fileSizeValues'); + const fill = document.getElementById('fileSizeFill'); + const origLabel = document.getElementById('fileSizeOrigLabel'); + const reductionLabel = document.getElementById('fileSizeReduction'); + + if (values) values.textContent = `${formatSize(originalBytes)} โ†’ ~${formatSize(estimatedBytes)}`; + if (fill) fill.style.width = `${Math.max(2, 100 - reduction)}%`; + if (origLabel) origLabel.textContent = `Original: ${formatSize(originalBytes)}`; + if (reductionLabel) reductionLabel.textContent = `~${reduction}% smaller (${ratio}ร— compression)`; +} diff --git a/web/public/js/wasm-bridge.js b/web/public/js/wasm-bridge.js new file mode 100644 index 0000000..eb46910 --- /dev/null +++ b/web/public/js/wasm-bridge.js @@ -0,0 +1,64 @@ +import { state } from './state.js'; + +// Setup WASM runtime callback +export function setupWasm(onReady) { + if (typeof Module !== 'undefined' && Module.calledRun) { + onReady(); + } else { + // Assume Module is available globally via codec.js + Module.onRuntimeInitialized = onReady; + } +} + +export function initSession() { + if (!state.originalImageData) return; + + const rgbaSize = state.originalImageData.data.length; + let inputPtr = 0; + try { + inputPtr = Module._malloc(rgbaSize); + Module.HEAPU8.set(state.originalImageData.data, inputPtr); + Module._init_session(inputPtr, state.imgWidth, state.imgHeight); + } finally { + if (inputPtr) Module._free(inputPtr); + } +} + +export function processImage(quality, csMode) { + Module._process_image(quality, csMode); +} + +export function getViewPtr(viewMode) { + return Module._get_view_ptr(viewMode); +} + +export function getStats() { + return { + psnr: { + y: Module._get_psnr_y(), + cr: Module._get_psnr_cr(), + cb: Module._get_psnr_cb() + }, + ssim: { + y: Module._get_ssim_y(), + cr: Module._get_ssim_cr(), + cb: Module._get_ssim_cb() + } + }; +} + +export function setViewTint(enabled) { + Module._set_view_tint(enabled ? 1 : 0); +} + +export function inspectBlockData(blockX, blockY, channelIndex, quality) { + return Module._inspect_block_data(blockX, blockY, channelIndex, quality); +} + +export function getHeapU8() { + return Module.HEAPU8; +} + +export function free(ptr) { + Module._free(ptr); +} diff --git a/web/public/main.js b/web/public/main.js deleted file mode 100644 index dc6a34c..0000000 --- a/web/public/main.js +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Codec Explorer: An interactive codec laboratory. - * Copyright (C) 2026 Abhinav Tanniru - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -let originalImageData = null; -let imgWidth = 0; -let imgHeight = 0; - -const ViewMode = { - RGB: 0, - Artifacts: 1, - Y: 2, - Cr: 3, - Cb: 4 -}; -let currentViewMode = ViewMode.RGB; -let currentCsMode = 444; -const maxDim = 1024; // Downscale large images for performance -let wasmReady = false; - -// DOM Elements -const fileInput = document.getElementById('fileInput'); -const qualitySlider = document.getElementById('qualitySlider'); -const qualityValue = document.getElementById('qualityValue'); -const statusDiv = document.getElementById('status'); -const originalCanvas = document.getElementById('originalCanvas'); -const processedCanvas = document.getElementById('processedCanvas'); -const origCtx = originalCanvas.getContext('2d'); -const procCtx = processedCanvas.getContext('2d', { willReadFrequently: true }); -const psnrY = document.getElementById('psnrY'); -const psnrCr = document.getElementById('psnrCr'); -const psnrCb = document.getElementById('psnrCb'); -const viewModeRadios = document.querySelectorAll('input[name="view_mode"]'); -const csRadios = document.querySelectorAll('input[name="chroma_subsampling"]'); -const comparisonSlider = document.getElementById('comparisonSlider'); -const comparisonViewer = document.querySelector('.comparison-viewer'); -const tintToggle = document.getElementById('tint_toggle'); - -// --- 1. WASM Initialization Handler --- -Module.onRuntimeInitialized = () => { - wasmReady = true; - if (statusDiv) { - statusDiv.innerText = "WASM Module Ready!"; - statusDiv.classList.add('ready'); - } - - // Enable controls now that WASM is ready - if (fileInput) fileInput.disabled = false; - if (qualitySlider) qualitySlider.disabled = false; - viewModeRadios.forEach(radio => radio.disabled = false); - csRadios.forEach(radio => radio.disabled = false); - if (comparisonSlider) comparisonSlider.disabled = false; - if (tintToggle) tintToggle.disabled = false; - - console.log("WASM Runtime Initialized"); -}; - -// --- 2. Handle File Input --- -fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (!file) return; - - const img = new Image(); - img.onload = () => { - // Calculate scale to fit max dimensions - let scale = 1.0; - if (img.width > maxDim || img.height > maxDim) { - scale = Math.min(maxDim / img.width, maxDim / img.height); - } - - imgWidth = Math.floor(img.width * scale); - imgHeight = Math.floor(img.height * scale); - - // Resize canvases - originalCanvas.width = imgWidth; - originalCanvas.height = imgHeight; - processedCanvas.width = imgWidth; - processedCanvas.height = imgHeight; - - // Draw original image to canvas to get pixel data - origCtx.drawImage(img, 0, 0, imgWidth, imgHeight); - originalImageData = origCtx.getImageData(0, 0, imgWidth, imgHeight); - - // Reset comparison slider to a 50/50 view - if (comparisonSlider && processedCanvas) { - comparisonSlider.value = 50; - processedCanvas.style.clipPath = `polygon(50% 0, 100% 0, 100% 100%, 50% 100%)`; - } - - // Initialize a new session in WASM - initSession(); - }; - img.src = URL.createObjectURL(file); -}); - -// --- 3. Debounce Helper --- -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -// --- 4. Handle Controls --- -const debouncedUpdate = debounce(() => updateAndRender(), 150); - -qualitySlider.addEventListener('input', () => { - qualityValue.textContent = qualitySlider.value; - if (originalImageData) { - debouncedUpdate(); - } -}); - -viewModeRadios.forEach(radio => { - radio.addEventListener('change', (e) => { - switch (e.target.value) { - case 'artifacts': currentViewMode = ViewMode.Artifacts; break; - case 'y': currentViewMode = ViewMode.Y; break; - case 'cr': currentViewMode = ViewMode.Cr; break; - case 'cb': currentViewMode = ViewMode.Cb; break; - default: currentViewMode = ViewMode.RGB; break; - } - // Only re-render, don't re-process - if (originalImageData) { - render(); - } - }); -}); - -csRadios.forEach(radio => { - radio.addEventListener('change', (e) => { - currentCsMode = parseInt(e.target.value, 10); - // Re-process the image with the new setting - if (originalImageData) { - updateAndRender(); - } - }); -}); - -if (tintToggle) { - tintToggle.addEventListener('change', (e) => { - if (wasmReady) { - Module._set_view_tint(e.target.checked ? 1 : 0); - if (originalImageData) { - render(); - } - } - }); -} - -function updateComparisonView(percent) { - // Clamp the value between 0 and 100 - const clampedPercent = Math.max(0, Math.min(100, percent)); - - // Update the slider's visual position - comparisonSlider.value = clampedPercent; - - // Update the clip-path of the top canvas (processed) to reveal the bottom (original) - processedCanvas.style.clipPath = `polygon(${clampedPercent}% 0, 100% 0, 100% 100%, ${clampedPercent}% 100%)`; -} - -comparisonSlider.addEventListener('input', (e) => updateComparisonView(e.target.value)); - -let isDragging = false; - -function handleInteraction(clientX) { - if (!comparisonViewer) return; - const rect = comparisonViewer.getBoundingClientRect(); - const x = clientX - rect.left; - const percent = (x / rect.width) * 100; - updateComparisonView(percent); -} - -// --- Mouse Events for Direct Image Interaction --- -comparisonViewer.addEventListener('mousedown', (e) => { - isDragging = true; - comparisonViewer.style.cursor = 'grabbing'; - handleInteraction(e.clientX); -}); - -document.addEventListener('mouseup', () => { - isDragging = false; - if (comparisonViewer) comparisonViewer.style.cursor = 'col-resize'; -}); - -document.addEventListener('mouseleave', () => { isDragging = false; }); - -document.addEventListener('mousemove', (e) => { - if (isDragging) { - handleInteraction(e.clientX); - } -}); - -// --- Touch Events for Direct Image Interaction --- -comparisonViewer.addEventListener('touchstart', (e) => { - isDragging = true; - handleInteraction(e.touches[0].clientX); -}); - -document.addEventListener('touchend', () => { isDragging = false; }); -document.addEventListener('touchcancel', () => { isDragging = false; }); - -document.addEventListener('touchmove', (e) => { - if (isDragging) { - // Prevent scrolling while dragging on the image - e.preventDefault(); - handleInteraction(e.touches[0].clientX); - } -}, { passive: false }); // Required to allow preventDefault - -// --- 5. Core WASM Interaction Functions --- - -function initSession() { - if (!originalImageData) return; - - // The C++ side now expects a 4-channel RGBA buffer. - const rgbaSize = originalImageData.data.length; - - let inputPtr = 0; - try { - // Allocate memory in the WASM heap and copy the image data. - inputPtr = Module._malloc(rgbaSize); - Module.HEAPU8.set(originalImageData.data, inputPtr); - Module._init_session(inputPtr, imgWidth, imgHeight); - } finally { - if (inputPtr) Module._free(inputPtr); - } - - // Trigger initial processing and rendering - updateAndRender(); -} - -function updateAndRender() { - if (!wasmReady || !originalImageData) return; - - const quality = parseInt(qualitySlider.value); - // Call the updated C++ function with quality and chroma subsampling mode. - Module._process_image(quality, currentCsMode); - - // After updating, render the current view - render(); -} - -function render() { - if (!wasmReady || !originalImageData) return; - - const rgbaSize = imgWidth * imgHeight * 4; - let outputPtr = 0; - try { - // Get the pointer for the current view - outputPtr = Module._get_view_ptr(currentViewMode); - if (!outputPtr) throw new Error("WASM get_view_ptr returned null"); - - // The C++ module now returns a 4-channel RGBA buffer. - // We can create an ImageData object directly from this buffer. - const imageDataBytes = new Uint8ClampedArray(Module.HEAPU8.buffer, outputPtr, rgbaSize); - const imageData = new ImageData(imageDataBytes, imgWidth, imgHeight); - procCtx.putImageData(imageData, 0, 0); - - // Update stats - psnrY.textContent = Module._get_psnr_y().toFixed(2); - psnrCr.textContent = Module._get_psnr_cr().toFixed(2); - psnrCb.textContent = Module._get_psnr_cb().toFixed(2); - - // Update SSIM - document.getElementById('ssimY').textContent = Module._get_ssim_y().toFixed(4); - document.getElementById('ssimCr').textContent = Module._get_ssim_cr().toFixed(4); - document.getElementById('ssimCb').textContent = Module._get_ssim_cb().toFixed(4); - - } catch (err) { - console.error("WASM render error:", err); - } finally { - // The C++ code malloc'd this, so JS must free it. - if (outputPtr) Module._free(outputPtr); - } -} \ No newline at end of file diff --git a/web/public/style.css b/web/public/style.css deleted file mode 100644 index 368b850..0000000 --- a/web/public/style.css +++ /dev/null @@ -1,468 +0,0 @@ -/* web/public/style.css */ - -:root { - --primary: #2563eb; - /* Royal Blue */ - --primary-hover: #1d4ed8; - --bg: #f8fafc; - /* Light Slate Background */ - --card-bg: #ffffff; - --text: #1e293b; - /* Slate 800 */ - --text-muted: #64748b; - --border: #e2e8f0; - --success-bg: #dcfce7; - --success-text: #166534; -} - -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - background-color: var(--bg); - color: var(--text); - margin: 0; - padding: 20px; - line-height: 1.5; - display: flex; - flex-direction: column; - align-items: center; -} - -/* --- Header & Privacy Badge --- */ -header { - text-align: center; - margin-bottom: 32px; -} - -h1 { - margin: 0 0 12px 0; - font-size: 1.8rem; - letter-spacing: -0.025em; -} - -.privacy-badge { - display: inline-flex; - align-items: center; - gap: 8px; - background-color: var(--success-bg); - color: var(--success-text); - padding: 6px 12px; - border-radius: 99px; - font-size: 0.85rem; - font-weight: 600; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.github-link { - display: inline-flex; - align-items: center; - gap: 8px; - background-color: #f1f5f9; - /* Slate 100 */ - color: #475569; - /* Slate 600 */ - padding: 6px 12px; - border-radius: 99px; - font-size: 0.85rem; - font-weight: 600; - text-decoration: none; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - transition: all 0.2s ease; - margin-left: 8px; -} - -.github-link:hover { - background-color: #e2e8f0; - /* Slate 200 */ - color: #1e293b; - /* Slate 800 */ - transform: translateY(-1px); -} - -/* --- Layout --- */ -.container { - width: 100%; - max-width: 1280px; - display: grid; - grid-template-columns: 1fr 320px; - /* Sidebar layout */ - gap: 24px; - align-items: start; -} - -/* --- Components --- */ -.card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 12px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - padding: 12px; -} - -.controls { - overflow: visible; -} - -canvas { - max-width: 100%; - height: auto; - display: block; -} - -/* --- Controls --- */ -.controls { - display: flex; - flex-direction: column; - gap: 20px; -} - -.control-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -label { - font-weight: 600; - font-size: 0.9rem; - display: flex; - justify-content: space-between; -} - -input[type="range"] { - width: 100%; - cursor: pointer; -} - -button.primary { - background-color: var(--primary); - color: white; - border: none; - padding: 12px; - border-radius: 8px; - font-weight: 600; - font-size: 1rem; - cursor: pointer; - transition: background-color 0.2s; -} - -button.primary:hover { - background-color: var(--primary-hover); -} - -button.primary:active { - transform: translateY(1px); -} - -.stats { - margin-top: 10px; - padding: 12px; - background: #f1f5f9; - border-radius: 6px; - font-family: monospace; - font-size: 0.85rem; - white-space: pre-wrap; - border: 1px solid var(--border); -} - -/* --- Mobile Responsiveness --- */ -@media (max-width: 768px) { - .container { - grid-template-columns: 1fr; - /* Stack vertically */ - } - - body { - padding: 16px; - } - - /* Better touch targets */ - input[type="range"] { - height: 40px; - } - - button.primary { - padding: 16px; - } -} - -/* --- Segmented Control (Toggle Bar) --- */ - -.group-label { - margin-bottom: 8px; - display: block; - font-size: 0.9rem; - color: var(--text); -} - -.toggle-group { - display: flex; - background: #e2e8f0; - /* Light gray track */ - padding: 4px; - border-radius: 8px; - gap: 4px; - /* Space between buttons */ -} - -/* 1. Hide the actual radio input circle */ -.toggle-group input[type="radio"] { - display: none; -} - -/* 2. Style the Labels to look like Buttons */ -.toggle-group label { - display: flex; - /* 1. Make the label a container */ - justify-content: center; - /* 2. Center horizontally */ - align-items: center; - flex: 1; - /* Make them all equal width */ - text-align: center; - padding: 8px 4px; - font-size: 0.85rem; - font-weight: 500; - border-radius: 6px; - cursor: pointer; - color: #64748b; - /* Muted text */ - transition: all 0.2s ease; - user-select: none; - /* Prevent text highlighting on clicks */ -} - -/* 3. Hover Effect */ -.toggle-group label:hover { - background: rgba(255, 255, 255, 0.5); - color: var(--text); -} - -/* 4. Active (Checked) State */ -/* This magic selector targets the label immediately following a checked input */ -.toggle-group input[type="radio"]:checked+label { - background: #ffffff; - color: var(--primary); - /* Blue text */ - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - /* Subtle shadow pop */ - font-weight: 600; -} - -/* --- Comparison Viewer (Container) --- */ -.canvas-wrapper { - background: #0f172a; - border-radius: 12px; - padding: 16px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.viewer-info { - text-align: center; - font-size: 0.85rem; - color: #94a3b8; - /* Muted slate */ - user-select: none; -} - -.comparison-viewer { - position: relative; - width: 100%; - /* Remove fixed height to let canvas define it */ - line-height: 0; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5); - cursor: col-resize; -} - -.comparison-viewer canvas { - display: block; - width: 100%; - height: auto; -} - -/* Top Layer: Processed */ -#processedCanvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - /* Default to a 50/50 split view */ - clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%); -} - -/* --- The Crossfader Slider --- */ -.slider-container { - display: flex; - align-items: center; - gap: 12px; - padding: 0 8px; -} - -.slider-label { - font-size: 0.8rem; - font-weight: 600; - color: #94a3b8; - /* Muted slate */ - text-transform: uppercase; - letter-spacing: 0.05em; - width: 80px; - /* Fixed width prevents jitter */ -} - -.slider-label:first-child { - text-align: right; -} - -.slider-label:last-child { - text-align: left; - color: var(--primary); -} - -.comparison-slider-input { - flex: 1; - /* Take up remaining space */ - -webkit-appearance: none; - appearance: none; - height: 6px; - background: linear-gradient(to right, #64748b 0%, var(--primary) 100%); - border-radius: 3px; - outline: none; - cursor: pointer; -} - -/* Slider Thumb (Chrome/Safari) */ -.comparison-slider-input::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - background: white; - border: 2px solid var(--primary); - border-radius: 50%; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - transition: transform 0.1s; -} - -/* --- Tooltip --- */ -.tooltip-container { - position: relative; - display: flex; - align-items: center; - cursor: help; -} - -.info-icon { - display: flex; - align-items: center; - color: var(--text-muted); - transition: color 0.2s; -} - -.info-icon:hover { - color: var(--primary); -} - -.tooltip-content { - position: absolute; - bottom: 125%; - right: 0; - transform: translateY(10px); - width: 280px; - background-color: #1e293b; - /* Dark Slate */ - color: #ffffff; - padding: 10px 14px; - border-radius: 8px; - font-size: 0.75rem; - line-height: 1.4; - font-weight: normal; - text-align: left; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); - opacity: 0; - visibility: hidden; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 1000; - pointer-events: none; -} - -/* Tooltip Arrow */ -.tooltip-content::after { - content: ""; - position: absolute; - top: 100%; - right: 6px; - border-width: 5px; - border-style: solid; - border-color: #1e293b transparent transparent transparent; -} - -.tooltip-container:hover .tooltip-content { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -/* Adjust for narrow sidebar */ -@media (max-width: 350px) { - .tooltip-content { - width: 180px; - left: auto; - right: 0; - transform: translateY(10px); - } - - .tooltip-container:hover .tooltip-content { - transform: translateY(0); - } - - .tooltip-content::after { - left: 90%; - } -} - -.comparison-slider-input::-webkit-slider-thumb:hover { - transform: scale(1.2); -} - -/* Slider Thumb (Firefox) */ -.comparison-slider-input::-moz-range-thumb { - width: 20px; - height: 20px; - background: white; - border: 2px solid var(--primary); - border-radius: 50%; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - transition: transform 0.1s; -}/* Existing styles... */ - -/* Add this to your existing css file */ -.metrics-table { - width: 100%; - border-collapse: collapse; - margin-top: 10px; - font-size: 0.9rem; - color: var(--text-color); -} - -.metrics-table th, -.metrics-table td { - border: 1px solid var(--border-color); - padding: 6px; - text-align: center; -} - -.metrics-table th { - background-color: var(--bg-secondary); - font-weight: 600; -} - -.metrics-table td { - background-color: var(--bg-primary); -} \ No newline at end of file diff --git a/web/src/codec_web.cpp b/web/src/codec_web.cpp index 66fd438..d516238 100644 --- a/web/src/codec_web.cpp +++ b/web/src/codec_web.cpp @@ -173,4 +173,38 @@ double get_ssim_cb() { return g_session.initialized ? g_session.metrics.ssimCb : 0.0; } + +// Re-declaring to match the plan's arguments +EMSCRIPTEN_KEEPALIVE +double* inspect_block_data(int blockX, int blockY, int channelIndex, int quality) { + if (!g_session.initialized) return nullptr; + + Image ycrcb = bgrToYCrCb(g_session.originalImage); + Image channel(ycrcb.width(), ycrcb.height(), 1); + const double* src = ycrcb.data(); + double* dst = channel.data(); + int offset = (channelIndex == 0) ? 0 : (channelIndex == 1 ? 1 : 2); // Y=0, Cr=1, Cb=2 + + const size_t numPixels = static_cast(ycrcb.width()) * ycrcb.height(); + for(size_t i=0; i < numPixels; ++i) { + dst[i] = src[i*3 + offset]; + } + + bool isChroma = (channelIndex != 0); + + // Create temp codec + // Note: CS mode doesn't affect the *inspection* of a single 8x8 block of the *source* image + // vis-a-vis the quantization tables. Subsampling logic happens before this in the pipeline typically, + // but `inspectBlock` treats the input image as the plane to be blocked. + // If we want to inspect the *subsampled* block, we would need to pass the subsampled image. + // For simplicity, let's inspect the Full Resolution block using the appropriate Quant Table. + ImageCodec codec(quality, true, ImageCodec::ChromaSubsampling::CS_444); + + static ImageCodec::BlockDebugData debugData; // Static to persist return pointer + debugData = codec.inspectBlock(channel, blockX, blockY, isChroma); + + // Return pointer to the start of the struct (which is just a sequence of double[8][8]) + return (double*)&debugData; +} + } // extern "C" \ No newline at end of file