# 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

```html
<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/{base64}` where `{base64}` = `btoa(imageUrl)`.

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

---

## Events Reference

```js
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.

```js
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., `"data:image/png;base64,..."`) |
| `callback` | function | `(err)` |

```js
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 `<img>` tag, or trigger a download.

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

```js
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)` |

```js
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)` |

```js
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)` |

```js
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.

```js
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).

```js
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)` |

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

---

### `getScale` -- Get Current Zoom

Returns the current zoom scale factor.

```js
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.

```js
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.

```js
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.

```js
// 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"); }
    );
});
```

```js
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 `{ name: string, params: object }` |
| `callback` | function | `(err)` |

```js
// 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:'social_1977',params:{}}])">Try: 1977 Preset</button>

#### Available Filters

**Color Adjustment**

| 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 |

**Black & White**

| 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 |

**Blur & Sharpen**

| 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 |

**Artistic**

| 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 |

**Social Media Presets**

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

---

## Complete Example

```html
<!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>
```
