UNPKG

image-stitch

Version:

Stitch images together efficiently with multi-format support (PNG, JPEG, HEIC), streaming, for node.js and web

176 lines 6.46 kB
/** * PNG Filter Types * Each scanline in a PNG is preceded by a filter type byte */ export var FilterType; (function (FilterType) { FilterType[FilterType["None"] = 0] = "None"; FilterType[FilterType["Sub"] = 1] = "Sub"; FilterType[FilterType["Up"] = 2] = "Up"; FilterType[FilterType["Average"] = 3] = "Average"; FilterType[FilterType["Paeth"] = 4] = "Paeth"; })(FilterType || (FilterType = {})); /** * Paeth predictor function used in PNG filtering */ function paethPredictor(a, b, c) { const p = a + b - c; const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c); if (pa <= pb && pa <= pc) return a; if (pb <= pc) return b; return c; } /** * Unfilter a PNG scanline * @param filterType The filter type byte * @param scanline The filtered scanline (without filter type byte) * @param previousLine The previous unfiltered scanline (or null for first line) * @param bytesPerPixel Number of bytes per pixel */ export function unfilterScanline(filterType, scanline, previousLine, bytesPerPixel) { const result = new Uint8Array(scanline.length); switch (filterType) { case FilterType.None: result.set(scanline); break; case FilterType.Sub: for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? result[i - bytesPerPixel] : 0; result[i] = (scanline[i] + left) & 0xff; } break; case FilterType.Up: for (let i = 0; i < scanline.length; i++) { const up = previousLine ? previousLine[i] : 0; result[i] = (scanline[i] + up) & 0xff; } break; case FilterType.Average: for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? result[i - bytesPerPixel] : 0; const up = previousLine ? previousLine[i] : 0; result[i] = (scanline[i] + Math.floor((left + up) / 2)) & 0xff; } break; case FilterType.Paeth: for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? result[i - bytesPerPixel] : 0; const up = previousLine ? previousLine[i] : 0; const upLeft = previousLine && i >= bytesPerPixel ? previousLine[i - bytesPerPixel] : 0; result[i] = (scanline[i] + paethPredictor(left, up, upLeft)) & 0xff; } break; default: throw new Error(`Unknown filter type: ${filterType}`); } return result; } /** * Apply Sub filter to a scanline */ function filterSub(scanline, bytesPerPixel) { const result = new Uint8Array(scanline.length); for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? scanline[i - bytesPerPixel] : 0; result[i] = (scanline[i] - left) & 0xff; } return result; } /** * Apply Up filter to a scanline */ function filterUp(scanline, previousLine) { const result = new Uint8Array(scanline.length); for (let i = 0; i < scanline.length; i++) { const up = previousLine ? previousLine[i] : 0; result[i] = (scanline[i] - up) & 0xff; } return result; } /** * Apply Average filter to a scanline */ function filterAverage(scanline, previousLine, bytesPerPixel) { const result = new Uint8Array(scanline.length); for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? scanline[i - bytesPerPixel] : 0; const up = previousLine ? previousLine[i] : 0; result[i] = (scanline[i] - Math.floor((left + up) / 2)) & 0xff; } return result; } /** * Apply Paeth filter to a scanline */ function filterPaeth(scanline, previousLine, bytesPerPixel) { const result = new Uint8Array(scanline.length); for (let i = 0; i < scanline.length; i++) { const left = i >= bytesPerPixel ? scanline[i - bytesPerPixel] : 0; const up = previousLine ? previousLine[i] : 0; const upLeft = previousLine && i >= bytesPerPixel ? previousLine[i - bytesPerPixel] : 0; result[i] = (scanline[i] - paethPredictor(left, up, upLeft)) & 0xff; } return result; } /** * Choose the best filter for a scanline and apply it * Uses a simple heuristic: choose the filter that produces the smallest sum of absolute values */ export function filterScanline(scanline, previousLine, bytesPerPixel) { // Try different filters and choose the best one const candidates = [ { type: FilterType.None, data: scanline }, { type: FilterType.Sub, data: filterSub(scanline, bytesPerPixel) }, { type: FilterType.Up, data: filterUp(scanline, previousLine) }, { type: FilterType.Average, data: filterAverage(scanline, previousLine, bytesPerPixel) }, { type: FilterType.Paeth, data: filterPaeth(scanline, previousLine, bytesPerPixel) } ]; // Calculate sum of absolute values for each filter let bestFilter = candidates[0]; let bestSum = Infinity; for (const candidate of candidates) { let sum = 0; for (let i = 0; i < candidate.data.length; i++) { // Treat bytes as signed for better filter selection const signed = candidate.data[i] > 127 ? candidate.data[i] - 256 : candidate.data[i]; sum += Math.abs(signed); } if (sum < bestSum) { bestSum = sum; bestFilter = candidate; } } return { filterType: bestFilter.type, filtered: bestFilter.data }; } /** * Calculate bytes per pixel from PNG header information */ export function getBytesPerPixel(bitDepth, colorType) { // ColorType: 0=grayscale, 2=RGB, 3=palette, 4=grayscale+alpha, 6=RGBA let samplesPerPixel = 1; switch (colorType) { case 0: // Grayscale samplesPerPixel = 1; break; case 2: // RGB samplesPerPixel = 3; break; case 3: // Palette samplesPerPixel = 1; break; case 4: // Grayscale + Alpha samplesPerPixel = 2; break; case 6: // RGBA samplesPerPixel = 4; break; default: throw new Error(`Unknown color type: ${colorType}`); } return Math.ceil((samplesPerPixel * bitDepth) / 8); } //# sourceMappingURL=png-filter.js.map