@digibuffer/image-core
v1.0.2
Published
Node.js image processing — metadata extraction, variant generation, optimization, watermarking. Uses sharp (native binaries, not Edge-compatible).
Maintainers
Readme
@digibuffer/image-core
Node.js image processing — metadata extraction, EXIF parsing, variant generation, optimization, color analysis, and watermarking. Powered by sharp and exifr.
Node.js only. sharp uses native binaries — not compatible with Edge runtimes or browser environments. Use in API routes, job workers, or server-side scripts.
Installation
npm install @digibuffer/image-coreFunctions
extractMetadata(input)
Basic image dimensions and format from sharp.
const meta = await extractMetadata(buffer);interface ImageMetadata {
width: number;
height: number;
format: 'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'tiff';
size?: number; // file size in bytes
orientation?: number; // EXIF orientation 1-8
hasAlpha?: boolean;
space?: string; // 'srgb', 'rgb', 'cmyk', etc.
depth?: string; // bits per channel e.g. 'uchar'
density?: number; // DPI
isProgressive?: boolean; // JPEG progressive encoding
}extractExif(input)
Full EXIF/GPS data parsed from the image. Returns null if no EXIF is present (screenshots, web images, most PNGs).
const exif = await extractExif(buffer);
// → ExifData | nullinterface ExifData {
// Camera
make?: string; // e.g. 'Apple', 'Canon', 'Sony'
model?: string; // e.g. 'iPhone 15 Pro', 'EOS R5'
lens?: string; // e.g. 'iPhone 15 Pro back camera 6.86mm f/1.78'
software?: string; // e.g. 'Lightroom 7.0'
// Exposure
iso?: number; // e.g. 100, 800, 3200
aperture?: number; // f-number e.g. 1.78, 2.8, 5.6
shutterSpeed?: string; // human-readable e.g. '1/1000', '2s'
focalLength?: number; // mm e.g. 24, 85
focalLengthIn35mm?: number; // 35mm equivalent
exposureProgram?: string; // 'Aperture Priority', 'Shutter Priority', 'Manual', etc.
exposureMode?: string; // 'Auto', 'Manual', 'Auto Bracket'
meteringMode?: string; // 'Spot', 'Center Weighted', 'Pattern', etc.
flash?: string; // 'Flash Fired', 'No Flash', 'Flash Did Not Fire'
whiteBalance?: string; // 'Auto', 'Manual'
// Date
dateTimeOriginal?: Date; // when the photo was taken
dateTimeDigitized?: Date; // when digitized (usually same)
// GPS
gps?: {
latitude: number; // decimal degrees e.g. 51.5074
longitude: number; // decimal degrees e.g. -0.1278
altitude?: number; // metres above sea level
};
// Image
orientation?: number; // EXIF orientation 1-8
colorSpace?: string; // 'sRGB', 'Uncalibrated'
xResolution?: number; // DPI
yResolution?: number; // DPI
raw?: Record<string, unknown>; // all raw parsed EXIF fields
}Images that contain EXIF: unedited JPEGs from cameras and smartphones, raw files converted to JPEG. Images that don't: screenshots, web-downloaded PNGs, images processed through most editors (EXIF stripped).
extractColors(input)
Per-channel color statistics using sharp's pixel analysis.
const colors = await extractColors(buffer);interface ImageColors {
dominant: { r: number; g: number; b: number }; // channel means → approximate dominant color
channels: {
mean: number; // average pixel value 0-255
stdev: number; // standard deviation
min: number;
max: number;
}[]; // [R, G, B] or [R, G, B, A]
hasAlpha: boolean;
brightness: number; // perceived brightness 0-100 (ITU-R BT.601 luma)
}isImage(input)
Returns true if the buffer is a recognised image format.
const ok = await isImage(buffer); // → booleangenerateVariants(input, configs?)
Generate one or more resized/reformatted variants from a single input.
const variants = await generateVariants(buffer, [
{ name: 'thumb', width: 400, height: 400, fit: 'inside', format: 'webp', quality: 90 },
{ name: 'medium', width: 1200, height: 1200, fit: 'inside', format: 'webp', quality: 85 },
]);interface VariantConfig {
name: string;
width: number;
height: number;
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; // default: 'inside'
format?: ImageFormat; // default: 'webp'
quality?: number; // 1-100, default: 85
autoRotate?: boolean; // auto-rotate via EXIF orientation, default: true
}
interface VariantResult {
name: string;
buffer: Buffer;
width: number;
height: number;
format: ImageFormat;
size: number; // bytes
}generateVariant(input, config)
Single variant — same as generateVariants but for one config.
generateStandardVariants(input)
Shortcut for the two platform-standard variants:
const { thumbnail, medium } = await generateStandardVariants(buffer);
// thumbnail → 800×800 max, WebP, q90, auto-rotate
// medium → 1600×1600 max, WebP, q85, auto-rotateBoth use fit: 'inside' (never upscales, preserves aspect ratio).
transformImage(input, options)
Resize, reformat, and/or strip metadata in one call.
const result = await transformImage(buffer, {
width: 1280,
height: 720,
fit: 'cover',
format: 'webp',
quality: 85,
autoRotate: true,
stripMetadata: true,
});interface TransformOptions {
width?: number;
height?: number;
fit?: FitMode;
format?: ImageFormat;
quality?: number;
autoRotate?: boolean;
stripMetadata?: boolean;
}
interface TransformResult {
buffer: Buffer;
width: number;
height: number;
format: ImageFormat;
size: number;
}optimizeImage(input, options?)
Reduce file size while preserving quality. Auto-detects format if none specified.
const result = await optimizeImage(buffer, {
quality: 80,
stripMetadata: true,
});
// Force JPEG with mozjpeg + progressive encoding
const result = await optimizeImage(buffer, {
format: 'jpeg',
quality: 85,
mozjpeg: true,
progressive: true,
});
// PNG with max compression
const result = await optimizeImage(buffer, {
format: 'png',
quality: 80,
pngCompressionLevel: 9,
});interface OptimizeOptions {
format?: ImageFormat;
quality?: number; // 1-100
mozjpeg?: boolean; // JPEG: use mozjpeg encoder (better compression)
pngCompressionLevel?: number; // PNG: 1-9
progressive?: boolean; // JPEG: progressive encoding
stripMetadata?: boolean; // strip EXIF, ICC, etc.
}convertFormat(input, format, quality?)
Convert to a different format.
const webp = await convertFormat(buffer, 'webp', 85);
const avif = await convertFormat(buffer, 'avif', 80);applyWatermark(input, options)
Composite a text watermark (+ optional logo) over an image using SVG.
const result = await applyWatermark(buffer, {
text: '© MyBrand',
position: 'bottom-right',
opacity: 0.7,
color: '#FFFFFF',
});
// With logo
const result = await applyWatermark(buffer, {
text: 'digibuffer.com',
position: 'bottom-left',
opacity: 0.8,
logo: logoBuffer,
logoSizeRatio: 0.06, // 6% of image width
fontSizeRatio: 0.02, // 2% of image width
color: '#FFFFFF',
});interface WatermarkOptions {
text: string;
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'center';
opacity?: number; // 0-1, default 0.7
color?: string; // hex, default '#FFFFFF'
logo?: Buffer; // optional logo image
logoSizeRatio?: number; // logo width as fraction of image width, default 0.08
fontSizeRatio?: number; // font size as fraction of image width, default 0.025
}
interface WatermarkResult {
buffer: Buffer;
width: number;
height: number;
}Typical job handler (Node.js / Hono)
import {
extractMetadata,
extractExif,
extractColors,
generateStandardVariants,
optimizeImage,
} from '@digibuffer/image-core';
async function processImage(key: string, buffer: Buffer) {
const [meta, exif, colors, variants] = await Promise.all([
extractMetadata(buffer),
extractExif(buffer),
extractColors(buffer),
generateStandardVariants(buffer),
]);
// Upload variants to R2/S3
await storage.put(`${key}/thumbnail.webp`, variants.thumbnail.buffer);
await storage.put(`${key}/medium.webp`, variants.medium.buffer);
// Insert to DB
await db.insert(mediaFiles).values({
key,
width: meta.width,
height: meta.height,
format: meta.format,
size: meta.size,
// EXIF
cameraMake: exif?.make,
cameraModel: exif?.model,
lens: exif?.lens,
iso: exif?.iso,
aperture: exif?.aperture,
shutterSpeed: exif?.shutterSpeed,
focalLength: exif?.focalLength,
takenAt: exif?.dateTimeOriginal,
gpsLatitude: exif?.gps?.latitude,
gpsLongitude: exif?.gps?.longitude,
gpsAltitude: exif?.gps?.altitude,
// Colors
dominantR: colors.dominant.r,
dominantG: colors.dominant.g,
dominantB: colors.dominant.b,
brightness: colors.brightness,
hasAlpha: colors.hasAlpha,
// Variants
thumbnailKey: `${key}/thumbnail.webp`,
thumbnailWidth: variants.thumbnail.width,
thumbnailHeight: variants.thumbnail.height,
mediumKey: `${key}/medium.webp`,
mediumWidth: variants.medium.width,
mediumHeight: variants.medium.height,
});
}STANDARD_VARIANTS
The two built-in variant configs:
import { STANDARD_VARIANTS } from '@digibuffer/image-core';
// [
// { name: 'thumbnail', width: 800, height: 800, fit: 'inside', format: 'webp', quality: 90, autoRotate: true },
// { name: 'medium', width: 1600, height: 1600, fit: 'inside', format: 'webp', quality: 85, autoRotate: true },
// ]Pass your own array to generateVariants() to override.
Supported formats
| Input | Output | |---|---| | JPEG, PNG, WebP, AVIF, GIF, TIFF | JPEG, PNG, WebP, AVIF, GIF, TIFF |
EXIF parsing works on JPEG and TIFF. PNG, WebP, AVIF files rarely carry EXIF.
License
Proprietary — All rights reserved.
