You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

484 lines
16 KiB

  1. const fs = require('fs-extra');
  2. const imageTypes = ['PNG', 'GIF', 'BMP', 'JPEG', 'TIFF'];
  3. const videoTypes = ['AVI', 'MP4', 'MOV', 'MKV', 'WEBM', 'OGV'];
  4. const soundTypes = ['OGG', 'WAV', 'MP3', 'FLAC'];
  5. const read = async (path, offset, length) => {
  6. const fp = await fs.open(path);
  7. const buff = Buffer.alloc(length);
  8. await fs.read(fd, buff, 0, length, offset);
  9. await fs.close();
  10. return buff;
  11. };
  12. const parser = {
  13. PNG: async (path, result) => {
  14. const buff = await read(path, 18, 6);
  15. result.width = buff.readUInt16BE(0);
  16. result.height = buff.readUInt16BE(4);
  17. },
  18. GIF: async (path, result) => {
  19. const buff = await read(path, 6, 4);
  20. result.width = buff.readUInt16LE(0);
  21. result.height = buff.readUInt16LE(2);
  22. },
  23. BMP: async (path, result) => {
  24. const buff = await read(path, 18, 8);
  25. result.width = buff.readUInt32LE(0);
  26. result.height = buff.readUInt32LE(4);
  27. },
  28. JPEG: async (path, result) => {
  29. const sof = [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf, 0xde];
  30. const buff = await read(path, 2, 64000);
  31. let pos = 0;
  32. while (buff[pos++] == 0xff) {
  33. let marker = buff[pos++];
  34. let size = buff.readUInt16BE(pos);
  35. if (marker == 0xda) break;
  36. if (sof.includes(marker)) {
  37. result.height = buff.readUInt16BE(pos + 3);
  38. result.width = buff.readUInt16BE(pos + 5);
  39. break;
  40. }
  41. pos += size;
  42. }
  43. },
  44. TIFF: async (path, result) => {
  45. const buff = await read(path, 0, 64000);
  46. const le = buff.toString('ascii', 0, 2) == 'II';
  47. let pos = 0;
  48. const readUInt16 = () => { pos += 2; return buff[le ? 'readUInt16LE' : 'readUInt16BE'](pos - 2); }
  49. const readUInt32 = () => { pos += 4; return buff[le ? 'readUInt32LE' : 'readUInt32BE'](pos - 4); }
  50. let offset = readUInt32();
  51. while (pos < buff.length && offset > 0) {
  52. let entries = readUInt16(offset);
  53. let start = pos;
  54. for (let i = 0; i < entries; i++) {
  55. let tag = readUInt16();
  56. let type = readUInt16();
  57. let length = readUInt32();
  58. let data = (type == 3) ? readUInt16() : readUInt32();
  59. if (type == 3) pos += 2;
  60. if (tag == 256) {
  61. result.width = data;
  62. } else if (tag == 257) {
  63. result.height = data;
  64. }
  65. if (result.width > 0 && result.height > 0) {
  66. return;
  67. }
  68. }
  69. offset = readUInt32();
  70. pos += offset;
  71. }
  72. },
  73. AVI: async (path, result) => {
  74. const buff = await read(path, 0, 144);
  75. result.width = buff.readUInt32LE(64);
  76. result.height = buff.readUInt32LE(68);
  77. result.duration = ~~(buff.readUInt32LE(128) / buff.readUInt32LE(132) * buff.readUInt32LE(140));
  78. },
  79. MP4: async (path, result) => {
  80. return parser.MOV(path, result);
  81. },
  82. MOV: async (path, result, pos = 0) => {
  83. const buff = await read(path, 0, 64000);
  84. while (pos < buff.length) {
  85. let size = buff.readUInt32BE(pos);
  86. let name = buff.toString('ascii', pos + 4, 4);
  87. if (name == 'mvhd') {
  88. let scale = buff.readUInt32BE(pos + 20);
  89. let duration = buff.readUInt32BE(pos + 24);
  90. result.duration = ~~(duration / scale);
  91. }
  92. if (name == 'tkhd') {
  93. let m0 = buff.readUInt32BE(pos + 48);
  94. let m4 = buff.readUInt32BE(pos + 64);
  95. let w = buff.readUInt32BE(pos + 84);
  96. let h = buff.readUInt32BE(pos + 88);
  97. if (w > 0 && h > 0) {
  98. result.width = w / m0;
  99. result.height = h / m4;
  100. return;
  101. }
  102. }
  103. if (name == 'moov' || name == 'trak') {
  104. await parser.MOV(path, pos + 8);
  105. }
  106. pos += size;
  107. }
  108. },
  109. WEBM: async (path, result) => {
  110. return parser.EBML(path, result);
  111. },
  112. MKV: async (path, result) => {
  113. return parser.EBML(path, result);
  114. },
  115. EBML: async (path, result) => {
  116. const containers = ['\x1a\x45\xdf\xa3', '\x18\x53\x80\x67', '\x15\x49\xa9\x66', '\x16\x54\xae\x6b', '\xae', '\xe0'];
  117. const buff = await read(path, 0, 64000);
  118. // TODO parse EBML
  119. },
  120. OGV: async (path, result) => {
  121. return parser.OGG(path, result);
  122. },
  123. OGG: async (path, result) => {
  124. const buff = await read(apth, 0, 64000);
  125. let pos = 0, vorbis;
  126. while (buff.toString('ascii', pos, pos + 4) == 'OggS') {
  127. let version = buff[pos + 4];
  128. let b = buff[pos + 5];
  129. let continuation = !!(b & 0x01);
  130. let bos = !!(b & 0x02);
  131. let eos = !!(b & 0x04);
  132. let position = Number(buff.readBigUInt64LE(pos + 6));
  133. let serial = buff.readUInt32LE(pos + 14);
  134. let pageNumber = buff.readUInt32LE(pos + 18);
  135. let checksum = buff.readUInt32LE(pos + 22);
  136. let pageSegments = buff[path + 26];
  137. let lacing = buff.slice(pos + 27, pos + 27 + pageSegments);
  138. let pageSize = lacing.reduce((p, v) => p + v, 0);
  139. let start = pos + 27 + pageSegments;
  140. let pageHeader = buff.slice(start, start + 7);
  141. if (pageHeader.compare(Buffer.from([0x01, 'v', 'o', 'r', 'b', 'i', 's']))) {
  142. vorbis = { serial, sampleRate: buff.readUInt32LE(start + 12) };
  143. }
  144. if (pageHeader.compare(Buffer.from([0x80, 't', 'h', 'e', 'o', 'r', 'a']))) {
  145. let version = buff.slice(start + 7, start + 10);
  146. result.width = buff.readUInt16BE(start + 10) << 4;
  147. result.height = buff.readUInt16BE(start + 12) << 4;
  148. if (version >= 0x030200) {
  149. let width = buff.slice(start + 14, start + 17);
  150. let height = buff.slice(start + 17, start + 20);
  151. if (width <= result.width && width > result.width - 16 && height <= result.height && height > result.height - 16) {
  152. result.width = width;
  153. result.height = height;
  154. }
  155. }
  156. }
  157. if (eos && vorbis && serail == vorbis.serial) {
  158. result.duration = ~~(position / vorbis.sampleRate);
  159. }
  160. pos = start + pageSize;
  161. }
  162. },
  163. WAV: async (path, result) => {
  164. const buff = await read(path, 0, 32);
  165. let size = buff.readUInt32LE(4);
  166. let rate = buff.readUInt32LE(28);
  167. result.duration = ~~(size / rate);
  168. },
  169. MP3: async (path, result) => {
  170. const versions = [2.5, 0, 2, 1];
  171. const layers = [0, 3, 2, 1];
  172. const bitrates = [
  173. [ // version 2.5
  174. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  175. [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
  176. [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
  177. [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1
  178. ],
  179. [ // reserved
  180. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  181. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  182. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  183. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // reserved
  184. ],
  185. [ // version 2
  186. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  187. [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
  188. [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
  189. [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1
  190. ],
  191. [ // version 1
  192. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
  193. [0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320], // layer 3
  194. [0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384], // layer 2
  195. [0,32,64,96,128,160,192,224,256,288,320,352,384,416,448] // layer 1
  196. ]
  197. ]
  198. const srates = [
  199. [11025, 12000, 8000, 0], // mpeg 2.5
  200. [ 0, 0, 0, 0], // reserved
  201. [22050, 24000, 16000, 0], // mpeg 2
  202. [44100, 48000, 32000, 0] // mpeg 1
  203. ]
  204. const tsamples = [
  205. [0, 576, 1152, 384], // mpeg 2.5
  206. [0, 0, 0, 0], // reserved
  207. [0, 576, 1152, 384], // mpeg 2
  208. [0, 1152, 1152, 384] // mpeg 1
  209. ];
  210. const slotSizes = [0, 1, 1, 4];
  211. const modes = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
  212. const buff = await read(path, 0, 64000);
  213. let duration = 0;
  214. let count = 0;
  215. let skip = 0;
  216. let pos = 0;
  217. while (pos < buff.length) {
  218. let start = pos;
  219. if (buff.toString('ascii', pos, 4) == 'TAG+') {
  220. skip += 227;
  221. pos += 227;
  222. } else if (buff.toString('ascii', pos, 3) == 'TAG') {
  223. skip += 128;
  224. pos += 128;
  225. } else if (buff.toString('ascii', pos, 3) == 'ID3') {
  226. let bytes = buff.readUInt32BE(pos + 6);
  227. let size = 10 + (bytes[0] << 21 | bytes[1] << 14 | bytes[2] << 7 | bytes[3]);
  228. skip += size;
  229. pos += size;
  230. } else {
  231. let hdr = buff.slice(pos, pos + 4);
  232. while (pos < buff.length && !(hdr[0] == 0xff && (hdr[1] & 0xe0) == 0xe0)) {
  233. pos++;
  234. hdr = buff.slice(pos, pos + 4);
  235. }
  236. let ver = (hdr[1] & 0x18) >> 3;
  237. let lyr = (hdr[1] & 0x06) >> 1;
  238. let pad = (hdr[2] & 0x02) >> 1;
  239. let brx = (hdr[2] & 0xf0) >> 4;
  240. let srx = (hdr[2] & 0x0c) >> 2;
  241. let mdx = (hdr[3] & 0xc0) >> 6;
  242. let version = versions[ver];
  243. let layer = layers[lyr];
  244. let bitrate = bitrates[ver][lyr][brx] * 1000;
  245. let samprate = srates[ver][srx];
  246. let samples = tsamples[ver][lyr];
  247. let slotSize = slotSizes[lyr];
  248. let mode = modes[mdx];
  249. let fsize = ~~(((samples / 8 * bitrate) / samprate) + (pad ? slotSize : 0));
  250. count++;
  251. if (count == 1) {
  252. if (layer != 3) {
  253. pos += 2;
  254. } else {
  255. if (mode != 'mono') {
  256. if (version == 1) {
  257. pos += 32;
  258. } else {
  259. pos += 17;
  260. }
  261. } else {
  262. if (version == 1) {
  263. pos += 17;
  264. } else {
  265. pos += 9;
  266. }
  267. }
  268. }
  269. if (buff.toString('ascii', pos, pos + 4) == 'Xing' && (buff.readUInt32BE(pos + 4) & 0x0001) == 0x0001) {
  270. let totalFrames = buff.readUInt32BE(pos + 8);
  271. duration = totalFrames * samples / samprate;
  272. break;
  273. }
  274. }
  275. if (fsize < 1) break;
  276. pos = start + fsize;
  277. duration += (samples / samprate);
  278. }
  279. }
  280. result.duration = ~~duration;
  281. },
  282. FLAC: async (path, result) => {
  283. const buff = await read(path, 18, 8);
  284. let rate = (buff[0] << 12) | (buff[1] << 4) | ((buff[2] & 0xf0) >> 4);
  285. let size = ((buff[3] & 0x0f) << 32) | (buff[4] << 24) | (buff[5] << 16) | (buff[6] << 8) | buff[7];
  286. result.duration = ~~(size / rate);
  287. },
  288. };
  289. async function detect(path) {
  290. const buff = await read(path, 0, 12);
  291. if (buff.slice(0, 8).compare(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
  292. return 'PNG';
  293. }
  294. if (buff.toString('ascii', 0, 3) == 'GIF') {
  295. return 'GIF';
  296. }
  297. if (buff.toString('ascii', 0, 2) == 'BM') {
  298. return 'BMP';
  299. }
  300. if (buff.slice(0, 2).compare(Buffer.from([0xff, 0xd8]))) {
  301. return 'JPEG';
  302. }
  303. if (buff.toString('ascii', 0, 2) == 'II' && buff.readUInt16LE(2) == 42) {
  304. return 'TIFF';
  305. }
  306. if (buff.toString('ascii', 0, 2) == 'MM' && buff.readUInt16BE(2) == 42) {
  307. return 'TIFF';
  308. }
  309. if (buff.toString('ascii', 0, 4) == 'RIFF' && buff.toString('ascii', 8, 4) == 'AVI ') {
  310. return 'AVI';
  311. }
  312. if (buff.toString('ascii', 4, 4) == 'ftyp') {
  313. return 'MP4';
  314. }
  315. if (buff.toString('ascii', 4, 4) == 'moov') {
  316. return 'MOV';
  317. }
  318. if (buff.slice(0, 4).compare(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]))) {
  319. // TODO detect MKV
  320. return 'EBML';
  321. }
  322. if (buff.toString('ascii', 0, 4) == 'OggS') {
  323. // TODO detect OGV
  324. return 'OGG'
  325. }
  326. if (buff.toString('ascii', 0, 4) == 'RIFF', buff.toString('ascii', 8, 4) == 'WAVE') {
  327. return 'WAV';
  328. }
  329. if (buff.toString('ascii', 0, 3) == 'ID3' || (buf[0] == 0xff && (buff[1] & 0xe0))) {
  330. return 'MP3';
  331. }
  332. if (buff.toString('ascii', 0, 4) == 'fLaC') {
  333. return 'FLAC';
  334. }
  335. return null
  336. }
  337. module.exports = {
  338. detect: async function(options) {
  339. let path = this.parseRequired(options.path, 'string', 'metadata.detect: path is required.');
  340. return detect(path);
  341. },
  342. isImage: async function(options) {
  343. let path = this.parseRequired(options.path, 'string', 'metadata.isImage: path is required.');
  344. let type = await detect(path);
  345. let cond = imageTypes.includes(type);
  346. if (cond) {
  347. if (options.then) {
  348. await this.exec(options.then, true);
  349. }
  350. } else if (options.else) {
  351. await this.exec(options.else, true);
  352. }
  353. return cond;
  354. },
  355. isVideo: async function(options) {
  356. let path = this.parseRequired(options.path, 'string', 'metadata.isVideo: path is required.');
  357. let type = await detect(path);
  358. let cond = videoTypes.includes(type);
  359. if (cond) {
  360. if (options.then) {
  361. await this.exec(options.then, true);
  362. }
  363. } else if (options.else) {
  364. await this.exec(options.else, true);
  365. }
  366. return cond;
  367. },
  368. isSound: async function(options) {
  369. let path = this.parseRequired(options.path, 'string', 'metadata.isSound: path is required.');
  370. let type = await detect(path);
  371. let cond = soundTypes.includes(type);
  372. if (cond) {
  373. if (options.then) {
  374. await this.exec(options.then, true);
  375. }
  376. } else if (options.else) {
  377. await this.exec(options.else, true);
  378. }
  379. return cond;
  380. },
  381. fileinfo: async function(options) {
  382. let path = this.parseRequired(options.path, 'string', 'metadata.fileinfo: path is required.');
  383. let type = await detect(path);
  384. let result = { type, width: null, height: null, duration: null };
  385. if (parser[type]) {
  386. await parser[type](path, result);
  387. }
  388. return result;
  389. },
  390. };