Options
All
  • Public
  • Public/Protected
  • All
Menu

hastily

npm downloads CircleCI codecov snyk peer dependency npm downloads

Drop-in compatible Express middleware to replicate the Fastly Image Optimization API

Usage

The middleware works a lot like compression, a standard Express middleware which transparently compresses any text-based response body that emits from something in the middleware chain.

So hastily works as an add-on to an Express server or middleware that is already serving images.

import express, { static } from 'express';
import { imageopto } from 'hastily';

const app = express();
app.use('/images', imageopto(), static('/www/images'));

app.listen(8000);

You now have an app which can serve any image from /www/images, and optimize it with URL parameters from the Fastly Image Optimization API.

Behavior

Request Filtering

Hastily is meant to be registered as a middleware within an app that may be handling non-image requests as well as image requests. By default, it will only attempt to transform responses that appear to be uncompressed images. Hastily verifies this in several steps.

  1. By URL: The default filter function for the imageopto middleware is hastily.hasSupportedExtension(req: Request) It. checks a request's URL path for an extension that indicates a Sharp-supported file type. It reads the supported extensions from Sharp itself. If a file does not have an image-file extension, it will do nothing.

    Supply an alternative function as the filter property of imageopto options to override this behavior.

    import { imageopto, hasSupportedExtension } from 'hastily';
    // Don't require an extension. Require a certain base directory and query param.
    imageopto({
      filter: (req) => req.path.startsWith('/media/') && 'optimize' in req.query,
    });
    // Require an extension, OR an 'imageServer' path.
    hastily.imageopto({
      filter: (req) => hasSupportedExtension(req) || req.path === '/imageServer',
    });
  2. By method and status: Hastily will not transform any response unless the request was a GET and the response code is in the 2xx range.

  3. By Cache-control: If the response has a Cache-Control header containing no-transform, Hastily will respect that.

  4. By dedupe: Both Hastily and Fastly (which Hastily emulates) set telltale headers on responses after processing them. If a fastly-io-info header is present, OR an X-Optimized: hastily header is present, Hastily will assume a transform has already occurred and won't attempt another. This behavior can be overridden by options.force, but you shouldn't do that.

  5. By content type: Hastily will try to get the Content-Type header of the response. If it cannot detect a content type, OR if Sharp does not support the content type, Hastily will not attempt to transform.

  6. By content encoding: Hastily will not attempt to decompress gzipped responses. Images shouldn't be gzipped (or deflated, or brotli'd, or what have you) and it's a mistake to configure a server to do so by default. The available gzip algorithms are designed for text data, and they don't compress binary data hardly at all. If your server is gzipping images before Hastily handles them, fix this configuration for the highest speeds.

Debugging

The Hastily middleware logs debugs, warnings and errors to standard error, which is what web server stacks expect. When NODE_ENV is production, only warnings will be logged, as parseable JSON lines.

When NODE_ENV is not production, Hastily respects the Node convention of path syntax in the DEBUG environment variable to determine log level. DEBUG=hastily:* will show all debug data in a pretty format. You can limit logging to subject areas by using paths like DEBUG=hastily:request,hastily:params,hastily:splice instead.

Full API doc at zetlen.github.io/hastily

TODO

  • implement resize and crop mappers
  • throw on unsupported
  • implement enable and disable for upscaling in resize
  • implement format, auto=webp, and quality params in post-manip phase
  • add unit tests
  • add image-diff automated testing
  • implement sharpen, mapping [amt, radius, threshold] to libvips sharpen params
  • implement brightness, contrast, saturation by figuring out percentage to multiplier mapping
  • use image.metadata() to implement relative and context-based methods
  • add header-based methods
    • montage
    • overlay

Index

Type aliases

Orienter

Orienter: (x: Sharp) => Sharp

Type declaration

    • (x: Sharp): Sharp
    • Parameters

      • x: Sharp

      Returns Sharp

Variables

Const HASTILY_STREAMABLE_FILETYPES

HASTILY_STREAMABLE_FILETYPES: Set<string> = new Set(Object.keys(sharp.format).filter((ext) => sharp.format[ext as keyof FormatEnum].input.stream))

Const HASTILY_STREAMABLE_PATH_REGEXP

HASTILY_STREAMABLE_PATH_REGEXP: RegExp = new RegExp(`/.+\\.(${[...HASTILY_STREAMABLE_FILETYPES].join('|')})(?:[?#].*)?`)

Const QUALITY

QUALITY: Param = 'quality' as Param

Const UNSET

UNSET: unique symbol = Symbol('unset')

Const cacheControlNoTransformRegExp

cacheControlNoTransformRegExp: RegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/

Const log

log: Logger = createLogger('splice')

Const mappers

mappers: [][] = [['width', resize],['height', resize],['dpr', resize],['fit', resize],['disable', resize],['enable', resize],['resize-filter', resize],['crop', extractCrop],['canvas', resizeCanvas],['pad', extend],['orient', orient],['bg-color', bgFlatten],['blur', blur],['brightness', unsupported('brightness', 'absolute brightness adjustment')],['contrast', unsupported('contrast', 'absolute contrast adjustment')],['saturation', unsupported('saturation', 'absolute saturation adjustment')],['sharpen', unsupported('sharpen', 'unsharp mask')],['trim', unsupported('trim', 'relative trimming from all four sides')],]

Const paramsLogger

paramsLogger: Logger = createLogger('params')

Functions

Private addListeners

  • addListeners(stream: IMutableResponse | IWorkStream, on: any, listeners: Listener[]): void
  • Add bufferred listeners to stream

    Parameters

    • stream: IMutableResponse | IWorkStream
    • on: any
    • listeners: Listener[]

    Returns void

chunkLength

  • chunkLength(chunk: any, encoding: any): number

createLogger

  • createLogger(label: string): Logger

Const exif

  • exif(sharp: Sharp): Sharp

Const flipH

  • flipH(sharp: Sharp): Sharp

Const flipHV

  • flipHV(sharp: Sharp): Sharp

Const flipV

  • flipV(sharp: Sharp): Sharp

Const hasSupportedExtension

  • hasSupportedExtension(req: Request<ParamsDictionary, any, any, ParsedQs>): boolean
  • Use the sharp.format manifest to determine if the current request's file extension matches a format that sharp can stream in to optimize.

    Parameters

    • req: Request<ParamsDictionary, any, any, ParsedQs>

    Returns boolean

imageopto

  • Returns a new imageopto middleware for use in Express app.use(). Won't do anything if the Express app isn't already serving images!

    Parameters

    Returns Middleware

Const left

  • left(sharp: Sharp): Sharp

Const right

  • right(sharp: Sharp): Sharp

Private shouldTransform

  • shouldTransform(req: Request, res: IMutableResponse, reqLog: Logger, options: ImageOptoOptions): boolean
  • Determine if the entity should be transformed.

    Parameters

    Returns boolean

Object literals

Const HASTILY_HEADER

HASTILY_HEADER: object

NAME

NAME: string = "X-Optimized"

VALUE

VALUE: string = "hastily"

Const formatters

formatters: object

gif

gif: Mapper = unsupported('format' as Param, 'GIF output unsupported by node-hastily')

jpg

  • jpg(transform: Sharp, params: IFastlyParams): Sharp

pjpg

  • pjpg(transform: Sharp, params: IFastlyParams): Sharp

png

  • png(transform: Sharp, params: IFastlyParams): Sharp

png8

  • png8(transform: Sharp, params: IFastlyParams): Sharp

webp

  • webp(transform: Sharp, params: IFastlyParams): Sharp

webpll

  • webpll(transform: Sharp, params: IFastlyParams): Sharp

webply

  • webply(transform: Sharp, params: IFastlyParams): Sharp

Const optoFitToSharp

optoFitToSharp: object

bounds

bounds: "inside" = "inside"

cover

cover: "outside" = "outside"

crop

crop: "cover" = "cover"

Const optoResizeFilterToSharp

optoResizeFilterToSharp: object

bicubic

bicubic: "cubic" = "cubic"

bilinear

bilinear: "mitchell" = "mitchell"

cubic

cubic: "cubic" = "cubic"

lanczos

lanczos: "lanczos3" = "lanczos3"

lanczos2

lanczos2: "lanczos2" = "lanczos2"

lanczos3

lanczos3: "lanczos3" = "lanczos3"

linear

linear: "mitchell" = "mitchell"

nearest

nearest: "nearest" = "nearest"

Const orienters

orienters: object

1

1: Orienter = exif

2

2: Orienter = flipH

3

3: Orienter = flipHV

4

4: Orienter = flipV

6

6: Orienter = right

8

8: Orienter = left

h

h: Orienter = flipH

hv

hv: Orienter = flipHV

l

l: Orienter = left

r

r: Orienter = right

v

v: Orienter = flipV

vh

vh: Orienter = flipHV

5

  • 5(sharp: Sharp): Sharp

7

  • 7(sharp: Sharp): Sharp

Legend

  • Constructor
  • Property
  • Method
  • Private property
  • Private method
  • Property

Generated using TypeDoc