diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..f59038f --- /dev/null +++ b/src/script.js @@ -0,0 +1,220 @@ +// ─── イベント ───────────────────────────────────────── +const dz = document.getElementById('dropzone'); +const input = document.getElementById('fileInput'); + +let currentImageUrl = null; + +dz.addEventListener('click', () => input.click()); + +input.addEventListener('change', e => handleFile(e.target.files[0])); + +dz.addEventListener('dragover', e => { + e.preventDefault(); + dz.classList.add('over'); +}); + +dz.addEventListener('dragleave', () => { + dz.classList.remove('over'); +}); + +dz.addEventListener('drop', e => { + e.preventDefault(); + dz.classList.remove('over'); + handleFile(e.dataTransfer.files[0]); +}); + +// ─── メイン ───────────────────────────────────────── +function handleFile(file) { + if (!file) return; + + // ★ 安全チェック + if (!file.type.startsWith("image/")) { + alert("画像ファイルを選択してください"); + return; + } + + // ★ 前のURL解放(メモリ対策) + if (currentImageUrl) { + URL.revokeObjectURL(currentImageUrl); + } + + currentImageUrl = URL.createObjectURL(file); + + const reader = new FileReader(); + reader.onload = e => { + const u8 = new Uint8Array(e.target.result); + parse(file, u8, currentImageUrl); + }; + reader.readAsArrayBuffer(file); +} + +function parse(file, u8, imageUrl) { + const result = { + basic: { + fileName: file.name, + fileSize: formatBytes(file.size), + fileType: file.type, + lastModified: new Date(file.lastModified).toLocaleString("ja-JP") + }, + png: {}, + xmp: {}, + vrc: [] + }; + + // PNG判定 + if (u8[0] === 0x89 && u8[1] === 0x50) { + parsePNG(u8, result); + } + + render(result, imageUrl); +} + +// ─── XMPパーサ ───────────────────────────────────── +function readXMP(xmlStr, result) { + result.xmp = {}; + + let doc; + try { + doc = new DOMParser().parseFromString(xmlStr, 'text/xml'); + } catch (e) { return; } + + const MAP = { + "xmp:CreatorTool": "作成ツール", + "xmp:Author": "作者", + "xmp:CreateDate": "作成日時", + "xmp:ModifyDate": "更新日時", + "tiff:DateTime": "撮影日時", + "vrc:WorldID": "ワールドID", + "vrc:WorldDisplayName": "ワールド名", + "vrc:AuthorID": "作者ID", + }; + + function extractText(el) { + const li = el.querySelectorAll('li'); + if (li.length) return Array.from(li).map(l => l.textContent.trim()).join(', '); + return el.textContent.trim(); + } + + const descs = doc.querySelectorAll('Description'); + + descs.forEach(desc => { + Array.from(desc.children).forEach(child => { + const key = child.prefix ? `${child.prefix}:${child.localName}` : child.localName; + let val = extractText(child); + if (!val) return; + + if (key.includes("Date")) { + val = new Date(val).toLocaleString("ja-JP"); + } + + const label = MAP[key] || key; + + result.xmp[label] = val; + + if (key.startsWith("vrc:")) { + result.vrc.push({ key: label, value: val }); + } + }); + }); +} + +// ─── PNGパーサ(iTXt完全対応) ───────────────────── +function parsePNG(u8, result) { + let off = 8; + + while (off < u8.length - 8) { + const len = (u8[off]<<24)|(u8[off+1]<<16)|(u8[off+2]<<8)|u8[off+3]; + const type = String.fromCharCode(u8[off+4],u8[off+5],u8[off+6],u8[off+7]); + + // IHDR + if (type === 'IHDR') { + result.png['幅'] = ((u8[off+8]<<24)|(u8[off+9]<<16)|(u8[off+10]<<8)|u8[off+11]) + 'px'; + result.png['高さ'] = ((u8[off+12]<<24)|(u8[off+13]<<16)|(u8[off+14]<<8)|u8[off+15]) + 'px'; + } + + // テキストチャンク + if (type === 'tEXt' || type === 'iTXt' || type === 'zTXt') { + const chunk = u8.slice(off+8, off+8+len); + const nullIdx = chunk.indexOf(0); + + if (nullIdx > 0) { + const key = String.fromCharCode(...chunk.slice(0, nullIdx)); + let val = ''; + + if (type === 'iTXt') { + let p = nullIdx + 1; + + const compressed = chunk[p]; p++; + p++; // compression method + + while (chunk[p] !== 0) p++; p++; + while (chunk[p] !== 0) p++; p++; + + const data = chunk.slice(p); + + if (compressed === 1) continue; + val = new TextDecoder().decode(data); + + } else { + const valStart = nullIdx + 1; + val = new TextDecoder().decode(chunk.slice(valStart)); + } + + if (key === 'XML:com.adobe.xmp' || key === 'xmp') { + readXMP(val, result); + } else { + result.png[key] = val; + } + } + } + + if (type === 'IEND') break; + off += 12 + len; + } +} + +// ─── 表示 ───────────────────────────────────────── +function render(result, imageUrl) { + const el = document.getElementById('results'); + + let html = ` +

${result.basic.fileName}

+ +
+ +
+ `; + + // 基本情報 + html += `

基本情報

`; + Object.entries(result.basic).forEach(([k,v])=>{ + html += `
${k}: ${v}
`; + }); + + // VRC + if (result.vrc.length) { + html += `

VRChat

`; + result.vrc.forEach(v=>{ + html += `
${v.key}: ${v.value}
`; + }); + } + + // XMP + if (Object.keys(result.xmp).length) { + html += `

XMP

`; + Object.entries(result.xmp).forEach(([k,v])=>{ + html += `
${k}: ${v}
`; + }); + } + + el.innerHTML = html; +} + +// ─── util ───────────────────────────────────────── +function formatBytes(b){ + const u=['B','KB','MB']; + let i=0; + while(b>=1024 && i<2){b/=1024;i++;} + return b.toFixed(1)+' '+u[i]; +} \ No newline at end of file