정적 HTML 단일 출력
이 예제는 사이트의 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 앱은 빠른 시작의 완전한 예제를 사용합니다.
---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>