Merge pull request #21086 from Snuffleupagus/JpegImage-DataView

Re-factor the `JpegImage` class, and related code, to use `DataView`s when reading data
This commit is contained in:
Jonas Jenwald 2026-04-11 17:34:39 +02:00 committed by GitHub
commit 5089cceec7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -17,7 +17,6 @@ import { assert, BaseException, warn } from "../shared/util.js";
import { ColorSpaceUtils } from "./colorspace_utils.js"; import { ColorSpaceUtils } from "./colorspace_utils.js";
import { DeviceCmykCS } from "./colorspace.js"; import { DeviceCmykCS } from "./colorspace.js";
import { grayToRGBA } from "../shared/image_utils.js"; import { grayToRGBA } from "../shared/image_utils.js";
import { readUint16 } from "./core_utils.js";
class JpegError extends BaseException { class JpegError extends BaseException {
constructor(msg) { constructor(msg) {
@ -122,6 +121,7 @@ function getBlockBufferOffset(component, row, col) {
function decodeScan( function decodeScan(
data, data,
view,
offset, offset,
frame, frame,
components, components,
@ -151,7 +151,7 @@ function decodeScan(
if (nextByte === /* DNL = */ 0xdc && parseDNLMarker) { if (nextByte === /* DNL = */ 0xdc && parseDNLMarker) {
offset += 2; // Skip marker length. offset += 2; // Skip marker length.
const scanLines = readUint16(data, offset); const scanLines = view.getUint16(offset);
offset += 2; offset += 2;
if (scanLines > 0 && scanLines !== frame.scanLines) { if (scanLines > 0 && scanLines !== frame.scanLines) {
throw new DNLMarkerError( throw new DNLMarkerError(
@ -434,7 +434,7 @@ function decodeScan(
// find marker // find marker
bitsCount = 0; bitsCount = 0;
fileMarker = findNextFileMarker(data, offset); fileMarker = findNextFileMarker(data, view, offset);
if (!fileMarker) { if (!fileMarker) {
break; // Reached the end of the image data without finding any marker. break; // Reached the end of the image data without finding any marker.
} }
@ -717,14 +717,14 @@ function buildComponentData(frame, component) {
return component.blockData; return component.blockData;
} }
function findNextFileMarker(data, currentPos, startPos = currentPos) { function findNextFileMarker(data, view, currentPos, startPos = currentPos) {
const maxPos = data.length - 1; const maxPos = data.length - 1;
let newPos = startPos < currentPos ? startPos : currentPos; let newPos = startPos < currentPos ? startPos : currentPos;
if (currentPos >= maxPos) { if (currentPos >= maxPos) {
return null; // Don't attempt to read non-existent data and just return. return null; // Don't attempt to read non-existent data and just return.
} }
const currentMarker = readUint16(data, currentPos); const currentMarker = view.getUint16(currentPos);
if (currentMarker >= 0xffc0 && currentMarker <= 0xfffe) { if (currentMarker >= 0xffc0 && currentMarker <= 0xfffe) {
return { return {
invalid: null, invalid: null,
@ -732,12 +732,12 @@ function findNextFileMarker(data, currentPos, startPos = currentPos) {
offset: currentPos, offset: currentPos,
}; };
} }
let newMarker = readUint16(data, newPos); let newMarker = view.getUint16(newPos);
while (!(newMarker >= 0xffc0 && newMarker <= 0xfffe)) { while (!(newMarker >= 0xffc0 && newMarker <= 0xfffe)) {
if (++newPos >= maxPos) { if (++newPos >= maxPos) {
return null; // Don't attempt to read non-existent data and just return. return null; // Don't attempt to read non-existent data and just return.
} }
newMarker = readUint16(data, newPos); newMarker = view.getUint16(newPos);
} }
return { return {
invalid: currentMarker.toString(16), invalid: currentMarker.toString(16),
@ -769,12 +769,12 @@ function prepareComponents(frame) {
frame.mcusPerColumn = mcusPerColumn; frame.mcusPerColumn = mcusPerColumn;
} }
function readDataBlock(data, offset) { function readDataBlock(data, view, offset) {
const length = readUint16(data, offset); const length = view.getUint16(offset);
offset += 2; offset += 2;
let endOffset = offset + length - 2; let endOffset = offset + length - 2;
const fileMarker = findNextFileMarker(data, endOffset, offset); const fileMarker = findNextFileMarker(data, view, endOffset, offset);
if (fileMarker?.invalid) { if (fileMarker?.invalid) {
warn( warn(
"readDataBlock - incorrect length, current marker is: " + "readDataBlock - incorrect length, current marker is: " +
@ -791,12 +791,12 @@ function readDataBlock(data, offset) {
}; };
} }
function skipData(data, offset) { function skipData(data, view, offset) {
const length = readUint16(data, offset); const length = view.getUint16(offset);
offset += 2; offset += 2;
const endOffset = offset + length - 2; const endOffset = offset + length - 2;
const fileMarker = findNextFileMarker(data, endOffset, offset); const fileMarker = findNextFileMarker(data, view, endOffset, offset);
if (fileMarker?.invalid) { if (fileMarker?.invalid) {
return fileMarker.offset; return fileMarker.offset;
} }
@ -810,15 +810,16 @@ class JpegImage {
} }
static canUseImageDecoder(data, colorTransform = -1) { static canUseImageDecoder(data, colorTransform = -1) {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let exifOffsets = null; let exifOffsets = null;
let offset = 0; let offset = 0;
let numComponents = null; let numComponents = null;
let fileMarker = readUint16(data, offset); let fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) { if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
throw new JpegError("SOI not found"); throw new JpegError("SOI not found");
} }
fileMarker = readUint16(data, offset); fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) { markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) {
@ -826,7 +827,11 @@ class JpegImage {
case 0xffe1: // APP1 - Exif case 0xffe1: // APP1 - Exif
// TODO: Remove this once https://github.com/w3c/webcodecs/issues/870 // TODO: Remove this once https://github.com/w3c/webcodecs/issues/870
// is fixed. // is fixed.
const { appData, oldOffset, newOffset } = readDataBlock(data, offset); const { appData, oldOffset, newOffset } = readDataBlock(
data,
view,
offset
);
offset = newOffset; offset = newOffset;
// 'Exif\x00\x00' // 'Exif\x00\x00'
@ -845,7 +850,7 @@ class JpegImage {
// since that can modify the original PDF document. // since that can modify the original PDF document.
exifOffsets = { exifStart: oldOffset + 6, exifEnd: newOffset }; exifOffsets = { exifStart: oldOffset + 6, exifEnd: newOffset };
} }
fileMarker = readUint16(data, offset); fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
continue; continue;
case 0xffc0: // SOF0 (Start of Frame, Baseline DCT) case 0xffc0: // SOF0 (Start of Frame, Baseline DCT)
@ -864,8 +869,8 @@ class JpegImage {
} }
break; break;
} }
offset = skipData(data, offset); offset = skipData(data, view, offset);
fileMarker = readUint16(data, offset); fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
} }
if (numComponents === 4) { if (numComponents === 4) {
@ -878,6 +883,8 @@ class JpegImage {
} }
parse(data, { dnlScanLines = null } = {}) { parse(data, { dnlScanLines = null } = {}) {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const maxOffset = data.length - 1;
let offset = 0; let offset = 0;
let jfif = null; let jfif = null;
let adobe = null; let adobe = null;
@ -887,12 +894,12 @@ class JpegImage {
const huffmanTablesAC = [], const huffmanTablesAC = [],
huffmanTablesDC = []; huffmanTablesDC = [];
let fileMarker = readUint16(data, offset); let fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) { if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
throw new JpegError("SOI not found"); throw new JpegError("SOI not found");
} }
fileMarker = readUint16(data, offset); fileMarker = view.getUint16(offset);
offset += 2; offset += 2;
markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) { markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) {
@ -915,7 +922,7 @@ class JpegImage {
case 0xffee: // APP14 case 0xffee: // APP14
case 0xffef: // APP15 case 0xffef: // APP15
case 0xfffe: // COM (Comment) case 0xfffe: // COM (Comment)
const { appData, newOffset } = readDataBlock(data, offset); const { appData, newOffset } = readDataBlock(data, view, offset);
offset = newOffset; offset = newOffset;
if (fileMarker === 0xffe0) { if (fileMarker === 0xffe0) {
@ -962,7 +969,7 @@ class JpegImage {
break; break;
case 0xffdb: // DQT (Define Quantization Tables) case 0xffdb: // DQT (Define Quantization Tables)
const quantizationTablesLength = readUint16(data, offset); const quantizationTablesLength = view.getUint16(offset);
offset += 2; offset += 2;
const quantizationTablesEnd = quantizationTablesLength + offset - 2; const quantizationTablesEnd = quantizationTablesLength + offset - 2;
let z; let z;
@ -979,7 +986,7 @@ class JpegImage {
// 16 bit values // 16 bit values
for (j = 0; j < 64; j++) { for (j = 0; j < 64; j++) {
z = dctZigZag[j]; z = dctZigZag[j];
tableData[z] = readUint16(data, offset); tableData[z] = view.getUint16(offset);
offset += 2; offset += 2;
} }
} else { } else {
@ -1001,10 +1008,10 @@ class JpegImage {
frame.extended = fileMarker === 0xffc1; frame.extended = fileMarker === 0xffc1;
frame.progressive = fileMarker === 0xffc2; frame.progressive = fileMarker === 0xffc2;
frame.precision = data[offset++]; frame.precision = data[offset++];
const sofScanLines = readUint16(data, offset); const sofScanLines = view.getUint16(offset);
offset += 2; offset += 2;
frame.scanLines = dnlScanLines || sofScanLines; frame.scanLines = dnlScanLines || sofScanLines;
frame.samplesPerLine = readUint16(data, offset); frame.samplesPerLine = view.getUint16(offset);
offset += 2; offset += 2;
frame.components = []; frame.components = [];
frame.componentIds = {}; frame.componentIds = {};
@ -1037,7 +1044,7 @@ class JpegImage {
break; break;
case 0xffc4: // DHT (Define Huffman Tables) case 0xffc4: // DHT (Define Huffman Tables)
const huffmanLength = readUint16(data, offset); const huffmanLength = view.getUint16(offset);
offset += 2; offset += 2;
for (i = 2; i < huffmanLength; ) { for (i = 2; i < huffmanLength; ) {
const huffmanTableSpec = data[offset++]; const huffmanTableSpec = data[offset++];
@ -1061,7 +1068,7 @@ class JpegImage {
case 0xffdd: // DRI (Define Restart Interval) case 0xffdd: // DRI (Define Restart Interval)
offset += 2; // Skip marker length. offset += 2; // Skip marker length.
resetInterval = readUint16(data, offset); resetInterval = view.getUint16(offset);
offset += 2; offset += 2;
break; break;
@ -1092,6 +1099,7 @@ class JpegImage {
try { try {
const processed = decodeScan( const processed = decodeScan(
data, data,
view,
offset, offset,
frame, frame,
components, components,
@ -1133,6 +1141,7 @@ class JpegImage {
// `startPos = offset - 3` when looking for the next valid marker. // `startPos = offset - 3` when looking for the next valid marker.
const nextFileMarker = findNextFileMarker( const nextFileMarker = findNextFileMarker(
data, data,
view,
/* currentPos = */ offset - 2, /* currentPos = */ offset - 2,
/* startPos = */ offset - 3 /* startPos = */ offset - 3
); );
@ -1144,7 +1153,7 @@ class JpegImage {
offset = nextFileMarker.offset; offset = nextFileMarker.offset;
break; break;
} }
if (!nextFileMarker || offset >= data.length - 1) { if (!nextFileMarker || offset >= maxOffset) {
warn( warn(
"JpegImage.parse - reached the end of the image data " + "JpegImage.parse - reached the end of the image data " +
"without finding an EOI marker (0xFFD9)." "without finding an EOI marker (0xFFD9)."
@ -1155,8 +1164,13 @@ class JpegImage {
"JpegImage.parse - unknown marker: " + fileMarker.toString(16) "JpegImage.parse - unknown marker: " + fileMarker.toString(16)
); );
} }
fileMarker = readUint16(data, offset);
offset += 2; if (offset < maxOffset) {
fileMarker = view.getUint16(offset);
offset += 2;
} else {
fileMarker = 0;
}
} }
if (!frame) { if (!frame) {