236 lines
5.9 KiB
JavaScript
236 lines
5.9 KiB
JavaScript
import { Resvg } from "@resvg/resvg-js";
|
|
import { promises } from "fs";
|
|
import { JSDOM } from "jsdom";
|
|
import * as d3 from "d3";
|
|
import yargs from "yargs/yargs";
|
|
import { hideBin } from "yargs/helpers";
|
|
import spawn from "await-spawn";
|
|
import random from "random";
|
|
|
|
const xmlns = "http://www.w3.org/2000/xmlns/";
|
|
const xlinkns = "http://www.w3.org/1999/xlink";
|
|
const svgns = "http://www.w3.org/2000/svg";
|
|
|
|
import {RADIUS_OPTS, RADIUS_DESC, DotMaker} from './src/components/dots.js';
|
|
import {PALETTES, ColourNamer} from './src/components/palettes.js';
|
|
|
|
const CELL = 10;
|
|
const MAG = 2;
|
|
const WIDTH = 20;
|
|
const HEIGHT = WIDTH;
|
|
const VISIBLE_DOG = 1000;
|
|
|
|
function randomise_params() {
|
|
const palette_name = random.choice(Array.from(PALETTES.keys()));
|
|
const palette_fn = PALETTES.get(palette_name);
|
|
const palette = palette_fn();
|
|
const patterns = [1,2].map((n) => { return {
|
|
i: n,
|
|
colour: palette[n],
|
|
m: random.choice([1, 2, 3, 4, 5]),
|
|
n: random.choice([1, 2, 3, 4, 5]),
|
|
f: random.choice(RADIUS_OPTS),
|
|
r: random.float(0, 0.4),
|
|
}});
|
|
return {
|
|
background: palette[0],
|
|
palette: palette_name,
|
|
patterns: patterns
|
|
}
|
|
}
|
|
|
|
|
|
// lol the best way I found to to this was imagemagick!
|
|
|
|
async function get_histogram(imgfile) {
|
|
try {
|
|
const bl = await spawn('convert', [ imgfile, '-format', '%c', 'histogram:info:' ]);
|
|
return parse_histogram(bl.toString());
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
|
|
function parse_histogram(convert_out) {
|
|
const colour_re = /(\d+): \(\d+,\d+,\d+,\d+\) (#[A-F0-9]+) /;
|
|
const colours = new Map();
|
|
convert_out.split("\n").forEach((l) => {
|
|
const m = l.match(colour_re);
|
|
if( m ) {
|
|
const colour = m[2].substring(0, 7);
|
|
colours.set(colour, Number(m[1]));
|
|
}
|
|
});
|
|
return new Map([...colours].sort((a, b) => b[1] - a[1]));
|
|
}
|
|
|
|
|
|
function colour_visible(hist, colour) {
|
|
const hexcolour = colour.formatHex().toUpperCase();
|
|
return hist.get(hexcolour) > VISIBLE_DOG;
|
|
}
|
|
|
|
|
|
function image_description(namer, params, histogram) {
|
|
const colours = [
|
|
params.background,
|
|
params.patterns[0].colour,
|
|
params.patterns[1].colour,
|
|
];
|
|
const i_vis = [0, 1, 2].filter((i) => colour_visible(histogram, colours[i]));
|
|
const named_colours = i_vis.map((i) => namer.colour_to_text(colours[i]));
|
|
const gradients = i_vis.map((i) => {
|
|
if( i > 0 ) {
|
|
return RADIUS_DESC[params.patterns[i - 1].f];
|
|
} else {
|
|
return '';
|
|
}
|
|
});
|
|
if( named_colours.length == 1 ) {
|
|
return `A solid field of ${named_colours[0]}`;
|
|
}
|
|
if( named_colours.length > 1 ) {
|
|
const bgc = named_colours[0];
|
|
const a = bgc.match(/^[aeiou]/) ? 'an' : 'a';
|
|
const dot_desc = named_colours.slice(1);
|
|
const gs = gradients.slice(1);
|
|
const pattern_desc = dot_desc.map((d, i) => `${d} dots ${gs[i]}`);
|
|
const patterns = pattern_desc.join(' and ');
|
|
return `A pattern of ${patterns} on ${a} ${bgc} background`;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
|
|
function poptimal_svg(params) {
|
|
const window = new JSDOM().window;
|
|
const document = window.document;
|
|
const container = d3.select(document.body).append("div");
|
|
|
|
const dm = new DotMaker(WIDTH);
|
|
|
|
const svg = container.append("svg")
|
|
.attr("width", WIDTH * CELL * MAG)
|
|
.attr("height", HEIGHT * CELL * MAG)
|
|
.attr("viewBox", [ 0, 0, WIDTH, HEIGHT ]);
|
|
|
|
const background = svg.append("rect")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("width", WIDTH)
|
|
.attr("height", WIDTH)
|
|
.attr("fill", params.background);
|
|
|
|
|
|
params.patterns.map((p) => {
|
|
const dots = dm.dots(1 / p.m, p.n);
|
|
const dots_g = svg.append("g")
|
|
.attr("id", `dots${p.i}`);
|
|
|
|
dots_g.selectAll("circle")
|
|
.data(dots)
|
|
.join("circle")
|
|
.attr("r", (d) => dm.radius(d, p.f, p.r))
|
|
.attr("fill", p.colour)
|
|
.attr("cx", (d) => d.x)
|
|
.attr("cy", (d) => d.y);
|
|
});
|
|
|
|
const node = svg.node();
|
|
|
|
node.setAttributeNS(xmlns, "xmlns", svgns);
|
|
node.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
|
|
const serializer = new window.XMLSerializer;
|
|
return serializer.serializeToString(node);
|
|
|
|
}
|
|
|
|
|
|
async function post_image(image, alt_text, cf) {
|
|
const status_url = `${cf.base_url}/api/v1/statuses`;
|
|
const media_url = `${cf.base_url}/api/v1/media`;
|
|
const headers = {
|
|
'Authorization': `Bearer ${cf.access_token}`,
|
|
};
|
|
const rawData = await promises.readFile(image);
|
|
const blob = new Blob([rawData]);
|
|
const fd = new FormData();
|
|
fd.set('description', alt_text);
|
|
fd.set('file', blob, image, 'text/png');
|
|
const resp = await fetch(media_url, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: fd
|
|
});
|
|
|
|
const bodyjson = await resp.text();
|
|
const response = JSON.parse(bodyjson);
|
|
const media_id = response["id"];
|
|
|
|
headers['Accept'] = 'application/json';
|
|
headers['Content-Type'] = 'application/json';
|
|
const resp2 = await fetch(status_url, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify({ media_ids: [ media_id ] })
|
|
});
|
|
const bodyjson2 = await resp2.text();
|
|
|
|
}
|
|
|
|
|
|
async function main() {
|
|
const argv = yargs(hideBin(process.argv))
|
|
.usage("Usage: -s SIZE -o output.png -c config.json")
|
|
.default('s', 1200)
|
|
.default('c', 'config.json').argv;
|
|
|
|
const cfjson = await promises.readFile(argv.c);
|
|
const cf = JSON.parse(cfjson);
|
|
|
|
|
|
|
|
const fn = argv.o || String(Date.now()) + '.png';
|
|
|
|
const imgfile = cf['working_dir'] + '/' + fn;
|
|
|
|
const params = randomise_params();
|
|
const colourf = params.palette === 'grayscale' ? cf['grayscale'] : cf['colour'];
|
|
|
|
const namer = new ColourNamer();
|
|
await namer.load_colours(colourf);
|
|
|
|
|
|
const svg = poptimal_svg(params);
|
|
const opts = {
|
|
background: 'rgba(255, 255, 255, 1.0)',
|
|
fitTo: {
|
|
mode: 'width',
|
|
value: argv.s,
|
|
},
|
|
};
|
|
|
|
const resvg = new Resvg(svg, opts);
|
|
const pngData = resvg.render();
|
|
const pngBuffer = pngData.asPng();
|
|
|
|
|
|
await promises.writeFile(imgfile, pngBuffer);
|
|
|
|
// generate the alt_text last to check the image file histogram
|
|
// so we don't include obscured colours
|
|
const hist = await get_histogram(imgfile);
|
|
const alt_text = image_description(namer, params, hist);
|
|
|
|
console.log(alt_text);
|
|
console.log(imgfile);
|
|
if( cf['base_url'] ) {
|
|
await post_image(imgfile, alt_text, cf);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
main();
|