// Identify the untouched local sources with bounded memory: // 1) raster header (resolution / extent) without loading the file, // 2) attribute text inside the large geodatabase tables. import { openSync, readSync, closeSync, statSync } from 'node:fs' function tiffSummary(path) { const size = statSync(path).size const fd = openSync(path, 'r') const head = Buffer.alloc(8) readSync(fd, head, 0, 8, 0) const little = head.toString('latin1', 0, 2) === 'II' const ifdOffset = little ? head.readUInt32LE(4) : head.readUInt32BE(4) const countBuf = Buffer.alloc(2) readSync(fd, countBuf, 0, 2, ifdOffset) const count = little ? countBuf.readUInt16LE(0) : countBuf.readUInt16BE(0) const entries = Buffer.alloc(count * 12) readSync(fd, entries, 0, entries.length, ifdOffset + 2) const NAMES = { 256: 'width', 257: 'height', 258: 'bits', 259: 'compression', 33550: 'pixelScale', 33922: 'tiepoint', 34735: 'geoKeys', 34737: 'geoAscii', 42112: 'gdaldoc', 42113: 'nodata' } const tags = {} const data = [] for (let i = 0; i < count; i++) { const base = i * 12 const tag = little ? entries.readUInt16LE(base) : entries.readUInt16BE(base) const type = little ? entries.readUInt16LE(base + 2) : entries.readUInt16BE(base + 2) const num = little ? entries.readUInt32LE(base + 4) : entries.readUInt32BE(base + 4) const sizes = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8 } const total = (sizes[type] || 1) * num if (!NAMES[tag]) continue if (total <= 4) { const raw = entries.subarray(base + 8, base + 12) tags[NAMES[tag]] = type === 3 ? (little ? raw.readUInt16LE(0) : raw.readUInt16BE(0)) : (little ? raw.readUInt32LE(0) : raw.readUInt32BE(0)) } else { const pointer = little ? entries.readUInt32LE(base + 8) : entries.readUInt32BE(base + 8) const payload = Buffer.alloc(Math.min(total, 4096)) readSync(fd, payload, 0, payload.length, pointer) if (type === 12) { const values = [] for (let v = 0; v + 8 <= payload.length && values.length < 16; v += 8) values.push(little ? payload.readDoubleLE(v) : payload.readDoubleBE(v)) tags[NAMES[tag]] = values } else if (type === 2) { tags[NAMES[tag]] = payload.toString('latin1').replace(/\0+$/, '').slice(0, 300) } else if (type === 3) { const values = [] for (let v = 0; v + 2 <= payload.length && values.length < 64; v += 2) values.push(little ? payload.readUInt16LE(v) : payload.readUInt16BE(v)) tags[NAMES[tag]] = values } } } closeSync(fd) const scale = tags.pixelScale const tie = tags.tiepoint const resolution = scale ? Math.round(scale[0] * 1e6) / 1e6 : null return { sizeGB: Math.round((size / 1024 ** 3) * 100) / 100, littleEndian: little, width: tags.width, height: tags.height, bitsPerSample: tags.bits, compression: tags.compression, metresPerPixel: resolution, upperLeftLonLat: tie && scale ? [tie[3], tie[4]] : null, extentKm: width2(tags.width, resolution), } function width2(w, r) { return w && r ? [Math.round((w * r) / 100) / 10, Math.round((tags.height * r) / 100) / 10] : null } } const HINTS = /(建筑|房屋|砖混|砖木|框架|结构|层数|楼层|居民|住宅|楼房|厂房|building|floor|storey|structure)/ /** Scan a big table in chunks, keeping only strings that look like real attribute values. */ function scanTable(path, chunkSize = 4 * 1024 * 1024) { const size = statSync(path).size const fd = openSync(path, 'r') const buffer = Buffer.alloc(chunkSize) const hints = new Map() const chinese = new Map() let offset = 0 let carry = Buffer.alloc(0) while (offset < size) { const read = readSync(fd, buffer, 0, chunkSize, offset) if (read <= 0) break const data = carry.length ? Buffer.concat([carry, buffer.subarray(0, read)]) : buffer.subarray(0, read) for (let i = 0; i + 2 <= data.length; i += 2) { for (const [enc, readCode] of [['be', (k) => data.readUInt16BE(k)], ['le', (k) => data.readUInt16LE(k)]]) { let s = '', j = i while (j + 1 < data.length) { const code = readCode(j) if (code === 0 || code < 0x20 || code > 0xffe0) break s += String.fromCharCode(code); j += 2 if (s.length > 24) { s = ''; break } } if (s.length < 2 || !/[\u4e00-\u9fff]/.test(s)) continue if (HINTS.test(s)) hints.set(s, (hints.get(s) || 0) + 1) else if (chinese.size < 400) chinese.set(s, (chinese.get(s) || 0) + 1) } } carry = data.subarray(Math.max(0, data.length - 4)) offset += read } closeSync(fd) return { sizeMB: Math.round(size / 1024 / 1024), buildingHints: [...hints.entries()].sort((a, b) => b[1] - a[1]).slice(0, 30), sampleChinese: [...chinese.keys()].slice(0, 40), } } console.log('===== RASTER D:/GIS/ArcGIS/yuanling/yuanling20.tif') console.log(JSON.stringify(tiffSummary('D:/GIS/ArcGIS/yuanling/yuanling20.tif'), null, 1)) for (const name of ['a00000009', 'a0000000a', 'a0000000b', 'a0000000c', 'a0000000d']) { const path = `D:/GIS/ArcGIS/yuanling/DB/YUANLING.gdb/${name}.gdbtable` console.log(`\n===== GDB TABLE ${name}`) console.log(JSON.stringify(scanTable(path), null, 1)) }