Merge pull request #16588 from calixteman/editor_stamp_2
[Editor] Add support for printing/saving newly added Stamp annotations
This commit is contained in:
commit
88c7c8b5bf
@ -23,6 +23,7 @@ import {
|
|||||||
AnnotationType,
|
AnnotationType,
|
||||||
assert,
|
assert,
|
||||||
BASELINE_FACTOR,
|
BASELINE_FACTOR,
|
||||||
|
FeatureTest,
|
||||||
getModificationDate,
|
getModificationDate,
|
||||||
IDENTITY_MATRIX,
|
IDENTITY_MATRIX,
|
||||||
LINE_DESCENT_FACTOR,
|
LINE_DESCENT_FACTOR,
|
||||||
@ -52,15 +53,16 @@ import {
|
|||||||
parseDefaultAppearance,
|
parseDefaultAppearance,
|
||||||
} from "./default_appearance.js";
|
} from "./default_appearance.js";
|
||||||
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
import { Dict, isName, Name, Ref, RefSet } from "./primitives.js";
|
||||||
|
import { Stream, StringStream } from "./stream.js";
|
||||||
import { writeDict, writeObject } from "./writer.js";
|
import { writeDict, writeObject } from "./writer.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { bidi } from "./bidi.js";
|
import { bidi } from "./bidi.js";
|
||||||
import { Catalog } from "./catalog.js";
|
import { Catalog } from "./catalog.js";
|
||||||
import { ColorSpace } from "./colorspace.js";
|
import { ColorSpace } from "./colorspace.js";
|
||||||
import { FileSpec } from "./file_spec.js";
|
import { FileSpec } from "./file_spec.js";
|
||||||
|
import { JpegStream } from "./jpeg_stream.js";
|
||||||
import { ObjectLoader } from "./object_loader.js";
|
import { ObjectLoader } from "./object_loader.js";
|
||||||
import { OperatorList } from "./operator_list.js";
|
import { OperatorList } from "./operator_list.js";
|
||||||
import { StringStream } from "./stream.js";
|
|
||||||
import { XFAFactory } from "./xfa/factory.js";
|
import { XFAFactory } from "./xfa/factory.js";
|
||||||
|
|
||||||
class AnnotationFactory {
|
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;
|
const xref = evaluator.xref;
|
||||||
let baseFontRef;
|
let baseFontRef;
|
||||||
const dependencies = [];
|
const dependencies = [];
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
const { isOffscreenCanvasSupported } = evaluator.options;
|
||||||
|
|
||||||
for (const annotation of annotations) {
|
for (const annotation of annotations) {
|
||||||
if (annotation.deleted) {
|
if (annotation.deleted) {
|
||||||
@ -293,6 +315,36 @@ class AnnotationFactory {
|
|||||||
promises.push(
|
promises.push(
|
||||||
InkAnnotation.createNewAnnotation(xref, annotation, dependencies)
|
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) {
|
if (!annotations) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -331,6 +388,23 @@ class AnnotationFactory {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
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.annotationType = AnnotationType.STAMP;
|
||||||
this.data.hasOwnCanvas = this.data.noRotate;
|
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 {
|
class FileAttachmentAnnotation extends MarkupAnnotation {
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnnotationFactory, PopupAnnotation } from "./annotation.js";
|
|
||||||
import {
|
import {
|
||||||
|
AnnotationEditorPrefix,
|
||||||
assert,
|
assert,
|
||||||
FormatError,
|
FormatError,
|
||||||
info,
|
info,
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
|
import { AnnotationFactory, PopupAnnotation } from "./annotation.js";
|
||||||
import {
|
import {
|
||||||
collectActions,
|
collectActions,
|
||||||
getInheritableProperty,
|
getInheritableProperty,
|
||||||
@ -277,7 +278,7 @@ class Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveNewAnnotations(handler, task, annotations) {
|
async saveNewAnnotations(handler, task, annotations, imagePromises) {
|
||||||
if (this.xfaFactory) {
|
if (this.xfaFactory) {
|
||||||
throw new Error("XFA: Cannot save new annotations.");
|
throw new Error("XFA: Cannot save new annotations.");
|
||||||
}
|
}
|
||||||
@ -306,7 +307,8 @@ class Page {
|
|||||||
const newData = await AnnotationFactory.saveNewAnnotations(
|
const newData = await AnnotationFactory.saveNewAnnotations(
|
||||||
partialEvaluator,
|
partialEvaluator,
|
||||||
task,
|
task,
|
||||||
annotations
|
annotations,
|
||||||
|
imagePromises
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const { ref } of newData.annotations) {
|
for (const { ref } of newData.annotations) {
|
||||||
@ -433,14 +435,52 @@ class Page {
|
|||||||
|
|
||||||
let newAnnotationsPromise = Promise.resolve(null);
|
let newAnnotationsPromise = Promise.resolve(null);
|
||||||
if (newAnnotationsByPage) {
|
if (newAnnotationsByPage) {
|
||||||
|
let imagePromises;
|
||||||
const newAnnotations = newAnnotationsByPage.get(this.pageIndex);
|
const newAnnotations = newAnnotationsByPage.get(this.pageIndex);
|
||||||
if (newAnnotations) {
|
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();
|
deletedAnnotations = new RefSet();
|
||||||
this.#replaceIdByRef(newAnnotations, deletedAnnotations, null);
|
this.#replaceIdByRef(newAnnotations, deletedAnnotations, null);
|
||||||
newAnnotationsPromise = AnnotationFactory.printNewAnnotations(
|
newAnnotationsPromise = AnnotationFactory.printNewAnnotations(
|
||||||
partialEvaluator,
|
partialEvaluator,
|
||||||
task,
|
task,
|
||||||
newAnnotations
|
newAnnotations,
|
||||||
|
imagePromises
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
} from "./core_utils.js";
|
} from "./core_utils.js";
|
||||||
import { Dict, Ref } from "./primitives.js";
|
import { Dict, Ref } from "./primitives.js";
|
||||||
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
||||||
|
import { AnnotationFactory } from "./annotation.js";
|
||||||
import { clearGlobalCaches } from "./cleanup_helper.js";
|
import { clearGlobalCaches } from "./cleanup_helper.js";
|
||||||
import { incrementalUpdate } from "./writer.js";
|
import { incrementalUpdate } from "./writer.js";
|
||||||
import { isNodeJS } from "../shared/is_node.js";
|
import { isNodeJS } from "../shared/is_node.js";
|
||||||
@ -537,12 +538,11 @@ class WorkerMessageHandler {
|
|||||||
|
|
||||||
handler.on(
|
handler.on(
|
||||||
"SaveDocument",
|
"SaveDocument",
|
||||||
function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
async function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
||||||
const promises = [
|
const promises = [
|
||||||
pdfManager.requestLoadedStream(),
|
pdfManager.requestLoadedStream(),
|
||||||
pdfManager.ensureCatalog("acroForm"),
|
pdfManager.ensureCatalog("acroForm"),
|
||||||
pdfManager.ensureCatalog("acroFormRef"),
|
pdfManager.ensureCatalog("acroFormRef"),
|
||||||
pdfManager.ensureDoc("xref"),
|
|
||||||
pdfManager.ensureDoc("startXRef"),
|
pdfManager.ensureDoc("startXRef"),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -550,13 +550,21 @@ class WorkerMessageHandler {
|
|||||||
? getNewAnnotationsMap(annotationStorage)
|
? getNewAnnotationsMap(annotationStorage)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const xref = await pdfManager.ensureDoc("xref");
|
||||||
|
|
||||||
if (newAnnotationsByPage) {
|
if (newAnnotationsByPage) {
|
||||||
|
const imagePromises = AnnotationFactory.generateImages(
|
||||||
|
annotationStorage.values(),
|
||||||
|
xref,
|
||||||
|
pdfManager.evaluatorOptions.isOffscreenCanvasSupported
|
||||||
|
);
|
||||||
|
|
||||||
for (const [pageIndex, annotations] of newAnnotationsByPage) {
|
for (const [pageIndex, annotations] of newAnnotationsByPage) {
|
||||||
promises.push(
|
promises.push(
|
||||||
pdfManager.getPage(pageIndex).then(page => {
|
pdfManager.getPage(pageIndex).then(page => {
|
||||||
const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
|
const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
|
||||||
return page
|
return page
|
||||||
.saveNewAnnotations(handler, task, annotations)
|
.saveNewAnnotations(handler, task, annotations, imagePromises)
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
finishWorkerTask(task);
|
finishWorkerTask(task);
|
||||||
});
|
});
|
||||||
@ -586,7 +594,6 @@ class WorkerMessageHandler {
|
|||||||
stream,
|
stream,
|
||||||
acroForm,
|
acroForm,
|
||||||
acroFormRef,
|
acroFormRef,
|
||||||
xref,
|
|
||||||
startXRef,
|
startXRef,
|
||||||
...refs
|
...refs
|
||||||
]) {
|
]) {
|
||||||
|
@ -1819,6 +1819,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(
|
const readableStream = this._transport.messageHandler.sendWithStream(
|
||||||
"GetOperatorList",
|
"GetOperatorList",
|
||||||
{
|
{
|
||||||
@ -1826,7 +1835,8 @@ class PDFPageProxy {
|
|||||||
intent: renderingIntent,
|
intent: renderingIntent,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
annotationStorage: annotationStorageMap,
|
annotationStorage: annotationStorageMap,
|
||||||
}
|
},
|
||||||
|
transfers
|
||||||
);
|
);
|
||||||
const reader = readableStream.getReader();
|
const reader = readableStream.getReader();
|
||||||
|
|
||||||
@ -2905,13 +2915,26 @@ class WorkerTransport {
|
|||||||
"please use the getData-method instead."
|
"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
|
return this.messageHandler
|
||||||
.sendWithPromise("SaveDocument", {
|
.sendWithPromise(
|
||||||
isPureXfa: !!this._htmlForXfa,
|
"SaveDocument",
|
||||||
numPages: this._numPages,
|
{
|
||||||
annotationStorage: this.annotationStorage.serializable,
|
isPureXfa: !!this._htmlForXfa,
|
||||||
filename: this._fullReader?.filename ?? null,
|
numPages: this._numPages,
|
||||||
})
|
annotationStorage,
|
||||||
|
filename: this._fullReader?.filename ?? null,
|
||||||
|
},
|
||||||
|
transfers
|
||||||
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.annotationStorage.resetModified();
|
this.annotationStorage.resetModified();
|
||||||
});
|
});
|
||||||
|
@ -70,6 +70,7 @@ const AnnotationEditorType = {
|
|||||||
DISABLE: -1,
|
DISABLE: -1,
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
FREETEXT: 3,
|
FREETEXT: 3,
|
||||||
|
STAMP: 13,
|
||||||
INK: 15,
|
INK: 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -490,8 +490,24 @@ class Driver {
|
|||||||
});
|
});
|
||||||
let promise = loadingTask.promise;
|
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) {
|
if (task.save) {
|
||||||
promise = loadingTask.promise.then(async doc => {
|
promise = promise.then(async doc => {
|
||||||
if (!task.annotationStorage) {
|
if (!task.annotationStorage) {
|
||||||
throw new Error("Missing `annotationStorage` entry.");
|
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,
|
"rounds": 1,
|
||||||
"type": "eq",
|
"type": "eq",
|
||||||
"annotations": true
|
"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();
|
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 () {
|
describe("Cross-origin", function () {
|
||||||
let loadingTask;
|
let loadingTask;
|
||||||
function _checkCanLoad(expectSuccess, filename, options) {
|
function _checkCanLoad(expectSuccess, filename, options) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user