Please bookmark this page to avoid losing your image tool!

Image Film Stock Emulation 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.
async function processImage(originalImg, filmStock = "kodak_portra_400") {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization for frequent getImageData/putImageData

    canvas.width = originalImg.naturalWidth;
    canvas.height = originalImg.naturalHeight;
    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

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

    // Helper function to clamp values between 0 and 255
    const clamp = (value) => Math.max(0, Math.min(255, Math.round(value)));

    // Helper for contrast adjustment
    // uiContrast: -100 to 100. 0 is no change.
    const adjustContrast = (value, uiContrast) => {
        if (uiContrast === 0) return value;
        // The formula expects contrast in range -255 to 255.
        // Let's map uiContrast [-100, 100] to a suitable internal range, e.g. by multiplying.
        // A smaller multiplier might be better to prevent extreme changes.
        // Let's use the common formula where factor depends on contrast.
        // factor = (259 * (c + 255)) / (255 * (259 - c)); where c is typically -255 to 255
        // If uiContrast is -100 to 100:
        const c = uiContrast; // Let's assume this range is directly usable for a milder effect.
        const factor = (259 * (c + 255)) / (255 * (259 - c));
        return clamp(factor * (value - 128) + 128);
    };
    
    // Helper for saturation adjustment
    // saturationFactor: 0.0 (grayscale) to 2.0+ (super saturated). 1.0 is no change.
    const adjustSaturation = (r, g, b, saturationFactor) => {
        if (saturationFactor === 1.0) return [r, g, b];
        const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminance
        return [
            clamp(gray + saturationFactor * (r - gray)),
            clamp(gray + saturationFactor * (g - gray)),
            clamp(gray + saturationFactor * (b - gray))
        ];
    };

    for (let i = 0; i < data.length; i += 4) {
        let r = data[i];
        let g = data[i + 1];
        let b = data[i + 2];

        const origR = r;
        const origG = g;
        const origB = b;

        let processedR = r;
        let processedG = g;
        let processedB = b;

        switch (filmStock.toLowerCase()) {
            case "kodak_portra_400": {
                // Warm tones, good skin tones, slightly desaturated greens/blues, smooth roll-off.
                processedR = clamp(r * 1.1 + 10);
                processedG = clamp(g * 1.05 + 5);
                processedB = clamp(b * 0.92 - 5); // Slightly less blue reduction

                // Apply contrast *after* color shifts
                processedR = adjustContrast(processedR, -25); 
                processedG = adjustContrast(processedG, -25);
                processedB = adjustContrast(processedB, -25);
                
                // Desaturate blues and greens a bit using original luminance as reference
                const luma = 0.299 * origR + 0.587 * origG + 0.114 * origB;
                [processedR, processedG, processedB] = adjustSaturation(processedR, processedG, processedB, 0.9); // Overall slight desat
                
                // Specific desaturation for greens/blues
                processedG = clamp(luma + 0.85 * (processedG - luma)); 
                processedB = clamp(luma + 0.75 * (processedB - luma)); 

                // Skin tone protection
                if (processedR > processedG && processedR > processedB && processedG > processedB * 0.8) { 
                    processedR = clamp(processedR * 0.97); // Less red
                    processedG = clamp(processedG * 1.03); // Slightly more green
                }
                break;
            }
            case "fuji_velvia_50": {
                // High saturation, high contrast, vivid reds/greens.
                [processedR, processedG, processedB] = adjustSaturation(r, g, b, 1.45);
                
                processedR = adjustContrast(processedR, 30);
                processedG = adjustContrast(processedG, 30);
                processedB = adjustContrast(processedB, 30);

                // Boost reds and greens more
                processedR = clamp(processedR * 1.12);
                processedG = clamp(processedG * 1.12);
                processedB = clamp(processedB * 0.93);
                break;
            }
            case "ilford_hp5_plus_400": {
                // B&W, good contrast.
                let gray = 0.299 * r + 0.587 * g + 0.114 * b;
                gray = adjustContrast(gray, 35); 
                
                // Add grain
                const grainAmount = 12; 
                const grain = (Math.random() * 2 - 1) * grainAmount;
                processedR = processedG = processedB = clamp(gray + grain);
                break;
            }
            case "fuji_pro_400h": {
                // Slightly cool, teal-ish greens, good skin tones.
                processedR = clamp(r * 0.96 - 3);
                processedG = clamp(g * 1.04); // Boost greens
                processedB = clamp(b * 1.08 + 7); // Boost blues for coolness

                // Shift greens towards teal if green is dominant
                if (origG > origR && origG > origB) {
                    processedG = clamp(processedG * 1.03); 
                    processedB = clamp(processedB * 1.03); // Add blue to green
                }
                
                [processedR, processedG, processedB] = adjustSaturation(processedR, processedG, processedB, 1.15);
                
                processedR = adjustContrast(processedR, 15);
                processedG = adjustContrast(processedG, 15);
                processedB = adjustContrast(processedB, 15);
                break;
            }
             case "cinestill_800t": {
                // Tungsten balanced, blue shadows, warm/reddish highlights, halation idea.
                let tempR = r, tempG = g, tempB = b;
                const luminance = 0.299 * origR + 0.587 * origG + 0.114 * origB;

                if (luminance < 70) { // Shadows
                    tempB = clamp(b * 1.25 + 20);
                    tempR = clamp(r * 0.80 - 5);
                    tempG = clamp(g * 0.88);
                } else if (luminance > 190) { // Highlights
                    tempR = clamp(r * 1.20 + 15);
                    tempG = clamp(g * 1.08);
                    tempB = clamp(b * 0.80 - 10);
                } else { // Midtones - subtle shifts
                    tempB = clamp(b * 1.08);
                    tempR = clamp(r * 0.96);
                }
                
                [processedR, processedG, processedB] = adjustSaturation(tempR, tempG, tempB, 1.25);
                
                processedR = adjustContrast(processedR, 20);
                processedG = adjustContrast(processedG, 20);
                processedB = adjustContrast(processedB, 20);

                // "Halation" (crude: make bright areas redder/more orange)
                if (origR > 210 && origG > 190 && origB > 170) { 
                    processedR = clamp(processedR * 1.15 + 25); 
                    processedG = clamp(processedG * 0.95); // Reduce green in brightest spots
                }
                break;
            }
            case "sepia": {
                const gray = 0.299 * r + 0.587 * g + 0.114 * b;
                processedR = clamp(gray + 45); // Increased depth
                processedG = clamp(gray + 20); // Increased depth
                processedB = clamp(gray - 15); // Less blue reduction
                break;
            }
            default: // Unknown film stock, or "none"
                // No changes applied, keeps original colors
                break;
        }

        data[i] = processedR;
        data[i + 1] = processedG;
        data[i + 2] = processedB;
        // Alpha channel (data[i+3]) remains unchanged
    }

    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 Film Stock Emulation Filter is a web-based tool that allows users to apply various film stock emulations to their images. By selecting from different presets such as Kodak Portra 400, Fuji Velvia 50, Ilford HP5 Plus 400, and many others, users can transform their digital images to mimic the unique color profiles and contrasts characteristic of traditional film photography. This tool is useful for photographers, graphic designers, and enthusiasts who wish to achieve a specific aesthetic in their photographs, enhancing their creativity by bringing a nostalgic or artistic feel to their digital images.

Leave a Reply

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