[Editor] Add support for printing/saving newly added Stamp annotations
In order to minimize the size the of a saved pdf, we generate only one image and use a reference in each annotation using it. When printing, it's slightly different since we have to render each page independantly but we use the same image within a page.
This commit is contained in:
parent
ccb72073b0
commit
599b9498f2
@ -23,6 +23,7 @@ import {
|
||||
AnnotationType,
|
||||
assert,
|
||||
BASELINE_FACTOR,
|
||||
FeatureTest,
|
||||
getModificationDate,
|
||||
IDENTITY_MATRIX,
|
||||
LINE_DESCENT_FACTOR,
|
||||
@ -52,15 +53,16 @@ import {
|
||||
parseDefaultAppearance,
|
||||
} from "./default_appearance.js";
|
||||
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
||||
import { Stream, StringStream } from "./stream.js";
|
||||
import { writeDict, writeObject } from "./writer.js";
|
||||
import { BaseStream } from "./base_stream.js";
|
||||
import { bidi } from "./bidi.js";
|
||||
import { Catalog } from "./catalog.js";
|
||||
import { ColorSpace } from "./colorspace.js";
|
||||
import { FileSpec } from "./file_spec.js";
|
||||
import { JpegStream } from "./jpeg_stream.js";
|
||||
import { ObjectLoader } from "./object_loader.js";
|
||||
import { OperatorList } from "./operator_list.js";
|
||||
import { StringStream } from "./stream.js";
|
||||
import { XFAFactory } from "./xfa/factory.js";
|
||||
|
||||
class AnnotationFactory {
|
||||
@ -257,11 +259,31 @@ class AnnotationFactory {
|
||||
}
|
||||
}
|
||||
|
||||
static async saveNewAnnotations(evaluator, task, annotations) {
|
||||
static generateImages(annotations, xref, isOffscreenCanvasSupported) {
|
||||
if (!isOffscreenCanvasSupported) {
|
||||
warn(
|
||||
"generateImages: OffscreenCanvas is not supported, cannot save or print some annotations with images."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let imagePromises;
|
||||
for (const { bitmapId, bitmap } of annotations) {
|
||||
if (!bitmap) {
|
||||
continue;
|
||||
}
|
||||
imagePromises ||= new Map();
|
||||
imagePromises.set(bitmapId, StampAnnotation.createImage(bitmap, xref));
|
||||
}
|
||||
|
||||
return imagePromises;
|
||||
}
|
||||
|
||||
static async saveNewAnnotations(evaluator, task, annotations, imagePromises) {
|
||||
const xref = evaluator.xref;
|
||||
let baseFontRef;
|
||||
const dependencies = [];
|
||||
const promises = [];
|
||||
const { isOffscreenCanvasSupported } = evaluator.options;
|
||||
|
||||
for (const annotation of annotations) {
|
||||
if (annotation.deleted) {
|
||||
@ -293,6 +315,36 @@ class AnnotationFactory {
|
||||
promises.push(
|
||||
InkAnnotation.createNewAnnotation(xref, annotation, dependencies)
|
||||
);
|
||||
break;
|
||||
case AnnotationEditorType.STAMP:
|
||||
if (!isOffscreenCanvasSupported) {
|
||||
break;
|
||||
}
|
||||
const image = await imagePromises.get(annotation.bitmapId);
|
||||
if (image.imageStream) {
|
||||
const { imageStream, smaskStream } = image;
|
||||
const buffer = [];
|
||||
if (smaskStream) {
|
||||
const smaskRef = xref.getNewTemporaryRef();
|
||||
await writeObject(smaskRef, smaskStream, buffer, null);
|
||||
dependencies.push({ ref: smaskRef, data: buffer.join("") });
|
||||
imageStream.dict.set("SMask", smaskRef);
|
||||
buffer.length = 0;
|
||||
}
|
||||
const imageRef = (image.imageRef = xref.getNewTemporaryRef());
|
||||
await writeObject(imageRef, imageStream, buffer, null);
|
||||
dependencies.push({ ref: imageRef, data: buffer.join("") });
|
||||
image.imageStream = image.smaskStream = null;
|
||||
}
|
||||
promises.push(
|
||||
StampAnnotation.createNewAnnotation(
|
||||
xref,
|
||||
annotation,
|
||||
dependencies,
|
||||
image
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +354,12 @@ class AnnotationFactory {
|
||||
};
|
||||
}
|
||||
|
||||
static async printNewAnnotations(evaluator, task, annotations) {
|
||||
static async printNewAnnotations(
|
||||
evaluator,
|
||||
task,
|
||||
annotations,
|
||||
imagePromises
|
||||
) {
|
||||
if (!annotations) {
|
||||
return null;
|
||||
}
|
||||
@ -331,6 +388,23 @@ class AnnotationFactory {
|
||||
})
|
||||
);
|
||||
break;
|
||||
case AnnotationEditorType.STAMP:
|
||||
if (!isOffscreenCanvasSupported) {
|
||||
break;
|
||||
}
|
||||
const image = await imagePromises.get(annotation.bitmapId);
|
||||
if (image.imageStream) {
|
||||
const { imageStream, smaskStream } = image;
|
||||
if (smaskStream) {
|
||||
imageStream.dict.set("SMask", smaskStream);
|
||||
}
|
||||
image.imageRef = new JpegStream(imageStream, imageStream.length);
|
||||
image.imageStream = image.smaskStream = null;
|
||||
}
|
||||
promises.push(
|
||||
StampAnnotation.createNewPrintAnnotation(xref, annotation, image)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4361,6 +4435,143 @@ class StampAnnotation extends MarkupAnnotation {
|
||||
this.data.annotationType = AnnotationType.STAMP;
|
||||
this.data.hasOwnCanvas = this.data.noRotate;
|
||||
}
|
||||
|
||||
static async createImage(bitmap, xref) {
|
||||
// TODO: when printing, we could have a specific internal colorspace
|
||||
// (e.g. something like DeviceRGBA) in order avoid any conversion (i.e. no
|
||||
// jpeg, no rgba to rgb conversion, etc...)
|
||||
|
||||
const { width, height } = bitmap;
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d", { alpha: true });
|
||||
|
||||
// Draw the image and get the data in order to extract the transparency.
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, width, height).data;
|
||||
const buf32 = new Uint32Array(data.buffer);
|
||||
const hasAlpha = buf32.some(
|
||||
FeatureTest.isLittleEndian
|
||||
? x => x >>> 24 !== 0xff
|
||||
: x => (x & 0xff) !== 0xff
|
||||
);
|
||||
|
||||
if (hasAlpha) {
|
||||
// Redraw the image on a white background in order to remove the thin gray
|
||||
// line which can appear when exporting to jpeg.
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
const jpegBufferPromise = canvas
|
||||
.convertToBlob({ type: "image/jpeg", quality: 1 })
|
||||
.then(blob => {
|
||||
return blob.arrayBuffer();
|
||||
});
|
||||
|
||||
const xobjectName = Name.get("XObject");
|
||||
const imageName = Name.get("Image");
|
||||
const image = new Dict(xref);
|
||||
image.set("Type", xobjectName);
|
||||
image.set("Subtype", imageName);
|
||||
image.set("BitsPerComponent", 8);
|
||||
image.set("ColorSpace", Name.get("DeviceRGB"));
|
||||
image.set("Filter", Name.get("DCTDecode"));
|
||||
image.set("BBox", [0, 0, width, height]);
|
||||
image.set("Width", width);
|
||||
image.set("Height", height);
|
||||
|
||||
let smaskStream = null;
|
||||
if (hasAlpha) {
|
||||
const alphaBuffer = new Uint8Array(buf32.length);
|
||||
if (FeatureTest.isLittleEndian) {
|
||||
for (let i = 0, ii = buf32.length; i < ii; i++) {
|
||||
alphaBuffer[i] = buf32[i] >>> 24;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0, ii = buf32.length; i < ii; i++) {
|
||||
alphaBuffer[i] = buf32[i] & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
const smask = new Dict(xref);
|
||||
smask.set("Type", xobjectName);
|
||||
smask.set("Subtype", imageName);
|
||||
smask.set("BitsPerComponent", 8);
|
||||
smask.set("ColorSpace", Name.get("DeviceGray"));
|
||||
smask.set("Width", width);
|
||||
smask.set("Height", height);
|
||||
|
||||
smaskStream = new Stream(alphaBuffer, 0, 0, smask);
|
||||
}
|
||||
const imageStream = new Stream(await jpegBufferPromise, 0, 0, image);
|
||||
|
||||
return {
|
||||
imageStream,
|
||||
smaskStream,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
static createNewDict(annotation, xref, { apRef, ap }) {
|
||||
const { rect, rotation, user } = annotation;
|
||||
const stamp = new Dict(xref);
|
||||
stamp.set("Type", Name.get("Annot"));
|
||||
stamp.set("Subtype", Name.get("Stamp"));
|
||||
stamp.set("CreationDate", `D:${getModificationDate()}`);
|
||||
stamp.set("Rect", rect);
|
||||
stamp.set("F", 4);
|
||||
stamp.set("Border", [0, 0, 0]);
|
||||
stamp.set("Rotate", rotation);
|
||||
|
||||
if (user) {
|
||||
stamp.set(
|
||||
"T",
|
||||
isAscii(user) ? user : stringToUTF16String(user, /* bigEndian = */ true)
|
||||
);
|
||||
}
|
||||
|
||||
if (apRef || ap) {
|
||||
const n = new Dict(xref);
|
||||
stamp.set("AP", n);
|
||||
|
||||
if (apRef) {
|
||||
n.set("N", apRef);
|
||||
} else {
|
||||
n.set("N", ap);
|
||||
}
|
||||
}
|
||||
|
||||
return stamp;
|
||||
}
|
||||
|
||||
static async createNewAppearanceStream(annotation, xref, params) {
|
||||
const { rotation } = annotation;
|
||||
const { imageRef, width, height } = params;
|
||||
const resources = new Dict(xref);
|
||||
const xobject = new Dict(xref);
|
||||
resources.set("XObject", xobject);
|
||||
xobject.set("Im0", imageRef);
|
||||
const appearance = `q ${width} 0 0 ${height} 0 0 cm /Im0 Do Q`;
|
||||
|
||||
const appearanceStreamDict = new Dict(xref);
|
||||
appearanceStreamDict.set("FormType", 1);
|
||||
appearanceStreamDict.set("Subtype", Name.get("Form"));
|
||||
appearanceStreamDict.set("Type", Name.get("XObject"));
|
||||
appearanceStreamDict.set("BBox", [0, 0, width, height]);
|
||||
appearanceStreamDict.set("Resources", resources);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = getRotationMatrix(rotation, width, height);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
const ap = new StringStream(appearance);
|
||||
ap.dict = appearanceStreamDict;
|
||||
|
||||
return ap;
|
||||
}
|
||||
}
|
||||
|
||||
class FileAttachmentAnnotation extends MarkupAnnotation {
|
||||
|
@ -13,8 +13,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AnnotationFactory, PopupAnnotation } from "./annotation.js";
|
||||
import {
|
||||
AnnotationEditorPrefix,
|
||||
assert,
|
||||
FormatError,
|
||||
info,
|
||||
@ -30,6 +30,7 @@ import {
|
||||
Util,
|
||||
warn,
|
||||
} from "../shared/util.js";
|
||||
import { AnnotationFactory, PopupAnnotation } from "./annotation.js";
|
||||
import {
|
||||
collectActions,
|
||||
getInheritableProperty,
|
||||
@ -277,7 +278,7 @@ class Page {
|
||||
}
|
||||
}
|
||||
|
||||
async saveNewAnnotations(handler, task, annotations) {
|
||||
async saveNewAnnotations(handler, task, annotations, imagePromises) {
|
||||
if (this.xfaFactory) {
|
||||
throw new Error("XFA: Cannot save new annotations.");
|
||||
}
|
||||
@ -306,7 +307,8 @@ class Page {
|
||||
const newData = await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
task,
|
||||
annotations
|
||||
annotations,
|
||||
imagePromises
|
||||
);
|
||||
|
||||
for (const { ref } of newData.annotations) {
|
||||
@ -433,14 +435,52 @@ class Page {
|
||||
|
||||
let newAnnotationsPromise = Promise.resolve(null);
|
||||
if (newAnnotationsByPage) {
|
||||
let imagePromises;
|
||||
const newAnnotations = newAnnotationsByPage.get(this.pageIndex);
|
||||
if (newAnnotations) {
|
||||
// An annotation can contain a reference to a bitmap, but this bitmap
|
||||
// is defined in another annotation. So we need to find this annotation
|
||||
// and generate the bitmap.
|
||||
const missingBitmaps = new Set();
|
||||
for (const { bitmapId, bitmap } of newAnnotations) {
|
||||
if (bitmapId && !bitmap && !missingBitmaps.has(bitmapId)) {
|
||||
missingBitmaps.add(bitmapId);
|
||||
}
|
||||
}
|
||||
|
||||
const { isOffscreenCanvasSupported } = this.evaluatorOptions;
|
||||
if (missingBitmaps.size > 0) {
|
||||
const annotationWithBitmaps = [];
|
||||
for (const [key, annotation] of annotationStorage) {
|
||||
if (!key.startsWith(AnnotationEditorPrefix)) {
|
||||
continue;
|
||||
}
|
||||
if (annotation.bitmap && missingBitmaps.has(annotation.bitmapId)) {
|
||||
annotationWithBitmaps.push(annotation);
|
||||
}
|
||||
}
|
||||
// The array annotationWithBitmaps cannot be empty: the check above
|
||||
// makes sure to have at least one annotation containing the bitmap.
|
||||
imagePromises = AnnotationFactory.generateImages(
|
||||
annotationWithBitmaps,
|
||||
this.xref,
|
||||
isOffscreenCanvasSupported
|
||||
);
|
||||
} else {
|
||||
imagePromises = AnnotationFactory.generateImages(
|
||||
newAnnotations,
|
||||
this.xref,
|
||||
isOffscreenCanvasSupported
|
||||
);
|
||||
}
|
||||
|
||||
deletedAnnotations = new RefSet();
|
||||
this.#replaceIdByRef(newAnnotations, deletedAnnotations, null);
|
||||
newAnnotationsPromise = AnnotationFactory.printNewAnnotations(
|
||||
partialEvaluator,
|
||||
task,
|
||||
newAnnotations
|
||||
newAnnotations,
|
||||
imagePromises
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
} from "./core_utils.js";
|
||||
import { Dict, Ref } from "./primitives.js";
|
||||
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
||||
import { AnnotationFactory } from "./annotation.js";
|
||||
import { clearGlobalCaches } from "./cleanup_helper.js";
|
||||
import { incrementalUpdate } from "./writer.js";
|
||||
import { isNodeJS } from "../shared/is_node.js";
|
||||
@ -537,12 +538,11 @@ class WorkerMessageHandler {
|
||||
|
||||
handler.on(
|
||||
"SaveDocument",
|
||||
function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
||||
async function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
||||
const promises = [
|
||||
pdfManager.requestLoadedStream(),
|
||||
pdfManager.ensureCatalog("acroForm"),
|
||||
pdfManager.ensureCatalog("acroFormRef"),
|
||||
pdfManager.ensureDoc("xref"),
|
||||
pdfManager.ensureDoc("startXRef"),
|
||||
];
|
||||
|
||||
@ -550,13 +550,21 @@ class WorkerMessageHandler {
|
||||
? getNewAnnotationsMap(annotationStorage)
|
||||
: null;
|
||||
|
||||
const xref = await pdfManager.ensureDoc("xref");
|
||||
|
||||
if (newAnnotationsByPage) {
|
||||
const imagePromises = AnnotationFactory.generateImages(
|
||||
annotationStorage.values(),
|
||||
xref,
|
||||
pdfManager.evaluatorOptions.isOffscreenCanvasSupported
|
||||
);
|
||||
|
||||
for (const [pageIndex, annotations] of newAnnotationsByPage) {
|
||||
promises.push(
|
||||
pdfManager.getPage(pageIndex).then(page => {
|
||||
const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
|
||||
return page
|
||||
.saveNewAnnotations(handler, task, annotations)
|
||||
.saveNewAnnotations(handler, task, annotations, imagePromises)
|
||||
.finally(function () {
|
||||
finishWorkerTask(task);
|
||||
});
|
||||
@ -586,7 +594,6 @@ class WorkerMessageHandler {
|
||||
stream,
|
||||
acroForm,
|
||||
acroFormRef,
|
||||
xref,
|
||||
startXRef,
|
||||
...refs
|
||||
]) {
|
||||
|
@ -1812,6 +1812,15 @@ class PDFPageProxy {
|
||||
);
|
||||
}
|
||||
|
||||
const transfers = [];
|
||||
if (annotationStorageMap) {
|
||||
for (const annotation of annotationStorageMap.values()) {
|
||||
if (annotation.bitmap) {
|
||||
transfers.push(annotation.bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const readableStream = this._transport.messageHandler.sendWithStream(
|
||||
"GetOperatorList",
|
||||
{
|
||||
@ -1819,7 +1828,8 @@ class PDFPageProxy {
|
||||
intent: renderingIntent,
|
||||
cacheKey,
|
||||
annotationStorage: annotationStorageMap,
|
||||
}
|
||||
},
|
||||
transfers
|
||||
);
|
||||
const reader = readableStream.getReader();
|
||||
|
||||
@ -2898,13 +2908,26 @@ class WorkerTransport {
|
||||
"please use the getData-method instead."
|
||||
);
|
||||
}
|
||||
const annotationStorage = this.annotationStorage.serializable;
|
||||
const transfers = [];
|
||||
if (annotationStorage) {
|
||||
for (const annotation of annotationStorage.values()) {
|
||||
if (annotation.bitmap) {
|
||||
transfers.push(annotation.bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.messageHandler
|
||||
.sendWithPromise("SaveDocument", {
|
||||
isPureXfa: !!this._htmlForXfa,
|
||||
numPages: this._numPages,
|
||||
annotationStorage: this.annotationStorage.serializable,
|
||||
filename: this._fullReader?.filename ?? null,
|
||||
})
|
||||
.sendWithPromise(
|
||||
"SaveDocument",
|
||||
{
|
||||
isPureXfa: !!this._htmlForXfa,
|
||||
numPages: this._numPages,
|
||||
annotationStorage,
|
||||
filename: this._fullReader?.filename ?? null,
|
||||
},
|
||||
transfers
|
||||
)
|
||||
.finally(() => {
|
||||
this.annotationStorage.resetModified();
|
||||
});
|
||||
|
@ -70,6 +70,7 @@ const AnnotationEditorType = {
|
||||
DISABLE: -1,
|
||||
NONE: 0,
|
||||
FREETEXT: 3,
|
||||
STAMP: 13,
|
||||
INK: 15,
|
||||
};
|
||||
|
||||
|
@ -490,8 +490,24 @@ class Driver {
|
||||
});
|
||||
let promise = loadingTask.promise;
|
||||
|
||||
if (task.annotationStorage) {
|
||||
for (const annotation of Object.values(task.annotationStorage)) {
|
||||
if (annotation.bitmapName) {
|
||||
promise = promise.then(async doc => {
|
||||
const response = await fetch(
|
||||
new URL(`./images/${annotation.bitmapName}`, window.location)
|
||||
);
|
||||
const blob = await response.blob();
|
||||
annotation.bitmap = await createImageBitmap(blob);
|
||||
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (task.save) {
|
||||
promise = loadingTask.promise.then(async doc => {
|
||||
promise = promise.then(async doc => {
|
||||
if (!task.annotationStorage) {
|
||||
throw new Error("Missing `annotationStorage` entry.");
|
||||
}
|
||||
|
BIN
test/images/firefox_logo.png
Executable file
BIN
test/images/firefox_logo.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 169 KiB |
@ -7814,5 +7814,155 @@
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"annotations": true
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-stamp-editor-print",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 13,
|
||||
"pageIndex": 0,
|
||||
"bitmapName": "firefox_logo.png",
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"rect": [37.5, 32.406246185302734, 496.5000057220459, 519.1875],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-stamp-editor-save-print",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 1,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 13,
|
||||
"pageIndex": 0,
|
||||
"bitmapName": "firefox_logo.png",
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"rect": [37.5, 32.406246185302734, 496.5000057220459, 519.1875],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-stamp-editor-print-2-pages",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 2,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 13,
|
||||
"pageIndex": 0,
|
||||
"bitmapName": "firefox_logo.png",
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"rect": [37.5, 32.406246185302734, 496.5000057220459, 519.1875],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_2": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 1,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-stamp-editor-save-print-2-pages",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 2,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 13,
|
||||
"pageIndex": 0,
|
||||
"bitmapName": "firefox_logo.png",
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"rect": [37.5, 32.406246185302734, 496.5000057220459, 519.1875],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_2": {
|
||||
"annotationType": 13,
|
||||
"bitmapId": "image_c29eaeb9-8839-4a09-bf7c-75a5785805cd_0",
|
||||
"pageIndex": 1,
|
||||
"rect": [
|
||||
458.06250572204584,
|
||||
473.04686737060547,
|
||||
571.5000057220459,
|
||||
582.7187461853027
|
||||
],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -2134,6 +2134,58 @@ describe("api", function () {
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
|
||||
it("write a new stamp annotation, save the pdf and check that the same image has the same ref", async function () {
|
||||
if (isNodeJS) {
|
||||
pending("Cannot create a bitmap from Node.js.");
|
||||
}
|
||||
|
||||
const TEST_IMAGES_PATH = "../images/";
|
||||
const filename = "firefox_logo.png";
|
||||
const path = new URL(TEST_IMAGES_PATH + filename, window.location).href;
|
||||
|
||||
const response = await fetch(path);
|
||||
const blob = await response.blob();
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
|
||||
let loadingTask = getDocument(buildGetDocumentParams("empty.pdf"));
|
||||
let pdfDoc = await loadingTask.promise;
|
||||
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", {
|
||||
annotationType: AnnotationEditorType.STAMP,
|
||||
rect: [12, 34, 56, 78],
|
||||
rotation: 0,
|
||||
bitmap,
|
||||
bitmapId: "im1",
|
||||
pageIndex: 0,
|
||||
});
|
||||
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
|
||||
annotationType: AnnotationEditorType.STAMP,
|
||||
rect: [112, 134, 156, 178],
|
||||
rotation: 0,
|
||||
bitmapId: "im1",
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
const data = await pdfDoc.saveDocument();
|
||||
await loadingTask.destroy();
|
||||
|
||||
loadingTask = getDocument(data);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const opList = await page.getOperatorList();
|
||||
|
||||
// The pdf contains two stamp annotations with the same image.
|
||||
// The image should be stored only once in the pdf and referenced twice.
|
||||
// So we can verify that the image is referenced twice in the opList.
|
||||
|
||||
for (let i = 0; i < opList.fnArray.length; i++) {
|
||||
if (opList.fnArray[i] === OPS.paintImageXObject) {
|
||||
expect(opList.argsArray[i][0]).toEqual("img_p0_1");
|
||||
}
|
||||
}
|
||||
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
|
||||
describe("Cross-origin", function () {
|
||||
let loadingTask;
|
||||
function _checkCanLoad(expectSuccess, filename, options) {
|
||||
|
Loading…
Reference in New Issue
Block a user