| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- // 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))
- }
|