Please bookmark this page to avoid losing your image tool!

Image Tone Mapping Filter

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
function processImage(originalImg, exposure = 1.0) {
    const canvas = document.createElement('canvas');
    // Use naturalWidth/Height for HTMLImageElement, otherwise width/height
    // (e.g. if originalImg is another canvas or ImageData)
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;
    canvas.width = width;
    canvas.height = height;

    const ctx = canvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0, width, height);

    const imageData = ctx.getImageData(0, 0, width, height);
    const pixels = imageData.data;

    // Helper for sRGB (gamma-corrected) to Linear conversion
    // Assumes c_srgb is a byte value [0, 255]
    function srgbToLinear(c_srgb) {
        const val = c_srgb / 255.0;
        if (val <= 0.04045) {
            return val / 12.92;
        }
        return Math.pow((val + 0.055) / 1.055, 2.4);
    }

    // Helper for Linear to sRGB (gamma-corrected) conversion
    // Assumes c_lin is in [0,無限大) range, will clamp to [0,1] for sRGB standard
    function linearToSrgb(c_lin) {
        // Clamp linear value to [0, 1] range for sRGB output
        const c_clamped = Math.max(0.0, Math.min(1.0, c_lin));
        
        if (c_clamped <= 0.0031308) {
            return c_clamped * 12.92;
        }
        return 1.055 * Math.pow(c_clamped, 1.0 / 2.4) - 0.055;
    }
    
    let numExposure;
    // Validate and parse the exposure parameter
    if (typeof exposure === 'string') {
        numExposure = parseFloat(exposure);
        if (!Number.isFinite(numExposure)) { // Handles NaN, Infinity from parseFloat
            numExposure = 1.0;
        }
    } else if (typeof exposure === 'number') {
        if (!Number.isFinite(exposure)) { // Handles NaN, Infinity from numeric input
             numExposure = 1.0;
        } else {
            numExposure = exposure;
        }
    } else { // Default for other types or if parsing fails gently
        numExposure = 1.0;
    }

    // Clamp exposure to a safe, small positive value.
    // Very small positive exposure makes the image approach its original state.
    // Negative or zero exposure would lead to issues or unnaturally dark/black images.
    const effExposure = Math.max(0.001, numExposure);

    // Precompute normalization factor for the Reinhard tone mapping curve.
    // This ensures that an input L_lin = 1.0 (max typical linear luminance for sRGB white)
    // maps to L_final_lin = 1.0 after tone mapping, preserving white point.
    const normFactor = (1.0 + effExposure) / effExposure;

    for (let i = 0; i < pixels.length; i += 4) {
        const r_srgb_byte = pixels[i];
        const g_srgb_byte = pixels[i + 1];
        const b_srgb_byte = pixels[i + 2];

        // Convert sRGB components to linear space
        const r_lin = srgbToLinear(r_srgb_byte);
        const g_lin = srgbToLinear(g_srgb_byte);
        const b_lin = srgbToLinear(b_srgb_byte);

        // Calculate linear luminance (using Rec. 709 primaries, common for sRGB)
        const L_lin = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin;

        // If luminance is zero or extremely small, the pixel is black or near black.
        // Output black and skip further calculations to avoid division by zero or precision issues.
        if (L_lin <= 0.00001) { // Epsilon for floating point checks
            pixels[i] = 0;
            pixels[i + 1] = 0;
            pixels[i + 2] = 0;
            // Alpha channel (pixels[i+3]) remains unchanged
            continue;
        }

        // Apply Reinhard-style tone mapping operator to the linear luminance
        const L_adjusted = L_lin * effExposure; // Apply exposure adjustment
        const L_tonemapped_lin = L_adjusted / (1.0 + L_adjusted); // Basic Reinhard curve
        
        // Normalize the tone-mapped luminance to ensure white point is preserved
        const L_final_lin = L_tonemapped_lin * normFactor;
        
        // Calculate the scale factor to apply to each color channel.
        // This attempts to preserve the original color ratios (hue and relative saturation).
        const scale = L_final_lin / L_lin;

        // Apply the scale to the linear RGB channels
        const r_tm_lin = r_lin * scale;
        const g_tm_lin = g_lin * scale;
        const b_tm_lin = b_lin * scale;

        // Convert the tone-mapped linear RGB values back to sRGB space
        const r_final_srgb = linearToSrgb(r_tm_lin);
        const g_final_srgb = linearToSrgb(g_tm_lin);
        const b_final_srgb = linearToSrgb(b_tm_lin);
        
        // Convert sRGB values (now in [0,1] range) back to 0-255 byte range.
        // Math.round is used for perceptually potentially better results than truncation.
        pixels[i]   = Math.round(r_final_srgb * 255);
        pixels[i+1] = Math.round(g_final_srgb * 255);
        pixels[i+2] = Math.round(b_final_srgb * 255);
    }

    ctx.putImageData(imageData, 0, 0);
    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Tone Mapping Filter is a web-based tool that allows users to adjust the tone mapping of images by modifying the exposure levels. This tool is useful for photographers, graphic designers, and anyone needing to enhance image details in highlights and shadows. By applying a Reinhard-style tone mapping technique, users can achieve more balanced exposures, making their images appear more dynamic and visually appealing. This tool can be used in various contexts, such as improving images for social media, creating artistic effects, or preparing photos for professional presentations.

Leave a Reply

Your email address will not be published. Required fields are marked *