Personal-Website/gatsby-node.ts

300 lines
8.5 KiB
TypeScript

import type { GatsbyNode } from "gatsby";
import path from "path";
import Vibrant from "node-vibrant";
import chroma, { Color } from "chroma-js";
import chalk from "chalk";
import * as R from "ramda";
import exifr from "exifr";
import sharp from "sharp";
import { Palette } from "node-vibrant/lib/color";
import { performance } from "perf_hooks";
import util from "node:util";
import { exec as _exec } from "child_process";
const exec = util.promisify(_exec);
// const path = require("path");
// const Vibrant = require("node-vibrant");
// const chroma = require("chroma-js");
// const chalk = require("chalk");
// const R = require("ramda");
// const exifr = require("exifr");
// const sharp = require("sharp");
// const { graphql } = require("gatsby");
const badContrast = (color1: Color, color2: Color) =>
chroma.contrast(color1, color2) < 4.5;
const logColorsWithContrast = (color1: Color, color2: Color, text: string) => {
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: Palette, imagePath: string) {
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)) {
// Vibrant = Vibrant.brighten(2);
// }
// if (badContrast(DarkVibrant, LightMuted)) {
// LightMuted = LightMuted.brighten(2);
// }
// // only used for hover styles, so we should give it a shot but it's not a huge deal if it's not very legible
// if (badContrast(Muted, LightMuted)) {
// Muted = Muted.darken();
// }
// 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;
// }
// 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"
// );
// }
// }
function transformMetaToNodeData(
metaData: Record<string, unknown>,
vibrantData: Palette,
imagePath: string,
{ r, g, b }: { r: number; b: number; g: number },
datePublished: string
) {
const vibrant = vibrantData ? processColors(vibrantData, imagePath) : null;
const vibrantHue = vibrantData.Vibrant!.getHsl()[0] * 360;
let dominantHue = chroma(r, g, b).hsl();
if (isNaN(dominantHue[0])) {
dominantHue[0] = 0;
}
let Keywords = metaData.Keywords;
if (!Keywords) {
Keywords = [];
}
if (!Array.isArray(Keywords)) {
Keywords = [Keywords];
}
return {
dateTaken: metaData.DateTimeOriginal,
datePublished,
meta: {
Make: metaData.Make,
Model: metaData.Model,
ExposureTime: metaData.ExposureTime,
FNumber: metaData.FNumber,
ISO: metaData.ISO,
DateTimeOriginal: metaData.DateTimeOriginal,
CreateDate: metaData.CreateDate,
ModifyDate: metaData.ModifyDate,
ShutterSpeedValue: metaData.ShutterSpeedValue,
ApertureValue: metaData.ApertureValue,
FocalLength: metaData.FocalLength,
LensModel: metaData.LensModel,
ObjectName: metaData.ObjectName,
Caption: metaData.Caption,
City: metaData.City,
State: metaData.State,
Location: metaData.Location,
Rating: metaData.Rating,
Keywords,
},
vibrant,
vibrantHue,
dominantHue,
};
}
// exports.createSchemaCustomization = function ({ actions }) {
// const { createTypes } = actions;
// const typedefs = `
// type FileFieldsImageMetaMeta{
// Keywords: [String]
// }`;
// createTypes(typedefs);
// };
export const onCreateNode: GatsbyNode["onCreateNode"] = async function ({
node,
actions,
}) {
const { createNodeField } = actions;
if (node.internal.type === "File" && node.sourceInstanceName === "gallery") {
const { stdout: datePublished, stderr } = await exec(
`git log --diff-filter=A --follow --format=%aI -1 -- ${node.absolutePath}`
);
if (stderr.length) {
console.error("something went wrong checking publish date: ", stderr);
}
let metaData;
try {
metaData = await exifr.parse(node.absolutePath as string, {
iptc: true,
xmp: true,
// icc: true
});
} catch (e) {
console.error(`something wen wrong with exifr on image ${node.base}`, e);
throw e;
}
let sharpImage: sharp.Sharp;
try {
sharpImage = sharp(node.absolutePath as string);
} catch (e) {
console.error(`something wen wrong with sharp on image ${node.base}`, e);
throw e;
}
const { dominant } = await sharpImage.stats();
const resizedImage = await sharpImage
.resize({
width: 3000,
height: 3000,
fit: "inside",
})
.toBuffer();
const vibrantData = await Vibrant.from(resizedImage)
// .quality(1)
.getPalette();
createNodeField({
node,
name: "imageMeta",
value: transformMetaToNodeData(
metaData,
vibrantData,
node.absolutePath as string,
dominant,
// if datePublished is empty, image has not been committed to git yet and is thus brand new
datePublished.length ? datePublished.replace("\n", "") : new Date().toISOString()
),
});
}
};
// Implement the Gatsby API “createPages”. This is called once the
// data layer is bootstrapped to let plugins create pages from data.
export const createPages: GatsbyNode["createPages"] = async ({
graphql,
actions,
reporter,
}) => {
const { createPage } = actions;
const galleryImages = await graphql<Queries.GalleryImagesNodeQuery>(`
query GalleryImagesNode {
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);
});
};