UNPKG

@rr0/cms

Version:

RR0 Content Management System (CMS)

200 lines (199 loc) 7.99 kB
import { createCanvas, loadImage } from "canvas"; import fs from "fs"; import path from "path"; import assert from "assert"; /** * Create a preview image for each page sharing. */ export class OpenGraphCommand { constructor(outDir, timeFiles, baseUrl, timeService, timeTextBuilder, width = 1200, height = 600) { this.outDir = outDir; this.timeFiles = timeFiles; this.baseUrl = baseUrl; this.timeService = timeService; this.timeTextBuilder = timeTextBuilder; this.width = width; this.height = height; this.num = 0; this.supportedFiles = "img[src$='.png'],img[src$='.jpg'],img[src$='.gif']"; } async execute(context) { const title = context.file.title; if (!title) { // Nothing to write in preview? return; } const canvas = createCanvas(this.width, this.height); const canvasCtx = canvas.getContext("2d"); const imageWidthRatio = await this.drawImage(context, canvasCtx); this.drawGradient(canvasCtx, imageWidthRatio); const margin = 40; canvasCtx.fillStyle = "#666"; this.drawText(canvasCtx, title, margin, 70, "400 3em system-ui,sans-serif"); const infoStr = this.getInfoStr(context); canvasCtx.font = "400 1.25em system-ui,sans-serif"; canvasCtx.fillText(infoStr, margin, this.height - 50); this.num++; const imageUrl = this.writeImageFile(context, canvas); const outDoc = context.file.document; const ogMeta = outDoc.createElement("meta"); ogMeta.setAttribute("property", "og:image"); ogMeta.setAttribute("content", imageUrl); outDoc.head.append(ogMeta); const docType = outDoc.doctype ? `<!DOCTYPE ${outDoc.doctype.name}>` : ""; context.file.contents = `${docType}${outDoc.documentElement.outerHTML}`; } getInfoStr(context) { const authors = context.file.meta.author; const authorsStr = authors && authors.length > 0 ? authors.join(" & ") : ""; let timeStr = ""; const fileName = context.file.name; if (this.timeFiles.includes(fileName)) { timeStr = "Chronologie"; } else { const timeContext = this.timeService.setContextFromFile(context, fileName); if (timeContext) { context.time.setYear(timeContext.getYear()); context.time.setMonth(timeContext.getMonth()); context.time.setDayOfMonth(timeContext.getDayOfMonth()); context.time.setHour(undefined); context.time.setMinutes(undefined); timeStr = this.timeTextBuilder.build(context); } } const copyrightStr = context.file.meta.copyright || "RR0.org"; let infoStr = authorsStr ? authorsStr : ""; infoStr = infoStr ? [infoStr, copyrightStr].join(" : ") : copyrightStr; if (timeStr) { if (timeStr === "Chronologie") { infoStr = [timeStr, infoStr].join(", "); } else { infoStr = [infoStr, timeStr].join(", "); } } return infoStr; } async contentStepEnd() { // NOP } /** * Draw text on the canvas, with line returns when required. * * @param canvasCtx * @param text The text to write. * @param margin * @param lineHeight * @param font * @protected */ drawText(canvasCtx, text, margin, lineHeight, font) { canvasCtx.font = font; let lineText = text; let remainingText = lineText; let splitPos = text.length; let line = 0; let overflow = true; while (overflow && remainingText.length > 0) { const textWidth = canvasCtx.measureText(lineText); overflow = textWidth.width > this.width - margin; if (overflow) { splitPos = lineText.lastIndexOf(" "); if (splitPos > 0) { remainingText = lineText.substring(splitPos).trim(); lineText = lineText.substring(0, splitPos); } else { remainingText = ""; } } else { canvasCtx.fillText(lineText, margin, 100 + line * lineHeight); line++; overflow = lineText != remainingText; lineText = remainingText; } } } /** * Draw a left-to-right gradient from white to transparent. * * @param canvasCtx * @param widthRatio * @param startColor * @param endColor * @private */ drawGradient(canvasCtx, widthRatio, startColor = "rgba(255, 255, 255, 1)", endColor = "rgba(255, 255, 255, 0)") { canvasCtx.beginPath(); { canvasCtx.strokeStyle = "transparent"; // draw rectangle towards right hand side canvasCtx.rect(0, 0, this.width, this.height); // create linear gradient const grdLinear = canvasCtx.createLinearGradient(0, 0, this.width, 0); // Important bit here is to use rgba() grdLinear.addColorStop(0, startColor); grdLinear.addColorStop(widthRatio, startColor); grdLinear.addColorStop(1, endColor); // add gradient to rectangle canvasCtx.fillStyle = grdLinear; // step below are pretty much standard to finish drawing an object to canvas canvasCtx.fill(); canvasCtx.stroke(); } canvasCtx.closePath(); } /** * Draw a height-scaled image on the right of the canvas. * * @param context * @param canvasCtx * @param dy * @protected */ async drawImage(context, canvasCtx, dy = 0) { const outDoc = context.file.document; const docImages = outDoc.documentElement.querySelectorAll(this.supportedFiles); let widthRatio = 0.5; let imageIndex = 0; if (imageIndex < docImages.length) { const firstImage = docImages[0]; const firstImageSrc = firstImage.getAttribute("src"); let firstImageUrl; try { assert.ok(firstImageSrc, "Undefined image src"); firstImageUrl = (firstImageSrc === null || firstImageSrc === void 0 ? void 0 : firstImageSrc.startsWith(this.baseUrl)) ? firstImageSrc.substring(this.baseUrl.length) : firstImageSrc; assert.ok(firstImageUrl, "Undefined image url"); const dir = path.dirname(context.file.name); const src = firstImageUrl.startsWith("/") ? firstImageUrl.substring(1) : path.join(dir, firstImageUrl); const image = await loadImage(src); const heightRatio = this.height / image.height; const dw = image.width * heightRatio; const dx = this.width - dw; widthRatio = dx / this.width; canvasCtx.drawImage(image, dx, dy, dw, this.height); } catch (e) { context.error(`Error loading image "${firstImageUrl}", skipping it`); imageIndex++; // Try next image } } return widthRatio; } writeImageFile(context, canvas) { const buffer = canvas.toBuffer("image/png"); const outputName = context.file.name; const imageName = "og.png"; const dir = path.dirname(outputName); const imageUrl = path.join("/", dir, imageName); const imageOutPath = path.join(this.outDir, imageUrl); const imageOutDir = path.dirname(imageOutPath); if (!fs.existsSync(imageOutDir)) { fs.mkdirSync(imageOutDir, { recursive: true }); } context.debug("Writing OG image", imageOutPath); fs.writeFileSync(imageOutPath, buffer); return imageUrl; } }