Skip to content

Commit 320fe65

Browse files
committed
Merge mozilla master
1 parent 2d1fbf3 commit 320fe65

File tree

9 files changed

+223
-67
lines changed

9 files changed

+223
-67
lines changed

l10n/en-US/viewer.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ pdfjs-editor-free-highlight-thickness-input = Thickness
348348
pdfjs-editor-free-highlight-thickness-title =
349349
.title = Change thickness when highlighting items other than text
350350
351+
# .default-content is used as a placeholder in an empty text editor.
351352
pdfjs-free-text2 =
352353
.aria-label = Text Editor
353354
.default-content = Start typing…

src/core/base_stream.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class BaseStream {
6868
return false;
6969
}
7070

71+
async getTransferableImage() {
72+
return null;
73+
}
74+
7175
peekByte() {
7276
const peekedByte = this.getByte();
7377
if (peekedByte !== -1) {

src/core/image.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,10 @@ class PDFImage {
752752
drawWidth === originalWidth &&
753753
drawHeight === originalHeight
754754
) {
755+
const image = await this.#getImage(originalWidth, originalHeight);
756+
if (image) {
757+
return image;
758+
}
755759
const data = await this.getImageBytes(originalHeight * rowBytes, {});
756760
if (isOffscreenCanvasSupported) {
757761
if (mustBeResized) {
@@ -810,6 +814,10 @@ class PDFImage {
810814
}
811815

812816
if (isHandled) {
817+
const image = await this.#getImage(drawWidth, drawHeight);
818+
if (image) {
819+
return image;
820+
}
813821
const rgba = await this.getImageBytes(imageLength, {
814822
drawWidth,
815823
drawHeight,
@@ -1013,6 +1021,20 @@ class PDFImage {
10131021
};
10141022
}
10151023

1024+
async #getImage(width, height) {
1025+
const bitmap = await this.image.getTransferableImage();
1026+
if (!bitmap) {
1027+
return null;
1028+
}
1029+
return {
1030+
data: null,
1031+
width,
1032+
height,
1033+
bitmap,
1034+
interpolate: this.interpolate,
1035+
};
1036+
}
1037+
10161038
async getImageBytes(
10171039
length,
10181040
{

src/core/jpeg_stream.js

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
* limitations under the License.
1414
*/
1515

16+
import { shadow, warn } from "../shared/util.js";
1617
import { DecodeStream } from "./decode_stream.js";
1718
import { Dict } from "./primitives.js";
1819
import { JpegImage } from "./jpg.js";
19-
import { shadow } from "../shared/util.js";
2020

2121
/**
2222
* For JPEG's we use a library to decode these images and the stream behaves
@@ -32,6 +32,18 @@ class JpegStream extends DecodeStream {
3232
this.params = params;
3333
}
3434

35+
static get canUseImageDecoder() {
36+
return shadow(
37+
this,
38+
"canUseImageDecoder",
39+
// eslint-disable-next-line no-undef
40+
typeof ImageDecoder === "undefined"
41+
? Promise.resolve(false)
42+
: // eslint-disable-next-line no-undef
43+
ImageDecoder.isTypeSupported("image/jpeg")
44+
);
45+
}
46+
3547
get bytes() {
3648
// If `this.maybeLength` is null, we'll get the entire stream.
3749
return shadow(this, "bytes", this.stream.getBytes(this.maybeLength));
@@ -46,22 +58,7 @@ class JpegStream extends DecodeStream {
4658
this.decodeImage();
4759
}
4860

49-
decodeImage(bytes) {
50-
if (this.eof) {
51-
return this.buffer;
52-
}
53-
bytes ||= this.bytes;
54-
55-
// Some images may contain 'junk' before the SOI (start-of-image) marker.
56-
// Note: this seems to mainly affect inline images.
57-
for (let i = 0, ii = bytes.length - 1; i < ii; i++) {
58-
if (bytes[i] === 0xff && bytes[i + 1] === 0xd8) {
59-
if (i > 0) {
60-
bytes = bytes.subarray(i);
61-
}
62-
break;
63-
}
64-
}
61+
get jpegOptions() {
6562
const jpegOptions = {
6663
decodeTransform: undefined,
6764
colorTransform: undefined,
@@ -93,8 +90,34 @@ class JpegStream extends DecodeStream {
9390
jpegOptions.colorTransform = colorTransform;
9491
}
9592
}
96-
const jpegImage = new JpegImage(jpegOptions);
93+
return shadow(this, "jpegOptions", jpegOptions);
94+
}
95+
96+
#skipUselessBytes(data) {
97+
// Some images may contain 'junk' before the SOI (start-of-image) marker.
98+
// Note: this seems to mainly affect inline images.
99+
for (let i = 0, ii = data.length - 1; i < ii; i++) {
100+
if (data[i] === 0xff && data[i + 1] === 0xd8) {
101+
if (i > 0) {
102+
data = data.subarray(i);
103+
}
104+
break;
105+
}
106+
}
107+
return data;
108+
}
109+
110+
decodeImage(bytes) {
111+
if (this.eof) {
112+
return this.buffer;
113+
}
114+
bytes = this.#skipUselessBytes(bytes || this.bytes);
97115

116+
// TODO: if an image has a mask we need to combine the data.
117+
// So ideally get a VideoFrame from getTransferableImage and then use
118+
// copyTo.
119+
120+
const jpegImage = new JpegImage(this.jpegOptions);
98121
jpegImage.parse(bytes);
99122
const data = jpegImage.getData({
100123
width: this.drawWidth,
@@ -113,6 +136,48 @@ class JpegStream extends DecodeStream {
113136
get canAsyncDecodeImageFromBuffer() {
114137
return this.stream.isAsync;
115138
}
139+
140+
async getTransferableImage() {
141+
if (!(await JpegStream.canUseImageDecoder)) {
142+
return null;
143+
}
144+
const jpegOptions = this.jpegOptions;
145+
if (jpegOptions.decodeTransform) {
146+
// TODO: We could decode the image thanks to ImageDecoder and then
147+
// get the pixels with copyTo and apply the decodeTransform.
148+
return null;
149+
}
150+
let decoder;
151+
try {
152+
// TODO: If the stream is Flate & DCT we could try to just pipe the
153+
// the DecompressionStream into the ImageDecoder: it'll avoid the
154+
// intermediate ArrayBuffer.
155+
const bytes =
156+
(this.canAsyncDecodeImageFromBuffer &&
157+
(await this.stream.asyncGetBytes())) ||
158+
this.bytes;
159+
if (!bytes) {
160+
return null;
161+
}
162+
const data = this.#skipUselessBytes(bytes);
163+
if (!JpegImage.canUseImageDecoder(data, jpegOptions.colorTransform)) {
164+
return null;
165+
}
166+
// eslint-disable-next-line no-undef
167+
decoder = new ImageDecoder({
168+
data,
169+
type: "image/jpeg",
170+
preferAnimation: false,
171+
});
172+
173+
return (await decoder.decode()).image;
174+
} catch (reason) {
175+
warn(`getTransferableImage - failed: "${reason}".`);
176+
return null;
177+
} finally {
178+
decoder?.close();
179+
}
180+
}
116181
}
117182

118183
export { JpegStream };

src/core/jpg.js

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -744,55 +744,109 @@ function findNextFileMarker(data, currentPos, startPos = currentPos) {
744744
};
745745
}
746746

747+
function prepareComponents(frame) {
748+
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
749+
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
750+
for (const component of frame.components) {
751+
const blocksPerLine = Math.ceil(
752+
(Math.ceil(frame.samplesPerLine / 8) * component.h) / frame.maxH
753+
);
754+
const blocksPerColumn = Math.ceil(
755+
(Math.ceil(frame.scanLines / 8) * component.v) / frame.maxV
756+
);
757+
const blocksPerLineForMcu = mcusPerLine * component.h;
758+
const blocksPerColumnForMcu = mcusPerColumn * component.v;
759+
760+
const blocksBufferSize =
761+
64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
762+
component.blockData = new Int16Array(blocksBufferSize);
763+
component.blocksPerLine = blocksPerLine;
764+
component.blocksPerColumn = blocksPerColumn;
765+
}
766+
frame.mcusPerLine = mcusPerLine;
767+
frame.mcusPerColumn = mcusPerColumn;
768+
}
769+
770+
function readDataBlock(data, offset) {
771+
const length = readUint16(data, offset);
772+
offset += 2;
773+
let endOffset = offset + length - 2;
774+
775+
const fileMarker = findNextFileMarker(data, endOffset, offset);
776+
if (fileMarker?.invalid) {
777+
warn(
778+
"readDataBlock - incorrect length, current marker is: " +
779+
fileMarker.invalid
780+
);
781+
endOffset = fileMarker.offset;
782+
}
783+
784+
const array = data.subarray(offset, endOffset);
785+
offset += array.length;
786+
return { appData: array, newOffset: offset };
787+
}
788+
789+
function skipData(data, offset) {
790+
const length = readUint16(data, offset);
791+
offset += 2;
792+
const endOffset = offset + length - 2;
793+
794+
const fileMarker = findNextFileMarker(data, endOffset, offset);
795+
if (fileMarker?.invalid) {
796+
return fileMarker.offset;
797+
}
798+
return endOffset;
799+
}
800+
747801
class JpegImage {
748802
constructor({ decodeTransform = null, colorTransform = -1 } = {}) {
749803
this._decodeTransform = decodeTransform;
750804
this._colorTransform = colorTransform;
751805
}
752806

753-
parse(data, { dnlScanLines = null } = {}) {
754-
function readDataBlock() {
755-
const length = readUint16(data, offset);
756-
offset += 2;
757-
let endOffset = offset + length - 2;
758-
759-
const fileMarker = findNextFileMarker(data, endOffset, offset);
760-
if (fileMarker?.invalid) {
761-
warn(
762-
"readDataBlock - incorrect length, current marker is: " +
763-
fileMarker.invalid
764-
);
765-
endOffset = fileMarker.offset;
766-
}
767-
768-
const array = data.subarray(offset, endOffset);
769-
offset += array.length;
770-
return array;
807+
static canUseImageDecoder(data, colorTransform = -1) {
808+
let offset = 0;
809+
let numComponents = null;
810+
let fileMarker = readUint16(data, offset);
811+
offset += 2;
812+
if (fileMarker !== /* SOI (Start of Image) = */ 0xffd8) {
813+
throw new JpegError("SOI not found");
771814
}
815+
fileMarker = readUint16(data, offset);
816+
offset += 2;
772817

773-
function prepareComponents(frame) {
774-
const mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
775-
const mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
776-
for (const component of frame.components) {
777-
const blocksPerLine = Math.ceil(
778-
(Math.ceil(frame.samplesPerLine / 8) * component.h) / frame.maxH
779-
);
780-
const blocksPerColumn = Math.ceil(
781-
(Math.ceil(frame.scanLines / 8) * component.v) / frame.maxV
782-
);
783-
const blocksPerLineForMcu = mcusPerLine * component.h;
784-
const blocksPerColumnForMcu = mcusPerColumn * component.v;
785-
786-
const blocksBufferSize =
787-
64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
788-
component.blockData = new Int16Array(blocksBufferSize);
789-
component.blocksPerLine = blocksPerLine;
790-
component.blocksPerColumn = blocksPerColumn;
818+
markerLoop: while (fileMarker !== /* EOI (End of Image) = */ 0xffd9) {
819+
switch (fileMarker) {
820+
case 0xffc0: // SOF0 (Start of Frame, Baseline DCT)
821+
case 0xffc1: // SOF1 (Start of Frame, Extended DCT)
822+
case 0xffc2: // SOF2 (Start of Frame, Progressive DCT)
823+
// Skip marker length.
824+
// Skip precision.
825+
// Skip scanLines.
826+
// Skip samplesPerLine.
827+
numComponents = data[offset + (2 + 1 + 2 + 2)];
828+
break markerLoop;
829+
case 0xffff: // Fill bytes
830+
if (data[offset] !== 0xff) {
831+
// Avoid skipping a valid marker.
832+
offset--;
833+
}
834+
break;
791835
}
792-
frame.mcusPerLine = mcusPerLine;
793-
frame.mcusPerColumn = mcusPerColumn;
836+
offset = skipData(data, offset);
837+
fileMarker = readUint16(data, offset);
838+
offset += 2;
839+
}
840+
if (numComponents === 4) {
841+
return false;
842+
}
843+
if (numComponents === 3 && colorTransform === 0) {
844+
return false;
794845
}
846+
return true;
847+
}
795848

849+
parse(data, { dnlScanLines = null } = {}) {
796850
let offset = 0;
797851
let jfif = null;
798852
let adobe = null;
@@ -830,7 +884,8 @@ class JpegImage {
830884
case 0xffee: // APP14
831885
case 0xffef: // APP15
832886
case 0xfffe: // COM (Comment)
833-
const appData = readDataBlock();
887+
const { appData, newOffset } = readDataBlock(data, offset);
888+
offset = newOffset;
834889

835890
if (fileMarker === 0xffe0) {
836891
// 'JFIF\x00'

0 commit comments

Comments
 (0)