const fs = require("fs"); const util = require("util"); const path = require("path"); const { read } = require("fast-exif"); const iptc = require("node-iptc"); const Vibrant = require("node-vibrant"); const chroma = require("chroma-js"); const chalk = require("chalk"); const R = require("ramda"); const readFile = util.promisify(fs.readFile); const badContrast = (color1, color2) => chroma.contrast(color1, color2) < 4.5; const logColorsWithContrast = (color1, color2, text) => { const c1hex = color1.hex(); const c2hex = color2.hex(); console.log( chalk.hex(c1hex).bgHex(c2hex)( `${text} ${c1hex}/${c2hex} ${chroma.contrast(color1, color2)}` ) ); }; function processColors(vibrantData, imagePath) { let Vibrant = chroma(vibrantData.Vibrant.getRgb()); let DarkVibrant = chroma(vibrantData.DarkVibrant.getRgb()); let LightVibrant = chroma(vibrantData.LightVibrant.getRgb()); let Muted = chroma(vibrantData.Muted.getRgb()); let DarkMuted = chroma(vibrantData.DarkMuted.getRgb()); let LightMuted = chroma(vibrantData.LightMuted.getRgb()); // first pass - darken bg and lighten relevant fg colors if ( badContrast(DarkVibrant, Vibrant) || badContrast(DarkVibrant, LightMuted) ) { DarkVibrant = DarkVibrant.darken(); if (badContrast(DarkVibrant, Vibrant)) { Vibrant = Vibrant.brighten(); } if (badContrast(DarkVibrant, Vibrant)) { Vibrant = Vibrant.brighten(); } } // second pass - first doesn't always do enough if ( badContrast(DarkVibrant, Vibrant) || badContrast(DarkVibrant, LightMuted) ) { // DarkVibrant = DarkVibrant.darken(); if (badContrast(DarkVibrant, Vibrant)) { Vibrant = Vibrant.brighten(2); } if (badContrast(DarkVibrant, LightMuted)) { LightMuted = LightMuted.brighten(2); } } if (badContrast(DarkVibrant, Vibrant)) { console.warn("contrast still too low", imagePath); logColorsWithContrast(Vibrant, DarkVibrant, "V-DV"); } if (badContrast(DarkVibrant, LightMuted)) { console.warn("contrast still too low", imagePath); logColorsWithContrast(LightMuted, DarkVibrant, "LM-DV"); } return { Vibrant: Vibrant.rgb(), DarkVibrant: DarkVibrant.rgb(), LightVibrant: LightVibrant.rgb(), Muted: Muted.rgb(), DarkMuted: DarkMuted.rgb(), LightMuted: LightMuted.rgb(), }; } function convertDMSToDD(dms, positiveDirection) { const res = dms .map((item, i) => { return item / Math.pow(60, i); }) .reduce((a, b) => a + b); return positiveDirection ? res : -res; } function transformMetaToNodeData(exifData, iptcData, vibrantData, imagePath) { const gps = { longitude: null, latitude: null }; if (exifData) { if (exifData.gps && exifData.gps.GPSLongitude && exifData.gps.GPSLatitude) { gps.longitude = convertDMSToDD( exifData.gps.GPSLongitude, exifData.gps.GPSLongitudeRef === "E" ); gps.latitude = convertDMSToDD( exifData.gps.GPSLatitude, exifData.gps.GPSLatitudeRef === "N" ); } } const vibrant = vibrantData ? processColors(vibrantData, imagePath) : null; return { exif: exifData?.exif, gps, dateTaken: exifData?.exif?.DateTimeOriginal, iptc: iptcData || undefined, vibrant, }; } exports.onCreateNode = async function ({ node, actions }) { const { createNodeField } = actions; if (node.internal.type === "File" && node.sourceInstanceName === "gallery") { const file = await readFile(node.absolutePath); const iptcData = iptc(file); const exifData = await read(node.absolutePath); const vibrantData = await Vibrant.from(node.absolutePath) .quality(3) .getPalette(); createNodeField({ node, name: "imageMeta", value: transformMetaToNodeData( exifData, iptcData, vibrantData, node.absolutePath ), }); } }; // Implement the Gatsby API “createPages”. This is called once the // data layer is bootstrapped to let plugins create pages from data. exports.createPages = async ({ graphql, actions, reporter }) => { const { createPage } = actions; // get all images const galleryImages = await graphql(` { allFile(filter: { sourceInstanceName: { eq: "gallery" } }) { edges { node { relativePath base fields { imageMeta { dateTaken } } } } } } `); // Handle errors if (galleryImages.errors) { reporter.panicOnBuild("Error while running GraphQL query."); return; } // Create pages for each markdown file. const galleryImageTemplate = path.resolve( "src/components/GalleryImage/GalleryImage.js" ); // const diffDate = (a, b) => // new Date(R.path(['node', 'childImageSharp', 'fields', 'imageMeta', 'dateTaken'], a)).getTime() - new Date(R.path(['node', 'childImageSharp', 'fields', 'imageMeta', 'dateTaken'],b)).getTime(); const edges = R.sort( R.descend( (edge) => new Date(R.path(["node", "fields", "imageMeta", "dateTaken"], edge)) ), galleryImages.data.allFile.edges ); edges.forEach(({ node }, index) => { const nextImage = index === edges.length - 1 ? null : edges[index + 1].node.base; const prevImage = index === 0 ? null : edges[index - 1].node.base; const page = { path: `photogallery/${node.base}`, component: galleryImageTemplate, context: { imageFilename: node.base, nextImage, prevImage, }, }; createPage(page); }); };