콘텐츠로 이동
Capture실행 예제

정적 HTML 단일 출력

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

이 예제는 사이트의 Astro 컴포넌트 안에서 공개 패키지 루트의 capture와 CaptureError를 사용합니다. 버튼은 정적 대상 밖에 있으며, 자원 정책은 기본 deny를 유지합니다. Chrome에서 실행하고 형식과 크기를 바꾼 뒤 이미지 만들기를 선택합니다. SVG 용지는 mm, 래스터 용지는 300 PPI의 px로 표시합니다.

comins-capture@0.0.1

Chrome에서 확인한 정적 HTML 예제입니다. WebP는 네이티브 인코더가 필요하며 Safari는 정식 지원 범위에서 제외됩니다.

COMINS CAPTURE

함께 만드는 문서

브라우저에서 이미지로 저장합니다.

Static HTML · 320 × 240

출력할 형식과 크기를 선택합니다.

새 실행·실패·초기화·페이지 종료 시 이전 Blob URL을 해제하고 결과를 지웁니다. 작업 중 취소를 제공하며 성공·실패 모두 snapshot을 dispose합니다. WebP 인코더 미지원은 FORMAT_UNSUPPORTED로 표시합니다. Native Safari는 알려진 그림자·clip 실패로 지원에서 제외합니다.

사이트와 같은 Astro 프로젝트에 npm install --ignore-scripts comins-capture@0.0.1로 설치하고 아래 파일을 페이지에서 import합니다. 별도 CSS·프레임워크 peer 패키지는 필요하지 않습니다. 일반 HTML/JavaScript 앱은 빠른 시작의 완전한 예제를 사용합니다.

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>

출력 옵션 · API · 지원 범위