| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- 'use strict'
- const { Readable } = require('node:stream')
- const {
- createCanvas: _createCanvas,
- Canvas,
- CanvasElement,
- SVGCanvas,
- GlobalFonts,
- Image,
- ImageData,
- Path2D,
- DOMPoint,
- DOMMatrix,
- DOMRect,
- loadImage,
- PDFDocument,
- SvgExportFlag,
- } = require('./index.js')
- // CanvasRenderingContext2D is not re-exported by index.js, grab from native bindings
- const { CanvasRenderingContext2D } = require('./js-binding')
- // node-canvas defaults JPEG quality to 0.75 across all paths.
- // @napi-rs/canvas native default is 0.92 (matching Blink/Chrome).
- const NODE_CANVAS_DEFAULT_QUALITY = 0.75
- // ---------------------------------------------------------------------------
- // Stream classes (node-canvas returns Node.js Readable streams)
- // ---------------------------------------------------------------------------
- class PNGStream extends Readable {
- constructor(canvas, options) {
- super()
- this._canvas = canvas
- this._options = options || {}
- this._done = false
- }
- _read() {
- if (this._done) return
- this._done = true
- this._canvas
- .encode('png')
- .then((buf) => {
- this.push(buf)
- this.push(null)
- })
- .catch((err) => {
- this.destroy(err)
- })
- }
- }
- class JPEGStream extends Readable {
- constructor(canvas, options) {
- super()
- this._canvas = canvas
- const opts = options || {}
- // node-canvas quality: 0-1 (default 0.75), encode() expects 0-100
- this._quality = Math.round((opts.quality != null ? opts.quality : NODE_CANVAS_DEFAULT_QUALITY) * 100)
- this._done = false
- }
- _read() {
- if (this._done) return
- this._done = true
- this._canvas
- .encode('jpeg', this._quality)
- .then((buf) => {
- this.push(buf)
- this.push(null)
- })
- .catch((err) => {
- this.destroy(err)
- })
- }
- }
- // ---------------------------------------------------------------------------
- // Quality normalization helpers
- // ---------------------------------------------------------------------------
- const MIME_FORMAT_MAP = {
- 'image/png': 'png',
- 'image/jpeg': 'jpeg',
- 'image/webp': 'webp',
- 'image/avif': 'avif',
- 'image/gif': 'gif',
- }
- /**
- * Extract quality from a node-canvas config object or number.
- * Returns the raw 0-1 quality value, defaulting to NODE_CANVAS_DEFAULT_QUALITY
- * for JPEG/WebP when not specified.
- *
- * @param {string} mime
- * @param {number|object|undefined} configOrQuality
- * @returns {number|undefined} 0-1 scale for JPEG/WebP, undefined for other mimes
- */
- function _extractQuality(mime, configOrQuality) {
- if (mime !== 'image/jpeg' && mime !== 'image/webp') return undefined
- if (configOrQuality == null) return NODE_CANVAS_DEFAULT_QUALITY
- if (typeof configOrQuality === 'number') return configOrQuality
- if (typeof configOrQuality === 'object' && configOrQuality.quality != null) {
- return configOrQuality.quality
- }
- return NODE_CANVAS_DEFAULT_QUALITY
- }
- // ---------------------------------------------------------------------------
- // Compat methods added to each canvas instance created via this module
- // ---------------------------------------------------------------------------
- // Keep references to the original prototype methods
- const _origToBuffer = CanvasElement.prototype.toBuffer
- const _origToDataURL = CanvasElement.prototype.toDataURL
- function _compatCreatePNGStream(options) {
- return new PNGStream(this, options)
- }
- function _compatCreateJPEGStream(options) {
- return new JPEGStream(this, options)
- }
- /**
- * node-canvas compatible toBuffer:
- * - toBuffer() → PNG (sync)
- * - toBuffer('image/png', config?) → PNG (sync)
- * - toBuffer('image/jpeg', config?) → JPEG (sync, quality 0-1)
- * - toBuffer('raw') → raw pixel data (sync)
- * - toBuffer(callback) → PNG (async)
- * - toBuffer(callback, mime, config?) → specified format (async)
- */
- function _compatToBuffer(mimeOrCallback, configOrQuality) {
- // Callback form: toBuffer(callback) or toBuffer(callback, mime, config)
- if (typeof mimeOrCallback === 'function') {
- const callback = mimeOrCallback
- const mime = typeof configOrQuality === 'string' ? configOrQuality : 'image/png'
- const config = arguments[2]
- if (mime === 'raw') {
- let buf
- try {
- buf = this.data()
- } catch (err) {
- callback(err)
- return
- }
- callback(null, buf)
- return
- }
- const format = MIME_FORMAT_MAP[mime]
- if (!format) {
- callback(new TypeError(`Unsupported MIME type "${mime}". Supported: ${Object.keys(MIME_FORMAT_MAP).join(', ')}`))
- return
- }
- const q = _extractQuality(mime, config)
- this.encode(format, q != null ? Math.round(q * 100) : undefined).then(
- (buf) => callback(null, buf),
- (err) => callback(err),
- )
- return
- }
- // Sync: no args → PNG
- if (mimeOrCallback === undefined) {
- return _origToBuffer.call(this, 'image/png')
- }
- // Sync: raw pixel data
- if (mimeOrCallback === 'raw') {
- return this.data()
- }
- // Sync: extract quality (0-1) and scale to 0-100 for native toBuffer
- const q = _extractQuality(mimeOrCallback, configOrQuality)
- return _origToBuffer.call(this, mimeOrCallback, q != null ? Math.round(q * 100) : undefined)
- }
- /**
- * node-canvas compatible toDataURL:
- * - toDataURL() → PNG data URL (sync)
- * - toDataURL('image/jpeg', quality) → JPEG data URL, quality 0-1 (sync)
- * - toDataURL(callback) → PNG data URL (async)
- * - toDataURL(mime, callback) → data URL (async)
- * - toDataURL(mime, quality, callback) → data URL with quality (async)
- */
- function _compatToDataURL(mimeOrCallback, qualityOrCallback) {
- // toDataURL(callback)
- if (typeof mimeOrCallback === 'function') {
- this.toDataURLAsync('image/png').then(
- (url) => mimeOrCallback(null, url),
- (err) => mimeOrCallback(err),
- )
- return
- }
- // toDataURL(mime, callback)
- if (typeof qualityOrCallback === 'function') {
- this.toDataURLAsync(mimeOrCallback, _extractQuality(mimeOrCallback, undefined)).then(
- (url) => qualityOrCallback(null, url),
- (err) => qualityOrCallback(err),
- )
- return
- }
- // toDataURL(mime, quality, callback)
- const cb = arguments[2]
- if (typeof cb === 'function') {
- // Native toDataURLAsync expects 0-1 (f64) — pass quality through as-is
- const quality = _extractQuality(mimeOrCallback, qualityOrCallback)
- this.toDataURLAsync(mimeOrCallback, quality).then(
- (url) => cb(null, url),
- (err) => cb(err),
- )
- return
- }
- // Sync form: native toDataURL expects 0-1 (f64) — pass quality through as-is
- const quality = _extractQuality(mimeOrCallback, qualityOrCallback)
- return _origToDataURL.call(this, mimeOrCallback, quality)
- }
- /**
- * Attach node-canvas compatible methods to a CanvasElement instance.
- */
- function _addCompatMethods(canvas) {
- canvas.createPNGStream = _compatCreatePNGStream
- canvas.createJPEGStream = _compatCreateJPEGStream
- canvas.toBuffer = _compatToBuffer
- canvas.toDataURL = _compatToDataURL
- return canvas
- }
- // ---------------------------------------------------------------------------
- // Public API: registerFont / deregisterAllFonts
- // ---------------------------------------------------------------------------
- // Track FontKeys from registerFont() so deregisterAllFonts() can remove
- // only user-registered fonts (matching node-canvas behavior which leaves
- // system fonts untouched).
- const _registeredFontKeys = []
- /**
- * Register a font file with the specified font face properties.
- * Compatible with node-canvas's registerFont(path, { family, weight?, style? }).
- *
- * Note: @napi-rs/canvas auto-detects weight and style from font file metadata
- * (matching browser behavior). The weight and style properties in fontFace are
- * accepted for API compatibility but the actual values are read from the font.
- *
- * @param {string} path Absolute path to the font file (.ttf, .otf, etc.)
- * @param {{ family: string, weight?: string, style?: string }} fontFace
- */
- function registerFont(path, fontFace) {
- if (!fontFace || typeof fontFace.family !== 'string') {
- throw new TypeError('registerFont requires a fontFace with a "family" property')
- }
- const key = GlobalFonts.registerFromPath(path, fontFace.family)
- if (!key) {
- throw new Error(`Failed to register font from "${path}" with family "${fontFace.family}"`)
- }
- _registeredFontKeys.push(key)
- }
- /**
- * Deregister all fonts previously registered via registerFont().
- * Compatible with node-canvas's deregisterAllFonts() which only removes
- * user-registered fonts and leaves system fonts untouched.
- */
- function deregisterAllFonts() {
- if (_registeredFontKeys.length > 0) {
- GlobalFonts.removeBatch(_registeredFontKeys)
- _registeredFontKeys.length = 0
- }
- }
- // ---------------------------------------------------------------------------
- // Public API: createCanvas / createImageData
- // ---------------------------------------------------------------------------
- /**
- * Create a new canvas instance with node-canvas compatible API.
- *
- * @param {number} width
- * @param {number} height
- * @param {'image'|'svg'} [type='image']
- * @returns {Canvas}
- */
- function createCanvas(width, height, type) {
- if (type === 'svg') {
- // SvgExportFlag enum requires a valid variant; NoPrettyXML (0x02) is the
- // least impactful on rendering behavior (only affects XML whitespace).
- return _createCanvas(width, height, SvgExportFlag.NoPrettyXML)
- }
- if (type === 'pdf') {
- throw new Error(
- 'createCanvas with type "pdf" is not supported. Use the PDFDocument class from @napi-rs/canvas directly.',
- )
- }
- if (type != null && type !== 'image') {
- throw new TypeError(`createCanvas: unknown type "${type}". Supported types: "image", "svg".`)
- }
- return _addCompatMethods(_createCanvas(width, height))
- }
- /**
- * Create an ImageData instance.
- * Compatible with node-canvas's createImageData().
- *
- * @param {Uint8ClampedArray|number} dataOrWidth
- * @param {number} widthOrHeight
- * @param {number} [height]
- * @returns {ImageData}
- */
- function createImageData(dataOrWidth, widthOrHeight, height) {
- if (typeof dataOrWidth === 'number') {
- return new ImageData(dataOrWidth, widthOrHeight)
- }
- if (height != null) {
- return new ImageData(dataOrWidth, widthOrHeight, height)
- }
- return new ImageData(dataOrWidth, widthOrHeight)
- }
- // ---------------------------------------------------------------------------
- // Canvas constructor wrapper
- // ---------------------------------------------------------------------------
- // In node-canvas, `new Canvas(w, h)` and `createCanvas(w, h)` produce
- // equivalent canvases. Wrap the native Canvas constructor so that
- // `new Canvas(w, h)` also gets compat methods, without polluting the
- // prototype (which would affect @napi-rs/canvas users who don't use
- // this compat layer).
- const CompatCanvas = new Proxy(Canvas, {
- construct(target, args) {
- const canvas = new target(...args)
- if (canvas instanceof CanvasElement) {
- return _addCompatMethods(canvas)
- }
- return canvas
- },
- })
- // ---------------------------------------------------------------------------
- // Exports (matches node-canvas export shape)
- // ---------------------------------------------------------------------------
- module.exports = {
- // Factory functions
- Canvas: CompatCanvas,
- createCanvas,
- createImageData,
- loadImage,
- registerFont,
- deregisterAllFonts,
- // Classes
- Image,
- ImageData,
- CanvasRenderingContext2D,
- Context2d: CanvasRenderingContext2D,
- PNGStream,
- JPEGStream,
- Path2D,
- // Geometry
- DOMPoint,
- DOMMatrix,
- DOMRect,
- // @napi-rs/canvas extras (available but not part of node-canvas)
- GlobalFonts,
- PDFDocument,
- CanvasElement,
- SVGCanvas,
- }
|