import { createReadStream } from 'fs'; import { ExifImage, TYPE_NO_EXIF_SEGMENT, Exif, ExifError } from 'exif'; import * as marked from 'marked'; import * as slug from 'slug'; import nodeIptc = require('node-iptc'); import getImageSize = require('probe-image-size'); import { decode } from 'utf8'; import { pReadFile, sortAlphabetically } from './util'; import { GPS, LatLng, Dimensions, Orientation, Meta, PhotoMeta, Tag, } from './definitions/global'; const ORIENTATION_SQUARE = 'square'; const ORIENTATION_LANDSCAPE = 'landscape'; const ORIENTATION_PORTRAIT = 'portrait'; const NO_EXIF_SEGMENT: TYPE_NO_EXIF_SEGMENT = 'NO_EXIF_SEGMENT'; const isError = (err: ExifError) => err && err.code !== NO_EXIF_SEGMENT; const hasNoExifData = (err: ExifError) => err && err.code === NO_EXIF_SEGMENT; const toTagData = (tag: string): Tag => ({ slug: slug(tag), title: tag, }); const getExifData = async (filePath: string): Promise => new Promise((resolve, reject) => { const args = { image: filePath }; const extractor: ExifImage = new ExifImage( args, (err: any, data?: Exif) => { if (isError(err)) { reject({}); return; } const exif = hasNoExifData(err) ? extractor.exifData : data; resolve(exif); } ); }); const getIptcData = async (filePath: string): Promise => { try { const data = await pReadFile(filePath); return nodeIptc(data) || {}; } catch (e) { return {}; } }; const getDimensions = async (filePath: string): Promise => { const input = createReadStream(filePath); let orientation: Orientation = null; let size = null; try { size = await getImageSize(input); const { width, height } = size; if (width === height) { orientation = ORIENTATION_SQUARE; } if (width > height) { orientation = ORIENTATION_LANDSCAPE; } else { orientation = ORIENTATION_PORTRAIT; } } catch (e) {} // tslint:disable-line:no-empty input.destroy(); return { width: size.width, height: size.height, orientation, }; }; const getCreationDateFromString = (date: string): string => { const year = date.slice(0, 4); const month = date.slice(4, 6); const day = date.slice(6); return new Date(`${year}-${month}-${day}`) .toUTCString() .replace(/GMT.*$/, 'GMT'); }; const coordToDecimal = (gps: GPS): LatLng => { const latArr = gps.GPSLatitude; const lngArr = gps.GPSLongitude; if (!latArr || !lngArr) { return {}; } const latRef = gps.GPSLatitudeRef || 'N'; const lngRef = gps.GPSLongitudeRef || 'W'; const lat = (latArr[0] + latArr[1] / 60 + latArr[2] / 3600) * (latRef === 'N' ? 1 : -1); const lng = (lngArr[0] + lngArr[1] / 60 + lngArr[2] / 3600) * (lngRef === 'W' ? -1 : 1); return { lat, lng }; }; const getDetailsFromMeta = ( exif: Meta, iptc: Meta, dimensions: Dimensions ): PhotoMeta => ({ ...dimensions, title: decode(iptc.object_name || ''), tags: (iptc.keywords || []) .map(decode) .filter(Boolean) .sort(sortAlphabetically) .map(toTagData), description: marked(decode(iptc.caption || '')), createdAt: iptc.date_created ? getCreationDateFromString(iptc.date_created) : null, camera: exif.image.Model || '', lens: exif.exif.LensModel || '', iso: exif.exif.ISO ? Number(exif.exif.ISO) : null, aperture: exif.exif.FNumber ? exif.exif.FNumber.toFixed(1) : null, focalLength: exif.exif.FocalLength ? exif.exif.FocalLength.toFixed(1) : null, exposureTime: exif.exif.ExposureTime ? Number(exif.exif.ExposureTime) : null, flash: Boolean(exif.exif.Flash), gps: coordToDecimal(exif.gps), }); export default async (filePath: string): Promise => { const [exif, iptc, dimensions] = await Promise.all([ getExifData(filePath), getIptcData(filePath), getDimensions(filePath), ]); return getDetailsFromMeta(exif, iptc, dimensions); };