All files / metadata/src/utilities/metadataProvider pixelDataUpdate.ts

41.25% Statements 33/80
30.13% Branches 22/73
80% Functions 4/5
44.59% Lines 33/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187                          18x             18x       18x 18x 18x 18x                                                                                     18x 18x 18x 18x 18x 18x             18x 18x 9x   9x                                                                                   135x 135x     135x               135x   135x         6x         6x         6x             135x 21x 21x   21x     21x 21x     21x         135x       384x    
/**
 * Converts the palette data information into
 * an array when it is present as an array buffer, typed array, array of buffers, or InlineBinary.
 */
 
import { MetadataModules } from '../../enums';
import { addTypedProvider } from '../../metaData';
import { getSingleBufferFromArray } from '../bulkDataFromArray';
 
function normalizePaletteLUT(raw: unknown): {
  view: Uint8Array | Uint16Array;
  byteLength: number;
} {
  Iif (raw instanceof ArrayBuffer) {
    const len = raw.byteLength;
    return {
      view: len <= 256 ? new Uint8Array(raw) : new Uint16Array(raw),
      byteLength: len,
    };
  }
  Iif (ArrayBuffer.isView(raw)) {
    const v = raw as Uint8Array | Uint16Array;
    return { view: v, byteLength: v.byteLength };
  }
  Eif (Array.isArray(raw)) {
    const view = getSingleBufferFromArray(raw);
    Eif (view) {
      return {
        view: view as Uint8Array | Uint16Array,
        byteLength: view.byteLength,
      };
    }
  }
  const inline =
    raw != null &&
    typeof raw === 'object' &&
    'InlineBinary' in (raw as Record<string, unknown>) &&
    typeof (raw as { InlineBinary?: string }).InlineBinary === 'string';
  if (inline) {
    const b64 = (raw as { InlineBinary: string }).InlineBinary;
    const binary = atob(b64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return {
      view: bytes,
      byteLength: bytes.byteLength,
    };
  }
  const desc = describeValue(raw);
  throw new Error(
    'Palette color lookup table data could not be normalized: expected ArrayBuffer, ArrayBufferView, or object with InlineBinary string. ' +
      desc
  );
}
 
/**
 * Normalizes raw palette LUT data to the final typed array using the descriptor.
 * Handles validation and 8-bit vs 16-bit conversion in one place.
 * @param raw - Raw LUT data (ArrayBuffer, view, array of buffers, or InlineBinary)
 * @param descriptor - [numEntries, firstMappedValue, bitsPerEntry]
 * @param color - Channel name for error messages ('red' | 'green' | 'blue')
 * @returns Uint8Array or Uint16Array ready to assign to the module
 */
function normalizePaletteLUTToFinal(
  raw: unknown,
  descriptor: number[],
  color: 'red' | 'green' | 'blue'
): Uint8Array | Uint16Array {
  descriptor[0] ||= 65536;
  const tableLen = descriptor[0];
  const bits = descriptor[2] ?? 16;
  const { view, byteLength } = normalizePaletteLUT(raw);
  const expectedByteLengths = [tableLen, tableLen * 2];
  Iif (!expectedByteLengths.includes(byteLength)) {
    const actualEntries =
      byteLength === tableLen ? view.length : Math.floor(byteLength / 2);
    throw new Error(
      `Palette color lookup table length mismatch (${color}): descriptor has ${tableLen} entries (expected byteLength ${tableLen} or ${tableLen * 2}), but got ${byteLength} bytes (${actualEntries} effective entries). This may indicate duplicated or concatenated buffer data from the natural filter.`
    );
  }
  const use8 = tableLen === byteLength;
  if (use8) {
    return view instanceof Uint8Array ? view : new Uint8Array(view);
  }
  return view instanceof Uint16Array
    ? view
    : new Uint16Array(view.buffer, view.byteOffset, view.byteLength / 2);
}
 
function describeValue(raw: unknown): string {
  if (raw === null) return 'Got null.';
  if (raw === undefined) return 'Got undefined.';
  if (typeof raw !== 'object') return `Got primitive: ${typeof raw}.`;
  const obj = raw as Record<string, unknown>;
  const constructorName =
    obj.constructor != null && typeof obj.constructor === 'function'
      ? (obj.constructor as Function).name
      : 'unknown';
  const keys = Object.keys(obj);
  const preview: Record<string, string> = {};
  for (const k of keys) {
    const v = obj[k];
    if (v === null) preview[k] = 'null';
    else if (v === undefined) preview[k] = 'undefined';
    else if (typeof v === 'string')
      preview[k] =
        v.length > 80
          ? `"${v.slice(0, 80)}..." (len=${v.length})`
          : JSON.stringify(v);
    else if (Array.isArray(v))
      preview[k] =
        `Array(${v.length})${v.length > 0 ? ` e.g. ${JSON.stringify(v[0])}` : ''}`;
    else if (v instanceof ArrayBuffer)
      preview[k] = `ArrayBuffer(${v.byteLength})`;
    else if (ArrayBuffer.isView(v))
      preview[k] =
        `${(v as object).constructor?.name ?? 'ArrayBufferView'}(${(v as ArrayBufferView).byteLength})`;
    else preview[k] = typeof v;
  }
  return `Got object: constructor=${constructorName}, keys=[${keys.join(', ')}], preview=${JSON.stringify(preview)}.`;
}
 
/**
 * Converts pixel data to the appropriate array type
 */
export function pixelDataUpdate(next, query, data, options) {
  const basePixelData = next(query, data, options);
  Iif (!basePixelData) {
    return basePixelData;
  }
  const result = { ...basePixelData };
  const {
    redPaletteColorLookupTableData,
    greenPaletteColorLookupTableData,
    bluePaletteColorLookupTableData,
    pixelPaddingValue,
    pixelPaddingRangeLimit,
    pixelRepresentation,
  } = basePixelData;
 
  if (
    redPaletteColorLookupTableData != null &&
    greenPaletteColorLookupTableData != null &&
    bluePaletteColorLookupTableData != null
  ) {
    result.redPaletteColorLookupTableData = normalizePaletteLUTToFinal(
      redPaletteColorLookupTableData,
      result.redPaletteColorLookupTableDescriptor,
      'red'
    );
    result.greenPaletteColorLookupTableData = normalizePaletteLUTToFinal(
      greenPaletteColorLookupTableData,
      result.greenPaletteColorLookupTableDescriptor,
      'green'
    );
    result.bluePaletteColorLookupTableData = normalizePaletteLUTToFinal(
      bluePaletteColorLookupTableData,
      result.bluePaletteColorLookupTableDescriptor,
      'blue'
    );
  }
 
  if (pixelRepresentation == 1) {
    Eif (pixelPaddingValue < 0) {
      result.pixelPaddingValue = pixelPaddingValue & 0xffff;
    }
    Iif (pixelPaddingRangeLimit < 0) {
      result.pixelPaddingValue = pixelPaddingRangeLimit & 0xffff;
    }
    const { smallestPixelValue, largestPixelValue } = result;
    Iif (smallestPixelValue < 0) {
      result.smallestPixelValue = smallestPixelValue & 0xffff;
    }
    Iif (largestPixelValue < 0) {
      result.largestPixelValue = largestPixelValue & 0xffff;
    }
  }
 
  return result;
}
 
export function registerPixelDataUpdate() {
  addTypedProvider(MetadataModules.IMAGE_PIXEL, pixelDataUpdate);
}