"src" にファイルをアップロード
This commit is contained in:
220
src/script.js
Normal file
220
src/script.js
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user