✨ MagicImage

Vue 3 image input, crop and adjustment component with clipboard support.

Live demo Vue 3 Client side No backend PNG JPEG WebP

Upload · Clipboard · Crop · Levels · Brightness · Contrast · Saturation · Sharpen · Resize · PNG / JPEG / WebP

👉 Try the live demo

Pick a case, load a picture, tune it, crop it and download the two files — nothing leaves the browser.

Why MagicImage?

There are plenty of excellent libraries for cropping and processing images. But almost every time, the same surrounding code has to be written again: file uploads, clipboard paste, controls, UI state management, getting the result, and passing it on to the application.

MagicImage brings all of that together in a single Vue 3 component — a kind of Swiss Army knife for working with images.

It accepts an image from a file or the clipboard, lets the user crop and adjust it, and returns two files:

The component knows nothing about servers. The result leaves through a save event, and what happens after that is entirely up to the application.

MagicImage is not intended to replace specialised image-processing libraries.

Its purpose is to package the entire common browser-side image workflow into one ready-to-use component.


✨ Features


🧭 Contents


📦 Install

npm i @dixipro/magic-image

⚠️ Not published yet.

Until the first release, copy the src/ folder into your project and import the component by a relative path.

import MagicImage from './magic-image/src/MagicImage.vue';

vue and vue-advanced-cropper are peer dependencies. MagicImage uses the copies already installed in the host project.

The package ships the Vue source, so the host bundler compiles the .vue file.

No separate stylesheet import is required.


🚀 Quick start

<script setup>
import { ref } from 'vue';
import MagicImage from '@dixipro/magic-image';

const busy = ref(false);

function onSave({ original, crop }) {
  // original.file
  // crop.file
}
</script>

<template>
  <div v-if="!busy">…anything that should step aside while the editor works…</div>

  <MagicImage
    :max-pixels="16000000"
    output-format="png"
    :output-quality="100"
    clipboard-format="png"
    :clipboard-quality="100"
    crop-mode="fixed"
    :crop-x="1200"
    :crop-y="675"
    @stage="busy = $event !== 'empty'"
    @save="onSave"
  >
    <template #actions="{ canSave, apply }">
      <button v-if="canSave" @click="apply()">Continue</button>
    </template>
  </MagicImage>
</template>

The minimal call:

<MagicImage @save="onSave" />

🔄 How it works

  1. Upload — choose a file or paste an image with Ctrl+V.
  2. Tune — brightness, contrast, saturation, levels and sharpening.
  3. Crop — set the stencil. Cropping is mandatory.
  4. Resize down — optionally choose a smaller final size.
  5. Continue — emits save, then the component clears itself.

If an image exceeds maxPixels, MagicImage shrinks the working copy before editing.

The original file is never modified.


✅ Requirements

The component is fully client-side.


⚙️ Props

Prop Type Default Values Description
maxPixels number 16000000 — Images above this limit are shrunk for editing
outputFormat string 'png' png, jpg, webp Processed crop format
outputQuality number 100 1–100 Crop quality
clipboardFormat string 'png' png, jpg, webp Format for clipboard originals
clipboardQuality number 100 1–100 Clipboard re-encoding quality
cropMode string 'any' any, fixed, min Crop mode
cropRatio string '' 1, 1.5, 16/9, … Stencil ratio
cropX number — — Required width
cropY number — — Required height

Numbers are passed with a colon:

<MagicImage :crop-x="1200" />

Format notes

🟣 PNG has no quality setting.
PNG is lossless, so outputQuality and clipboardQuality do not affect PNG output.

⚫ JPEG has no transparency.
A transparent PNG exported as JPEG gets a black background. MagicImage does not add a white underlay.


🎛️ Tuning

MagicImage has three adjustment tabs.

☀️ Light

📊 Levels

✨ Sharpen

Each tab has its own Apply switch.

Until the switch is enabled, the tab affects neither preview nor output.

Controls


✂️ Crop modes

🆓 any

cropX and cropY are ignored.

With cropRatio, the stencil keeps the requested proportion. Without it, the stencil is free.

The result leaves at the current stencil size.

Resize down is available with no lower bound.


🎯 fixed

The crop is always scaled to the required width and computed height.

Nothing is enlarged.

The save action appears only when the stencil is large enough.

Resize down is not shown because the final size comes from the props.

Parameter normalization:

# cropRatio cropX cropY Result
1 ✅ ✅ ❌ main case
2 ✅ ✅ ✅ cropY is ignored
3 ❌ ✅ ✅ cropRatio = x / y, then cropY is ignored
4 ✅ ❌ ✅ cropX = cropY × ratio
5 ✅ ❌ ❌ invalid parameters

Everything ends up as:

ratio + X

Any other combination is invalid.


📐 min

Works like fixed, but the result is never squeezed down automatically.


📏 Resize down

Available in:

Not available in:

Type one side and the other follows the stencil ratio.

The fields do not silently correct the user while typing. Validation is handled by the Continue state.

Moving the stencil:


▶️ Continue and status

The Continue action is not disabled — it is hidden until the current state is valid.

The status line explains why.

✅ Ready to save

The stencil satisfies the current mode and Resize down values are valid.

❌ The stencil is smaller than required

For fixed or min, the stencil is below the required dimensions.

❌ The final size is out of range

Resize down is:

While the cropper is still loading, there is no status and no Continue action.


📡 Events

Event Payload Description
save { original, crop } Final files are ready
stage 'empty' | 'edit' | 'crop' Current component stage changed

save

{
  original: { file, width, height, format },
  crop:     { file, width, height, format },
}

original

The source file.

crop

The processed crop in outputFormat.

Both objects are always present.

Immediately after save, the component clears itself.


stage

Possible values:

empty
edit
crop

Example:

<MagicImage @stage="busy = $event !== 'empty'" @save="onSave" />

empty means the component is free and holds no image.

The event fires only on a change: the initial empty is not emitted. Start with the component considered free — busy = false — and let the event switch it.


🧩 Slots

actions

<template #actions="{ canSave, apply }">
  <button v-if="canSave" @click="apply()">Continue</button>
</template>

Scope:

Value Meaning
canSave true when the current crop and final size are valid
apply() performs the save action

Without the slot, MagicImage renders its own Continue button, shown only while canSave is true.

The status line remains visible either way.


⚠️ Errors

Errors are shown inside the component instead of the action buttons.

Invalid parameters: the component cannot work with them

The crop props form an unsupported combination.

The component does not accept an image until the props are fixed.

This picture cannot give the required size

Used by fixed and min when the working copy is smaller than the required size — narrower than cropX or lower than the computed height.

This may happen because:

This file could not be read as a picture

The image is broken or unsupported by the browser.

The browser could not encode the picture

canvas.toBlob() returned nothing, usually because browser canvas limits were exceeded.

There is no picture in the clipboard

Something was pasted into the paste field, but it was not an image — text, for example. Shown under the paste field; nothing is taken.

The crop is always produced from the working copy, not directly from the full-size original.


🏷️ File names

Source original crop
logo.jpg logo.jpg logo.<outputFormat>
clipboard clipBoard.<clipboardFormat> clipBoard.<outputFormat>

The names may match.

For example:

logo.png
logo.png

That is intentional because the files arrive as separate properties.


📋 Clipboard

Paste works only inside the dedicated paste field.

MagicImage does not listen to arbitrary Ctrl+V events elsewhere on the page.

The paste field exists only while the component is empty, so an open image cannot be replaced accidentally.

Clipboard images are usually already encoded as PNG.

If the clipboard MIME type already matches clipboardFormat, MagicImage does not re-encode it.

A canvas is created only when conversion is required.


🏗️ Architecture

src/
  MagicImage.vue        stages, input image, preview, save event
  TunePanel.vue         light, levels, sharpen, histogram
  CropStage.vue         crop stencil, sizes, Resize down
  lib/
    plan.js             formats, ratios and crop modes
    pixels.js           levels, sharpening, shrinking, histogram
    text.js             all component text
  assets/
    style.css           component styles, mi- prefix

test/
  plan.test.js          crop-mode cases

App.vue                 demo page
DemoParams.vue          demo controls
ShowResult.vue          last save result
InstallMagicImage.vue   install/example output
demoProps.js            demo-side prop definitions

MagicImage.vue

The orchestrator.

It owns:

It intentionally contains no crop arithmetic.

Session counter

Image decoding and encoding are asynchronous.

Every new image, cancel and unmount increments a session counter.

Late async results are discarded if they belong to an older session.

This prevents:

All data required for save is captured before asynchronous encoding starts.


TunePanel.vue

Receives through v-model:

{ light, levels, sharpen }

It never mutates the object directly. Every change emits a new object.

Each section contains its own on state — the tab's Apply switch.


CropStage.vue

Owns:

Reports:

and exposes:

getCanvas() → { canvas } | { error }

Canvas limits are validated against the actual generated canvas.


lib/plan.js

The single source of truth for:

It is fully covered by the crop-mode tests.


lib/pixels.js

Contains pixel operations:

It receives settings as arguments and knows nothing about Vue component state.

Disabled tuning sections are skipped.


🛠️ Development

npm install
npm run dev
npm run build
npm test
Command Purpose
npm run dev run the demo
npm run build build the demo into public/magic-image/
npm test run crop-mode tests in Node

vite.config.js exists only for the local demo.

Its paths currently point into MagicPro, where this component originated:

Outside MagicPro, those paths must be changed.

Inside MagicPro the component is imported by relative path:

admin/js/magic-image/src/MagicImage.vue

npm publish sends only:

src/
README.md
LICENSE

as defined by files in package.json.


💡 Philosophy

MagicImage is not trying to replace specialised image libraries.

Its job is narrower:

take the repetitive everyday browser workflow around an image and make it one reusable Vue component.

Upload it. Paste it. Tune it. Crop it. Return the files.

Nothing more is assumed.