"src" にファイルをアップロード

This commit is contained in:
2026-03-23 16:06:25 +00:00
parent 997f5ea4eb
commit 1c940ceb92

220
src/script.js Normal file
View File

@@ -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 = `
<h2>${result.basic.fileName}</h2>
<div style="margin-bottom:16px;">
<img src="${imageUrl}"
style="max-width:100%; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.2);" />
</div>
`;
// 基本情報
html += `<h3>基本情報</h3>`;
Object.entries(result.basic).forEach(([k,v])=>{
html += `<div>${k}: ${v}</div>`;
});
// VRC
if (result.vrc.length) {
html += `<h3>VRChat</h3>`;
result.vrc.forEach(v=>{
html += `<div>${v.key}: ${v.value}</div>`;
});
}
// XMP
if (Object.keys(result.xmp).length) {
html += `<h3>XMP</h3>`;
Object.entries(result.xmp).forEach(([k,v])=>{
html += `<div>${k}: ${v}</div>`;
});
}
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];
}