(function attachPyroBitmapModel(root) {
  'use strict';

  const modes = Object.freeze(['placed', 'direct']);
  const interpolations = Object.freeze(['bicubic', 'bilinear', 'nearest']);
  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
  const close = (a, b, tolerance = .001) => Math.abs(finite(a) - finite(b)) <= tolerance;
  const validDpi = (value) => Number.isFinite(Number(value)) && Number(value) >= 1 && Number(value) <= 2400;

  function modeFor(object = {}) {
    if (modes.includes(object.bitmapMode)) return object.bitmapMode;
    const width = Math.max(1, finite(object.width, 1)); const height = Math.max(1, finite(object.height, 1));
    const pixelWidth = Math.max(1, Math.round(finite(object.pixelWidth, width))); const pixelHeight = Math.max(1, Math.round(finite(object.pixelHeight, height)));
    return close(width, pixelWidth) && close(height, pixelHeight) ? 'direct' : 'placed';
  }

  function interpolationFor(object = {}) {
    return interpolations.includes(object.bitmapInterpolation) ? object.bitmapInterpolation : 'bicubic';
  }

  function normalizedFields(object = {}, fallbackDpi = 72) {
    const result = { bitmapMode: modeFor(object), bitmapInterpolation: interpolationFor(object) };
    if (validDpi(object.sourceDpiX)) result.sourceDpiX = Math.round(Number(object.sourceDpiX) * 100) / 100;
    if (validDpi(object.sourceDpiY)) result.sourceDpiY = Math.round(Number(object.sourceDpiY) * 100) / 100;
    if (!result.sourceDpiX && validDpi(object.sourceDpi)) result.sourceDpiX = Math.round(Number(object.sourceDpi) * 100) / 100;
    if (!result.sourceDpiY && validDpi(object.sourceDpi)) result.sourceDpiY = Math.round(Number(object.sourceDpi) * 100) / 100;
    if (object.bitmapMode === 'direct') {
      if (!result.sourceDpiX && validDpi(fallbackDpi)) result.sourceDpiX = Number(fallbackDpi);
      if (!result.sourceDpiY && validDpi(fallbackDpi)) result.sourceDpiY = Number(fallbackDpi);
    }
    return result;
  }

  function pixelScale(object = {}) {
    const width = Math.max(.0001, finite(object.width, 1)); const height = Math.max(.0001, finite(object.height, 1));
    const pixelWidth = Math.max(1, Math.round(finite(object.pixelWidth, width))); const pixelHeight = Math.max(1, Math.round(finite(object.pixelHeight, height)));
    return { x: width / pixelWidth, y: height / pixelHeight, pixelWidth, pixelHeight };
  }

  function needsLocalGrid(object = {}) {
    const scale = pixelScale(object);
    return modeFor(object) === 'placed' || !close(scale.x, 1) || !close(scale.y, 1)
      || !close(finite(object.rotation) % 360, 0) || Boolean(object.flipX || object.flipY)
      || !close(finite(object.x), Math.round(finite(object.x))) || !close(finite(object.y), Math.round(finite(object.y)));
  }

  function targetPixelSize(object = {}) {
    return {
      width: clamp(Math.round(Math.abs(finite(object.width, 1))), 1, 100000),
      height: clamp(Math.round(Math.abs(finite(object.height, 1))), 1, 100000)
    };
  }

  function placementSize(pixelWidth, pixelHeight, sourceDpi, documentDpi, choice = 'pixels') {
    const width = Math.max(1, Math.round(finite(pixelWidth, 1))); const height = Math.max(1, Math.round(finite(pixelHeight, 1)));
    if (choice !== 'physical' || !validDpi(sourceDpi?.x) || !validDpi(sourceDpi?.y) || !validDpi(documentDpi)) return { width, height };
    return {
      width: Math.max(.01, width * Number(documentDpi) / Number(sourceDpi.x)),
      height: Math.max(.01, height * Number(documentDpi) / Number(sourceDpi.y))
    };
  }

  function canvasSmoothing(interpolation) {
    const normalized = interpolations.includes(interpolation) ? interpolation : 'bicubic';
    return normalized === 'nearest' ? { enabled: false, quality: 'low' }
      : normalized === 'bilinear' ? { enabled: true, quality: 'low' }
        : { enabled: true, quality: 'high' };
  }

  function alphaBounds(pixels, width, height, threshold = 0) {
    const columns = Math.max(0, Math.round(finite(width))); const rows = Math.max(0, Math.round(finite(height)));
    if (!pixels || columns < 1 || rows < 1 || pixels.length < columns * rows * 4) return null;
    const minimumAlpha = clamp(Math.round(finite(threshold)), 0, 254);
    let left = columns; let top = rows; let right = -1; let bottom = -1;
    for (let y = 0; y < rows; y += 1) {
      for (let x = 0; x < columns; x += 1) {
        if (pixels[(y * columns + x) * 4 + 3] <= minimumAlpha) continue;
        left = Math.min(left, x); top = Math.min(top, y); right = Math.max(right, x); bottom = Math.max(bottom, y);
      }
    }
    return right < left || bottom < top ? null : { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
  }

  function trimmedPlacement(object = {}, sourceWidth, sourceHeight, bounds = {}) {
    const pixelsWide = Math.max(1, Math.round(finite(sourceWidth, 1))); const pixelsHigh = Math.max(1, Math.round(finite(sourceHeight, 1)));
    const x = clamp(Math.round(finite(bounds.x)), 0, pixelsWide - 1); const y = clamp(Math.round(finite(bounds.y)), 0, pixelsHigh - 1);
    const croppedWidth = clamp(Math.round(finite(bounds.width, pixelsWide - x)), 1, pixelsWide - x);
    const croppedHeight = clamp(Math.round(finite(bounds.height, pixelsHigh - y)), 1, pixelsHigh - y);
    const oldWidth = Math.max(.0001, finite(object.width, 1)); const oldHeight = Math.max(.0001, finite(object.height, 1));
    const scaleX = oldWidth / pixelsWide; const scaleY = oldHeight / pixelsHigh;
    const width = croppedWidth * scaleX; const height = croppedHeight * scaleY;
    const deltaX = (x + croppedWidth / 2) * scaleX - oldWidth / 2;
    const deltaY = (y + croppedHeight / 2) * scaleY - oldHeight / 2;
    const flippedX = object.flipX ? -deltaX : deltaX; const flippedY = object.flipY ? -deltaY : deltaY;
    const radians = finite(object.rotation) * Math.PI / 180; const cosine = Math.cos(radians); const sine = Math.sin(radians);
    const centerX = finite(object.x) + oldWidth / 2 + cosine * flippedX - sine * flippedY;
    const centerY = finite(object.y) + oldHeight / 2 + sine * flippedX + cosine * flippedY;
    const result = { x: centerX - width / 2, y: centerY - height / 2, width, height };
    if (object.transformPivot && typeof object.transformPivot === 'object') {
      result.transformPivot = {
        x: (finite(object.transformPivot.x, .5) * oldWidth - x * scaleX) / width,
        y: (finite(object.transformPivot.y, .5) * oldHeight - y * scaleY) / height
      };
    }
    return result;
  }

  const ascii = (bytes, offset, length) => String.fromCharCode(...bytes.slice(offset, offset + length));

  function pngDpi(bytes) {
    const signature = [137, 80, 78, 71, 13, 10, 26, 10];
    if (bytes.length < 8 || signature.some((value, index) => bytes[index] !== value)) return null;
    let offset = 8;
    while (offset + 12 <= bytes.length) {
      const length = new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0); const end = offset + 12 + length;
      if (end > bytes.length) return null;
      if (ascii(bytes, offset + 4, 4) === 'pHYs' && length === 9) {
        const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 8, 9); if (view.getUint8(8) !== 1) return null;
        return { x: view.getUint32(0) * .0254, y: view.getUint32(4) * .0254, source: 'png-phys' };
      }
      offset = end;
    }
    return null;
  }

  function tiffMetadata(bytes, tiffOffset = 0) {
    if (tiffOffset < 0 || tiffOffset + 8 > bytes.length) return null;
    const order = ascii(bytes, tiffOffset, 2); const little = order === 'II'; if (!little && order !== 'MM') return null;
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const u16 = (offset) => offset + 2 <= bytes.length ? view.getUint16(offset, little) : NaN;
    const u32 = (offset) => offset + 4 <= bytes.length ? view.getUint32(offset, little) : NaN;
    if (u16(tiffOffset + 2) !== 42) return null;
    const directory = tiffOffset + u32(tiffOffset + 4); if (!Number.isFinite(directory) || directory + 2 > bytes.length) return null;
    const count = Math.min(4096, u16(directory)); let x = null; let y = null; let unit = 2; let orientation = null;
    const rational = (entry) => {
      const position = tiffOffset + u32(entry + 8); if (!Number.isFinite(position) || position + 8 > bytes.length) return null;
      const numerator = u32(position); const denominator = u32(position + 4); return denominator ? numerator / denominator : null;
    };
    for (let index = 0; index < count; index += 1) {
      const entry = directory + 2 + index * 12; if (entry + 12 > bytes.length) break;
      const tag = u16(entry); const type = u16(entry + 2); const values = u32(entry + 4);
      if (tag === 0x011a && type === 5 && values >= 1) x = rational(entry);
      else if (tag === 0x011b && type === 5 && values >= 1) y = rational(entry);
      else if (tag === 0x0128 && type === 3 && values >= 1) unit = u16(entry + 8);
      else if (tag === 0x0112 && type === 3 && values >= 1) {
        const value = u16(entry + 8); if (value >= 1 && value <= 8) orientation = value;
      }
    }
    let dpi = null;
    if (validDpi(x) || validDpi(y)) {
      x = validDpi(x) ? x : y; y = validDpi(y) ? y : x;
      if (unit === 3) { x *= 2.54; y *= 2.54; }
      if (validDpi(x) && validDpi(y)) dpi = { x, y, source: 'exif' };
    }
    return { orientation, dpi };
  }

  const tiffDpi = (bytes, tiffOffset = 0) => tiffMetadata(bytes, tiffOffset)?.dpi || null;

  function jpegDpi(bytes) {
    if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null;
    let offset = 2; let jfif = null;
    while (offset + 4 <= bytes.length) {
      if (bytes[offset] !== 0xff) { offset += 1; continue; }
      const marker = bytes[offset + 1]; offset += 2;
      if (marker === 0xd9 || marker === 0xda) break;
      if (marker >= 0xd0 && marker <= 0xd7) continue;
      if (offset + 2 > bytes.length) break;
      const length = new DataView(bytes.buffer, bytes.byteOffset + offset, 2).getUint16(0); const data = offset + 2; const end = offset + length;
      if (length < 2 || end > bytes.length) break;
      if (marker === 0xe1 && length >= 14 && ascii(bytes, data, 6) === 'Exif\0\0') {
        const exif = tiffDpi(bytes, data + 6); if (exif) return exif;
      }
      if (marker === 0xe0 && length >= 16 && ascii(bytes, data, 5) === 'JFIF\0') {
        const unit = bytes[data + 7]; const view = new DataView(bytes.buffer, bytes.byteOffset + data + 8, 4);
        let x = view.getUint16(0); let y = view.getUint16(2);
        if (unit === 2) { x *= 2.54; y *= 2.54; }
        if ((unit === 1 || unit === 2) && validDpi(x) && validDpi(y)) jfif = { x, y, source: 'jpeg-jfif' };
      }
      offset = end;
    }
    return jfif;
  }

  function webpDpi(bytes) {
    if (bytes.length < 20 || ascii(bytes, 0, 4) !== 'RIFF' || ascii(bytes, 8, 4) !== 'WEBP') return null;
    let offset = 12;
    while (offset + 8 <= bytes.length) {
      const type = ascii(bytes, offset, 4); const length = new DataView(bytes.buffer, bytes.byteOffset + offset + 4, 4).getUint32(0, true); const data = offset + 8;
      if (data + length > bytes.length) break;
      if (type === 'EXIF') {
        const header = ascii(bytes, data, Math.min(6, length)); const result = tiffDpi(bytes, header === 'Exif\0\0' ? data + 6 : data);
        if (result) return result;
      }
      offset = data + length + (length % 2);
    }
    return null;
  }

  function pngMetadata(bytes) {
    const signature = [137, 80, 78, 71, 13, 10, 26, 10];
    if (bytes.length < 8 || signature.some((value, index) => bytes[index] !== value)) return null;
    let offset = 8; let orientation = null; let colorProfile = null;
    while (offset + 12 <= bytes.length) {
      const length = new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0); const data = offset + 8; const end = offset + 12 + length;
      if (end > bytes.length) break;
      const type = ascii(bytes, offset + 4, 4);
      if (type === 'eXIf') {
        const metadata = tiffMetadata(bytes, data); if (metadata?.orientation) orientation = metadata.orientation;
      } else if (type === 'iCCP') colorProfile = 'icc';
      else if (type === 'sRGB' && colorProfile !== 'icc') colorProfile = 'srgb';
      else if (type === 'gAMA' && !colorProfile) colorProfile = 'gamma';
      offset = end;
    }
    return { orientation, colorProfile };
  }

  function jpegMetadata(bytes) {
    if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return null;
    let offset = 2; let orientation = null; let colorProfile = null;
    while (offset + 4 <= bytes.length) {
      if (bytes[offset] !== 0xff) { offset += 1; continue; }
      const marker = bytes[offset + 1]; offset += 2;
      if (marker === 0xd9 || marker === 0xda) break;
      if (marker >= 0xd0 && marker <= 0xd7) continue;
      if (offset + 2 > bytes.length) break;
      const length = new DataView(bytes.buffer, bytes.byteOffset + offset, 2).getUint16(0); const data = offset + 2; const end = offset + length;
      if (length < 2 || end > bytes.length) break;
      if (marker === 0xe1 && length >= 14 && ascii(bytes, data, 6) === 'Exif\0\0') {
        const metadata = tiffMetadata(bytes, data + 6); if (metadata?.orientation) orientation = metadata.orientation;
      } else if (marker === 0xe2 && length >= 16 && ascii(bytes, data, 12) === 'ICC_PROFILE\0') colorProfile = 'icc';
      offset = end;
    }
    return { orientation, colorProfile };
  }

  function webpMetadata(bytes) {
    if (bytes.length < 20 || ascii(bytes, 0, 4) !== 'RIFF' || ascii(bytes, 8, 4) !== 'WEBP') return null;
    let offset = 12; let orientation = null; let colorProfile = null;
    while (offset + 8 <= bytes.length) {
      const type = ascii(bytes, offset, 4); const length = new DataView(bytes.buffer, bytes.byteOffset + offset + 4, 4).getUint32(0, true); const data = offset + 8;
      if (data + length > bytes.length) break;
      if (type === 'EXIF') {
        const header = ascii(bytes, data, Math.min(6, length));
        const metadata = tiffMetadata(bytes, header === 'Exif\0\0' ? data + 6 : data);
        if (metadata?.orientation) orientation = metadata.orientation;
      } else if (type === 'ICCP') colorProfile = 'icc';
      offset = data + length + (length % 2);
    }
    return { orientation, colorProfile };
  }

  function imageMetadata(buffer) {
    const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || 0);
    const result = pngMetadata(bytes) || jpegMetadata(bytes) || webpMetadata(bytes);
    return result || { orientation: null, colorProfile: null };
  }

  function pngHasTransparency(bytes) {
    const signature = [137, 80, 78, 71, 13, 10, 26, 10];
    if (bytes.length < 8 || signature.some((value, index) => bytes[index] !== value)) return false;
    let offset = 8;
    while (offset + 12 <= bytes.length) {
      const length = new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0); const data = offset + 8; const end = offset + 12 + length;
      if (end > bytes.length) return false;
      const type = ascii(bytes, offset + 4, 4);
      if (type === 'IHDR' && length === 13 && (bytes[data + 9] === 4 || bytes[data + 9] === 6)) return true;
      if (type === 'tRNS') return true;
      offset = end;
    }
    return false;
  }

  function gifHasTransparency(bytes) {
    if (bytes.length < 6 || !['GIF87a', 'GIF89a'].includes(ascii(bytes, 0, 6))) return false;
    for (let offset = 6; offset + 7 < bytes.length; offset += 1) {
      if (bytes[offset] === 0x21 && bytes[offset + 1] === 0xf9 && bytes[offset + 2] === 4 && (bytes[offset + 3] & 1)) return true;
    }
    return false;
  }

  function webpHasTransparency(bytes) {
    if (bytes.length < 20 || ascii(bytes, 0, 4) !== 'RIFF' || ascii(bytes, 8, 4) !== 'WEBP') return false;
    let offset = 12;
    while (offset + 8 <= bytes.length) {
      const type = ascii(bytes, offset, 4); const length = new DataView(bytes.buffer, bytes.byteOffset + offset + 4, 4).getUint32(0, true); const data = offset + 8;
      if (data + length > bytes.length) return false;
      if (type === 'ALPH' || (type === 'VP8X' && length >= 1 && (bytes[data] & 0x10))) return true;
      if (type === 'VP8L' && length >= 5 && bytes[data] === 0x2f) {
        const packed = new DataView(bytes.buffer, bytes.byteOffset + data + 1, 4).getUint32(0, true);
        if (packed & 0x10000000) return true;
      }
      offset = data + length + (length % 2);
    }
    return false;
  }

  function imageHasTransparency(buffer) {
    const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || 0);
    return pngHasTransparency(bytes) || gifHasTransparency(bytes) || webpHasTransparency(bytes);
  }

  function imageDpi(buffer) {
    const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || 0);
    const result = pngDpi(bytes) || jpegDpi(bytes) || webpDpi(bytes);
    if (!result || !validDpi(result.x) || !validDpi(result.y)) return null;
    return { x: Math.round(result.x * 100) / 100, y: Math.round(result.y * 100) / 100, source: result.source };
  }

  const api = Object.freeze({
    modes, interpolations, modeFor, interpolationFor, normalizedFields, pixelScale, needsLocalGrid,
    targetPixelSize, placementSize, canvasSmoothing, alphaBounds, trimmedPlacement, imageDpi, imageHasTransparency, imageMetadata
  });
  root.PyroBitmapModel = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
