Photo Editor

Online image editing with crop, resize, rotate, flip, undo/redo, and 30+ filters.

Live Demo

<iframe id="pe-demo" src="/online/webapp/photo-editor" width="100%" height="500" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('pe-demo', 'peSocket');</script>

Embed

<iframe src="https://sgapps.io/online/webapp/photo-editor"
    width="100%" height="600" frameborder="0"></iframe>

Open with a Remote Image

URL pattern: /online/webapp/photo-editor/url/&#x7B;base64&#x7D; where &#x7B;base64&#x7D; = btoa(imageUrl).

<iframe src="https://sgapps.io/online/webapp/photo-editor/url/aHR0cHM6Ly9leGFtcGxlLmNvbS9waG90by5qcGc="
    width="100%" height="600" frameborder="0"></iframe>

Events Reference

var socket = new WindowSocket(iframe.contentWindow);
socket.start();
socket.fire("webapp::instance::request", eventName, ...args, callback);

reset -- Clear Canvas

Clears the editor to a blank state, removing all image data.

socket.fire("webapp::instance::request", "reset", function (err) {
    console.log("Canvas cleared");
});

setDataBase64 -- Load Image from Base64

Loads an image from a base64-encoded data URL. Use this to programmatically set the image content from your application -- for example after capturing from a canvas or receiving from an API.

Arg Type Description
base64Url string Full data URL (e.g., &#x22;data:image/png;base64,...&#x22;)
callback function (err)

socket.fire("webapp::instance::request", "setDataBase64",
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
    function (err) { console.log("Image loaded into editor"); }
);

getDataBase64 -- Export Image

Exports the current canvas as a base64 data URL. This is the primary way to get the edited image back into your application -- you can send it to a server, display it in an &#x3C;img&#x3E; tag, or trigger a download.

Arg Type Description
type string Output MIME type: &#x22;image/png&#x22;, &#x22;image/jpeg&#x22;, &#x22;image/webp&#x22;
callback function (err, dataUrl: string)
quality number 0-1 quality for JPEG/WebP (optional, default 1)

socket.fire("webapp::instance::request", "getDataBase64", "image/png",
    function (err, dataUrl) {
        document.getElementById('preview').src = dataUrl;
    }
);

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','getDataBase64','image/png',function(e,d){if(d){var i=document.createElement('img');i.src=d;i.style.maxHeight='120px';document.getElementById('pe-export-result').innerHTML='';document.getElementById('pe-export-result').appendChild(i);}})">Try: Export PNG from demo above</button><div id="pe-export-result" style="margin:8px 0; min-height:20px;"></div>


loadFromURL -- Load Image from URL

Loads an image directly from a URL without base64 encoding. The URL must be accessible from the browser (same-origin or CORS-enabled).

Arg Type Description
url string Image URL
callback function (err)

socket.fire("webapp::instance::request", "loadFromURL",
    "https://example.com/photo.jpg",
    function (err) { console.log(err || "Loaded"); }
);

resize -- Resize Image

Resizes the entire canvas to new dimensions. The image content is scaled to fit.

Arg Type Description
width number New width in pixels
height number New height in pixels
callback function (err)

socket.fire("webapp::instance::request", "resize", 800, 600, function (err) {
    console.log("Resized to 800x600");
});

crop -- Crop Image

Crops the image to a rectangle defined by position and size. Pixels outside the rectangle are discarded.

Arg Type Description
x number Left offset in pixels
y number Top offset in pixels
width number Crop width
height number Crop height
callback function (err)

socket.fire("webapp::instance::request", "crop", 50, 50, 300, 200, function (err) {
    console.log("Cropped to 300x200 starting at (50,50)");
});

flipH, flipW -- Flip Image

Mirror the image along the horizontal axis (flipH) or vertical axis (flipW). Useful for correcting selfie-mode camera captures or creating mirror effects.

socket.fire("webapp::instance::request", "flipH", function () {});
socket.fire("webapp::instance::request", "flipW", function () {});

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','flipH')">Try: Flip Horizontal</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','flipW')">Try: Flip Vertical</button>


rotateCW, rotateCCW -- Rotate Image

Rotate the image 90 degrees clockwise (rotateCW) or counter-clockwise (rotateCCW). Canvas dimensions are swapped (width becomes height and vice versa).

socket.fire("webapp::instance::request", "rotateCW", function () {});
socket.fire("webapp::instance::request", "rotateCCW", function () {});

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','rotateCW')">Try: Rotate CW</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','rotateCCW')">Try: Rotate CCW</button>


zoom -- Set Zoom Level

Sets the editor's display zoom level. This doesn't change the actual image data -- only how it's displayed.

Arg Type Description
level number 1 = 100%, 2 = 200%, 0.5 = 50%
callback function (err)

socket.fire("webapp::instance::request", "zoom", 2, function () {
    console.log("Zoomed to 200%");
});

getScale -- Get Current Zoom

Returns the current zoom scale factor.

socket.fire("webapp::instance::request", "getScale", function (err, scale) {
    console.log("Current zoom:", Math.round(scale * 100) + "%");
});

fitToView -- Fit Image to Viewport

Automatically scales and centers the image so the entire canvas is visible within the editor viewport.

socket.fire("webapp::instance::request", "fitToView", function () {
    console.log("Image fitted to viewport");
});

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','fitToView')">Try: Fit to View</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','getScale',function(e,s){alert('Zoom: '+Math.round(s*100)+'%')})">Try: Get Scale</button>


getImageInfo -- Get Image Metadata

Returns dimensions, layer count, and current scale of the image being edited. Useful for displaying status information or making decisions about which operations to apply.

socket.fire("webapp::instance::request", "getImageInfo", function (err, info) {
    console.log(info);
    // { width: 1920, height: 1080, layerCount: 1, scale: 1 }
});

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','getImageInfo',function(e,i){alert(JSON.stringify(i,null,2))})">Try: Get Image Info</button>


history:undo, history:redo, history:save -- Undo/Redo

The editor maintains a history stack of image states. Use history:save to create a checkpoint before making changes, then history:undo and history:redo to navigate the stack.

// Save a checkpoint, apply a filter, then undo it
socket.fire("webapp::instance::request", "history:save", function () {
    socket.fire("webapp::instance::request", "applyFilters",
        [{ name: "sepia", params: { level: 0.9 } }],
        function () { console.log("Applied -- press Undo to revert"); }
    );
});
socket.fire("webapp::instance::request", "history:undo"); // go back
socket.fire("webapp::instance::request", "history:redo"); // go forward

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','history:save',function(){window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'sepia',params:{level:0.9}}])})">Try: Save + Sepia</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','history:undo')">Try: Undo</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','history:redo')">Try: Redo</button>


applyFilters -- Apply Image Filters

Apply one or more image filters in sequence. Filters are processed one after another on each layer. The editor includes 30+ filters ranging from basic adjustments to artistic effects and social media presets.

Arg Type Description
filters Array Array of &#x7B; name: string, params: object &#x7D;
callback function (err)

// Apply a single filter
socket.fire("webapp::instance::request", "applyFilters", [
    { name: "brightness", params: { level: 0.3 } }
]);

// Chain multiple filters
socket.fire("webapp::instance::request", "applyFilters", [
    { name: "brightness", params: { level: 0.1 } },
    { name: "contrast", params: { level: 0.5 } },
    { name: "sepia", params: { level: 0.3 } }
], function () { console.log("Vintage look applied"); });

<button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'sepia',params:{level:0.9}}])">Try: Sepia</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'invert',params:{level:1}}])">Try: Invert</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'blur-stack',params:{level:5}}])">Try: Blur</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'edge',params:{level:1}}])">Try: Edge Detect</button><button onclick="window._ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'mosaic',params:{level:15}}])">Try: Mosaic</button><button onclick="window.ws('peSocket')&&window.peSocket.fire('webapp::instance::request','applyFilters',[{name:'social1977',params:{}}])">Try: 1977 Preset</button>

Available Filters

Filter Parameters Description
brightness level: -1 to 1 Lighten or darken
contrast level: -3 to 3 Increase/decrease contrast
saturation level: -1 to 3 Color intensity (-1 = grayscale)
sepia level: -3 to 3 Warm brownish tone
vibrance level: -300 to 300 Smart saturation boost
hsl-adjustment hue: -180..180, saturation: -100..100, lightness: -100..100 Full HSL control
brightness-contrast-gimp brightness: -100..100, contrast: -100..100 GIMP-style adjustment
brightness-contrast-photo brightness: -100..100, contrast: -100..100 Photo-style adjustment
channels channel: 1-3 Extract channel (1=R, 2=G, 3=B)
channels-adjust redOffset, redMultiply, greenOffset, greenMultiply, blueOffset, blueMultiply, alphaOffset, alphaMultiply: 0-255 Per-channel color transform
gamma level: 0 to 3 Gamma correction
gamma-v2 level: 0.01 to 100 Advanced gamma
clip level: -1 to 1 Color value clipping

Filter Parameters Description
grayscale level: 0 or 1 Convert to grayscale
grayscale-v2 level: 0 or 1 Rec.601 weighted grayscale
desaturate level: 0 or 1 Desaturate via luminosity
invert level: 0 or 1 Negative image
binarize level: 0 to 1 Black/white threshold
posterize levels: 2 to 255 Reduce color levels
solarize level: 0 or 1 Solarization effect

Filter Parameters Description
blur-box width, height: 0-100, radius: 0-100 Box blur
blur-gaussian level: 1 to 4 Gaussian blur
blur-stack level: 1 to 100 Fast stack blur
sharpen level: 2 to 255 Sharpen edges

Filter Parameters Description
edge level: 0 or 1 Edge detection
emboss level: 0 or 1 3D emboss effect
enrich level: 0 or 1 Edge enhancement
mosaic level: 2 to 400 Pixelation
oil range: 1-200, levels: 1-20 Oil painting
dither level: 2 to 255 Floyd-Steinberg dithering
twirl centerX/Y: 0-1, radius: 0-1000, angle: -360..360, edge: -1..1, smooth: 0/1 Spiral distortion
transpose level: 0 or 1 Mirror X/Y coordinates

Filter Description
social_1977 Warm vintage (sepia + hue shift)
social_aden Soft warm tone (sepia + saturation + lightness)


Complete Example

<!DOCTYPE html>
<html>
<head><title>My Image Editor Widget</title></head>
<body>
    <div style="display:flex;gap:8px;padding:10px;flex-wrap:wrap;">
        <button id="btn-sepia">Sepia</button>
        <button id="btn-undo">Undo</button>
        <button id="btn-fit">Fit to View</button>
        <button id="btn-export">Export PNG</button>
        <button id="btn-info">Image Info</button>
    </div>

    <iframe id="editor" src="https://sgapps.io/online/webapp/photo-editor"
        width="100%" height="500" frameborder="0"></iframe>
    <img id="preview" style="max-width:300px;margin:10px;">
    <pre id="info-box" style="background:#f6f8fa;padding:8px;"></pre>

    <script src="https://sgapps.io/components/window-socket/index.js"></script>
    <script>
        var socket = new WindowSocket(document.getElementById('editor').contentWindow);
        socket.start();

        socket.on("webapp::connection::ping", function () {
            socket.fire("webapp::instance::embed-mode", true);
        });

        document.getElementById('btn-sepia').onclick = function () {
            socket.fire("webapp::instance::request", "history:save");
            socket.fire("webapp::instance::request", "applyFilters",
                [{ name: "sepia", params: { level: 0.9 } }]);
        };
        document.getElementById('btn-undo').onclick = function () {
            socket.fire("webapp::instance::request", "history:undo");
        };
        document.getElementById('btn-fit').onclick = function () {
            socket.fire("webapp::instance::request", "fitToView");
        };
        document.getElementById('btn-export').onclick = function () {
            socket.fire("webapp::instance::request", "getDataBase64", "image/png",
                function (err, d) { document.getElementById('preview').src = d; });
        };
        document.getElementById('btn-info').onclick = function () {
            socket.fire("webapp::instance::request", "getImageInfo",
                function (err, info) {
                    document.getElementById('info-box').textContent = JSON.stringify(info, null, 2);
                });
        };
    </script>
</body>
</html>