From 578b58794bd64d98e97e29bb4b9b7a082290cc76 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Fri, 3 Nov 2023 23:52:12 -0400 Subject: [PATCH] New command that will use Pexels to generate random cover images for media. --- commands/examples/coverimage.js | 161 ++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 commands/examples/coverimage.js diff --git a/commands/examples/coverimage.js b/commands/examples/coverimage.js new file mode 100644 index 0000000..0456ec3 --- /dev/null +++ b/commands/examples/coverimage.js @@ -0,0 +1,161 @@ +// you may not need all of these but they come up a lot +import fs from "fs"; +import assert from "assert"; +import logging from '../lib/logging.js'; +import glob from "fast-glob"; +import path from "path"; +import { fabric } from "fabric"; +import { defer } from "../lib/api.js"; +import { createClient } from 'pexels'; +import { exec_i } from "../lib/builderator.js"; +import { pexels } from "../lib/config.js"; +import fetch from "node-fetch"; +import { playstart, playstop } from "../lib/testing.js"; + +const log = logging.create(import.meta.url); + +export const description = "Gets a random image from Pexels and places the text on it." + +// your command uses the npm package commander's options format +export const options = [ + ["--background ", "The file name to save the background image.", "background.jpg"], + ["--force", "Force overwriting the background.jpg temp file."], + ["--query ", "Query to give pexels for the image", "keyboard"], + ["--color ", "General color of the image"], + ["--text-color ", "The border around text.", "#fff"], + ["--font-family ", "Font to use", "Andale Mono"], + ["--font-size ", "Font size to use", 140], + ["--blend-mode ", "One of: none, multiply, screen, add, diff, subtract, darken, lighten, overlay, exclusion, tint", "screen"], + ["--pixelate ", "Pixelate the image with blocksize (try 8)"], +] + +// example of a positional argument, it's the 1st argument to main +export const argument = ["text", "The message to put on the image."]; + +// put required options in the required variable +export const required = [ + ["--output ", "Image file to save."], +] + +// handy function for checking things are good and aborting +const check = (test, fail_message) => { + if(!test) { + log.error(fail_message); + process.exit(1); + } +} + +export const main = async (title, opts) => { + const per_page = 50; + + const ext_check = path.parse(opts.output).ext; + check(ext_check === "", `Do not add an extension to the output. You have ${ext_check}.`); + + title = title.replaceAll("\\n", "\n"); + + if(opts.force || !fs.existsSync(opts.background)) { + const client = createClient(pexels.key); + + const result = await client.photos.search({ + query: opts.query, + per_page, + orientation: opts.orientation, + color: opts.color }); + + const randi = Math.floor(Math.random() * per_page); + const image = await client.photos.show({id: result.photos[randi].id}); + const img_fetch = await fetch(image.src.large); + const img_data = await img_fetch.arrayBuffer(); + await fs.writeFileSync(opts.background, new Uint8Array(img_data)); + } + + const background = path.join(process.cwd(), opts.background); + + let canvas = new fabric.Canvas('c', { width: 1600, height: 900 }) + + let gradient = new fabric.Gradient({ + type: 'linear', + gradientUnits: 'pixels', // or 'percentage' + coords: { x1: 0, y1: 0, x2: canvas.height, y2: canvas.width }, + colorStops:[ + { offset: 0, color: '#000' }, + { offset: 1, color: '#fff'} + ] + }) + + let image_def = defer(); + + fabric.Image.fromURL(`file:///${background}`, img => { + let blur = new fabric.Image.filters.Blur({ blur: 0.05 }); + + img.set('scaleY', canvas.height / img.height); + img.set('scaleX', canvas.width / img.width); + + if(opts.pixelate) { + let pixelate = new fabric.Image.filters.Pixelate({ + blocksize: parseInt(opts.pixelate, 10) + }); + + img.filters.push(pixelate); + } + + let blend = new fabric.Image.filters.BlendColor({ + color: opts.color, + mode: opts.blendMode, + }); + + img.filters.push(blend); + img.filters.push(blur); + img.applyFilters(); + image_def.resolve(img); + }); + + let bg_img = await image_def; + + let rect = new fabric.Rect({ + left: 0, + top: 0, + fill: opts.color, + width: canvas.width, + height: canvas.height, + opacity: 0.3 + }); + + let text = new fabric.Text(title, { + fontFamily: opts.fontFamily, + fontSize: opts.fontSize, + strokeWidth: 5, + originX: "center", + originY: "center", + left: canvas.width / 2, + top: canvas.height / 2, + stroke: opts.textColor, + shadow: 'rgba(0, 0, 0, 1) 10px 10px 10px', + }); + + canvas.add(bg_img); + // canvas.add(rect); + canvas.add(text); + + const output_svg = `${opts.output}.svg`; + const output_jpg = `${opts.output}.jpg`; + const output_png = `${opts.output}.png`; + + fs.writeFileSync(output_svg, canvas.toSVG()); + + const {browser, context, p} = await playstart(`file:///${path.resolve(output_svg)}`); + + await p.setViewportSize({ + width: canvas.width, + height: canvas.height }); + + await p.screenshot({ path: output_png, + type: "png", omitBackground: true}); + + await playstop(browser, p); + + exec_i(`convert ${output_png} -sampling-factor 4:2:0 -strip -quality 85 -interlace JPEG -colorspace RGB ${output_jpg}`); + + // due to how async/await works it's just easier to manually exit with exit codes + process.exit(0); +}