PLWL/src/icons.js

211 lines
7.5 KiB
JavaScript

'use strict'
const fs = require('fs')
const path = require('path')
function rvaToOffset(sections, rva) {
for (const s of sections) {
if (rva >= s.va && rva < s.va + (s.vs || s.rawSize)) return rva - s.va + s.raw
}
return null
}
function dirEntries(buf, dirOff) {
const named = buf.readUInt16LE(dirOff + 12)
const ids = buf.readUInt16LE(dirOff + 14)
const out = []
for (let i = 0; i < named + ids; i++) {
const e = dirOff + 16 + i * 8
const nameOrId = buf.readUInt32LE(e)
const value = buf.readUInt32LE(e + 4)
out.push({ id: nameOrId & 0x7fffffff, isName: (nameOrId & 0x80000000) !== 0, value, isDir: (value & 0x80000000) !== 0 })
}
return out
}
function parseIcons(buf) {
try {
if (buf.length < 64 || buf.readUInt16LE(0) !== 0x5a4d) return null
const peOff = buf.readUInt32LE(0x3c)
if (peOff + 24 > buf.length || buf.readUInt32LE(peOff) !== 0x00004550) return null
const numSections = buf.readUInt16LE(peOff + 6)
const optSize = buf.readUInt16LE(peOff + 20)
const optOff = peOff + 24
const magic = buf.readUInt16LE(optOff)
const resRva = magic === 0x20b ? buf.readUInt32LE(optOff + 128) : buf.readUInt32LE(optOff + 112)
if (!resRva) return []
const secOff = optOff + optSize
const sections = []
for (let i = 0; i < numSections; i++) {
const s = secOff + i * 40
sections.push({
va: buf.readUInt32LE(s + 12),
vs: buf.readUInt32LE(s + 8),
rawSize: buf.readUInt32LE(s + 16),
raw: buf.readUInt32LE(s + 20)
})
}
const rootOff = rvaToOffset(sections, resRva)
if (rootOff == null) return []
const typeEntry = dirEntries(buf, rootOff).find(e => e.id === 3 && e.isDir)
if (!typeEntry) return []
const iconDirOff = rvaToOffset(sections, resRva + (typeEntry.value & 0x7fffffff))
if (iconDirOff == null) return []
const iconDatas = []
for (const nameEntry of dirEntries(buf, iconDirOff)) {
if (!nameEntry.isDir) continue
const langDirOff = rvaToOffset(sections, resRva + (nameEntry.value & 0x7fffffff))
if (langDirOff == null) continue
const langEntry = dirEntries(buf, langDirOff)[0]
if (!langEntry || langEntry.isDir) continue
const dataEntryOff = rvaToOffset(sections, resRva + langEntry.value)
if (dataEntryOff == null) continue
const dataRva = buf.readUInt32LE(dataEntryOff)
const dataSize = buf.readUInt32LE(dataEntryOff + 4)
const dataOff = rvaToOffset(sections, dataRva)
if (dataOff == null || !dataSize) continue
iconDatas.push({ id: nameEntry.id, buf: Buffer.from(buf.subarray(dataOff, dataOff + dataSize)) })
}
if (!iconDatas.length) return []
const groupEntry = dirEntries(buf, rootOff).find(e => e.id === 14 && e.isDir)
if (groupEntry) {
const grpDirOff = rvaToOffset(sections, resRva + (groupEntry.value & 0x7fffffff))
if (grpDirOff != null) {
for (const nameEntry of dirEntries(buf, grpDirOff)) {
if (!nameEntry.isDir) continue
const langDirOff = rvaToOffset(sections, resRva + (nameEntry.value & 0x7fffffff))
if (langDirOff == null) continue
const langEntry = dirEntries(buf, langDirOff)[0]
if (!langEntry || langEntry.isDir) continue
const dataEntryOff = rvaToOffset(sections, resRva + langEntry.value)
if (dataEntryOff == null) continue
const dataRva = buf.readUInt32LE(dataEntryOff)
const dataSize = buf.readUInt32LE(dataEntryOff + 4)
const dataOff = rvaToOffset(sections, dataRva)
if (dataOff == null || !dataSize) continue
const grp = buf.subarray(dataOff, dataOff + dataSize)
if (grp.length < 6) continue
const count = grp.readUInt16LE(4)
const best = { area: -1, buf: null }
for (let i = 0; i < count; i++) {
const e = 6 + i * 14
const w = grp.readUInt8(e) || 256
const h = grp.readUInt8(e + 1) || 256
const id = grp.readUInt16LE(e + 12)
const icon = iconDatas.find(ic => ic.id === id)
if (icon && w * h > best.area) {
best.area = w * h
best.buf = icon.buf
}
}
if (best.buf) return [best.buf]
}
}
}
return [iconDatas.reduce((a, b) => (b.buf.length > a.buf.length ? b : a)).buf]
} catch {
return null
}
}
function pngSize(img) {
if (img.length > 24 && img.readUInt32BE(0) === 0x89504e47) {
return { w: img.readUInt32BE(16), h: img.readUInt32BE(20) }
}
return null
}
// RT_ICON payloads are BITMAPINFOHEADER bitmaps; read real dimensions from
// there (ICONDIRENTRY-style first bytes yield garbage).
function bmpDims(img) {
if (img.length < 12) return null
const biSize = img.readUInt32LE(0)
if (biSize === 12) {
// BITMAPCOREHEADER (old icons): WORD width/height/planes/bitcount
const w = img.readUInt16LE(4)
const h = Math.floor(img.readUInt16LE(6) / 2)
const planes = img.readUInt16LE(8) || 1
const bpp = img.readUInt16LE(10) || 32
const colors = bpp <= 8 ? 1 << bpp : 0
if (w > 0 && h > 0 && w <= 1024 && h <= 1024) return { w, h, planes, bpp, colors }
return null
}
if (biSize >= 40 && biSize <= 124 && img.length > 40) {
const w = img.readUInt32LE(4)
const h = Math.floor(img.readUInt32LE(8) / 2)
const planes = img.readUInt16LE(12) || 1
const bpp = img.readUInt16LE(14) || 32
const colors = img.readUInt32LE(32) || (bpp <= 8 ? 1 << bpp : 0)
if (w > 0 && h > 0 && w <= 1024 && h <= 1024) return { w, h, planes, bpp, colors }
}
return null
}
function buildIco(images) {
const header = Buffer.alloc(6)
header.writeUInt16LE(0, 0)
header.writeUInt16LE(1, 2)
header.writeUInt16LE(images.length, 4)
const metas = []
const datas = []
let offset = 6 + images.length * 16
for (const img of images) {
let w, h, colors = 0, planes = 1, bpp = 32
if (img.readUInt32BE(0) === 0x89504e47) {
const dim = pngSize(img) || { w: 256, h: 256 }
w = dim.w
h = dim.h
} else {
const dims = bmpDims(img)
if (dims) {
w = dims.w
h = dims.h
colors = dims.colors
planes = dims.planes
bpp = dims.bpp
} else {
// legacy fallback: ICONDIRENTRY-style first bytes (PNG-era heuristic)
w = img.readUInt8(0) || 256
h = img.readUInt8(1) || 256
colors = img.readUInt8(2)
planes = img.length > 4 ? img.readUInt16LE(4) || 1 : 1
bpp = img.length > 6 ? img.readUInt16LE(6) || 32 : 32
}
}
const meta = Buffer.alloc(16)
meta.writeUInt8(w >= 256 ? 0 : w, 0)
meta.writeUInt8(h >= 256 ? 0 : h, 1)
// 0 in the ICO dir entry means "256+ colors" (palette implied by bpp)
meta.writeUInt8(colors >= 256 ? 0 : colors, 2)
meta.writeUInt8(0, 3)
meta.writeUInt16LE(planes, 4)
meta.writeUInt16LE(bpp, 6)
meta.writeUInt32LE(img.length, 8)
meta.writeUInt32LE(offset, 12)
metas.push(meta)
datas.push(img)
offset += img.length
}
return Buffer.concat([header, ...metas, ...datas])
}
function extractIconFromExe(exePath, outPath) {
try {
const buf = fs.readFileSync(exePath)
const icon = parseIcons(buf)
if (!icon || !icon.length) return { ok: false, error: 'no-icon' }
const ico = buildIco(icon)
fs.mkdirSync(path.dirname(outPath), { recursive: true })
fs.writeFileSync(outPath, ico)
return { ok: true, path: outPath }
} catch (err) {
return { ok: false, error: String(err && err.message || err) }
}
}
module.exports = { extractIconFromExe, parseIcons, buildIco }