Skip to content
CaptureExamples

Static HTML single output

comins-capture@0.0.1Vanilla · React · Vue · Svelte

This Astro component uses capture and CaptureError from the public package root. Controls stay outside the static target and resources retain the default deny policy. Run it in Chrome, choose a format and size, then create an image. SVG paper uses mm; raster paper uses pixels at 300 PPI.

comins-capture@0.0.1

A static HTML example verified in Chrome. WebP requires a native encoder; Safari is excluded from supported runtimes.

COMINS CAPTURE

A shared document

Save an image in your browser.

Static HTML · 320 × 240

Choose an output format and size.

A new run, failure, reset or page exit revokes the previous Blob URL and clears the result. Active work can be cancelled; snapshots are disposed on success and failure. Unsupported WebP encoders report FORMAT_UNSUPPORTED. Native Safari is excluded due to known shadow/clip failures.

In an Astro project, install npm install --ignore-scripts comins-capture@0.0.1 and import the component below from a page. No additional CSS or framework peers are required. Plain HTML/JavaScript applications can use the complete quick start instead.

CaptureDemo.astro
---
import { dependencies } from '../../../package.json';
interface Props { lang: 'ko' | 'en' }
const { lang } = Astro.props;
const ko = lang === 'ko';
---
<capture-demo data-language={lang} class="capture-demo">
<p><code>comins-capture@{dependencies['comins-capture']}</code></p>
<div class="capture-controls">
<label>{ko ? '형식' : 'Format'}<select data-format><option value="png">PNG</option><option value="jpeg">JPEG</option><option value="webp">WebP</option><option value="svg">SVG</option></select></label>
<label>{ko ? '출력 크기' : 'Output size'}<select data-size><option value="original">{ko ? '원본 1배' : 'Original 1×'}</option><option value="double">{ko ? '2배' : '2×'}</option><option value="custom">800 × 600</option><option value="a4">ISO A4 · 300 PPI</option><option value="jis">JIS B4 · {ko ? '가로' : 'landscape'}</option></select></label>
<button type="button" data-capture>{ko ? '이미지 만들기' : 'Create image'}</button>
<button type="button" data-cancel disabled>{ko ? '취소' : 'Cancel'}</button>
<button type="button" data-reset>{ko ? '초기화' : 'Reset'}</button>
</div>
<p>{ko ? 'Chrome에서 확인한 정적 HTML 예제입니다. WebP는 네이티브 인코더가 필요하며 Safari는 정식 지원 범위에서 제외됩니다.' : 'A static HTML example verified in Chrome. WebP requires a native encoder; Safari is excluded from supported runtimes.'}</p>
<div class="capture-source-scroll">
<section data-target style="box-sizing:border-box;width:320px;height:240px;padding:24px;background:#fff;color:#123b2d;font:16px/1.5 system-ui,sans-serif;border:1px solid #cbded3;border-radius:12px;box-shadow:0 8px 18px #123b2d18;">
<p style="margin:0 0 12px;font-size:12px;letter-spacing:2px;">COMINS CAPTURE</p>
<h3 style="margin:0 0 12px;font-size:24px;color:#123b2d;">{ko ? '함께 만드는 문서' : 'A shared document'}</h3>
<p style="margin:0 0 18px;">{ko ? '브라우저에서 이미지로 저장합니다.' : 'Save an image in your browser.'}</p>
<div style="height:16px;width:100%;border-radius:8px;background:#d6eee0;overflow:hidden;"><div style="width:68%;height:100%;background:#047857;"></div></div>
<p style="margin:12px 0 0;font-size:12px;">Static HTML · 320 × 240</p>
</section>
</div>
<p role="status" aria-live="polite" data-status>{ko ? '출력할 형식과 크기를 선택합니다.' : 'Choose an output format and size.'}</p>
<a data-download hidden>{ko ? '파일 저장' : 'Download file'}</a>
<img data-preview hidden alt={ko ? 'Capture로 생성한 출력 이미지' : 'Image generated by Capture'} />
</capture-demo>
<script>
import { capture, CaptureError, type BlobOptions, type CaptureSnapshot } from 'comins-capture';
class CaptureDemo extends HTMLElement {
cleanup?: () => void;
connectedCallback() {
if (this.cleanup) return;
const ko = this.dataset.language === 'ko';
const target = this.querySelector<HTMLElement>('[data-target]')!;
const format = this.querySelector<HTMLSelectElement>('[data-format]')!;
const size = this.querySelector<HTMLSelectElement>('[data-size]')!;
const run = this.querySelector<HTMLButtonElement>('[data-capture]')!;
const cancel = this.querySelector<HTMLButtonElement>('[data-cancel]')!;
const reset = this.querySelector<HTMLButtonElement>('[data-reset]')!;
const preview = this.querySelector<HTMLImageElement>('[data-preview]')!;
const download = this.querySelector<HTMLAnchorElement>('[data-download]')!;
const status = this.querySelector<HTMLElement>('[data-status]')!;
const listeners = new AbortController();
let pending: AbortController | undefined, snapshot: CaptureSnapshot | undefined, url: string | undefined;
const idle = ko ? '출력할 형식과 크기를 선택합니다.' : 'Choose an output format and size.';
const clear = () => {
preview.hidden = download.hidden = true;
preview.removeAttribute('src'); download.removeAttribute('href');
if (url) URL.revokeObjectURL(url);
url = undefined;
};
const busy = (value: boolean) => {
run.disabled = reset.disabled = format.disabled = size.disabled = value;
cancel.disabled = !value;
};
run.addEventListener('click', async () => {
if (pending) return;
clear(); busy(true);
const owner = new AbortController(); pending = owner;
status.textContent = ko ? '이미지를 생성하고 있습니다.' : 'Creating image.';
try {
snapshot = await capture(target, { signal: owner.signal });
const svg = format.value === 'svg';
// SVG에는 raster 전용 scale·PPI를 전달하지 않습니다.
const outputSize: BlobOptions['size'] = size.value === 'a4'
? (svg ? { kind: 'paper', name: 'A4' } : { kind: 'paper', name: 'A4', ppi: 300 })
: size.value === 'jis'
? (svg ? { kind: 'paper', name: 'B4', system: 'jis', orientation: 'landscape' } : { kind: 'paper', name: 'B4', system: 'jis', orientation: 'landscape', ppi: 300 })
: size.value === 'custom'
? { kind: svg ? 'css' : 'pixels', width: 800, height: 600 }
: svg ? { kind: 'css', width: size.value === 'double' ? 640 : 320, height: size.value === 'double' ? 480 : 240 }
: { kind: 'scale', value: size.value === 'double' ? 2 : 1 };
const options = { format: format.value, size: outputSize, background: '#ffffff', signal: owner.signal } as BlobOptions;
const { value, meta } = await snapshot.toBlob(options);
// 종료된 화면에 결과를 반영하거나 Blob URL을 남기지 않습니다.
if (owner.signal.aborted || !this.isConnected) return;
url = URL.createObjectURL(value);
preview.src = download.href = url;
download.download = `comins-capture.${format.value === 'jpeg' ? 'jpg' : format.value}`;
preview.hidden = download.hidden = false;
status.textContent = `${meta.width} × ${meta.height} ${meta.unit} · ${value.type}`;
} catch (error) {
clear();
if (this.isConnected) status.textContent = error instanceof CaptureError ? `${error.code} · ${error.stage}` : (ko ? '출력에 실패했습니다.' : 'Output failed.');
} finally {
snapshot?.dispose(); snapshot = undefined; pending = undefined;
if (this.isConnected) busy(false);
}
}, { signal: listeners.signal });
cancel.addEventListener('click', () => pending?.abort(), { signal: listeners.signal });
reset.addEventListener('click', () => { clear(); format.value = 'png'; size.value = 'original'; status.textContent = idle; }, { signal: listeners.signal });
const release = () => { pending?.abort(); snapshot?.dispose(); clear(); };
window.addEventListener('pagehide', release, { signal: listeners.signal });
this.cleanup = () => { listeners.abort(); release(); };
}
disconnectedCallback() { this.cleanup?.(); this.cleanup = undefined; }
}
if (!customElements.get('capture-demo')) customElements.define('capture-demo', CaptureDemo);
</script>
<style>
.capture-demo { display:block; border:1px solid #cbded3; border-radius:12px; padding:20px; margin-block:24px; color:#123b2d; background:#f4faf6; }
.capture-controls { display:flex; flex-wrap:wrap; align-items:end; gap:12px; }
label { display:grid; gap:6px; font-size:14px; }
select, button { font:inherit; min-height:40px; padding:8px 12px; border:1px solid #9ab9a8; border-radius:6px; background:white; color:#123b2d; }
button { cursor:pointer; } button:disabled { opacity:.55; cursor:default; }
:is(button,select,a):focus-visible { outline:2px solid #047857; outline-offset:3px; }
.capture-source-scroll { max-width:100%; overflow:auto; padding:10px 4px 24px; }
[data-status] { overflow-wrap:anywhere; }
[data-download] { display:inline-block; margin-bottom:16px; color:#047857; }
[data-preview] { display:block; max-width:100%; max-height:480px; height:auto; object-fit:contain; border:1px solid #cbded3; background:white; }
[hidden] { display:none; }
</style>

Output options · API · Browser limits