diff --git a/docs/plugins/Assets.md b/docs/plugins/Assets.md index 47589b2c3..b927426e6 100644 --- a/docs/plugins/Assets.md +++ b/docs/plugins/Assets.md @@ -11,7 +11,10 @@ Note that all static assets will then be accessible through its path on your gen > [!note] > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. -This plugin has no configuration options. +This plugin accepts the following configuration options: + +- `resizeImages`: A subset of the [Sharp resizing options](https://sharp.pixelplumbing.com/api-resize). Defaults to `{ width: 1700, fit: 'inside' }`. +- `compressImages`: If `true` (default), enable image compression. Disable to copy images as-is without modification. ## API diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts index d0da66ace..c4ea0b68a 100644 --- a/quartz/plugins/emitters/assets.ts +++ b/quartz/plugins/emitters/assets.ts @@ -1,19 +1,33 @@ -import { FilePath, joinSegments, slugifyFilePath } from "../../util/path" +import { FilePath, joinSegments, slugifyFilePath, getFileExtension } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import path from "path" import fs from "fs" +import sharp, { FitEnum } from "sharp" +import { styleText } from "util" import { glob } from "../../util/glob" import { Argv } from "../../util/ctx" import { QuartzConfig } from "../../cfg" +interface Options { + resizeImages?: { width?: number; height?: number; fit?: keyof FitEnum } + compressImages?: boolean +} + +const defaultOptions: Options = { + resizeImages: { width: 1700, fit: "inside" }, + compressImages: true, +} + +// Accepted Sharp formats https://sharp.pixelplumbing.com/#formats +const imageExtensions = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif", ".tif", ".tiff", ".svg"] + const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => { // glob all non MD files in content folder and copy it over return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns]) } -const copyFile = async (argv: Argv, fp: FilePath) => { +const copyFile = async (argv: Argv, fp: FilePath, opts: Options) => { const src = joinSegments(argv.directory, fp) as FilePath - const name = slugifyFilePath(fp) const dest = joinSegments(argv.output, name) as FilePath @@ -21,17 +35,48 @@ const copyFile = async (argv: Argv, fp: FilePath) => { const dir = path.dirname(dest) as FilePath await fs.promises.mkdir(dir, { recursive: true }) - await fs.promises.copyFile(src, dest) + const ext = getFileExtension(fp)?.toLowerCase() + if (ext && imageExtensions.includes(ext)) { + await copyImage(src, dest, opts) + } else { + await copyBlob(src, dest) + } + return dest } -export const Assets: QuartzEmitterPlugin = () => { +const copyImage = async (src: FilePath, dest: FilePath, opts: Options) => { + if (!opts.compressImages) { + await copyBlob(src, dest) + } else if (opts.resizeImages) { + await sharp(src).resize(opts.resizeImages).toFile(dest) + } else { + await sharp(src).toFile(dest) + } +} + +const copyBlob = async (src: FilePath, dest: FilePath) => { + await fs.promises.copyFile(src, dest) +} + +export const Assets: QuartzEmitterPlugin> = (opts) => { + opts = { ...defaultOptions, ...opts } + + if ((opts.resizeImages?.width || opts.resizeImages?.height) && !opts.compressImages) { + console.warn( + styleText( + "yellow", + "Your asset resizing options are incompatible - enable compression to resize images", + ), + ) + } + return { name: "Assets", async *emit({ argv, cfg }) { const fps = await filesToCopy(argv, cfg) for (const fp of fps) { - yield copyFile(argv, fp) + yield copyFile(argv, fp, opts) } }, async *partialEmit(ctx, _content, _resources, changeEvents) { @@ -40,7 +85,7 @@ export const Assets: QuartzEmitterPlugin = () => { if (ext === ".md") continue if (changeEvent.type === "add" || changeEvent.type === "change") { - yield copyFile(ctx.argv, changeEvent.path) + yield copyFile(ctx.argv, changeEvent.path, opts) } else if (changeEvent.type === "delete") { const name = slugifyFilePath(changeEvent.path) const dest = joinSegments(ctx.argv.output, name) as FilePath