▼class PartialEvaluator[4641 lines]
222class PartialEvaluator {
223 constructor({
224 xref,
225 handler,
226 pageIndex,
227 idFactory,
228 fontCache,
229 builtInCMapCache,
230 standardFontDataCache,
231 globalColorSpaceCache,
232 globalImageCache,
233 systemFontCache,
234 options = null,
235 }) {
236 this.xref = xref;
237 this.handler = handler;
238 this.pageIndex = pageIndex;
239 this.idFactory = idFactory;
240 this.fontCache = fontCache;
241 this.builtInCMapCache = builtInCMapCache;
242 this.standardFontDataCache = standardFontDataCache;
243 this.globalColorSpaceCache = globalColorSpaceCache;
244 this.globalImageCache = globalImageCache;
245 this.systemFontCache = systemFontCache;
246 this.options = options || DefaultPartialEvaluatorOptions;
247 this.type3FontRefs = null;
248
249 this._regionalImageCache = new RegionalImageCache();
250 this._fetchBuiltInCMapBound = this.fetchBuiltInCMap.bind(this);
251 }
252
253 /**
254 * Since Functions are only cached (locally) by reference, we can share one
255 * `PDFFunctionFactory` instance within this `PartialEvaluator` instance.
256 */
257 get _pdfFunctionFactory() {
258 return shadow(
259 this,
260 "_pdfFunctionFactory",
261 new PDFFunctionFactory({ xref: this.xref })
262 );
263 }
264
265 get parsingType3Font() {
266 return !!this.type3FontRefs;
267 }
268
269 clone(newOptions = null) {
270 const newEvaluator = Object.create(this);
271 newEvaluator.options = Object.assign(
272 Object.create(null),
273 this.options,
274 newOptions
275 );
276 return newEvaluator;
277 }
278
279 hasBlendModes(resources, nonBlendModesSet) {
280 if (!(resources instanceof Dict)) {
281 return false;
282 }
283 if (resources.objId && nonBlendModesSet.has(resources.objId)) {
284 return false;
285 }
286
287 const processed = new RefSet(nonBlendModesSet);
288 if (resources.objId) {
289 processed.put(resources.objId);
290 }
291
292 const nodes = [resources],
293 xref = this.xref;
294 while (nodes.length) {
295 const node = nodes.shift();
296 // First check the current resources for blend modes.
297 const graphicStates = node.get("ExtGState");
298 if (graphicStates instanceof Dict) {
299 for (let graphicState of graphicStates.getRawValues()) {
300 if (graphicState instanceof Ref) {
301 if (processed.has(graphicState)) {
302 continue; // The ExtGState has already been processed.
303 }
304 try {
305 graphicState = xref.fetch(graphicState);
306 } catch (ex) {
307 // Avoid parsing a corrupt ExtGState more than once.
308 processed.put(graphicState);
309
310 info(`hasBlendModes - ignoring ExtGState: "${ex}".`);
311 continue;
312 }
313 }
314 if (!(graphicState instanceof Dict)) {
315 continue;
316 }
317 if (graphicState.objId) {
318 processed.put(graphicState.objId);
319 }
320
321 const bm = graphicState.get("BM");
322 if (bm instanceof Name) {
323 if (bm.name !== "Normal") {
324 return true;
325 }
326 continue;
327 }
328 if (bm !== undefined && Array.isArray(bm)) {
329 for (const element of bm) {
330 if (element instanceof Name && element.name !== "Normal") {
331 return true;
332 }
333 }
334 }
335 }
336 }
337 // Descend into the XObjects to look for more resources and blend modes.
338 const xObjects = node.get("XObject");
339 if (!(xObjects instanceof Dict)) {
340 continue;
341 }
342 for (let xObject of xObjects.getRawValues()) {
343 if (xObject instanceof Ref) {
344 if (processed.has(xObject)) {
345 // The XObject has already been processed, and by avoiding a
346 // redundant `xref.fetch` we can *significantly* reduce the load
347 // time for badly generated PDF files (fixes issue6961.pdf).
348 continue;
349 }
350 try {
351 xObject = xref.fetch(xObject);
352 } catch (ex) {
353 // Avoid parsing a corrupt XObject more than once.
354 processed.put(xObject);
355
356 info(`hasBlendModes - ignoring XObject: "${ex}".`);
357 continue;
358 }
359 }
360 if (!(xObject instanceof BaseStream)) {
361 continue;
362 }
363 if (xObject.dict.objId) {
364 processed.put(xObject.dict.objId);
365 }
366 const xResources = xObject.dict.get("Resources");
367 if (!(xResources instanceof Dict)) {
368 continue;
369 }
370 // Checking objId to detect an infinite loop.
371 if (xResources.objId && processed.has(xResources.objId)) {
372 continue;
373 }
374
375 nodes.push(xResources);
376 if (xResources.objId) {
377 processed.put(xResources.objId);
378 }
379 }
380 }
381
382 // When no blend modes exist, there's no need re-fetch/re-parse any of the
383 // processed `Ref`s again for subsequent pages. This helps reduce redundant
384 // `XRef.fetch` calls for some documents (e.g. issue6961.pdf).
385 for (const ref of processed) {
386 nonBlendModesSet.put(ref);
387 }
388 return false;
389 }
390
391 async fetchBuiltInCMap(name) {
392 const cachedData = this.builtInCMapCache.get(name);
393 if (cachedData) {
394 return cachedData;
395 }
396 let data;
397
398 if (this.options.useWorkerFetch) {
399 // Only compressed CMaps are (currently) supported here.
400 data = {
401 cMapData: await fetchBinaryData(`${this.options.cMapUrl}${name}.bcmap`),
402 isCompressed: true,
403 };
404 } else {
405 if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
406 throw new Error("Only worker-thread fetching supported.");
407 }
408 // Get the data on the main-thread instead.
409 data = {
410 cMapData: await this.handler.sendWithPromise("FetchBinaryData", {
411 kind: "cMapUrl",
412 filename: `${name}${this.options.cMapPacked ? ".bcmap" : ""}`,
413 }),
414 isCompressed: this.options.cMapPacked,
415 };
416 }
417 // Cache the CMap data, to avoid fetching it repeatedly.
418 this.builtInCMapCache.set(name, data);
419
420 return data;
421 }
422
423 async fetchStandardFontData(name) {
424 const cachedData = this.standardFontDataCache.get(name);
425 if (cachedData) {
426 return new Stream(cachedData);
427 }
428
429 // The symbol fonts are not consistent across platforms, always load the
430 // standard font data for them.
431 if (
432 this.options.useSystemFonts &&
433 name !== "Symbol" &&
434 name !== "ZapfDingbats"
435 ) {
436 return null;
437 }
438
439 const standardFontNameToFileName = getFontNameToFileMap(),
440 filename = standardFontNameToFileName[name];
441 let data;
442
443 try {
444 if (this.options.useWorkerFetch) {
445 data = await fetchBinaryData(
446 `${this.options.standardFontDataUrl}${filename}`
447 );
448 } else {
449 if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
450 throw new Error("Only worker-thread fetching supported.");
451 }
452 // Get the data on the main-thread instead.
453 data = await this.handler.sendWithPromise("FetchBinaryData", {
454 kind: "standardFontDataUrl",
455 filename,
456 });
457 }
458 } catch (ex) {
459 warn(ex);
460 return null;
461 }
462 // Cache the "raw" standard font data, to avoid fetching it repeatedly
463 // (see e.g. issue 11399).
464 this.standardFontDataCache.set(name, data);
465
466 return new Stream(data);
467 }
468
469 async buildFormXObject(
470 resources,
471 xobj,
472 smask,
473 operatorList,
474 task,
475 initialState,
476 localColorSpaceCache,
477 seenRefs
478 ) {
479 const { dict } = xobj;
480 const matrix = lookupMatrix(dict.getArray("Matrix"), null);
481 const bbox = lookupNormalRect(dict.getArray("BBox"), null);
482 let f32bbox = bbox && new Float32Array(bbox);
483 if (f32bbox?.some(x => !isFinite(x))) {
484 f32bbox = null;
485 }
486
487 let optionalContent, groupOptions;
488 if (dict.has("OC")) {
489 optionalContent = await this.parseMarkedContentProps(
490 dict.get("OC"),
491 resources
492 );
493 }
494 if (optionalContent !== undefined) {
495 operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
496 }
497
498 const group = dict.get("Group");
499 let newOpList;
500
501 // If it's a group, a new canvas will be created that is the size of the
502 // bounding box and translated to the correct position so we don't need to
503 // apply the bounding box to it.
504 const f32matrix = matrix && new Float32Array(matrix);
505 const args = [f32matrix, (!group && f32bbox) || null];
506 const localResources = dict.get("Resources");
507
508 if (group) {
509 groupOptions = {
510 matrix,
511 bbox: f32bbox,
512 smask,
513 isolated: false,
514 knockout: false,
515 needsIsolation: false,
516 hasSoftMask: false,
517 isGray: false,
518 };
519
520 const groupSubtype = group.get("S");
521 let colorSpace = null;
522 if (isName(groupSubtype, "Transparency")) {
523 groupOptions.isolated = group.get("I") || false;
524 groupOptions.knockout = group.get("K") || false;
525 if (group.has("CS")) {
526 const cs = this._getColorSpace(
527 group.getRaw("CS"),
528 resources,
529 localColorSpaceCache
530 );
531 colorSpace =
532 cs instanceof ColorSpace ? cs : await this._handleColorSpace(cs);
533 }
534 }
535
536 // When the group color space is gray (a single component) the group's
537 // content must be rendered in grayscale, see issue 7998.
538 groupOptions.isGray = colorSpace?.numComps === 1;
539
540 if (smask?.backdrop) {
541 colorSpace ||= ColorSpaceUtils.rgb;
542 smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0);
543 } else if (smask?.subtype === "Luminosity") {
544 // Per PDF spec 11.6.5.2: when /BC is missing the backdrop is the
545 // initial value of the group colour space, which is black for all
546 // standard colour spaces.
547 smask.backdrop = "#000000";
548 }
549
550 newOpList = new CheckedOperatorList();
551 } else {
552 newOpList = operatorList;
553 operatorList.addOp(OPS.paintFormXObjectBegin, args);
554 }
555
556 await this.getOperatorList({
557 stream: xobj,
558 task,
559 resources: localResources instanceof Dict ? localResources : resources,
560 operatorList: newOpList,
561 initialState,
562 prevRefs: seenRefs,
563 });
564
565 if (group) {
566 groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
567 groupOptions.hasSoftMask = newOpList.hasSoftMask || !!smask;
568 operatorList.addOp(OPS.beginGroup, [groupOptions]);
569 operatorList.addOp(OPS.paintFormXObjectBegin, args);
570 operatorList.addOpList(newOpList);
571 operatorList.addOp(OPS.paintFormXObjectEnd, []);
572 operatorList.addOp(OPS.endGroup, [groupOptions]);
573 } else {
574 operatorList.addOp(OPS.paintFormXObjectEnd, []);
575 }
576
577 if (optionalContent !== undefined) {
578 operatorList.addOp(OPS.endMarkedContent, []);
579 }
580 }
581
582 _sendImgData(objId, imgData, cacheGlobally = false) {
583 if (
584 (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
585 imgData
586 ) {
587 assert(Number.isInteger(imgData.dataLen), "Expected dataLen to be set.");
588 }
589 const transfers = imgData ? [imgData.bitmap || imgData.data.buffer] : null;
590
591 if (this.parsingType3Font || cacheGlobally) {
592 return this.handler.send(
593 "commonobj",
594 [objId, "Image", imgData],
595 transfers
596 );
597 }
598 return this.handler.send(
599 "obj",
600 [objId, this.pageIndex, "Image", imgData],
601 transfers
602 );
603 }
604
605 async buildPaintImageXObject({
606 resources,
607 image,
608 isInline = false,
609 operatorList,
610 cacheKey,
611 localImageCache,
612 localColorSpaceCache,
613 }) {
614 const { maxImageSize, ignoreErrors, isOffscreenCanvasSupported } =
615 this.options;
616
617 const { dict } = image;
618 const imageRef = dict.objId;
619 const w = dict.get("W", "Width");
620 const h = dict.get("H", "Height");
621
622 if (!(w && typeof w === "number") || !(h && typeof h === "number")) {
623 warn("Image dimensions are missing, or not numbers.");
624 return;
625 }
626 if (maxImageSize !== -1 && w * h > maxImageSize) {
627 const msg = "Image exceeded maximum allowed size and was removed.";
628
629 if (!ignoreErrors) {
630 throw new Error(msg);
631 }
632 warn(msg);
633 return;
634 }
635
636 let optionalContent;
637 if (dict.has("OC")) {
638 optionalContent = await this.parseMarkedContentProps(
639 dict.get("OC"),
640 resources
641 );
642 }
643
644 const imageMask = dict.get("IM", "ImageMask") || false;
645 let imgData, fn, args;
646 if (imageMask) {
647 // This depends on a tmpCanvas being filled with the
648 // current fillStyle, such that processing the pixel
649 // data can't be done here. Instead of creating a
650 // complete PDFImage, only read the information needed
651 // for later.
652 imgData = await PDFImage.createMask({
653 image,
654 isOffscreenCanvasSupported:
655 isOffscreenCanvasSupported && !this.parsingType3Font,
656 });
657
658 if (imgData.isSingleOpaquePixel) {
659 // Handles special case of mainly LaTeX documents which use image
660 // masks to draw lines with the current fill style.
661 fn = OPS.paintSolidColorImageMask;
662 args = [];
663 operatorList.addImageOps(fn, args, optionalContent);
664
665 if (cacheKey) {
666 const cacheData = { fn, args, optionalContent };
667 localImageCache.set(cacheKey, imageRef, cacheData);
668
669 if (imageRef) {
670 this._regionalImageCache.set(
671 /* name = */ null,
672 imageRef,
673 cacheData
674 );
675 }
676 }
677 return;
678 }
679
680 if (this.parsingType3Font) {
681 // NOTE: Compared to other image resources we don't bother caching
682 // Type3-glyph image masks, since we've not come across any cases
683 // where that actually helps.
684 // In Type3-glyphs image masks are "always" inline resources,
685 // they're usually fairly small and aren't being re-used either.
686 if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
687 assert(
688 imgData.data instanceof Uint8Array,
689 "Type3 glyph image mask must be a TypedArray."
690 );
691 }
692 args = compileType3Glyph(imgData);
693
694 if (args) {
695 operatorList.addImageOps(OPS.constructPath, args, optionalContent);
696 return;
697 }
698 warn("Cannot compile Type3 glyph.");
699
700 // If compilation failed, or was disabled, fallback to using an inline
701 // image mask; this case should be extremely rare.
702 operatorList.addImageOps(
703 OPS.paintImageMaskXObject,
704 [imgData],
705 optionalContent
706 );
707 return;
708 }
709
710 const objId = `mask_${this.idFactory.createObjId()}`;
711 operatorList.addDependency(objId);
712
713 imgData.dataLen = imgData.bitmap
714 ? imgData.width * imgData.height * 4
715 : imgData.data.length;
716 this._sendImgData(objId, imgData);
717
718 fn = OPS.paintImageMaskXObject;
719 args = [
720 {
721 data: objId,
722 width: imgData.width,
723 height: imgData.height,
724 interpolate: imgData.interpolate,
725 count: 1,
726 },
727 ];
728 operatorList.addImageOps(fn, args, optionalContent);
729
730 if (cacheKey) {
731 const cacheData = { objId, fn, args, optionalContent };
732 localImageCache.set(cacheKey, imageRef, cacheData);
733
734 if (imageRef) {
735 this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
736 }
737 }
738 return;
739 }
740
741 const SMALL_IMAGE_DIMENSIONS = 200;
742 const hasMask = dict.has("SMask") || dict.has("Mask");
743 // Inlining small images into the queue as RGB data
744 if (isInline && w + h < SMALL_IMAGE_DIMENSIONS && !hasMask) {
745 try {
746 const imageObj = new PDFImage({
747 xref: this.xref,
748 res: resources,
749 image,
750 isInline,
751 pdfFunctionFactory: this._pdfFunctionFactory,
752 globalColorSpaceCache: this.globalColorSpaceCache,
753 localColorSpaceCache,
754 });
755 // We force the use of RGBA_32BPP images here, because we can't handle
756 // any other kind.
757 imgData = await imageObj.createImageData(
758 /* forceRGBA = */ true,
759 /* isOffscreenCanvasSupported = */ false
760 );
761 operatorList.addImageOps(
762 OPS.paintInlineImageXObject,
763 [imgData],
764 optionalContent
765 );
766 } catch (reason) {
767 const msg = `Unable to decode inline image: "${reason}".`;
768
769 if (!ignoreErrors) {
770 throw new Error(msg);
771 }
772 warn(msg);
773 }
774 return;
775 }
776
777 // If there is no imageMask, create the PDFImage and a lot
778 // of image processing can be done here.
779 let objId = `img_${this.idFactory.createObjId()}`,
780 cacheGlobally = false,
781 globalCacheData = null;
782
783 if (this.parsingType3Font) {
784 objId = `${this.idFactory.getDocId()}_type3_${objId}`;
785 } else if (cacheKey && imageRef) {
786 cacheGlobally = this.globalImageCache.shouldCache(
787 imageRef,
788 this.pageIndex
789 );
790
791 if (cacheGlobally) {
792 assert(!isInline, "Cannot cache an inline image globally.");
793
794 objId = `${this.idFactory.getDocId()}_${objId}`;
795 }
796 }
797
798 // Ensure that the dependency is added before the image is decoded.
799 operatorList.addDependency(objId);
800
801 fn = OPS.paintImageXObject;
802 args = [objId, w, h];
803 operatorList.addImageOps(fn, args, optionalContent, hasMask);
804
805 if (cacheGlobally) {
806 globalCacheData = {
807 objId,
808 fn,
809 args,
810 optionalContent,
811 hasMask,
812 byteSize: 0, // Temporary entry, to avoid `setData` returning early.
813 };
814
815 if (this.globalImageCache.hasDecodeFailed(imageRef)) {
816 this.globalImageCache.setData(imageRef, globalCacheData);
817
818 this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
819 return;
820 }
821
822 // For large (at least 500x500) or more complex images that we'll cache
823 // globally, check if the image is still cached locally on the main-thread
824 // to avoid having to re-parse the image (since that can be slow).
825 if (w * h > 250000 || hasMask) {
826 const localLength = await this.handler.sendWithPromise("commonobj", [
827 objId,
828 "CopyLocalImage",
829 { imageRef },
830 ]);
831
832 if (localLength) {
833 this.globalImageCache.setData(imageRef, globalCacheData);
834 this.globalImageCache.addByteSize(imageRef, localLength);
835 return;
836 }
837 }
838 }
839
840 PDFImage.buildImage({
841 xref: this.xref,
842 res: resources,
843 image,
844 isInline,
845 pdfFunctionFactory: this._pdfFunctionFactory,
846 globalColorSpaceCache: this.globalColorSpaceCache,
847 localColorSpaceCache,
848 })
849 .then(async imageObj => {
850 imgData = await imageObj.createImageData(
851 /* forceRGBA = */ false,
852 isOffscreenCanvasSupported
853 );
854 imgData.dataLen = imgData.bitmap
855 ? imgData.width * imgData.height * 4
856 : imgData.data.length;
857 imgData.ref = imageRef;
858
859 if (cacheGlobally) {
860 this.globalImageCache.addByteSize(imageRef, imgData.dataLen);
861 }
862 return this._sendImgData(objId, imgData, cacheGlobally);
863 })
864 .catch(reason => {
865 warn(`Unable to decode image "${objId}": "${reason}".`);
866
867 if (imageRef) {
868 this.globalImageCache.addDecodeFailed(imageRef);
869 }
870 return this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
871 });
872
873 if (cacheKey) {
874 const cacheData = { objId, fn, args, optionalContent, hasMask };
875 localImageCache.set(cacheKey, imageRef, cacheData);
876
877 if (imageRef) {
878 this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
879
880 if (cacheGlobally) {
881 assert(globalCacheData, "The global cache-data must be available.");
882 this.globalImageCache.setData(imageRef, globalCacheData);
883 }
884 }
885 }
886 }
887
888 #createTransferMap(fn) {
889 const transferFn = this._pdfFunctionFactory.create(fn),
890 tmp = new Float32Array(1);
891 return Uint8Array.from({ length: 256 }, (_, i) => {
892 tmp[0] = i / 255;
893 transferFn(tmp, 0, tmp, 0);
894 return (tmp[0] * 255) | 0;
895 });
896 }
897
898 handleSMask(
899 smask,
900 resources,
901 operatorList,
902 task,
903 stateManager,
904 localColorSpaceCache,
905 seenRefs
906 ) {
907 const smaskContent = smask.get("G");
908 const smaskOptions = {
909 subtype: smask.get("S").name,
910 backdrop: smask.get("BC"),
911 };
912
913 // The SMask might have a alpha/luminosity value transfer function --
914 // we will build a map of integer values in range 0..255 to be fast.
915 const transferObj = smask.get("TR");
916 if (isPDFFunction(transferObj)) {
917 smaskOptions.transferMap = this.#createTransferMap(transferObj);
918 }
919
920 return this.buildFormXObject(
921 resources,
922 smaskContent,
923 smaskOptions,
924 operatorList,
925 task,
926 stateManager.state.clone({ newPath: true }),
927 localColorSpaceCache,
928 seenRefs
929 );
930 }
931
932 handleTransferFunction(tr) {
933 let transferArray;
934 if (Array.isArray(tr)) {
935 // If all entries in the array are the same, we can just use one of them.
936 transferArray =
937 tr.length > 1 && tr.every(map => map === tr[0]) ? [tr[0]] : tr;
938 } else if (isPDFFunction(tr)) {
939 transferArray = [tr];
940 } else {
941 return null; // Not a valid transfer function entry.
942 }
943
944 const transferMaps = [];
945 let numFns = 0,
946 numEffectfulFns = 0;
947 for (const entry of transferArray) {
948 const transferObj = this.xref.fetchIfRef(entry);
949 numFns++;
950
951 if (isName(transferObj, "Identity")) {
952 transferMaps.push(null);
953 continue;
954 } else if (!isPDFFunction(transferObj)) {
955 return null; // Not a valid transfer function object.
956 }
957 transferMaps.push(this.#createTransferMap(transferObj));
958 numEffectfulFns++;
959 }
960
961 if (!(numFns === 1 || numFns === 4)) {
962 return null; // Only 1 or 4 functions are supported, by the specification.
963 }
964 if (numEffectfulFns === 0) {
965 return null; // Only /Identity transfer functions found, which are no-ops.
966 }
967 return transferMaps;
968 }
969
970 handleTilingType(
971 fn,
972 color,
973 resources,
974 pattern,
975 patternDict,
976 operatorList,
977 task,
978 localTilingPatternCache,
979 seenRefs
980 ) {
981 // Create an IR of the pattern code.
982 const tilingOpList = new CheckedOperatorList();
983 // Merge the available resources, to prevent issues when the patternDict
984 // is missing some /Resources entries (fixes issue6541.pdf).
985 const patternResources = Dict.merge({
986 xref: this.xref,
987 dictArray: [patternDict.get("Resources"), resources],
988 });
989
990 return this.getOperatorList({
991 stream: pattern,
992 task,
993 resources: patternResources,
994 operatorList: tilingOpList,
995 prevRefs: seenRefs,
996 })
997 .then(function () {
998 const operatorListIR = tilingOpList.getIR();
999 const { needsIsolation } = tilingOpList;
1000 const tilingPatternIR = getTilingPatternIR(
1001 operatorListIR,
1002 patternDict,
1003 color,
1004 needsIsolation
1005 );
1006 // Add the dependencies to the parent operator list so they are
1007 // resolved before the sub operator list is executed synchronously.
1008 operatorList.addDependencies(tilingOpList.dependencies);
1009 operatorList.addOp(fn, tilingPatternIR);
1010
1011 if (patternDict.objId) {
1012 localTilingPatternCache.set(/* name = */ null, patternDict.objId, {
1013 operatorListIR,
1014 needsIsolation,
1015 dict: patternDict,
1016 });
1017 }
1018 })
1019 .catch(reason => {
1020 if (reason instanceof AbortException) {
1021 return;
1022 }
1023 if (this.options.ignoreErrors) {
1024 warn(`handleTilingType - ignoring pattern: "${reason}".`);
1025 return;
1026 }
1027 throw reason;
1028 });
1029 }
1030
1031 async handleSetFont(
1032 resources,
1033 fontArgs,
1034 fontRef,
1035 operatorList,
1036 task,
1037 state,
1038 fallbackFontDict = null,
1039 cssFontInfo = null,
1040 seenRefs = null
1041 ) {
1042 const fontName = fontArgs?.[0] instanceof Name ? fontArgs[0].name : null;
1043
1044 const translated = await this.loadFont(
1045 fontName,
1046 fontRef,
1047 resources,
1048 task,
1049 fallbackFontDict,
1050 cssFontInfo,
1051 seenRefs
1052 );
1053
1054 if (translated.font.isType3Font) {
1055 // Add the dependencies to the parent operatorList so they are
1056 // resolved before Type3 operatorLists are executed synchronously.
1057 operatorList.addDependencies(translated.type3Dependencies);
1058 }
1059
1060 state.font = translated.font;
1061 translated.send(this.handler);
1062 return translated.loadedName;
1063 }
1064
1065 handleText(chars, state) {
1066 const font = state.font;
1067 const glyphs = font.charsToGlyphs(chars);
1068
1069 if (font.data) {
1070 const isAddToPathSet = !!(
1071 state.textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG
1072 );
1073 if (
1074 isAddToPathSet ||
1075 state.fillColorSpace.name === "Pattern" ||
1076 state.strokeColorSpace.name === "Pattern" ||
1077 font.disableFontFace
1078 ) {
1079 PartialEvaluator.buildFontPaths(
1080 font,
1081 glyphs,
1082 this.handler,
1083 this.options
1084 );
1085 }
1086 }
1087 return glyphs;
1088 }
1089
1090 ensureStateFont(state) {
1091 if (state.font) {
1092 return;
1093 }
1094 const reason = new FormatError(
1095 "Missing setFont (Tf) operator before text rendering operator."
1096 );
1097
1098 if (this.options.ignoreErrors) {
1099 warn(`ensureStateFont: "${reason}".`);
1100 return;
1101 }
1102 throw reason;
1103 }
1104
1105 async setGState({
1106 resources,
1107 gState,
1108 operatorList,
1109 cacheKey,
1110 task,
1111 stateManager,
1112 localGStateCache,
1113 localColorSpaceCache,
1114 seenRefs,
1115 }) {
1116 const gStateRef = gState.objId;
1117 let isSimpleGState = true;
1118 // This array holds the converted/processed state data.
1119 const gStateObj = [];
1120 let promise = Promise.resolve();
1121 for (const [key, value] of gState) {
1122 switch (key) {
1123 case "Type":
1124 break;
1125 case "LW":
1126 if (typeof value !== "number") {
1127 warn(`Invalid LW (line width): ${value}`);
1128 break;
1129 }
1130 gStateObj.push([key, Math.abs(value)]);
1131 break;
1132 case "LC":
1133 case "LJ":
1134 case "ML":
1135 case "D":
1136 case "RI":
1137 case "FL":
1138 case "CA":
1139 case "ca":
1140 gStateObj.push([key, value]);
1141 break;
1142 case "Font":
1143 isSimpleGState = false;
1144
1145 promise = promise.then(() =>
1146 this.handleSetFont(
1147 resources,
1148 null,
1149 value[0],
1150 operatorList,
1151 task,
1152 stateManager.state,
1153 /* fallbackFontDict = */ null,
1154 /* cssFontInfo = */ null,
1155 seenRefs
1156 ).then(function (loadedName) {
1157 operatorList.addDependency(loadedName);
1158 gStateObj.push([key, [loadedName, value[1]]]);
1159 })
1160 );
1161 break;
1162 case "BM":
1163 gStateObj.push([key, normalizeBlendMode(value)]);
1164 break;
1165 case "SMask":
1166 if (isName(value, "None")) {
1167 gStateObj.push([key, false]);
1168 break;
1169 }
1170 if (value instanceof Dict) {
1171 isSimpleGState = false;
1172
1173 promise = promise.then(() =>
1174 this.handleSMask(
1175 value,
1176 resources,
1177 operatorList,
1178 task,
1179 stateManager,
1180 localColorSpaceCache,
1181 seenRefs
1182 )
1183 );
1184 gStateObj.push([key, true]);
1185 } else {
1186 warn("Unsupported SMask type");
1187 }
1188 break;
1189 case "TR":
1190 // TR2 takes precedence over TR (see PDF 32000-1:2008, Table 58), so
1191 // ignore TR when a TR2 entry is present in the same dictionary.
1192 if (gState.has("TR2")) {
1193 break;
1194 }
1195 /* falls through */
1196 case "TR2": {
1197 // For TR2 the name /Default denotes "the transfer function that was
1198 // in effect at the start of the page" (PDF 32000-1:2008, Table 58).
1199 // A page always starts with the identity transfer function, hence
1200 // /Default (and /Identity) means "no transfer function" here, which
1201 // clears any filter previously set on the display side (issue 21406).
1202 // `handleTransferFunction` returns `null` for those names.
1203 const transferMaps = this.handleTransferFunction(value);
1204 gStateObj.push(["TR", transferMaps]);
1205 break;
1206 }
1207 // Only generate info log messages for the following since
1208 // they are unlikely to have a big impact on the rendering.
1209 case "OP":
1210 case "op":
1211 case "OPM":
1212 case "BG":
1213 case "BG2":
1214 case "UCR":
1215 case "UCR2":
1216 case "HT":
1217 case "SM":
1218 case "SA":
1219 case "AIS":
1220 case "TK":
1221 // TODO implement these operators.
1222 info("graphic state operator " + key);
1223 break;
1224 default:
1225 info("Unknown graphic state operator " + key);
1226 break;
1227 }
1228 }
1229 await promise;
1230
1231 if (gStateObj.length > 0) {
1232 operatorList.addOp(OPS.setGState, [gStateObj]);
1233 }
1234
1235 if (isSimpleGState) {
1236 localGStateCache.set(cacheKey, gStateRef, gStateObj);
1237 }
1238 }
1239
1240 loadFont(
1241 fontName,
1242 font,
1243 resources,
1244 task,
1245 fallbackFontDict = null,
1246 cssFontInfo = null,
1247 seenRefs = null
1248 ) {
1249 const errorFont = async () =>
1250 new TranslatedFont({
1251 loadedName: "g_font_error",
1252 font: new ErrorFont(`Font "${fontName}" is not available.`),
1253 dict: font,
1254 });
1255
1256 let fontRef;
1257 if (font) {
1258 // Loading by ref.
1259 if (font instanceof Ref) {
1260 fontRef = font;
1261 }
1262 } else {
1263 // Loading by name.
1264 const fontRes = resources.get("Font");
1265 if (fontRes) {
1266 fontRef = fontRes.getRaw(fontName);
1267 }
1268 }
1269 if (fontRef) {
1270 if (this.type3FontRefs?.has(fontRef)) {
1271 return errorFont();
1272 }
1273
1274 if (this.fontCache.has(fontRef)) {
1275 return this.fontCache.get(fontRef);
1276 }
1277
1278 try {
1279 font = this.xref.fetchIfRef(fontRef);
1280 } catch (ex) {
1281 warn(`loadFont - lookup failed: "${ex}".`);
1282 }
1283 }
1284
1285 if (!(font instanceof Dict)) {
1286 if (!this.options.ignoreErrors && !this.parsingType3Font) {
1287 warn(`Font "${fontName}" is not available.`);
1288 return errorFont();
1289 }
1290 warn(
1291 `Font "${fontName}" is not available -- attempting to fallback to a default font.`
1292 );
1293
1294 // Falling back to a default font to avoid completely broken rendering,
1295 // but note that there're no guarantees that things will look "correct".
1296 font = fallbackFontDict || PartialEvaluator.fallbackFontDict;
1297 }
1298
1299 // We are holding `font.cacheKey` references only for `fontRef`s that
1300 // are not actually `Ref`s, but rather `Dict`s. See explanation below.
1301 if (font.cacheKey && this.fontCache.has(font.cacheKey)) {
1302 return this.fontCache.get(font.cacheKey);
1303 }
1304
1305 const { promise, resolve } = Promise.withResolvers();
1306
1307 let preEvaluatedFont;
1308 try {
1309 preEvaluatedFont = this.preEvaluateFont(font);
1310 preEvaluatedFont.cssFontInfo = cssFontInfo;
1311 } catch (reason) {
1312 warn(`loadFont - preEvaluateFont failed: "${reason}".`);
1313 return errorFont();
1314 }
1315 const { descriptor, hash } = preEvaluatedFont;
1316
1317 const fontRefIsRef = fontRef instanceof Ref;
1318 let fontID;
1319
1320 if (hash && descriptor instanceof Dict) {
1321 const fontAliases = (descriptor.fontAliases ||= Object.create(null));
1322
1323 if (fontAliases[hash]) {
1324 const aliasFontRef = fontAliases[hash].aliasRef;
1325 if (fontRefIsRef && aliasFontRef && this.fontCache.has(aliasFontRef)) {
1326 this.fontCache.putAlias(fontRef, aliasFontRef);
1327 return this.fontCache.get(fontRef);
1328 }
1329 } else {
1330 fontAliases[hash] = {
1331 fontID: this.idFactory.createFontId(),
1332 };
1333 }
1334
1335 if (fontRefIsRef) {
1336 fontAliases[hash].aliasRef = fontRef;
1337 }
1338 fontID = fontAliases[hash].fontID;
1339 } else {
1340 fontID = this.idFactory.createFontId();
1341 }
1342 assert(
1343 fontID?.startsWith("f"),
1344 'The "fontID" must be (correctly) defined.'
1345 );
1346
1347 // Workaround for bad PDF generators that reference fonts incorrectly,
1348 // where `fontRef` is a `Dict` rather than a `Ref` (fixes bug946506.pdf).
1349 // In this case we cannot put the font into `this.fontCache` (which is
1350 // a `RefMap`), since it's not possible to use a `Dict` as a key.
1351 //
1352 // However, if we don't cache the font it's not possible to remove it
1353 // when `cleanup` is triggered from the API, which causes issues on
1354 // subsequent rendering operations (see issue7403.pdf) and would force us
1355 // to unnecessarily load the same fonts over and over.
1356 //
1357 // Instead, we cheat a bit by using a modified `fontID` as a key in
1358 // `this.fontCache`, to allow the font to be cached.
1359 // NOTE: This works because `RefMap` calls `toString()` on provided keys.
1360 // Also, since `fontRef` is used when getting cached fonts,
1361 // we'll not accidentally match fonts cached with the `fontID`.
1362 if (fontRefIsRef) {
1363 this.fontCache.put(fontRef, promise);
1364 } else {
1365 font.cacheKey = `cacheKey_${fontID}`;
1366 this.fontCache.put(font.cacheKey, promise);
1367 }
1368
1369 // Keep track of each font we translated so the caller can
1370 // load them asynchronously before calling display on a page.
1371 font.loadedName = `${this.idFactory.getDocId()}_${fontID}`;
1372
1373 this.translateFont(preEvaluatedFont)
1374 .then(async translatedFont => {
1375 const translated = new TranslatedFont({
1376 loadedName: font.loadedName,
1377 font: translatedFont,
1378 dict: font,
1379 });
1380
1381 if (translatedFont.isType3Font) {
1382 try {
1383 await translated.loadType3Data(this, resources, task, seenRefs);
1384 } catch (reason) {
1385 throw new Error(`Type3 font load error: ${reason}`);
1386 }
1387 }
1388 resolve(translated);
1389 })
1390 .catch(reason => {
1391 // TODO reject?
1392 warn(`loadFont - translateFont failed: "${reason}".`);
1393
1394 resolve(
1395 new TranslatedFont({
1396 loadedName: font.loadedName,
1397 font: new ErrorFont(reason?.message),
1398 dict: font,
1399 })
1400 );
1401 });
1402 return promise;
1403 }
1404
1405 buildPath(fn, args, state) {
1406 const { pathMinMax: minMax, pathBuffer } = state;
1407 switch (fn | 0) {
1408 case OPS.rectangle: {
1409 const x = (state.currentPointX = args[0]);
1410 const y = (state.currentPointY = args[1]);
1411 const width = args[2];
1412 const height = args[3];
1413 const xw = x + width;
1414 const yh = y + height;
1415 if (width === 0 || height === 0) {
1416 pathBuffer.push(
1417 DrawOPS.moveTo,
1418 x,
1419 y,
1420 DrawOPS.lineTo,
1421 xw,
1422 yh,
1423 DrawOPS.closePath
1424 );
1425 } else {
1426 pathBuffer.push(
1427 DrawOPS.moveTo,
1428 x,
1429 y,
1430 DrawOPS.lineTo,
1431 xw,
1432 y,
1433 DrawOPS.lineTo,
1434 xw,
1435 yh,
1436 DrawOPS.lineTo,
1437 x,
1438 yh,
1439 DrawOPS.closePath
1440 );
1441 }
1442 Util.rectBoundingBox(x, y, xw, yh, minMax);
1443 break;
1444 }
1445 case OPS.moveTo: {
1446 const x = (state.currentPointX = args[0]);
1447 const y = (state.currentPointY = args[1]);
1448 pathBuffer.push(DrawOPS.moveTo, x, y);
1449 Util.pointBoundingBox(x, y, minMax);
1450 break;
1451 }
1452 case OPS.lineTo: {
1453 const x = (state.currentPointX = args[0]);
1454 const y = (state.currentPointY = args[1]);
1455 pathBuffer.push(DrawOPS.lineTo, x, y);
1456 Util.pointBoundingBox(x, y, minMax);
1457 break;
1458 }
1459 case OPS.curveTo: {
1460 const startX = state.currentPointX;
1461 const startY = state.currentPointY;
1462 const [x1, y1, x2, y2, x, y] = args;
1463 state.currentPointX = x;
1464 state.currentPointY = y;
1465 pathBuffer.push(DrawOPS.curveTo, x1, y1, x2, y2, x, y);
1466 Util.bezierBoundingBox(startX, startY, x1, y1, x2, y2, x, y, minMax);
1467 break;
1468 }
1469 case OPS.curveTo2: {
1470 const startX = state.currentPointX;
1471 const startY = state.currentPointY;
1472 const [x1, y1, x, y] = args;
1473 state.currentPointX = x;
1474 state.currentPointY = y;
1475 pathBuffer.push(DrawOPS.curveTo, startX, startY, x1, y1, x, y);
1476 Util.bezierBoundingBox(
1477 startX,
1478 startY,
1479 startX,
1480 startY,
1481 x1,
1482 y1,
1483 x,
1484 y,
1485 minMax
1486 );
1487 break;
1488 }
1489 case OPS.curveTo3: {
1490 const startX = state.currentPointX;
1491 const startY = state.currentPointY;
1492 const [x1, y1, x, y] = args;
1493 state.currentPointX = x;
1494 state.currentPointY = y;
1495 pathBuffer.push(DrawOPS.curveTo, x1, y1, x, y, x, y);
1496 Util.bezierBoundingBox(startX, startY, x1, y1, x, y, x, y, minMax);
1497 break;
1498 }
1499 case OPS.closePath:
1500 pathBuffer.push(DrawOPS.closePath);
1501 break;
1502 }
1503 }
1504
1505 _getColorSpace(cs, resources, localColorSpaceCache) {
1506 return ColorSpaceUtils.parse({
1507 cs,
1508 xref: this.xref,
1509 resources,
1510 pdfFunctionFactory: this._pdfFunctionFactory,
1511 globalColorSpaceCache: this.globalColorSpaceCache,
1512 localColorSpaceCache,
1513 asyncIfNotCached: true,
1514 });
1515 }
1516
1517 async _handleColorSpace(csPromise) {
1518 try {
1519 return await csPromise;
1520 } catch (ex) {
1521 if (ex instanceof AbortException) {
1522 return null;
1523 }
1524 if (this.options.ignoreErrors) {
1525 warn(`_handleColorSpace - ignoring ColorSpace: "${ex}".`);
1526 return null;
1527 }
1528 throw ex;
1529 }
1530 }
1531
1532 parseShading({
1533 shading,
1534 resources,
1535 localColorSpaceCache,
1536 localShadingPatternCache,
1537 }) {
1538 // Shadings and patterns may be referenced by the same name but the resource
1539 // dictionary could be different so we can't use the name for the cache key.
1540 let id = localShadingPatternCache.get(shading);
1541 if (id) {
1542 return id;
1543 }
1544 let patternIR;
1545
1546 try {
1547 const shadingFill = Pattern.parseShading(
1548 shading,
1549 this.xref,
1550 resources,
1551 this._pdfFunctionFactory,
1552 this.globalColorSpaceCache,
1553 localColorSpaceCache
1554 );
1555 patternIR = shadingFill.getIR();
1556 } catch (reason) {
1557 if (reason instanceof AbortException) {
1558 return null;
1559 }
1560 if (this.options.ignoreErrors) {
1561 warn(`parseShading - ignoring shading: "${reason}".`);
1562
1563 localShadingPatternCache.set(shading, null);
1564 return null;
1565 }
1566 throw reason;
1567 }
1568
1569 id = `pattern_${this.idFactory.createObjId()}`;
1570 if (this.parsingType3Font) {
1571 id = `${this.idFactory.getDocId()}_type3_${id}`;
1572 }
1573 localShadingPatternCache.set(shading, id);
1574
1575 if (this.parsingType3Font) {
1576 const buffer = compilePatternInfo(patternIR);
1577 this.handler.send("commonobj", [id, "Pattern", buffer], [buffer]);
1578 } else {
1579 this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]);
1580 }
1581 return id;
1582 }
1583
1584 handleColorN(
1585 operatorList,
1586 fn,
1587 args,
1588 cs,
1589 patterns,
1590 resources,
1591 task,
1592 localColorSpaceCache,
1593 localTilingPatternCache,
1594 localShadingPatternCache,
1595 seenRefs
1596 ) {
1597 // compile tiling patterns
1598 const patternName = args.pop();
1599 // SCN/scn applies patterns along with normal colors
1600 if (patternName instanceof Name) {
1601 const rawPattern = patterns.getRaw(patternName.name);
1602
1603 const localTilingPattern =
1604 rawPattern instanceof Ref &&
1605 localTilingPatternCache.getByRef(rawPattern);
1606 if (localTilingPattern) {
1607 try {
1608 const color = cs.base ? cs.base.getRgbHex(args, 0) : null;
1609 const tilingPatternIR = getTilingPatternIR(
1610 localTilingPattern.operatorListIR,
1611 localTilingPattern.dict,
1612 color,
1613 localTilingPattern.needsIsolation
1614 );
1615 operatorList.addOp(fn, tilingPatternIR);
1616 return undefined;
1617 } catch {
1618 // Handle any errors during normal TilingPattern parsing.
1619 }
1620 }
1621
1622 const pattern = this.xref.fetchIfRef(rawPattern);
1623 if (pattern) {
1624 const dict = pattern instanceof BaseStream ? pattern.dict : pattern;
1625 const typeNum = dict.get("PatternType");
1626
1627 if (typeNum === PatternType.TILING) {
1628 const color = cs.base ? cs.base.getRgbHex(args, 0) : null;
1629 return this.handleTilingType(
1630 fn,
1631 color,
1632 resources,
1633 pattern,
1634 dict,
1635 operatorList,
1636 task,
1637 localTilingPatternCache,
1638 seenRefs
1639 );
1640 } else if (typeNum === PatternType.SHADING) {
1641 const shading = dict.get("Shading");
1642 const objId = this.parseShading({
1643 shading,
1644 resources,
1645 localColorSpaceCache,
1646 localShadingPatternCache,
1647 });
1648 if (objId) {
1649 const matrix = lookupMatrix(dict.getArray("Matrix"), null);
1650 operatorList.addOp(fn, ["Shading", objId, matrix]);
1651 }
1652 return undefined;
1653 }
1654 throw new FormatError(`Unknown PatternType: ${typeNum}`);
1655 }
1656 }
1657 throw new FormatError(`Unknown PatternName: ${patternName}`);
1658 }
1659
1660 async parseMarkedContentProps(contentProperties, resources) {
1661 return parseMarkedContentProps(this.xref, contentProperties, resources);
1662 }
1663
1664 async getOperatorList({
1665 stream,
1666 task,
1667 resources,
1668 operatorList,
1669 initialState = null,
1670 fallbackFontDict = null,
1671 prevRefs = null,
1672 }) {
1673 if (stream.isAsync) {
1674 const bytes = await stream.asyncGetBytes();
1675 if (bytes) {
1676 stream = new Stream(bytes, 0, bytes.length, stream.dict);
1677 }
1678 }
1679
1680 const objId = stream.dict?.objId;
1681 const seenRefs = new RefSet(prevRefs);
1682
1683 if (objId) {
1684 if (prevRefs?.has(objId)) {
1685 throw new Error(
1686 `getOperatorList - ignoring circular reference: ${objId}`
1687 );
1688 }
1689 seenRefs.put(objId);
1690 }
1691 // Ensure that `resources`/`initialState` is correctly initialized,
1692 // even if the provided parameter is e.g. `null`.
1693 resources ||= Dict.empty;
1694 initialState ||= new EvalState();
1695
1696 if (!operatorList) {
1697 throw new Error('getOperatorList: missing "operatorList" parameter');
1698 }
1699
1700 const self = this;
1701 const xref = this.xref;
1702 const localImageCache = new LocalImageCache();
1703 const localColorSpaceCache = new LocalColorSpaceCache();
1704 const localGStateCache = new LocalGStateCache();
1705 const localTilingPatternCache = new LocalTilingPatternCache();
1706 const localShadingPatternCache = new Map();
1707
1708 const xobjs = resources.get("XObject") || Dict.empty;
1709 const patterns = resources.get("Pattern") || Dict.empty;
1710 const stateManager = new StateManager(initialState);
1711 const preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
1712 const timeSlotManager = new TimeSlotManager();
1713 let markedContentLevel = 0;
1714
1715 function closePendingRestoreOPS(argument) {
1716 for (let i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
1717 operatorList.addOp(OPS.restore, []);
1718 }
1719 }
1720
1721 function closePendingMarkedContentOPS() {
1722 for (; markedContentLevel > 0; markedContentLevel--) {
1723 operatorList.addOp(OPS.endMarkedContent, []);
1724 }
1725 }
1726
1727 return new Promise(function promiseBody(resolve, reject) {
1728 const next = function (promise) {
1729 Promise.all([promise, operatorList.ready]).then(function () {
1730 try {
1731 promiseBody(resolve, reject);
1732 } catch (ex) {
1733 reject(ex);
1734 }
1735 }, reject);
1736 };
1737 task.ensureNotTerminated();
1738 timeSlotManager.reset();
1739
1740 const operation = {};
1741 let stop, cs, name, isValidName;
1742 while (!(stop = timeSlotManager.check())) {
1743 // The arguments parsed by read() are used beyond this loop, so we
1744 // cannot reuse the same array on each iteration. Therefore we pass
1745 // in |null| as the initial value (see the comment on
1746 // EvaluatorPreprocessor_read() for why).
1747 operation.args = null;
1748 if (!preprocessor.read(operation)) {
1749 break;
1750 }
1751 let args = operation.args;
1752 let fn = operation.fn;
1753
1754 switch (fn | 0) {
1755 case OPS.paintXObject:
1756 // eagerly compile XForm objects
1757 isValidName = args[0] instanceof Name;
1758 name = args[0].name;
1759
1760 if (isValidName) {
1761 const localImage = localImageCache.getByName(name);
1762 if (localImage) {
1763 addCachedImageOps(operatorList, localImage);
1764 args = null;
1765 continue;
1766 }
1767 }
1768
1769 next(
1770 new Promise(function (resolveXObject, rejectXObject) {
1771 if (!isValidName) {
1772 throw new FormatError("XObject must be referred to by name.");
1773 }
1774
1775 let xobj = xobjs.getRaw(name);
1776 if (xobj instanceof Ref) {
1777 const cachedImage =
1778 localImageCache.getByRef(xobj) ||
1779 self._regionalImageCache.getByRef(xobj) ||
1780 self.globalImageCache.getData(xobj, self.pageIndex);
1781 if (cachedImage) {
1782 addCachedImageOps(operatorList, cachedImage);
1783 resolveXObject();
1784 return;
1785 }
1786
1787 xobj = xref.fetch(xobj);
1788 }
1789
1790 if (!(xobj instanceof BaseStream)) {
1791 throw new FormatError("XObject should be a stream");
1792 }
1793
1794 const type = xobj.dict.get("Subtype");
1795 if (!(type instanceof Name)) {
1796 throw new FormatError("XObject should have a Name subtype");
1797 }
1798
1799 if (type.name === "Form") {
1800 stateManager.save();
1801 self
1802 .buildFormXObject(
1803 resources,
1804 xobj,
1805 null,
1806 operatorList,
1807 task,
1808 stateManager.state.clone({ newPath: true }),
1809 localColorSpaceCache,
1810 seenRefs
1811 )
1812 .then(function () {
1813 stateManager.restore();
1814 resolveXObject();
1815 }, rejectXObject);
1816 return;
1817 } else if (type.name === "Image") {
1818 self
1819 .buildPaintImageXObject({
1820 resources,
1821 image: xobj,
1822 operatorList,
1823 cacheKey: name,
1824 localImageCache,
1825 localColorSpaceCache,
1826 })
1827 .then(resolveXObject, rejectXObject);
1828 return;
1829 } else if (type.name === "PS") {
1830 // PostScript XObjects are unused when viewing documents.
1831 // See section 4.7.1 of Adobe's PDF reference.
1832 info("Ignored XObject subtype PS");
1833 } else {
1834 throw new FormatError(
1835 `Unhandled XObject subtype ${type.name}`
1836 );
1837 }
1838 resolveXObject();
1839 }).catch(function (reason) {
1840 if (reason instanceof AbortException) {
1841 return;
1842 }
1843 if (self.options.ignoreErrors) {
1844 warn(`getOperatorList - ignoring XObject: "${reason}".`);
1845 return;
1846 }
1847 throw reason;
1848 })
1849 );
1850 return;
1851 case OPS.setFont:
1852 const fontSize = args[1];
1853 // eagerly collect all fonts
1854 next(
1855 self
1856 .handleSetFont(
1857 resources,
1858 args,
1859 null,
1860 operatorList,
1861 task,
1862 stateManager.state,
1863 fallbackFontDict,
1864 /* cssFontInfo = */ null,
1865 seenRefs
1866 )
1867 .then(function (loadedName) {
1868 operatorList.addDependency(loadedName);
1869 operatorList.addOp(OPS.setFont, [loadedName, fontSize]);
1870 })
1871 );
1872 return;
1873 case OPS.endInlineImage:
1874 const cacheKey = args[0].cacheKey;
1875 if (cacheKey) {
1876 const localImage = localImageCache.getByName(cacheKey);
1877 if (localImage) {
1878 addCachedImageOps(operatorList, localImage);
1879 args = null;
1880 continue;
1881 }
1882 }
1883 next(
1884 self.buildPaintImageXObject({
1885 resources,
1886 image: args[0],
1887 isInline: true,
1888 operatorList,
1889 cacheKey,
1890 localImageCache,
1891 localColorSpaceCache,
1892 })
1893 );
1894 return;
1895 case OPS.showText:
1896 if (!stateManager.state.font) {
1897 self.ensureStateFont(stateManager.state);
1898 continue;
1899 }
1900 args[0] = self.handleText(args[0], stateManager.state);
1901 break;
1902 case OPS.showSpacedText:
1903 if (!stateManager.state.font) {
1904 self.ensureStateFont(stateManager.state);
1905 continue;
1906 }
1907 const combinedGlyphs = [],
1908 state = stateManager.state;
1909 for (const arrItem of args[0]) {
1910 if (typeof arrItem === "string") {
1911 combinedGlyphs.push(...self.handleText(arrItem, state));
1912 } else if (typeof arrItem === "number") {
1913 combinedGlyphs.push(arrItem);
1914 }
1915 }
1916 args[0] = combinedGlyphs;
1917 fn = OPS.showText;
1918 break;
1919 case OPS.nextLineShowText:
1920 if (!stateManager.state.font) {
1921 self.ensureStateFont(stateManager.state);
1922 continue;
1923 }
1924 operatorList.addOp(OPS.nextLine);
1925 args[0] = self.handleText(args[0], stateManager.state);
1926 fn = OPS.showText;
1927 break;
1928 case OPS.nextLineSetSpacingShowText:
1929 if (!stateManager.state.font) {
1930 self.ensureStateFont(stateManager.state);
1931 continue;
1932 }
1933 operatorList.addOp(OPS.nextLine);
1934 operatorList.addOp(OPS.setWordSpacing, [args.shift()]);
1935 operatorList.addOp(OPS.setCharSpacing, [args.shift()]);
1936 args[0] = self.handleText(args[0], stateManager.state);
1937 fn = OPS.showText;
1938 break;
1939 case OPS.setTextRenderingMode:
1940 stateManager.state.textRenderingMode = args[0];
1941 break;
1942
1943 case OPS.setFillColorSpace: {
1944 const fillCS = self._getColorSpace(
1945 args[0],
1946 resources,
1947 localColorSpaceCache
1948 );
1949 if (fillCS instanceof ColorSpace) {
1950 stateManager.state.fillColorSpace = fillCS;
1951 continue;
1952 }
1953
1954 next(
1955 self._handleColorSpace(fillCS).then(colorSpace => {
1956 stateManager.state.fillColorSpace =
1957 colorSpace || ColorSpaceUtils.gray;
1958 })
1959 );
1960 return;
1961 }
1962 case OPS.setStrokeColorSpace: {
1963 const strokeCS = self._getColorSpace(
1964 args[0],
1965 resources,
1966 localColorSpaceCache
1967 );
1968 if (strokeCS instanceof ColorSpace) {
1969 stateManager.state.strokeColorSpace = strokeCS;
1970 continue;
1971 }
1972
1973 next(
1974 self._handleColorSpace(strokeCS).then(colorSpace => {
1975 stateManager.state.strokeColorSpace =
1976 colorSpace || ColorSpaceUtils.gray;
1977 })
1978 );
1979 return;
1980 }
1981 case OPS.setFillColor:
1982 if (!isNumberArray(args, null)) {
1983 continue;
1984 }
1985 cs = stateManager.state.fillColorSpace;
1986 args = [cs.getRgbHex(args, 0)];
1987 fn = OPS.setFillRGBColor;
1988 break;
1989 case OPS.setStrokeColor:
1990 if (!isNumberArray(args, null)) {
1991 continue;
1992 }
1993 cs = stateManager.state.strokeColorSpace;
1994 args = [cs.getRgbHex(args, 0)];
1995 fn = OPS.setStrokeRGBColor;
1996 break;
1997 case OPS.setFillGray:
1998 if (!isNumberArray(args, null)) {
1999 continue;
2000 }
2001 stateManager.state.fillColorSpace = ColorSpaceUtils.gray;
2002 args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
2003 fn = OPS.setFillRGBColor;
2004 break;
2005 case OPS.setStrokeGray:
2006 if (!isNumberArray(args, null)) {
2007 continue;
2008 }
2009 stateManager.state.strokeColorSpace = ColorSpaceUtils.gray;
2010 args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
2011 fn = OPS.setStrokeRGBColor;
2012 break;
2013 case OPS.setFillCMYKColor:
2014 if (!isNumberArray(args, null)) {
2015 continue;
2016 }
2017 stateManager.state.fillColorSpace = ColorSpaceUtils.cmyk;
2018 args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
2019 fn = OPS.setFillRGBColor;
2020 break;
2021 case OPS.setStrokeCMYKColor:
2022 if (!isNumberArray(args, null)) {
2023 continue;
2024 }
2025 stateManager.state.strokeColorSpace = ColorSpaceUtils.cmyk;
2026 args = [ColorSpaceUtils.cmyk.getRgbHex(args, 0)];
2027 fn = OPS.setStrokeRGBColor;
2028 break;
2029 case OPS.setFillRGBColor:
2030 if (!isNumberArray(args, null)) {
2031 continue;
2032 }
2033 stateManager.state.fillColorSpace = ColorSpaceUtils.rgb;
2034 args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
2035 break;
2036 case OPS.setStrokeRGBColor:
2037 if (!isNumberArray(args, null)) {
2038 continue;
2039 }
2040 stateManager.state.strokeColorSpace = ColorSpaceUtils.rgb;
2041 args = [ColorSpaceUtils.rgb.getRgbHex(args, 0)];
2042 break;
2043 case OPS.setFillColorN:
2044 cs = stateManager.state.patternFillColorSpace;
2045 if (!cs) {
2046 if (isNumberArray(args, null)) {
2047 args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
2048 fn = OPS.setFillRGBColor;
2049 break;
2050 }
2051 args = [];
2052 fn = OPS.setFillTransparent;
2053 break;
2054 }
2055 if (cs.name === "Pattern") {
2056 if (!Array.isArray(args)) {
2057 continue;
2058 }
2059 next(
2060 self.handleColorN(
2061 operatorList,
2062 OPS.setFillColorN,
2063 args,
2064 cs,
2065 patterns,
2066 resources,
2067 task,
2068 localColorSpaceCache,
2069 localTilingPatternCache,
2070 localShadingPatternCache,
2071 seenRefs
2072 )
2073 );
2074 return;
2075 }
2076 if (!isNumberArray(args, null)) {
2077 continue;
2078 }
2079 args = [cs.getRgbHex(args, 0)];
2080 fn = OPS.setFillRGBColor;
2081 break;
2082 case OPS.setStrokeColorN:
2083 cs = stateManager.state.patternStrokeColorSpace;
2084 if (!cs) {
2085 if (isNumberArray(args, null)) {
2086 args = [ColorSpaceUtils.gray.getRgbHex(args, 0)];
2087 fn = OPS.setStrokeRGBColor;
2088 break;
2089 }
2090 args = [];
2091 fn = OPS.setStrokeTransparent;
2092 break;
2093 }
2094 if (cs.name === "Pattern") {
2095 if (!Array.isArray(args)) {
2096 continue;
2097 }
2098 next(
2099 self.handleColorN(
2100 operatorList,
2101 OPS.setStrokeColorN,
2102 args,
2103 cs,
2104 patterns,
2105 resources,
2106 task,
2107 localColorSpaceCache,
2108 localTilingPatternCache,
2109 localShadingPatternCache,
2110 seenRefs
2111 )
2112 );
2113 return;
2114 }
2115 if (!isNumberArray(args, null)) {
2116 continue;
2117 }
2118 args = [cs.getRgbHex(args, 0)];
2119 fn = OPS.setStrokeRGBColor;
2120 break;
2121
2122 case OPS.shadingFill:
2123 let shading;
2124 try {
2125 const shadingRes = resources.get("Shading");
2126 if (!shadingRes) {
2127 throw new FormatError("No shading resource found");
2128 }
2129
2130 shading = shadingRes.get(args[0].name);
2131 if (!shading) {
2132 throw new FormatError("No shading object found");
2133 }
2134 } catch (reason) {
2135 if (reason instanceof AbortException) {
2136 continue;
2137 }
2138 if (self.options.ignoreErrors) {
2139 warn(`getOperatorList - ignoring Shading: "${reason}".`);
2140 continue;
2141 }
2142 throw reason;
2143 }
2144 const patternId = self.parseShading({
2145 shading,
2146 resources,
2147 localColorSpaceCache,
2148 localShadingPatternCache,
2149 });
2150 if (!patternId) {
2151 continue;
2152 }
2153 args = [patternId];
2154 fn = OPS.shadingFill;
2155 break;
2156 case OPS.setGState:
2157 isValidName = args[0] instanceof Name;
2158 name = args[0].name;
2159
2160 if (isValidName) {
2161 const localGStateObj = localGStateCache.getByName(name);
2162 if (localGStateObj) {
2163 if (localGStateObj.length > 0) {
2164 operatorList.addOp(OPS.setGState, [localGStateObj]);
2165 }
2166 args = null;
2167 continue;
2168 }
2169 }
2170
2171 next(
2172 new Promise(function (resolveGState, rejectGState) {
2173 if (!isValidName) {
2174 throw new FormatError("GState must be referred to by name.");
2175 }
2176
2177 const extGState = resources.get("ExtGState");
2178 if (!(extGState instanceof Dict)) {
2179 throw new FormatError("ExtGState should be a dictionary.");
2180 }
2181
2182 const gState = extGState.get(name);
2183 // TODO: Attempt to lookup cached GStates by reference as well,
2184 // if and only if there are PDF documents where doing so
2185 // would significantly improve performance.
2186 if (!(gState instanceof Dict)) {
2187 throw new FormatError("GState should be a dictionary.");
2188 }
2189
2190 self
2191 .setGState({
2192 resources,
2193 gState,
2194 operatorList,
2195 cacheKey: name,
2196 task,
2197 stateManager,
2198 localGStateCache,
2199 localColorSpaceCache,
2200 seenRefs,
2201 })
2202 .then(resolveGState, rejectGState);
2203 }).catch(function (reason) {
2204 if (reason instanceof AbortException) {
2205 return;
2206 }
2207 if (self.options.ignoreErrors) {
2208 warn(`getOperatorList - ignoring ExtGState: "${reason}".`);
2209 return;
2210 }
2211 throw reason;
2212 })
2213 );
2214 return;
2215 case OPS.setLineWidth: {
2216 // The thickness should be a non-negative number, as per spec.
2217 // When the value is negative, Acrobat and Poppler take the absolute
2218 // value while PDFium takes the max of 0 and the value.
2219 const [thickness] = args;
2220 if (typeof thickness !== "number") {
2221 warn(`Invalid setLineWidth: ${thickness}`);
2222 continue;
2223 }
2224 args[0] = Math.abs(thickness);
2225 break;
2226 }
2227 case OPS.setDash: {
2228 const dashPhase = args[1];
2229 if (typeof dashPhase !== "number") {
2230 warn(`Invalid setDash: ${dashPhase}`);
2231 continue;
2232 }
2233 const dashArray = args[0];
2234 if (!Array.isArray(dashArray)) {
2235 warn(`Invalid setDash: ${dashArray}`);
2236 continue;
2237 }
2238 if (dashArray.some(x => typeof x !== "number")) {
2239 args[0] = dashArray.filter(x => typeof x === "number");
2240 }
2241 break;
2242 }
2243 case OPS.moveTo:
2244 case OPS.lineTo:
2245 case OPS.curveTo:
2246 case OPS.curveTo2:
2247 case OPS.curveTo3:
2248 case OPS.closePath:
2249 case OPS.rectangle:
2250 self.buildPath(fn, args, stateManager.state);
2251 continue;
2252 case OPS.stroke:
2253 case OPS.closeStroke:
2254 case OPS.fill:
2255 case OPS.eoFill:
2256 case OPS.fillStroke:
2257 case OPS.eoFillStroke:
2258 case OPS.closeFillStroke:
2259 case OPS.closeEOFillStroke:
2260 case OPS.endPath: {
2261 const {
2262 state: { pathBuffer, pathMinMax },
2263 } = stateManager;
2264 if (
2265 fn === OPS.closeStroke ||
2266 fn === OPS.closeFillStroke ||
2267 fn === OPS.closeEOFillStroke
2268 ) {
2269 pathBuffer.push(DrawOPS.closePath);
2270 }
2271 if (pathBuffer.length === 0) {
2272 operatorList.addOp(OPS.constructPath, [fn, [null], null]);
2273 } else {
2274 operatorList.addOp(OPS.constructPath, [
2275 fn,
2276 [new Float32Array(pathBuffer)],
2277 pathMinMax.slice(),
2278 ]);
2279 pathBuffer.length = 0;
2280 pathMinMax.set(BBOX_INIT, 0);
2281 }
2282 continue;
2283 }
2284 case OPS.setTextMatrix:
2285 operatorList.addOp(fn, [new Float32Array(args)]);
2286 continue;
2287 case OPS.markPoint:
2288 case OPS.markPointProps:
2289 case OPS.beginCompat:
2290 case OPS.endCompat:
2291 // Ignore operators where the corresponding handlers are known to
2292 // be no-op in CanvasGraphics (display/canvas.js). This prevents
2293 // serialization errors and is also a bit more efficient.
2294 // We could also try to serialize all objects in a general way,
2295 // e.g. as done in https://github.com/mozilla/pdf.js/pull/6266,
2296 // but doing so is meaningless without knowing the semantics.
2297 continue;
2298 case OPS.beginMarkedContentProps:
2299 markedContentLevel++;
2300 if (!(args[0] instanceof Name)) {
2301 warn(`Expected name for beginMarkedContentProps arg0=${args[0]}`);
2302 operatorList.addOp(OPS.beginMarkedContentProps, ["OC", null]);
2303 continue;
2304 }
2305 if (args[0].name === "OC") {
2306 next(
2307 self
2308 .parseMarkedContentProps(args[1], resources)
2309 .then(data => {
2310 operatorList.addOp(OPS.beginMarkedContentProps, [
2311 "OC",
2312 data,
2313 ]);
2314 })
2315 .catch(reason => {
2316 if (reason instanceof AbortException) {
2317 return;
2318 }
2319 if (self.options.ignoreErrors) {
2320 warn(
2321 `getOperatorList - ignoring beginMarkedContentProps: "${reason}".`
2322 );
2323 operatorList.addOp(OPS.beginMarkedContentProps, [
2324 "OC",
2325 null,
2326 ]);
2327 return;
2328 }
2329 throw reason;
2330 })
2331 );
2332 return;
2333 }
2334 // Preserve only the MCID from non-OC property dictionaries.
2335 args = [
2336 args[0].name,
2337 args[1] instanceof Dict ? args[1].get("MCID") : null,
2338 ];
2339
2340 break;
2341 case OPS.beginMarkedContent:
2342 if (args?.some(arg => arg instanceof Dict)) {
2343 warn(`getOperatorList - ignoring operator: ${fn}`);
2344 continue;
2345 }
2346 markedContentLevel++;
2347 break;
2348 case OPS.endMarkedContent:
2349 if (args?.some(arg => arg instanceof Dict)) {
2350 warn(`getOperatorList - ignoring operator: ${fn}`);
2351 continue;
2352 }
2353 if (markedContentLevel === 0) {
2354 continue;
2355 }
2356 markedContentLevel--;
2357 break;
2358 default:
2359 // Avoid postMessage errors from `Dict` arguments.
2360 if (args?.some(arg => arg instanceof Dict)) {
2361 warn(`getOperatorList - ignoring operator: ${fn}`);
2362 continue;
2363 }
2364 }
2365 operatorList.addOp(fn, args);
2366 }
2367 if (stop) {
2368 next(deferred);
2369 return;
2370 }
2371 // Close marked content and graphics states left open by this stream.
2372 closePendingMarkedContentOPS();
2373 closePendingRestoreOPS();
2374 resolve();
2375 }).catch(reason => {
2376 if (reason instanceof AbortException) {
2377 return;
2378 }
2379 if (this.options.ignoreErrors) {
2380 warn(
2381 `getOperatorList - ignoring errors during "${task.name}" ` +
2382 `task: "${reason}".`
2383 );
2384
2385 closePendingMarkedContentOPS();
2386 closePendingRestoreOPS();
2387 return;
2388 }
2389 throw reason;
2390 });
2391 }
2392
2393 async getTextContent({
2394 stream,
2395 task,
2396 resources,
2397 stateManager = null,
2398 includeMarkedContent = false,
2399 sink,
2400 seenStyles = new Set(),
2401 viewBox,
2402 lang = null,
2403 disableNormalization = false,
2404 keepWhiteSpace = false,
2405 prevRefs = null,
2406 intersector = null,
2407 }) {
2408 if (stream.isAsync) {
2409 const bytes = await stream.asyncGetBytes();
2410 if (bytes) {
2411 stream = new Stream(bytes, 0, bytes.length, stream.dict);
2412 }
2413 }
2414 sink ??= textSinkWrapper(null);
2415
2416 const objId = stream.dict?.objId;
2417 const seenRefs = new RefSet(prevRefs);
2418
2419 if (objId) {
2420 if (prevRefs?.has(objId)) {
2421 throw new Error(
2422 `getTextContent - ignoring circular reference: ${objId}`
2423 );
2424 }
2425 seenRefs.put(objId);
2426 }
2427 // Ensure that `resources`/`stateManager` is correctly initialized,
2428 // even if the provided parameter is e.g. `null`.
2429 resources ||= Dict.empty;
2430 stateManager ||= new StateManager(new TextState());
2431
2432 let markedContentLevel = 0;
2433 let textMarkedContentLevel = null;
2434
2435 const textContent = {
2436 items: [],
2437 styles: Object.create(null),
2438 lang,
2439 };
2440 const textContentItem = {
2441 initialized: false,
2442 str: [],
2443 totalWidth: 0,
2444 totalHeight: 0,
2445 width: 0,
2446 height: 0,
2447 vertical: false,
2448 prevTransform: null,
2449 prevTextRise: 0,
2450 textAdvanceScale: 0,
2451 spaceInFlowMin: 0,
2452 spaceInFlowMax: 0,
2453 trackingSpaceMin: Infinity,
2454 negativeSpaceMax: -Infinity,
2455 notASpace: -Infinity,
2456 transform: null,
2457 fontName: null,
2458 hasEOL: false,
2459 };
2460
2461 // Use a circular buffer (length === 2) to save the last chars in the
2462 // text stream.
2463 // This implementation of the circular buffer is using a fixed array
2464 // and the position of the next element:
2465 // function addElement(x) {
2466 // buffer[pos] = x;
2467 // pos = (pos + 1) % buffer.length;
2468 // }
2469 // It's a way faster than:
2470 // function addElement(x) {
2471 // buffer.push(x);
2472 // buffer.shift();
2473 // }
2474 //
2475 // It's useful to know when we need to add a whitespace in the
2476 // text chunk.
2477 const twoLastChars = [" ", " "];
2478 let twoLastCharsPos = 0;
2479
2480 /**
2481 * Save the last char.
2482 * @param {string} char
2483 * @returns {boolean} true when the two last chars before adding the new one
2484 * are a non-whitespace followed by a whitespace.
2485 */
2486 function saveLastChar(char) {
2487 const nextPos = (twoLastCharsPos + 1) % 2;
2488 const ret =
2489 twoLastChars[twoLastCharsPos] !== " " && twoLastChars[nextPos] === " ";
2490 twoLastChars[twoLastCharsPos] = char;
2491 twoLastCharsPos = nextPos;
2492
2493 return !keepWhiteSpace && ret;
2494 }
2495
2496 function shouldAddWhitepsace() {
2497 return (
2498 !keepWhiteSpace &&
2499 twoLastChars[twoLastCharsPos] !== " " &&
2500 twoLastChars[(twoLastCharsPos + 1) % 2] === " "
2501 );
2502 }
2503
2504 function resetLastChars() {
2505 twoLastChars[0] = twoLastChars[1] = " ";
2506 twoLastCharsPos = 0;
2507 }
2508
2509 // Used in addFakeSpaces.
2510
2511 // A white <= fontSize * TRACKING_SPACE_FACTOR is a tracking space
2512 // so it doesn't count as a space.
2513 const TRACKING_SPACE_FACTOR = 0.102;
2514
2515 // When a white <= fontSize * NOT_A_SPACE_FACTOR, there is no space
2516 // even if one is present in the text stream.
2517 const NOT_A_SPACE_FACTOR = 0.03;
2518
2519 // A negative white < fontSize * NEGATIVE_SPACE_FACTOR induces
2520 // a break (a new chunk of text is created).
2521 // It doesn't change anything when the text is copied but
2522 // it improves potential mismatch between text layer and canvas.
2523 const NEGATIVE_SPACE_FACTOR = -0.2;
2524
2525 // A white with a width in [fontSize * MIN_FACTOR; fontSize * MAX_FACTOR]
2526 // is a space which will be inserted in the current flow of words.
2527 // If the width is outside of this range then the flow is broken
2528 // (which means a new span in the text layer).
2529 // It's useful to adjust the best as possible the span in the layer
2530 // to what is displayed in the canvas.
2531 const SPACE_IN_FLOW_MIN_FACTOR = 0.102;
2532 const SPACE_IN_FLOW_MAX_FACTOR = 0.6;
2533
2534 // If a char is too high/too low compared to the previous we just create
2535 // a new chunk.
2536 // If the advance isn't in the +/-VERTICAL_SHIFT_RATIO * height range then
2537 // a new chunk is created.
2538 const VERTICAL_SHIFT_RATIO = 0.25;
2539
2540 const self = this;
2541 const xref = this.xref;
2542 const showSpacedTextBuffer = [];
2543
2544 // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
2545 let xobjs = null;
2546 const emptyXObjectCache = new LocalImageCache();
2547 const emptyGStateCache = new LocalGStateCache();
2548
2549 const preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
2550
2551 let textState, currentTextState;
2552
2553 function pushWhitespace({
2554 width = 0,
2555 height = 0,
2556 transform = textContentItem.prevTransform,
2557 fontName = textContentItem.fontName,
2558 }) {
2559 intersector?.addExtraChar(" ");
2560 textContent.items.push({
2561 str: " ",
2562 dir: "ltr",
2563 width,
2564 height,
2565 transform,
2566 fontName,
2567 hasEOL: false,
2568 });
2569 }
2570
2571 function getCurrentTextTransform() {
2572 // 9.4.4 Text Space Details
2573 const font = textState.font;
2574 const tsm = [
2575 textState.fontSize * textState.textHScale,
2576 0,
2577 0,
2578 textState.fontSize,
2579 0,
2580 textState.textRise,
2581 ];
2582
2583 if (
2584 font.isType3Font &&
2585 (textState.fontSize <= 1 || font.isCharBBox) &&
2586 !isArrayEqual(textState.fontMatrix, FONT_IDENTITY_MATRIX)
2587 ) {
2588 const glyphHeight = font.bbox[3] - font.bbox[1];
2589 if (glyphHeight > 0) {
2590 tsm[3] *= glyphHeight * textState.fontMatrix[3];
2591 }
2592 }
2593
2594 return Util.transform(
2595 textState.ctm,
2596 Util.transform(textState.textMatrix, tsm)
2597 );
2598 }
2599
2600 function ensureTextContentItem() {
2601 if (textContentItem.initialized) {
2602 return textContentItem;
2603 }
2604 const { font, loadedName } = textState;
2605 if (!seenStyles.has(loadedName)) {
2606 seenStyles.add(loadedName);
2607 textContent.styles[loadedName] = {
2608 fontFamily: font.fallbackName,
2609 ascent: font.ascent,
2610 descent: font.descent,
2611 vertical: font.vertical,
2612 };
2613 if (self.options.fontExtraProperties && font.systemFontInfo) {
2614 const style = textContent.styles[loadedName];
2615 style.fontSubstitution = font.systemFontInfo.css;
2616 style.fontSubstitutionLoadedName = font.systemFontInfo.loadedName;
2617 }
2618 }
2619 textContentItem.fontName = loadedName;
2620
2621 const trm = (textContentItem.transform = getCurrentTextTransform());
2622 if (!font.vertical) {
2623 textContentItem.width = textContentItem.totalWidth = 0;
2624 textContentItem.height = textContentItem.totalHeight = Math.hypot(
2625 trm[2],
2626 trm[3]
2627 );
2628 textContentItem.vertical = false;
2629 } else {
2630 textContentItem.width = textContentItem.totalWidth = Math.hypot(
2631 trm[0],
2632 trm[1]
2633 );
2634 textContentItem.height = textContentItem.totalHeight = 0;
2635 textContentItem.vertical = true;
2636 }
2637
2638 const scaleLineX = Math.hypot(
2639 textState.textLineMatrix[0],
2640 textState.textLineMatrix[1]
2641 );
2642 const scaleCtmX = Math.hypot(textState.ctm[0], textState.ctm[1]);
2643 textContentItem.textAdvanceScale = scaleCtmX * scaleLineX;
2644
2645 const { fontSize } = textState;
2646 textContentItem.trackingSpaceMin = fontSize * TRACKING_SPACE_FACTOR;
2647 textContentItem.notASpace = fontSize * NOT_A_SPACE_FACTOR;
2648 textContentItem.negativeSpaceMax = fontSize * NEGATIVE_SPACE_FACTOR;
2649 textContentItem.spaceInFlowMin = fontSize * SPACE_IN_FLOW_MIN_FACTOR;
2650 textContentItem.spaceInFlowMax = fontSize * SPACE_IN_FLOW_MAX_FACTOR;
2651 textContentItem.hasEOL = false;
2652
2653 textContentItem.initialized = true;
2654 return textContentItem;
2655 }
2656
2657 function updateAdvanceScale() {
2658 if (!textContentItem.initialized) {
2659 return;
2660 }
2661
2662 const scaleLineX = Math.hypot(
2663 textState.textLineMatrix[0],
2664 textState.textLineMatrix[1]
2665 );
2666 const scaleCtmX = Math.hypot(textState.ctm[0], textState.ctm[1]);
2667 const scaleFactor = scaleCtmX * scaleLineX;
2668 if (scaleFactor === textContentItem.textAdvanceScale) {
2669 return;
2670 }
2671
2672 if (!textContentItem.vertical) {
2673 textContentItem.totalWidth +=
2674 textContentItem.width * textContentItem.textAdvanceScale;
2675 textContentItem.width = 0;
2676 } else {
2677 textContentItem.totalHeight +=
2678 textContentItem.height * textContentItem.textAdvanceScale;
2679 textContentItem.height = 0;
2680 }
2681
2682 textContentItem.textAdvanceScale = scaleFactor;
2683 }
2684
2685 function runBidiTransform(textChunk) {
2686 let text = textChunk.str.join("");
2687 if (!disableNormalization) {
2688 text = normalizeUnicode(text);
2689 }
2690 const bidiResult = bidi(text, -1, textChunk.vertical);
2691 return {
2692 str: bidiResult.str,
2693 dir: bidiResult.dir,
2694 width: Math.abs(textChunk.totalWidth),
2695 height: Math.abs(textChunk.totalHeight),
2696 transform: textChunk.transform,
2697 fontName: textChunk.fontName,
2698 hasEOL: textChunk.hasEOL,
2699 };
2700 }
2701
2702 async function handleSetFont(fontName, fontRef) {
2703 const translated = await self.loadFont(
2704 fontName,
2705 fontRef,
2706 resources,
2707 task,
2708 /* fallbackFontDict = */ null,
2709 /* cssFontInfo = */ null,
2710 seenRefs
2711 );
2712
2713 textState.loadedName = translated.loadedName;
2714 textState.font = translated.font;
2715 textState.fontMatrix = translated.font.fontMatrix || FONT_IDENTITY_MATRIX;
2716 }
2717
2718 function applyInverseRotation(x, y, matrix) {
2719 const scale = Math.hypot(matrix[0], matrix[1]);
2720 return [
2721 (matrix[0] * x + matrix[1] * y) / scale,
2722 (matrix[2] * x + matrix[3] * y) / scale,
2723 ];
2724 }
2725
2726 function compareWithLastPosition(glyphWidth) {
2727 const currentTransform = getCurrentTextTransform();
2728 let posX = currentTransform[4];
2729 let posY = currentTransform[5];
2730
2731 // Check if the glyph is in the viewbox.
2732 if (textState.font?.vertical) {
2733 if (
2734 posX < viewBox[0] ||
2735 posX > viewBox[2] ||
2736 posY + glyphWidth < viewBox[1] ||
2737 posY > viewBox[3]
2738 ) {
2739 return false;
2740 }
2741 } else if (
2742 posX + glyphWidth < viewBox[0] ||
2743 posX > viewBox[2] ||
2744 posY < viewBox[1] ||
2745 posY > viewBox[3]
2746 ) {
2747 return false;
2748 }
2749
2750 if (!textState.font || !textContentItem.prevTransform) {
2751 return true;
2752 }
2753
2754 let lastPosX = textContentItem.prevTransform[4];
2755 let lastPosY = textContentItem.prevTransform[5];
2756
2757 if (lastPosX === posX && lastPosY === posY) {
2758 return true;
2759 }
2760
2761 let rotate = -1;
2762 // Take into account the rotation is the current transform.
2763 if (
2764 currentTransform[0] &&
2765 currentTransform[1] === 0 &&
2766 currentTransform[2] === 0
2767 ) {
2768 rotate = currentTransform[0] > 0 ? 0 : 180;
2769 } else if (
2770 currentTransform[1] &&
2771 currentTransform[0] === 0 &&
2772 currentTransform[3] === 0
2773 ) {
2774 rotate = currentTransform[1] > 0 ? 90 : 270;
2775 }
2776
2777 switch (rotate) {
2778 case 0:
2779 break;
2780 case 90:
2781 [posX, posY] = [posY, posX];
2782 [lastPosX, lastPosY] = [lastPosY, lastPosX];
2783 break;
2784 case 180:
2785 [posX, posY, lastPosX, lastPosY] = [
2786 -posX,
2787 -posY,
2788 -lastPosX,
2789 -lastPosY,
2790 ];
2791 break;
2792 case 270:
2793 [posX, posY] = [-posY, -posX];
2794 [lastPosX, lastPosY] = [-lastPosY, -lastPosX];
2795 break;
2796 default:
2797 // This is not a 0, 90, 180, 270 rotation so:
2798 // - remove the scale factor from the matrix to get a rotation matrix
2799 // - apply the inverse (which is the transposed) to the positions
2800 // and we can then compare positions of the glyphs to detect
2801 // a whitespace.
2802 [posX, posY] = applyInverseRotation(posX, posY, currentTransform);
2803 [lastPosX, lastPosY] = applyInverseRotation(
2804 lastPosX,
2805 lastPosY,
2806 textContentItem.prevTransform
2807 );
2808 }
2809
2810 if (textState.font.vertical) {
2811 const advanceY = (lastPosY - posY) / textContentItem.textAdvanceScale;
2812 const advanceX = posX - lastPosX;
2813
2814 // When the total height of the current chunk is negative
2815 // then we're writing from bottom to top.
2816 const textOrientation = Math.sign(
2817 textContentItem.height || textContentItem.totalHeight
2818 );
2819 if (advanceY < textOrientation * textContentItem.negativeSpaceMax) {
2820 if (
2821 Math.abs(advanceX) >
2822 0.5 * textContentItem.width /* not the same column */
2823 ) {
2824 appendEOL();
2825 return true;
2826 }
2827
2828 resetLastChars();
2829 flushTextContentItem();
2830 return true;
2831 }
2832
2833 if (Math.abs(advanceX) > textContentItem.width) {
2834 appendEOL();
2835 return true;
2836 }
2837
2838 if (advanceY <= textOrientation * textContentItem.notASpace) {
2839 // The real spacing between 2 consecutive chars is thin enough to be
2840 // considered a non-space.
2841 resetLastChars();
2842 }
2843
2844 if (advanceY <= textOrientation * textContentItem.trackingSpaceMin) {
2845 if (shouldAddWhitepsace()) {
2846 // The space is very thin, hence it deserves to have its own span in
2847 // order to avoid too much shift between the canvas and the text
2848 // layer.
2849 resetLastChars();
2850 flushTextContentItem();
2851 pushWhitespace({ height: Math.abs(advanceY) });
2852 } else {
2853 textContentItem.height += advanceY;
2854 }
2855 } else if (
2856 !addFakeSpaces(
2857 advanceY,
2858 textContentItem.prevTransform,
2859 textOrientation
2860 )
2861 ) {
2862 if (textContentItem.str.length === 0) {
2863 resetLastChars();
2864 pushWhitespace({ height: Math.abs(advanceY) });
2865 } else {
2866 textContentItem.height += advanceY;
2867 }
2868 }
2869
2870 if (Math.abs(advanceX) > textContentItem.width * VERTICAL_SHIFT_RATIO) {
2871 flushTextContentItem();
2872 }
2873
2874 return true;
2875 }
2876
2877 const advanceX = (posX - lastPosX) / textContentItem.textAdvanceScale;
2878 const advanceY = posY - lastPosY;
2879
2880 // When the total width of the current chunk is negative
2881 // then we're writing from right to left.
2882 const textOrientation = Math.sign(
2883 textContentItem.width || textContentItem.totalWidth
2884 );
2885 if (advanceX < textOrientation * textContentItem.negativeSpaceMax) {
2886 if (
2887 Math.abs(advanceY) >
2888 0.5 * textContentItem.height /* not the same line */
2889 ) {
2890 appendEOL();
2891 return true;
2892 }
2893
2894 // We're moving back so in case the last char was a whitespace
2895 // we cancel it: it doesn't make sense to insert it.
2896 resetLastChars();
2897 flushTextContentItem();
2898 return true;
2899 }
2900
2901 // Compensate for a textRise change (e.g. superscript/subscript dropping
2902 // back to baseline): textRise is baked into posY/lastPosY via tsm[5] in
2903 // getCurrentTextTransform(), scaled by the Y component of the CTM×TM
2904 // product, which equals currentTransform[3] / textState.fontSize.
2905 // Without this correction a superscript whose textRise exceeds the line
2906 // height triggers a spurious EOL when the rise returns to 0.
2907 const textRiseDelta = textState.textRise - textContentItem.prevTextRise;
2908 const advanceYCorrected =
2909 textRiseDelta === 0
2910 ? advanceY
2911 : advanceY -
2912 (currentTransform[3] / textState.fontSize) * textRiseDelta;
2913 if (Math.abs(advanceYCorrected) > textContentItem.height) {
2914 appendEOL();
2915 return true;
2916 }
2917
2918 if (advanceX <= textOrientation * textContentItem.notASpace) {
2919 // The real spacing between 2 consecutive chars is thin enough to be
2920 // considered a non-space.
2921 resetLastChars();
2922 }
2923
2924 if (advanceX <= textOrientation * textContentItem.trackingSpaceMin) {
2925 if (shouldAddWhitepsace()) {
2926 // The space is very thin, hence it deserves to have its own span in
2927 // order to avoid too much shift between the canvas and the text
2928 // layer.
2929 resetLastChars();
2930 flushTextContentItem();
2931 pushWhitespace({ width: Math.abs(advanceX) });
2932 } else {
2933 textContentItem.width += advanceX;
2934 }
2935 } else if (
2936 !addFakeSpaces(advanceX, textContentItem.prevTransform, textOrientation)
2937 ) {
2938 if (textContentItem.str.length === 0) {
2939 resetLastChars();
2940 pushWhitespace({ width: Math.abs(advanceX) });
2941 } else {
2942 textContentItem.width += advanceX;
2943 }
2944 }
2945
2946 if (Math.abs(advanceY) > textContentItem.height * VERTICAL_SHIFT_RATIO) {
2947 flushTextContentItem();
2948 }
2949
2950 return true;
2951 }
2952
2953 function buildTextContentItem({ chars, extraSpacing }) {
2954 if (
2955 currentTextState !== textState &&
2956 (currentTextState.fontSize !== textState.fontSize ||
2957 (currentTextState.fontName !== textState.fontName &&
2958 (currentTextState.font.name !== textState.font.name ||
2959 currentTextState.font.vertical !== textState.font.vertical)))
2960 ) {
2961 flushTextContentItem();
2962 currentTextState = textState.clone();
2963 }
2964
2965 const font = textState.font;
2966 const baseCharSpacing = font.vertical
2967 ? -textState.charSpacing
2968 : textState.charSpacing;
2969 if (!chars) {
2970 // Just move according to the space we have.
2971 const charSpacing = baseCharSpacing + extraSpacing;
2972 if (charSpacing) {
2973 if (!font.vertical) {
2974 textState.translateTextMatrix(
2975 charSpacing * textState.textHScale,
2976 0
2977 );
2978 } else {
2979 textState.translateTextMatrix(0, -charSpacing);
2980 }
2981 }
2982
2983 if (keepWhiteSpace) {
2984 compareWithLastPosition(0);
2985 }
2986
2987 return;
2988 }
2989
2990 const glyphs = font.charsToGlyphs(chars);
2991 const scale = textState.fontMatrix[0] * textState.fontSize;
2992
2993 for (let i = 0, ii = glyphs.length; i < ii; i++) {
2994 const glyph = glyphs[i];
2995 const { category, originalCharCode } = glyph;
2996
2997 if (category.isInvisibleFormatMark) {
2998 continue;
2999 }
3000 let charSpacing = baseCharSpacing + (i + 1 === ii ? extraSpacing : 0);
3001
3002 let glyphWidth = glyph.width;
3003 if (font.vertical) {
3004 glyphWidth = glyph.vmetric ? glyph.vmetric[0] : -glyphWidth;
3005 }
3006 let scaledDim = glyphWidth * scale;
3007
3008 if (originalCharCode === 0x20) {
3009 charSpacing += textState.wordSpacing;
3010 }
3011
3012 if (!keepWhiteSpace && category.isWhitespace) {
3013 // Don't push a " " in the textContentItem
3014 // (except when it's between two non-spaces chars),
3015 // it will be done (if required) in next call to
3016 // compareWithLastPosition.
3017 // This way we can merge real spaces and spaces due to cursor moves.
3018 if (!font.vertical) {
3019 charSpacing += scaledDim;
3020 textState.translateTextMatrix(
3021 charSpacing * textState.textHScale,
3022 0
3023 );
3024 } else {
3025 charSpacing += -scaledDim;
3026 textState.translateTextMatrix(0, -charSpacing);
3027 }
3028 saveLastChar(" ");
3029 continue;
3030 }
3031
3032 if (
3033 !category.isZeroWidthDiacritic &&
3034 !compareWithLastPosition(scaledDim)
3035 ) {
3036 // The glyph is not in page so just skip it but move the cursor.
3037 if (!font.vertical) {
3038 textState.translateTextMatrix(scaledDim * textState.textHScale, 0);
3039 } else {
3040 textState.translateTextMatrix(0, scaledDim);
3041 }
3042 continue;
3043 }
3044
3045 // Must be called after compareWithLastPosition because
3046 // the textContentItem could have been flushed.
3047 const textChunk = ensureTextContentItem();
3048 if (category.isZeroWidthDiacritic) {
3049 scaledDim = 0;
3050 }
3051
3052 if (!font.vertical) {
3053 scaledDim *= textState.textHScale;
3054 intersector?.addGlyph(
3055 getCurrentTextTransform(),
3056 scaledDim,
3057 0,
3058 glyph.unicode
3059 );
3060 textState.translateTextMatrix(scaledDim, 0);
3061 textChunk.width += scaledDim;
3062 } else {
3063 intersector?.addGlyph(
3064 getCurrentTextTransform(),
3065 0,
3066 scaledDim,
3067 glyph.unicode
3068 );
3069 textState.translateTextMatrix(0, scaledDim);
3070 scaledDim = Math.abs(scaledDim);
3071 textChunk.height += scaledDim;
3072 }
3073
3074 if (scaledDim) {
3075 // Save the position of the last visible character.
3076 textChunk.prevTransform = getCurrentTextTransform();
3077 textChunk.prevTextRise = textState.textRise;
3078 }
3079
3080 const glyphUnicode = glyph.unicode;
3081 if (saveLastChar(glyphUnicode)) {
3082 // The two last chars are a non-whitespace followed by a whitespace
3083 // and then this non-whitespace, so we insert a whitespace here.
3084 // Replaces all whitespaces with standard spaces (0x20), to avoid
3085 // alignment issues between the textLayer and the canvas if the text
3086 // contains e.g. tabs (fixes issue6612.pdf).
3087 textChunk.str.push(" ");
3088 intersector?.addExtraChar(" ");
3089 }
3090
3091 if (!intersector) {
3092 textChunk.str.push(glyphUnicode);
3093 }
3094
3095 if (charSpacing) {
3096 if (!font.vertical) {
3097 textState.translateTextMatrix(
3098 charSpacing * textState.textHScale,
3099 0
3100 );
3101 } else {
3102 textState.translateTextMatrix(0, -charSpacing);
3103 }
3104 }
3105 }
3106 }
3107
3108 function appendEOL() {
3109 intersector?.addExtraChar("\n");
3110 resetLastChars();
3111 if (textContentItem.initialized) {
3112 textContentItem.hasEOL = true;
3113 flushTextContentItem();
3114 } else {
3115 textContent.items.push({
3116 str: "",
3117 dir: "ltr",
3118 width: 0,
3119 height: 0,
3120 transform: getCurrentTextTransform(),
3121 fontName: textState.loadedName,
3122 hasEOL: true,
3123 });
3124 }
3125 }
3126
3127 function addFakeSpaces(width, transf, textOrientation) {
3128 if (
3129 textOrientation * textContentItem.spaceInFlowMin <= width &&
3130 width <= textOrientation * textContentItem.spaceInFlowMax
3131 ) {
3132 if (textContentItem.initialized) {
3133 resetLastChars();
3134 textContentItem.str.push(" ");
3135 intersector?.addExtraChar(" ");
3136 }
3137 return false;
3138 }
3139
3140 const fontName = textContentItem.fontName;
3141
3142 let height = 0;
3143 if (textContentItem.vertical) {
3144 height = width;
3145 width = 0;
3146 }
3147
3148 flushTextContentItem();
3149 resetLastChars();
3150 pushWhitespace({
3151 width: Math.abs(width),
3152 height: Math.abs(height),
3153 transform: transf || getCurrentTextTransform(),
3154 fontName,
3155 });
3156
3157 return true;
3158 }
3159
3160 function flushTextContentItem() {
3161 if (!textContentItem.initialized || !textContentItem.str) {
3162 return;
3163 }
3164
3165 // Do final text scaling.
3166 if (!textContentItem.vertical) {
3167 textContentItem.totalWidth +=
3168 textContentItem.width * textContentItem.textAdvanceScale;
3169 } else {
3170 textContentItem.totalHeight +=
3171 textContentItem.height * textContentItem.textAdvanceScale;
3172 }
3173
3174 textContent.items.push(runBidiTransform(textContentItem));
3175 textContentItem.initialized = false;
3176 textContentItem.str.length = 0;
3177 }
3178
3179 function closePendingMarkedContentItems(level = 0) {
3180 if (!includeMarkedContent || markedContentLevel <= level) {
3181 return;
3182 }
3183 flushTextContentItem();
3184
3185 for (; markedContentLevel > level; markedContentLevel--) {
3186 textContent.items.push({
3187 type: "endMarkedContent",
3188 });
3189 }
3190 }
3191
3192 function enqueueChunk(batch = false) {
3193 const length = textContent.items.length;
3194 if (length === 0) {
3195 return;
3196 }
3197 if (batch && length < TEXT_CHUNK_BATCH_SIZE) {
3198 return;
3199 }
3200 sink.enqueue(textContent, length);
3201 textContent.items = [];
3202 textContent.styles = Object.create(null);
3203 }
3204
3205 const timeSlotManager = new TimeSlotManager();
3206
3207 return new Promise(function promiseBody(resolve, reject) {
3208 const next = function (promise) {
3209 enqueueChunk(/* batch = */ true);
3210 Promise.all([promise, sink.ready]).then(function () {
3211 try {
3212 promiseBody(resolve, reject);
3213 } catch (ex) {
3214 reject(ex);
3215 }
3216 }, reject);
3217 };
3218 task.ensureNotTerminated();
3219 timeSlotManager.reset();
3220
3221 const operation = {};
3222 let stop,
3223 name,
3224 isValidName,
3225 args = [];
3226 while (!(stop = timeSlotManager.check())) {
3227 // The arguments parsed by read() are not used beyond this loop, so
3228 // we can reuse the same array on every iteration, thus avoiding
3229 // unnecessary allocations.
3230 args.length = 0;
3231 operation.args = args;
3232 if (!preprocessor.read(operation)) {
3233 break;
3234 }
3235
3236 // preprocessor.read() already handles save, restore and transform
3237 // operations, so we don't need to worry about them here.
3238
3239 textState = stateManager.state;
3240 currentTextState ||= textState.clone();
3241 const fn = operation.fn;
3242 args = operation.args;
3243
3244 switch (fn | 0) {
3245 case OPS.setFont:
3246 // Optimization to ignore multiple identical Tf commands.
3247 const fontNameArg = args[0].name,
3248 fontSizeArg = args[1];
3249 if (
3250 textState.font &&
3251 fontNameArg === textState.fontName &&
3252 fontSizeArg === textState.fontSize
3253 ) {
3254 break;
3255 }
3256
3257 textState.fontName = fontNameArg;
3258 textState.fontSize = fontSizeArg;
3259 next(handleSetFont(fontNameArg, null));
3260 return;
3261 case OPS.setTextRise:
3262 textState.textRise = args[0];
3263 break;
3264 case OPS.setHScale:
3265 textState.textHScale = args[0] / 100;
3266 break;
3267 case OPS.setLeading:
3268 textState.leading = args[0];
3269 break;
3270 case OPS.moveText:
3271 textState.translateTextLineMatrix(args[0], args[1]);
3272 textState.textMatrix = textState.textLineMatrix.slice();
3273 break;
3274 case OPS.setLeadingMoveText:
3275 textState.leading = -args[1];
3276 textState.translateTextLineMatrix(args[0], args[1]);
3277 textState.textMatrix = textState.textLineMatrix.slice();
3278 break;
3279 case OPS.nextLine:
3280 textState.carriageReturn();
3281 break;
3282 case OPS.setTextMatrix:
3283 textState.setTextMatrix(
3284 args[0],
3285 args[1],
3286 args[2],
3287 args[3],
3288 args[4],
3289 args[5]
3290 );
3291 textState.setTextLineMatrix(
3292 args[0],
3293 args[1],
3294 args[2],
3295 args[3],
3296 args[4],
3297 args[5]
3298 );
3299 updateAdvanceScale();
3300 break;
3301 case OPS.setCharSpacing:
3302 textState.charSpacing = args[0];
3303 break;
3304 case OPS.setWordSpacing:
3305 textState.wordSpacing = args[0];
3306 break;
3307 case OPS.beginText:
3308 textState.textMatrix = IDENTITY_MATRIX.slice();
3309 textState.textLineMatrix = IDENTITY_MATRIX.slice();
3310 textMarkedContentLevel = markedContentLevel;
3311 break;
3312 case OPS.endText:
3313 if (textMarkedContentLevel !== null) {
3314 closePendingMarkedContentItems(textMarkedContentLevel);
3315 textMarkedContentLevel = null;
3316 }
3317 break;
3318 case OPS.showSpacedText:
3319 if (!stateManager.state.font) {
3320 self.ensureStateFont(stateManager.state);
3321 continue;
3322 }
3323
3324 const spaceFactor =
3325 ((textState.font.vertical ? 1 : -1) * textState.fontSize) / 1000;
3326 for (const item of args[0]) {
3327 if (typeof item === "string") {
3328 showSpacedTextBuffer.push(item);
3329 } else if (typeof item === "number" && item !== 0) {
3330 // PDF Specification 5.3.2 states:
3331 // The number is expressed in thousandths of a unit of text
3332 // space.
3333 // This amount is subtracted from the current horizontal or
3334 // vertical coordinate, depending on the writing mode.
3335 // In the default coordinate system, a positive adjustment
3336 // has the effect of moving the next glyph painted either to
3337 // the left or down by the given amount.
3338 const str = showSpacedTextBuffer.join("");
3339 showSpacedTextBuffer.length = 0;
3340 buildTextContentItem({
3341 chars: str,
3342 extraSpacing: item * spaceFactor,
3343 });
3344 }
3345 }
3346
3347 if (showSpacedTextBuffer.length > 0) {
3348 const str = showSpacedTextBuffer.join("");
3349 showSpacedTextBuffer.length = 0;
3350 buildTextContentItem({
3351 chars: str,
3352 extraSpacing: 0,
3353 });
3354 }
3355 break;
3356 case OPS.showText:
3357 if (!stateManager.state.font) {
3358 self.ensureStateFont(stateManager.state);
3359 continue;
3360 }
3361 buildTextContentItem({
3362 chars: args[0],
3363 extraSpacing: 0,
3364 });
3365 break;
3366 case OPS.nextLineShowText:
3367 if (!stateManager.state.font) {
3368 self.ensureStateFont(stateManager.state);
3369 continue;
3370 }
3371 textState.carriageReturn();
3372 buildTextContentItem({
3373 chars: args[0],
3374 extraSpacing: 0,
3375 });
3376 break;
3377 case OPS.nextLineSetSpacingShowText:
3378 if (!stateManager.state.font) {
3379 self.ensureStateFont(stateManager.state);
3380 continue;
3381 }
3382 textState.wordSpacing = args[0];
3383 textState.charSpacing = args[1];
3384 textState.carriageReturn();
3385 buildTextContentItem({
3386 chars: args[2],
3387 extraSpacing: 0,
3388 });
3389 break;
3390 case OPS.paintXObject:
3391 flushTextContentItem();
3392 xobjs ??= resources.get("XObject") || Dict.empty;
3393
3394 isValidName = args[0] instanceof Name;
3395 name = args[0].name;
3396
3397 if (isValidName && emptyXObjectCache.getByName(name)) {
3398 break;
3399 }
3400
3401 next(
3402 new Promise(function (resolveXObject, rejectXObject) {
3403 if (!isValidName) {
3404 throw new FormatError("XObject must be referred to by name.");
3405 }
3406
3407 let xobj = xobjs.getRaw(name);
3408 if (xobj instanceof Ref) {
3409 if (emptyXObjectCache.getByRef(xobj)) {
3410 resolveXObject();
3411 return;
3412 }
3413
3414 const globalImage = self.globalImageCache.getData(
3415 xobj,
3416 self.pageIndex
3417 );
3418 if (globalImage) {
3419 resolveXObject();
3420 return;
3421 }
3422
3423 xobj = xref.fetch(xobj);
3424 }
3425
3426 if (!(xobj instanceof BaseStream)) {
3427 throw new FormatError("XObject should be a stream");
3428 }
3429 const { dict } = xobj;
3430
3431 const type = dict.get("Subtype");
3432 if (!(type instanceof Name)) {
3433 throw new FormatError("XObject should have a Name subtype");
3434 }
3435
3436 if (type.name !== "Form") {
3437 emptyXObjectCache.set(name, dict.objId, true);
3438
3439 resolveXObject();
3440 return;
3441 }
3442
3443 // Use a new `StateManager` to prevent incorrect positioning
3444 // of textItems *after* the Form XObject, since errors in the
3445 // data can otherwise prevent `restore` operators from
3446 // executing.
3447 // NOTE: Only an issue when `options.ignoreErrors === true`.
3448 const currentState = stateManager.state.clone();
3449 const xObjStateManager = new StateManager(currentState);
3450
3451 const matrix = lookupMatrix(dict.getArray("Matrix"), null);
3452 if (matrix) {
3453 xObjStateManager.transform(matrix);
3454 }
3455
3456 const localResources = dict.get("Resources");
3457
3458 // Enqueue the `textContent` chunk before parsing the /Form
3459 // XObject.
3460 enqueueChunk();
3461 const sinkWrapper = textSinkWrapper(sink);
3462
3463 self
3464 .getTextContent({
3465 stream: xobj,
3466 task,
3467 resources:
3468 localResources instanceof Dict
3469 ? localResources
3470 : resources,
3471 stateManager: xObjStateManager,
3472 includeMarkedContent,
3473 sink: sinkWrapper,
3474 seenStyles,
3475 viewBox,
3476 lang,
3477 disableNormalization,
3478 keepWhiteSpace,
3479 prevRefs: seenRefs,
3480 })
3481 .then(function () {
3482 if (!sinkWrapper.enqueueInvoked) {
3483 emptyXObjectCache.set(name, dict.objId, true);
3484 }
3485 resolveXObject();
3486 }, rejectXObject);
3487 }).catch(function (reason) {
3488 if (reason instanceof AbortException) {
3489 return;
3490 }
3491 if (self.options.ignoreErrors) {
3492 // Error(s) in the XObject -- allow text-extraction to
3493 // continue.
3494 warn(`getTextContent - ignoring XObject: "${reason}".`);
3495 return;
3496 }
3497 throw reason;
3498 })
3499 );
3500 return;
3501 case OPS.setGState:
3502 isValidName = args[0] instanceof Name;
3503 name = args[0].name;
3504
3505 if (isValidName && emptyGStateCache.getByName(name)) {
3506 break;
3507 }
3508
3509 next(
3510 new Promise(function (resolveGState, rejectGState) {
3511 if (!isValidName) {
3512 throw new FormatError("GState must be referred to by name.");
3513 }
3514
3515 const extGState = resources.get("ExtGState");
3516 if (!(extGState instanceof Dict)) {
3517 throw new FormatError("ExtGState should be a dictionary.");
3518 }
3519
3520 const gState = extGState.get(name);
3521 // TODO: Attempt to lookup cached GStates by reference as well,
3522 // if and only if there are PDF documents where doing so
3523 // would significantly improve performance.
3524 if (!(gState instanceof Dict)) {
3525 throw new FormatError("GState should be a dictionary.");
3526 }
3527
3528 const gStateFont = gState.get("Font");
3529 if (!gStateFont) {
3530 emptyGStateCache.set(name, gState.objId, true);
3531
3532 resolveGState();
3533 return;
3534 }
3535 flushTextContentItem();
3536
3537 textState.fontName = null;
3538 textState.fontSize = gStateFont[1];
3539 handleSetFont(null, gStateFont[0]).then(
3540 resolveGState,
3541 rejectGState
3542 );
3543 }).catch(function (reason) {
3544 if (reason instanceof AbortException) {
3545 return;
3546 }
3547 if (self.options.ignoreErrors) {
3548 // Error(s) in the ExtGState -- allow text-extraction to
3549 // continue.
3550 warn(`getTextContent - ignoring ExtGState: "${reason}".`);
3551 return;
3552 }
3553 throw reason;
3554 })
3555 );
3556 return;
3557 case OPS.beginMarkedContent:
3558 flushTextContentItem();
3559 if (includeMarkedContent) {
3560 markedContentLevel++;
3561
3562 textContent.items.push({
3563 type: "beginMarkedContent",
3564 tag: args[0] instanceof Name ? args[0].name : null,
3565 });
3566 }
3567 break;
3568 case OPS.beginMarkedContentProps:
3569 flushTextContentItem();
3570 if (includeMarkedContent) {
3571 markedContentLevel++;
3572
3573 const mcid = args[1] instanceof Dict ? args[1].get("MCID") : null;
3574 textContent.items.push({
3575 type: "beginMarkedContentProps",
3576 id: Number.isInteger(mcid)
3577 ? `${self.idFactory.getPageObjId()}_mc${mcid}`
3578 : null,
3579 tag: args[0] instanceof Name ? args[0].name : null,
3580 });
3581 }
3582 break;
3583 case OPS.endMarkedContent:
3584 flushTextContentItem();
3585 if (includeMarkedContent) {
3586 if (markedContentLevel === 0) {
3587 // Ignore unmatched EMC operators (issue 15629).
3588 break;
3589 }
3590 markedContentLevel--;
3591
3592 textContent.items.push({
3593 type: "endMarkedContent",
3594 });
3595 }
3596 break;
3597 } // switch
3598 if (textContent.items.length >= sink.desiredSize) {
3599 // Wait for ready, if we reach highWaterMark.
3600 stop = true;
3601 break;
3602 }
3603 } // while
3604 if (stop) {
3605 next(deferred);
3606 return;
3607 }
3608 flushTextContentItem();
3609 closePendingMarkedContentItems();
3610 enqueueChunk();
3611 resolve();
3612 }).catch(reason => {
3613 if (reason instanceof AbortException) {
3614 return;
3615 }
3616 if (this.options.ignoreErrors) {
3617 // Error(s) in the TextContent -- allow text-extraction to continue.
3618 warn(
3619 `getTextContent - ignoring errors during "${task.name}" ` +
3620 `task: "${reason}".`
3621 );
3622
3623 flushTextContentItem();
3624 closePendingMarkedContentItems();
3625 enqueueChunk();
3626 return;
3627 }
3628 throw reason;
3629 });
3630 }
3631
3632 async extractDataStructures(dict, properties) {
3633 const xref = this.xref;
3634 let cidToGidBytes;
3635 // 9.10.2
3636 const toUnicodePromise = this.readToUnicode(properties.toUnicode);
3637
3638 if (properties.composite) {
3639 // CIDSystemInfo helps to match CID to glyphs
3640 const cidSystemInfo = dict.get("CIDSystemInfo");
3641 if (cidSystemInfo instanceof Dict && !properties.cidSystemInfo) {
3642 properties.cidSystemInfo = {
3643 registry: stringToPDFString(cidSystemInfo.get("Registry")),
3644 ordering: stringToPDFString(cidSystemInfo.get("Ordering")),
3645 supplement: cidSystemInfo.get("Supplement"),
3646 };
3647 }
3648
3649 try {
3650 const cidToGidMap = dict.get("CIDToGIDMap");
3651 if (cidToGidMap instanceof BaseStream) {
3652 cidToGidBytes = cidToGidMap.getBytes();
3653 }
3654 } catch (ex) {
3655 if (!this.options.ignoreErrors) {
3656 throw ex;
3657 }
3658 warn(`extractDataStructures - ignoring CIDToGIDMap data: "${ex}".`);
3659 }
3660 }
3661
3662 // Based on 9.6.6 of the spec the encoding can come from multiple places
3663 // and depends on the font type. The base encoding and differences are
3664 // read here, but the encoding that is actually used is chosen during
3665 // glyph mapping in the font.
3666 // TODO: Loading the built in encoding in the font would allow the
3667 // differences to be merged in here not require us to hold on to it.
3668 const differences = [];
3669 let baseEncodingName = null;
3670 let encoding;
3671 if (dict.has("Encoding")) {
3672 encoding = dict.get("Encoding");
3673 if (encoding instanceof Dict) {
3674 baseEncodingName = encoding.get("BaseEncoding");
3675 baseEncodingName =
3676 baseEncodingName instanceof Name ? baseEncodingName.name : null;
3677 // Load the differences between the base and original
3678 if (encoding.has("Differences")) {
3679 const diffEncoding = encoding.get("Differences");
3680 let index = 0;
3681 for (const entry of diffEncoding) {
3682 const data = xref.fetchIfRef(entry);
3683 if (typeof data === "number") {
3684 index = data;
3685 } else if (data instanceof Name) {
3686 differences[index++] = data.name;
3687 } else {
3688 throw new FormatError(
3689 `Invalid entry in 'Differences' array: ${data}`
3690 );
3691 }
3692 }
3693 }
3694 } else if (encoding instanceof Name) {
3695 baseEncodingName = encoding.name;
3696 } else {
3697 const msg = "Encoding is not a Name nor a Dict";
3698
3699 if (!this.options.ignoreErrors) {
3700 throw new FormatError(msg);
3701 }
3702 warn(msg);
3703 }
3704 // According to table 114 if the encoding is a named encoding it must be
3705 // one of these predefined encodings.
3706 if (
3707 baseEncodingName !== "MacRomanEncoding" &&
3708 baseEncodingName !== "MacExpertEncoding" &&
3709 baseEncodingName !== "WinAnsiEncoding"
3710 ) {
3711 baseEncodingName = null;
3712 }
3713 }
3714
3715 const nonEmbeddedFont = !properties.file || properties.isInternalFont,
3716 isSymbolsFontName = getSymbolsFonts()[properties.name];
3717 // Ignore an incorrectly specified named encoding for non-embedded
3718 // symbol fonts (fixes issue16464.pdf).
3719 if (baseEncodingName && nonEmbeddedFont && isSymbolsFontName) {
3720 baseEncodingName = null;
3721 }
3722
3723 // Ignore incorrectly specified WinAnsiEncoding for non-embedded CJK fonts
3724 // (fixes issue20489). Some chinese fonts often have WinAnsiEncoding in the
3725 // PDF even though they should use Identity-H or GB-EUC-H encoding.
3726 if (
3727 baseEncodingName === "WinAnsiEncoding" &&
3728 nonEmbeddedFont &&
3729 properties.name?.charCodeAt(0) >= 0xb7
3730 ) {
3731 const fontName = properties.name;
3732 // This list is built from some names from Pdfium and mupdf:
3733 // - https://pdfium.googlesource.com/pdfium/+/master/core/fpdfapi/font/cpdf_font.cpp#41
3734 // - https://fossies.org/linux/mupdf/source/pdf/pdf-font.c#l_820
3735 const chineseFontNames = [
3736 "\xCB\xCE\xCC\xE5", // SimSun
3737 "\xBA\xDA\xCC\xE5", // SimHei
3738 "\xBF\xAC\xCC\xE5", // SimKai
3739 "\xB7\xC2\xCB\xCE", // SimFang
3740 "\xBF\xAC\xCC\xE5_GB2312", // SimKai
3741 "\xB7\xC2\xCB\xCE_GB2312", // SimFang
3742 "\xC1\xA5\xCA\xE9", // SimLi
3743 "\xD0\xC2\xCB\xCE", // SimSun
3744 "\xB7\xC2\xCB\xCE\xCC\xE5", // SimFang variant
3745 "\xD0\xA1\xB1\xEA\xCB\xCE", // XiaoBiaoSong
3746 ];
3747
3748 // Check for common Chinese font names and their GBK-encoded equivalents
3749 // (which may appear as Latin-1 when incorrectly decoded).
3750 if (chineseFontNames.includes(fontName)) {
3751 baseEncodingName = null;
3752 properties.defaultEncoding = "Adobe-GB1-UCS2";
3753 properties.composite = true;
3754 properties.cidEncoding = Name.get("GBK-EUC-H");
3755 const cMap = await CMapFactory.create({
3756 encoding: properties.cidEncoding,
3757 fetchBuiltInCMap: this._fetchBuiltInCMapBound,
3758 useCMap: null,
3759 });
3760 properties.cMap = cMap;
3761 properties.vertical = properties.cMap.vertical;
3762 properties.cidSystemInfo = {
3763 registry: "Adobe",
3764 ordering: "GB1",
3765 supplement: 0,
3766 };
3767 }
3768 }
3769
3770 if (baseEncodingName) {
3771 properties.defaultEncoding = getEncoding(baseEncodingName);
3772 } else {
3773 let isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
3774 const isNonsymbolicFont = !!(properties.flags & FontFlags.Nonsymbolic);
3775
3776 // The PDF specs state that the flags Symbolic and Nonsymbolic must be
3777 // mutually exclusive. However, some fonts are marked as both.
3778 // In that case we ignore the Symbolic flag when there is a Differences
3779 // entry (which indicates that the font is used as a non-symbolic
3780 // font).
3781 if (
3782 properties.type === "TrueType" &&
3783 isSymbolicFont &&
3784 isNonsymbolicFont &&
3785 differences.length !== 0
3786 ) {
3787 properties.flags &= ~FontFlags.Symbolic;
3788 isSymbolicFont = false;
3789 }
3790
3791 // According to "Table 114" in section "9.6.6.1 General" (under
3792 // "9.6.6 Character Encoding") of the PDF specification, a Nonsymbolic
3793 // font should use the `StandardEncoding` if no encoding is specified.
3794 encoding = StandardEncoding;
3795 if (properties.type === "TrueType" && !isNonsymbolicFont) {
3796 encoding = WinAnsiEncoding;
3797 }
3798 // The Symbolic attribute can be misused for regular fonts
3799 // Heuristic: we have to check if the font is a standard one also
3800 if (isSymbolicFont || isSymbolsFontName) {
3801 encoding = MacRomanEncoding;
3802 if (nonEmbeddedFont) {
3803 if (/Symbol/i.test(properties.name)) {
3804 encoding = SymbolSetEncoding;
3805 } else if (/Dingbats/i.test(properties.name)) {
3806 encoding = ZapfDingbatsEncoding;
3807 } else if (/Wingdings/i.test(properties.name)) {
3808 encoding = WinAnsiEncoding;
3809 }
3810 }
3811 }
3812 properties.defaultEncoding = encoding;
3813 }
3814
3815 properties.differences = differences;
3816 properties.baseEncodingName = baseEncodingName;
3817 properties.hasEncoding = !!baseEncodingName || differences.length > 0;
3818 properties.dict = dict;
3819
3820 properties.toUnicode = await toUnicodePromise;
3821
3822 const builtToUnicode = await this.buildToUnicode(properties);
3823 properties.toUnicode = builtToUnicode;
3824
3825 if (cidToGidBytes) {
3826 properties.cidToGidMap = this.readCidToGidMap(
3827 cidToGidBytes,
3828 builtToUnicode
3829 );
3830 }
3831 return properties;
3832 }
3833
3834 /**
3835 * @returns {Array}
3836 * @private
3837 */
3838 _simpleFontToUnicode(properties, forceGlyphs = false) {
3839 assert(!properties.composite, "Must be a simple font.");
3840
3841 const toUnicode = [];
3842 const encoding = properties.defaultEncoding.slice();
3843 const baseEncodingName = properties.baseEncodingName;
3844 // Merge in the differences array.
3845 const differences = properties.differences;
3846 for (const charcode in differences) {
3847 const glyphName = differences[charcode];
3848 if (glyphName === ".notdef") {
3849 // Skip .notdef to prevent rendering errors, e.g. boxes appearing
3850 // where there should be spaces (fixes issue5256.pdf).
3851 continue;
3852 }
3853 encoding[charcode] = glyphName;
3854 }
3855 const glyphsUnicodeMap = getGlyphsUnicode();
3856 for (const charcode in encoding) {
3857 // a) Map the character code to a character name.
3858 let glyphName = encoding[charcode];
3859 if (glyphName === "") {
3860 continue;
3861 }
3862 // b) Look up the character name in the Adobe Glyph List (see the
3863 // Bibliography) to obtain the corresponding Unicode value.
3864 let unicode = glyphsUnicodeMap[glyphName];
3865 if (unicode !== undefined) {
3866 toUnicode[charcode] = String.fromCharCode(unicode);
3867 continue;
3868 }
3869 // (undocumented) c) Few heuristics to recognize unknown glyphs
3870 // NOTE: Adobe Reader does not do this step, but OSX Preview does
3871 let code = 0;
3872 switch (glyphName[0]) {
3873 case "G": // Gxx glyph
3874 if (glyphName.length === 3) {
3875 code = parseInt(glyphName.substring(1), 16);
3876 }
3877 break;
3878 case "g": // g00xx glyph
3879 if (glyphName.length === 5) {
3880 code = parseInt(glyphName.substring(1), 16);
3881 }
3882 break;
3883 case "C": // Cdd{d} glyph
3884 case "c": // cdd{d} glyph
3885 if (glyphName.length >= 3 && glyphName.length <= 4) {
3886 const codeStr = glyphName.substring(1);
3887
3888 if (forceGlyphs) {
3889 code = parseInt(codeStr, 16);
3890 break;
3891 }
3892 // Normally the Cdd{d}/cdd{d} glyphName format will contain
3893 // regular, i.e. base 10, charCodes (see issue4550.pdf)...
3894 code = +codeStr;
3895
3896 // ... however some PDF generators violate that assumption by
3897 // containing glyph, i.e. base 16, codes instead.
3898 // In that case we need to re-parse the *entire* encoding to
3899 // prevent broken text-selection (fixes issue9655_reduced.pdf).
3900 if (Number.isNaN(code) && Number.isInteger(parseInt(codeStr, 16))) {
3901 return this._simpleFontToUnicode(
3902 properties,
3903 /* forceGlyphs */ true
3904 );
3905 }
3906 }
3907 break;
3908 case "u": // 'uniXXXX'/'uXXXX{XX}' glyphs
3909 unicode = getUnicodeForGlyph(glyphName, glyphsUnicodeMap);
3910 if (unicode !== -1) {
3911 code = unicode;
3912 }
3913 break;
3914 default:
3915 // Support (some) non-standard ligatures.
3916 switch (glyphName) {
3917 case "f_h":
3918 case "f_t":
3919 case "T_h":
3920 toUnicode[charcode] = glyphName.replaceAll("_", "");
3921 continue;
3922 }
3923 break;
3924 }
3925 if (code > 0 && code <= 0x10ffff && Number.isInteger(code)) {
3926 // If `baseEncodingName` is one the predefined encodings, and `code`
3927 // equals `charcode`, using the glyph defined in the baseEncoding
3928 // seems to yield a better `toUnicode` mapping (fixes issue 5070).
3929 if (baseEncodingName && code === +charcode) {
3930 const baseEncoding = getEncoding(baseEncodingName);
3931 if (baseEncoding && (glyphName = baseEncoding[charcode])) {
3932 toUnicode[charcode] = String.fromCharCode(
3933 glyphsUnicodeMap[glyphName]
3934 );
3935 continue;
3936 }
3937 }
3938 toUnicode[charcode] = String.fromCodePoint(code);
3939 }
3940 }
3941 return toUnicode;
3942 }
3943
3944 /**
3945 * Builds a char code to unicode map based on section 9.10 of the spec.
3946 * @param {Object} properties Font properties object.
3947 * @returns {Promise} A Promise that is resolved with a
3948 * {ToUnicodeMap|IdentityToUnicodeMap} object.
3949 */
3950 async buildToUnicode(properties) {
3951 properties.hasIncludedToUnicodeMap = properties.toUnicode?.length > 0;
3952
3953 // Section 9.10.2 Mapping Character Codes to Unicode Values
3954 if (properties.hasIncludedToUnicodeMap) {
3955 // Some fonts contain incomplete ToUnicode data, causing issues with
3956 // text-extraction. For simple fonts, containing encoding information,
3957 // use a fallback ToUnicode map to improve this (fixes issue8229.pdf).
3958 if (!properties.composite && properties.hasEncoding) {
3959 properties.fallbackToUnicode = this._simpleFontToUnicode(properties);
3960 }
3961 return properties.toUnicode;
3962 }
3963
3964 // According to the spec if the font is a simple font we should only map
3965 // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
3966 // the differences array only contains adobe standard or symbol set names,
3967 // in practice it seems better to always try to create a toUnicode map
3968 // based of the default encoding.
3969 if (!properties.composite /* is simple font */) {
3970 return new ToUnicodeMap(this._simpleFontToUnicode(properties));
3971 }
3972
3973 // If the font is a composite font that uses one of the predefined CMaps
3974 // listed in Table 118 (except Identity–H and Identity–V) or whose
3975 // descendant CIDFont uses the Adobe-GB1, Adobe-CNS1, Adobe-Japan1, or
3976 // Adobe-Korea1 character collection:
3977 if (
3978 properties.composite &&
3979 ((properties.cMap.builtInCMap &&
3980 !(properties.cMap instanceof IdentityCMap)) ||
3981 // The font is supposed to have a CIDSystemInfo dictionary, but some
3982 // PDFs don't include it (fixes issue 17689), hence the `?'.
3983 (properties.cidSystemInfo?.registry === "Adobe" &&
3984 (properties.cidSystemInfo.ordering === "GB1" ||
3985 properties.cidSystemInfo.ordering === "CNS1" ||
3986 properties.cidSystemInfo.ordering === "Japan1" ||
3987 properties.cidSystemInfo.ordering === "Korea1")))
3988 ) {
3989 // Then:
3990 // a) Map the character code to a character identifier (CID) according
3991 // to the font’s CMap.
3992 // b) Obtain the registry and ordering of the character collection used
3993 // by the font’s CMap (for example, Adobe and Japan1) from its
3994 // CIDSystemInfo dictionary.
3995 const { registry, ordering } = properties.cidSystemInfo;
3996 // c) Construct a second CMap name by concatenating the registry and
3997 // ordering obtained in step (b) in the format registry–ordering–UCS2
3998 // (for example, Adobe–Japan1–UCS2).
3999 const ucs2CMapName = Name.get(`${registry}-${ordering}-UCS2`);
4000 // d) Obtain the CMap with the name constructed in step (c) (available
4001 // from the ASN Web site; see the Bibliography).
4002 const ucs2CMap = await CMapFactory.create({
4003 encoding: ucs2CMapName,
4004 fetchBuiltInCMap: this._fetchBuiltInCMapBound,
4005 useCMap: null,
4006 });
4007 const toUnicode = [],
4008 buf = [];
4009 properties.cMap.forEach(function (charcode, cid) {
4010 if (cid > 0xffff) {
4011 throw new FormatError("Max size of CID is 65,535");
4012 }
4013 // e) Map the CID obtained in step (a) according to the CMap
4014 // obtained in step (d), producing a Unicode value.
4015 const ucs2 = ucs2CMap.lookup(cid);
4016 if (ucs2) {
4017 buf.length = 0;
4018 // Support multi-byte entries (fixes issue16176.pdf).
4019 for (let i = 0, ii = ucs2.length; i < ii; i += 2) {
4020 buf.push((ucs2.charCodeAt(i) << 8) + ucs2.charCodeAt(i + 1));
4021 }
4022 toUnicode[charcode] = String.fromCharCode(...buf);
4023 }
4024 });
4025 return new ToUnicodeMap(toUnicode);
4026 }
4027
4028 // The viewer's choice, just use an identity map.
4029 return new IdentityToUnicodeMap(properties.firstChar, properties.lastChar);
4030 }
4031
4032 async readToUnicode(cmapObj) {
4033 if (!cmapObj) {
4034 return null;
4035 }
4036 if (cmapObj instanceof Name) {
4037 const cmap = await CMapFactory.create({
4038 encoding: cmapObj,
4039 fetchBuiltInCMap: this._fetchBuiltInCMapBound,
4040 useCMap: null,
4041 });
4042
4043 if (cmap instanceof IdentityCMap) {
4044 return new IdentityToUnicodeMap(0, 0xffff);
4045 }
4046 return new ToUnicodeMap(cmap.getMap());
4047 }
4048 if (cmapObj instanceof BaseStream) {
4049 try {
4050 const cmap = await CMapFactory.create({
4051 encoding: cmapObj,
4052 fetchBuiltInCMap: this._fetchBuiltInCMapBound,
4053 useCMap: null,
4054 });
4055
4056 if (cmap instanceof IdentityCMap) {
4057 return new IdentityToUnicodeMap(0, 0xffff);
4058 }
4059 const map = new Array(cmap.length);
4060 // Convert UTF-16BE
4061 // NOTE: cmap can be a sparse array, so use forEach instead of
4062 // `for(;;)` to iterate over all keys.
4063 cmap.forEach(function (charCode, token) {
4064 // Some cmaps contain *only* CID characters (fixes issue9367.pdf).
4065 if (typeof token === "number") {
4066 map[charCode] = String.fromCodePoint(token);
4067 return;
4068 }
4069 // Add back omitted leading zeros on odd length tokens
4070 // (fixes issue #18099)
4071 if (token.length % 2 !== 0) {
4072 token = "\u0000" + token;
4073 }
4074 const str = [];
4075 for (let k = 0; k < token.length; k += 2) {
4076 const w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
4077 if ((w1 & 0xf800) !== 0xd800) {
4078 // w1 < 0xD800 || w1 > 0xDFFF
4079 str.push(w1);
4080 continue;
4081 }
4082 k += 2;
4083 const w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
4084 str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000);
4085 }
4086 map[charCode] = String.fromCodePoint(...str);
4087 });
4088 return new ToUnicodeMap(map);
4089 } catch (reason) {
4090 if (reason instanceof AbortException) {
4091 return null;
4092 }
4093 if (this.options.ignoreErrors) {
4094 warn(`readToUnicode - ignoring ToUnicode data: "${reason}".`);
4095 return null;
4096 }
4097 throw reason;
4098 }
4099 }
4100 return null;
4101 }
4102
4103 readCidToGidMap(glyphsData, toUnicode) {
4104 // Extract the encoding from the CIDToGIDMap
4105
4106 // Set encoding 0 to later verify the font has an encoding
4107 const result = [];
4108 for (let j = 0, jj = glyphsData.length; j < jj; j++) {
4109 const glyphID = (glyphsData[j++] << 8) | glyphsData[j];
4110 const code = j >> 1;
4111 if (glyphID === 0 && !toUnicode.has(code)) {
4112 continue;
4113 }
4114 result[code] = glyphID;
4115 }
4116 return result;
4117 }
4118
4119 extractWidths(dict, descriptor, properties) {
4120 const xref = this.xref;
4121 let glyphsWidths = [];
4122 let defaultWidth = 0;
4123 const glyphsVMetrics = [];
4124 let defaultVMetrics;
4125 if (properties.composite) {
4126 const dw = dict.get("DW");
4127 defaultWidth = typeof dw === "number" ? Math.ceil(dw) : 1000;
4128
4129 const widths = dict.get("W");
4130 if (Array.isArray(widths)) {
4131 for (let i = 0, ii = widths.length; i < ii; i++) {
4132 let start = xref.fetchIfRef(widths[i++]);
4133 if (!Number.isInteger(start)) {
4134 break; // Invalid /W data.
4135 }
4136 const code = xref.fetchIfRef(widths[i]);
4137
4138 if (Array.isArray(code)) {
4139 for (const c of code) {
4140 const width = xref.fetchIfRef(c);
4141 if (typeof width === "number") {
4142 glyphsWidths[start] = width;
4143 }
4144 start++;
4145 }
4146 } else if (Number.isInteger(code)) {
4147 const width = xref.fetchIfRef(widths[++i]);
4148 if (typeof width !== "number") {
4149 continue;
4150 }
4151 for (let j = start; j <= code; j++) {
4152 glyphsWidths[j] = width;
4153 }
4154 } else {
4155 break; // Invalid /W data.
4156 }
4157 }
4158 }
4159
4160 if (properties.vertical) {
4161 const dw2 = dict.getArray("DW2");
4162 let vmetrics = isNumberArray(dw2, 2) ? dw2 : [880, -1000];
4163 defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]];
4164 vmetrics = dict.get("W2");
4165 if (Array.isArray(vmetrics)) {
4166 for (let i = 0, ii = vmetrics.length; i < ii; i++) {
4167 let start = xref.fetchIfRef(vmetrics[i++]);
4168 if (!Number.isInteger(start)) {
4169 break; // Invalid /W2 data.
4170 }
4171 const code = xref.fetchIfRef(vmetrics[i]);
4172
4173 if (Array.isArray(code)) {
4174 for (let j = 0, jj = code.length; j < jj; j++) {
4175 const vmetric = [
4176 xref.fetchIfRef(code[j++]),
4177 xref.fetchIfRef(code[j++]),
4178 xref.fetchIfRef(code[j]),
4179 ];
4180 if (isNumberArray(vmetric, null)) {
4181 glyphsVMetrics[start] = vmetric;
4182 }
4183 start++;
4184 }
4185 } else if (Number.isInteger(code)) {
4186 const vmetric = [
4187 xref.fetchIfRef(vmetrics[++i]),
4188 xref.fetchIfRef(vmetrics[++i]),
4189 xref.fetchIfRef(vmetrics[++i]),
4190 ];
4191 if (!isNumberArray(vmetric, null)) {
4192 continue;
4193 }
4194 for (let j = start; j <= code; j++) {
4195 glyphsVMetrics[j] = vmetric;
4196 }
4197 } else {
4198 break; // Invalid /W2 data.
4199 }
4200 }
4201 }
4202 }
4203 } else {
4204 const widths = dict.get("Widths");
4205 if (Array.isArray(widths)) {
4206 let j = properties.firstChar;
4207 for (const w of widths) {
4208 const width = xref.fetchIfRef(w);
4209 if (typeof width === "number") {
4210 glyphsWidths[j] = width;
4211 }
4212 j++;
4213 }
4214 const missingWidth = descriptor.get("MissingWidth");
4215 defaultWidth = typeof missingWidth === "number" ? missingWidth : 0;
4216 } else {
4217 // Trying get the BaseFont metrics (see comment above).
4218 const baseFontName = dict.get("BaseFont");
4219 if (baseFontName instanceof Name) {
4220 const metrics = this.getBaseFontMetrics(baseFontName.name);
4221
4222 glyphsWidths = this.buildCharCodeToWidth(metrics.widths, properties);
4223 defaultWidth = metrics.defaultWidth;
4224 }
4225 }
4226 }
4227
4228 // Heuristic: detection of monospace font by checking all non-zero widths
4229 let isMonospace = true;
4230 let firstWidth = defaultWidth;
4231 for (const glyph in glyphsWidths) {
4232 const glyphWidth = glyphsWidths[glyph];
4233 if (!glyphWidth) {
4234 continue;
4235 }
4236 if (!firstWidth) {
4237 firstWidth = glyphWidth;
4238 continue;
4239 }
4240 if (firstWidth !== glyphWidth) {
4241 isMonospace = false;
4242 break;
4243 }
4244 }
4245 if (isMonospace) {
4246 properties.flags |= FontFlags.FixedPitch;
4247 } else {
4248 // Clear the flag.
4249 properties.flags &= ~FontFlags.FixedPitch;
4250 }
4251
4252 properties.defaultWidth = defaultWidth;
4253 properties.widths = glyphsWidths;
4254 properties.defaultVMetrics = defaultVMetrics;
4255 properties.vmetrics = glyphsVMetrics;
4256 }
4257
4258 isSerifFont(baseFontName) {
4259 // Simulating descriptor flags attribute
4260 const fontNameWoStyle = baseFontName.split("-", 1)[0];
4261 return fontNameWoStyle in getSerifFonts() || /serif/i.test(fontNameWoStyle);
4262 }
4263
4264 getBaseFontMetrics(name) {
4265 let defaultWidth = 0;
4266 let widths = Object.create(null);
4267 let monospace = false;
4268
4269 let fontName = normalizeFontName(name);
4270 const stdFontMap = getStdFontMap();
4271 fontName = stdFontMap[fontName] || fontName;
4272 const Metrics = getMetrics();
4273
4274 const glyphWidths =
4275 Metrics[fontName] ??
4276 // Use default fonts for looking up font metrics if the passed
4277 // font is not a base font
4278 Metrics[this.isSerifFont(name) ? "Times-Roman" : "Helvetica"];
4279
4280 if (typeof glyphWidths === "number") {
4281 defaultWidth = glyphWidths;
4282 monospace = true;
4283 } else {
4284 widths = glyphWidths(); // expand lazy widths array
4285 }
4286
4287 return {
4288 defaultWidth,
4289 monospace,
4290 widths,
4291 };
4292 }
4293
4294 buildCharCodeToWidth(widthsByGlyphName, properties) {
4295 const widths = Object.create(null);
4296 const differences = properties.differences;
4297 const encoding = properties.defaultEncoding;
4298 for (let charCode = 0; charCode < 256; charCode++) {
4299 if (charCode in differences && widthsByGlyphName[differences[charCode]]) {
4300 widths[charCode] = widthsByGlyphName[differences[charCode]];
4301 continue;
4302 }
4303 if (charCode in encoding && widthsByGlyphName[encoding[charCode]]) {
4304 widths[charCode] = widthsByGlyphName[encoding[charCode]];
4305 continue;
4306 }
4307 }
4308 return widths;
4309 }
4310
4311 preEvaluateFont(dict) {
4312 const baseDict = dict;
4313 let type = dict.get("Subtype");
4314 if (!(type instanceof Name)) {
4315 throw new FormatError("invalid font Subtype");
4316 }
4317
4318 let composite = false;
4319 let hash;
4320 if (type.name === "Type0") {
4321 // If font is a composite
4322 // - get the descendant font
4323 // - set the type according to the descendant font
4324 // - get the FontDescriptor from the descendant font
4325 const df = dict.get("DescendantFonts");
4326 if (!df) {
4327 throw new FormatError("Descendant fonts are not specified");
4328 }
4329 dict = Array.isArray(df) ? this.xref.fetchIfRef(df[0]) : df;
4330
4331 if (!(dict instanceof Dict)) {
4332 throw new FormatError("Descendant font is not a dictionary.");
4333 }
4334 type = dict.get("Subtype");
4335 if (!(type instanceof Name)) {
4336 throw new FormatError("invalid font Subtype");
4337 }
4338 composite = true;
4339 }
4340
4341 let firstChar = dict.get("FirstChar");
4342 if (!Number.isInteger(firstChar)) {
4343 firstChar = 0;
4344 }
4345 let lastChar = dict.get("LastChar");
4346 if (!Number.isInteger(lastChar)) {
4347 lastChar = composite ? 0xffff : 0xff;
4348 }
4349 const descriptor = dict.get("FontDescriptor");
4350 const toUnicode = dict.get("ToUnicode") || baseDict.get("ToUnicode");
4351
4352 if (descriptor) {
4353 hash = new MurmurHash3_64();
4354
4355 const encoding = baseDict.getRaw("Encoding");
4356 if (encoding instanceof Name) {
4357 hash.update(encoding.name);
4358 } else if (encoding instanceof Ref) {
4359 hash.update(encoding.toString());
4360 } else if (encoding instanceof Dict) {
4361 for (const entry of encoding.getRawValues()) {
4362 if (entry instanceof Name) {
4363 hash.update(entry.name);
4364 } else if (entry instanceof Ref) {
4365 hash.update(entry.toString());
4366 } else if (Array.isArray(entry)) {
4367 // 'Differences' array (fixes bug1157493.pdf).
4368 const diffLength = entry.length,
4369 diffBuf = new Array(diffLength);
4370
4371 for (let j = 0; j < diffLength; j++) {
4372 const diffEntry = entry[j];
4373 if (diffEntry instanceof Name) {
4374 diffBuf[j] = diffEntry.name;
4375 } else if (
4376 typeof diffEntry === "number" ||
4377 diffEntry instanceof Ref
4378 ) {
4379 diffBuf[j] = diffEntry.toString();
4380 }
4381 }
4382 hash.update(diffBuf.join());
4383 }
4384 }
4385 }
4386
4387 hash.update(`${firstChar}-${lastChar}`); // Fixes issue10665_reduced.pdf
4388
4389 if (toUnicode instanceof BaseStream) {
4390 const stream = toUnicode.stream || toUnicode;
4391 const uint8array = stream.buffer
4392 ? new Uint8Array(stream.buffer.buffer, 0, stream.bufferLength)
4393 : new Uint8Array(
4394 stream.bytes.buffer,
4395 stream.start,
4396 stream.end - stream.start
4397 );
4398 hash.update(uint8array);
4399 } else if (toUnicode instanceof Name) {
4400 hash.update(toUnicode.name);
4401 }
4402
4403 const widths = dict.get("Widths") || baseDict.get("Widths");
4404 if (Array.isArray(widths)) {
4405 const widthsBuf = [];
4406 for (const entry of widths) {
4407 if (typeof entry === "number" || entry instanceof Ref) {
4408 widthsBuf.push(entry.toString());
4409 }
4410 }
4411 hash.update(widthsBuf.join());
4412 }
4413
4414 if (composite) {
4415 hash.update("compositeFont");
4416
4417 const compositeWidths = dict.get("W") || baseDict.get("W");
4418 if (Array.isArray(compositeWidths)) {
4419 const widthsBuf = [];
4420 for (const entry of compositeWidths) {
4421 if (typeof entry === "number" || entry instanceof Ref) {
4422 widthsBuf.push(entry.toString());
4423 } else if (Array.isArray(entry)) {
4424 const subWidthsBuf = [];
4425 for (const element of entry) {
4426 if (typeof element === "number" || element instanceof Ref) {
4427 subWidthsBuf.push(element.toString());
4428 }
4429 }
4430 widthsBuf.push(`[${subWidthsBuf.join()}]`);
4431 }
4432 }
4433 hash.update(widthsBuf.join());
4434 }
4435
4436 const cidToGidMap =
4437 dict.getRaw("CIDToGIDMap") || baseDict.getRaw("CIDToGIDMap");
4438 if (cidToGidMap instanceof Name) {
4439 hash.update(cidToGidMap.name);
4440 } else if (cidToGidMap instanceof Ref) {
4441 hash.update(cidToGidMap.toString());
4442 } else if (cidToGidMap instanceof BaseStream) {
4443 hash.update(cidToGidMap.peekBytes());
4444 }
4445 }
4446
4447 if (type.name === "Type3") {
4448 // Type3 fonts with the same metrics/encoding but different CharProcs
4449 // must not be aliased, since their glyphs may render completely
4450 // differently (e.g. one font uses SMask glyph programs, another uses
4451 // plain paths, see issue 19634).
4452 const charProcs = baseDict.get("CharProcs");
4453 if (charProcs instanceof Dict) {
4454 for (const [key, entry] of charProcs.getRawEntries()) {
4455 hash.update(entry instanceof Ref ? `${key}\0${entry}` : key);
4456 }
4457 }
4458 }
4459 }
4460
4461 return {
4462 descriptor,
4463 dict,
4464 baseDict,
4465 composite,
4466 type: type.name,
4467 firstChar,
4468 lastChar,
4469 toUnicode,
4470 hash: hash ? hash.hexdigest() : "",
4471 };
4472 }
4473
4474 async translateFont({
4475 descriptor,
4476 dict,
4477 baseDict,
4478 composite,
4479 type,
4480 firstChar,
4481 lastChar,
4482 toUnicode,
4483 cssFontInfo,
4484 }) {
4485 const isType3Font = type === "Type3";
4486
4487 if (!descriptor) {
4488 if (isType3Font) {
4489 // FontDescriptor is only required for Type3 fonts when the document
4490 // is a tagged pdf.
4491 descriptor = Dict.empty;
4492 } else if (composite) {
4493 // Some PDFs omit the FontDescriptor on the descendant CIDFont when
4494 // referencing one of the standard Acrobat CJK fonts via a predefined
4495 // CMap (e.g. /Encoding /90ms-RKSJ-H with /BaseFont /HeiseiMin-W3).
4496 // Fall through so the CMap is loaded by the composite-font path
4497 // below; otherwise multi-byte codes would be decoded byte-by-byte.
4498 descriptor = Dict.empty;
4499 } else {
4500 // Before PDF 1.5 if the font was one of the base 14 fonts, having a
4501 // FontDescriptor was not required.
4502 // This case is here for compatibility.
4503 let baseFontName = dict.get("BaseFont");
4504 if (!(baseFontName instanceof Name)) {
4505 throw new FormatError("Base font is not specified");
4506 }
4507
4508 // Using base font name as a font name.
4509 baseFontName = normalizeFontName(baseFontName.name);
4510 const metrics = this.getBaseFontMetrics(baseFontName);
4511
4512 // Simulating descriptor flags attribute
4513 const fontNameWoStyle = baseFontName.split("-", 1)[0];
4514 const flags =
4515 (this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) |
4516 (metrics.monospace ? FontFlags.FixedPitch : 0) |
4517 (getSymbolsFonts()[fontNameWoStyle]
4518 ? FontFlags.Symbolic
4519 : FontFlags.Nonsymbolic);
4520
4521 const properties = {
4522 type,
4523 name: baseFontName,
4524 loadedName: baseDict.loadedName,
4525 systemFontInfo: null,
4526 widths: metrics.widths,
4527 defaultWidth: metrics.defaultWidth,
4528 isSimulatedFlags: true,
4529 flags,
4530 firstChar,
4531 lastChar,
4532 toUnicode,
4533 xHeight: 0,
4534 capHeight: 0,
4535 italicAngle: 0,
4536 isType3Font,
4537 };
4538 const widths = dict.get("Widths");
4539
4540 const standardFontName = getStandardFontName(baseFontName);
4541 let file = null;
4542 if (standardFontName) {
4543 file = await this.fetchStandardFontData(standardFontName);
4544 properties.isInternalFont = !!file;
4545 }
4546 if (!properties.isInternalFont && this.options.useSystemFonts) {
4547 properties.systemFontInfo = getFontSubstitution(
4548 this.systemFontCache,
4549 this.idFactory,
4550 this.options.standardFontDataUrl,
4551 baseFontName,
4552 standardFontName,
4553 type
4554 );
4555 }
4556
4557 const newProperties = await this.extractDataStructures(
4558 dict,
4559 properties
4560 );
4561 if (Array.isArray(widths)) {
4562 const glyphWidths = [];
4563 let j = firstChar;
4564 for (const w of widths) {
4565 const width = this.xref.fetchIfRef(w);
4566 if (typeof width === "number") {
4567 glyphWidths[j] = width;
4568 }
4569 j++;
4570 }
4571 newProperties.widths = glyphWidths;
4572 } else {
4573 newProperties.widths = this.buildCharCodeToWidth(
4574 metrics.widths,
4575 newProperties
4576 );
4577 }
4578 return new Font(baseFontName, file, newProperties, this.options);
4579 }
4580 }
4581
4582 // According to the spec if 'FontDescriptor' is declared, 'FirstChar',
4583 // 'LastChar' and 'Widths' should exist too, but some PDF encoders seem
4584 // to ignore this rule when a variant of a standard font is used.
4585 // TODO Fill the width array depending on which of the base font this is
4586 // a variant.
4587
4588 let fontName = descriptor.get("FontName");
4589 let baseFont = dict.get("BaseFont");
4590 // Some bad PDFs have a string as the font name.
4591 if (typeof fontName === "string") {
4592 fontName = Name.get(fontName);
4593 }
4594 if (typeof baseFont === "string") {
4595 baseFont = Name.get(baseFont);
4596 }
4597
4598 const fontNameStr = fontName?.name;
4599 const baseFontStr = baseFont?.name;
4600 if (isType3Font) {
4601 if (!fontNameStr) {
4602 // Since the /FontDescriptor is optional in Type3 fonts, ensure that we
4603 // always have a "valid" /FontName (fixes issue19954.pdf).
4604 fontName = Name.get(type);
4605 }
4606 } else if (fontNameStr !== baseFontStr) {
4607 info(
4608 `The FontDescriptor's FontName is "${fontNameStr}" but ` +
4609 `should be the same as the Font's BaseFont "${baseFontStr}".`
4610 );
4611 // - Workaround for cases where e.g. fontNameStr = 'Arial' and
4612 // baseFontStr = 'Arial,Bold' (needed when no font file is embedded).
4613 //
4614 // - Workaround for cases where e.g. fontNameStr = 'wg09np' and
4615 // baseFontStr = 'Wingdings-Regular' (fixes issue7454.pdf).
4616 if (
4617 fontNameStr &&
4618 baseFontStr &&
4619 (baseFontStr.startsWith(fontNameStr) ||
4620 (!isKnownFontName(fontNameStr) && isKnownFontName(baseFontStr)))
4621 ) {
4622 fontName = null;
4623 }
4624 fontName ||= baseFont;
4625 }
4626
4627 if (!(fontName instanceof Name)) {
4628 throw new FormatError("invalid font name");
4629 }
4630
4631 let fontFile, fontFileN, subtype, length1, length2, length3;
4632 try {
4633 for (const n of ["FontFile", "FontFile2", "FontFile3"]) {
4634 fontFile = descriptor.get(n);
4635 if (fontFile) {
4636 fontFileN = n;
4637 break;
4638 }
4639 }
4640
4641 if (fontFile) {
4642 if (!(fontFile instanceof BaseStream)) {
4643 throw new FormatError("FontFile should be a stream");
4644 } else {
4645 if (fontFile.isAsync) {
4646 const bytes = await fontFile.asyncGetBytes();
4647 if (bytes) {
4648 fontFile = new Stream(bytes, 0, bytes.length, fontFile.dict);
4649 }
4650 }
4651 if (fontFile.isEmpty) {
4652 throw new FormatError("FontFile is empty");
4653 }
4654 }
4655 }
4656 } catch (ex) {
4657 if (!this.options.ignoreErrors) {
4658 throw ex;
4659 }
4660 warn(`translateFont - fetching "${fontName.name}" font file: "${ex}".`);
4661 fontFile = null;
4662 }
4663 let isInternalFont = false;
4664 let glyphScaleFactors = null;
4665 let systemFontInfo = null;
4666 if (fontFile) {
4667 if (fontFile.dict) {
4668 const subtypeEntry = fontFile.dict.get("Subtype");
4669 if (subtypeEntry instanceof Name) {
4670 subtype = subtypeEntry.name;
4671 }
4672 length1 = fontFile.dict.get("Length1");
4673 length2 = fontFile.dict.get("Length2");
4674 length3 = fontFile.dict.get("Length3");
4675 }
4676 } else if (cssFontInfo) {
4677 // We've a missing XFA font.
4678 const standardFontName = getXfaFontName(fontName.name);
4679 if (standardFontName) {
4680 cssFontInfo.fontFamily = `${cssFontInfo.fontFamily}-PdfJS-XFA`;
4681 cssFontInfo.metrics = standardFontName.metrics || null;
4682 glyphScaleFactors = standardFontName.factors || null;
4683 fontFile = await this.fetchStandardFontData(standardFontName.name);
4684 isInternalFont = !!fontFile;
4685
4686 // We're using a substitution font but for example widths (if any)
4687 // are related to the glyph positions in the font.
4688 // So we overwrite everything here to be sure that widths are
4689 // correct.
4690 baseDict = dict = getXfaFontDict(fontName.name);
4691 composite = true;
4692 }
4693 } else if (!isType3Font) {
4694 const standardFontName = getStandardFontName(fontName.name);
4695 if (standardFontName) {
4696 fontFile = await this.fetchStandardFontData(standardFontName);
4697 isInternalFont = !!fontFile;
4698 }
4699 if (!isInternalFont && this.options.useSystemFonts) {
4700 systemFontInfo = getFontSubstitution(
4701 this.systemFontCache,
4702 this.idFactory,
4703 this.options.standardFontDataUrl,
4704 fontName.name,
4705 standardFontName,
4706 type
4707 );
4708 }
4709 }
4710
4711 const fontMatrix = lookupMatrix(
4712 dict.getArray("FontMatrix"),
4713 FONT_IDENTITY_MATRIX
4714 );
4715 const bbox = lookupNormalRect(
4716 descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
4717 isType3Font ? [0, 0, 0, 0] : undefined
4718 );
4719 let ascent = descriptor.get("Ascent");
4720 if (typeof ascent !== "number") {
4721 ascent = undefined;
4722 }
4723 let descent = descriptor.get("Descent");
4724 if (typeof descent !== "number") {
4725 descent = undefined;
4726 }
4727 let xHeight = descriptor.get("XHeight");
4728 if (typeof xHeight !== "number") {
4729 xHeight = 0;
4730 }
4731 let capHeight = descriptor.get("CapHeight");
4732 if (typeof capHeight !== "number") {
4733 capHeight = 0;
4734 }
4735 let flags = descriptor.get("Flags");
4736 if (!Number.isInteger(flags)) {
4737 flags = 0;
4738 }
4739 let italicAngle = descriptor.get("ItalicAngle");
4740 if (typeof italicAngle !== "number") {
4741 italicAngle = 0;
4742 }
4743
4744 const properties = {
4745 type,
4746 name: fontName.name,
4747 subtype,
4748 file: fontFile,
4749 fontFileN,
4750 length1,
4751 length2,
4752 length3,
4753 isInternalFont,
4754 loadedName: baseDict.loadedName,
4755 composite,
4756 fixedPitch: false,
4757 fontMatrix,
4758 firstChar,
4759 lastChar,
4760 toUnicode,
4761 bbox,
4762 ascent,
4763 descent,
4764 xHeight,
4765 capHeight,
4766 flags,
4767 italicAngle,
4768 isType3Font,
4769 cssFontInfo,
4770 scaleFactors: glyphScaleFactors,
4771 systemFontInfo,
4772 };
4773
4774 if (composite) {
4775 const cidEncoding = baseDict.get("Encoding");
4776 if (cidEncoding instanceof Name) {
4777 properties.cidEncoding = cidEncoding.name;
4778 }
4779 const cMap = await CMapFactory.create({
4780 encoding: cidEncoding,
4781 fetchBuiltInCMap: this._fetchBuiltInCMapBound,
4782 useCMap: null,
4783 });
4784 properties.cMap = cMap;
4785 properties.vertical = properties.cMap.vertical;
4786 }
4787
4788 const newProperties = await this.extractDataStructures(dict, properties);
4789 this.extractWidths(dict, descriptor, newProperties);
4790
4791 const font = new Font(fontName.name, fontFile, newProperties, this.options);
4792 // The embedded font may have been too corrupt to parse, in which case
4793 // we ended up in the fallback path without a substitution selected.
4794 // Try the substitution map now so text renders in a font close to what
4795 // the document asked for (issue 7625).
4796 if (
4797 font.missingFile &&
4798 !font.systemFontInfo &&
4799 !isType3Font &&
4800 this.options.useSystemFonts
4801 ) {
4802 const standardFontName = getStandardFontName(fontName.name);
4803 const substitution = getFontSubstitution(
4804 this.systemFontCache,
4805 this.idFactory,
4806 this.options.standardFontDataUrl,
4807 fontName.name,
4808 standardFontName,
4809 type
4810 );
4811 if (substitution) {
4812 if (substitution.guessFallback) {
4813 substitution.guessFallback = false;
4814 substitution.css += `,${font.fallbackName}`;
4815 }
4816 font.systemFontInfo = substitution;
4817 }
4818 }
4819 return font;
4820 }
4821
4822 static buildFontPaths(font, glyphs, handler, evaluatorOptions) {
4823 function buildPath(fontChar) {
4824 const glyphName = `${font.loadedName}_path_${fontChar}`;
4825 try {
4826 const buffer = font.renderer.getPath(fontChar);
4827 if (!buffer) {
4828 return; // Previously compiled, and sent to the main-thread.
4829 }
4830 handler.send("commonobj", [glyphName, "FontPath", buffer], [buffer]);
4831 } catch (reason) {
4832 if (evaluatorOptions.ignoreErrors) {
4833 warn(`buildFontPaths - ignoring ${glyphName} glyph: "${reason}".`);
4834 return;
4835 }
4836 throw reason;
4837 }
4838 }
4839
4840 for (const glyph of glyphs) {
4841 buildPath(glyph.fontChar);
4842
4843 // If the glyph has an accent we need to build a path for its
4844 // fontChar too, otherwise CanvasGraphics_paintChar will fail.
4845 const accent = glyph.accent;
4846 if (accent?.fontChar) {
4847 buildPath(accent.fontChar);
4848 }
4849 }
4850 }
4851
4852 static get fallbackFontDict() {
4853 const dict = new Dict();
4854 dict.set("BaseFont", Name.get("Helvetica"));
4855 dict.set("Type", Name.get("FallbackType"));
4856 dict.set("Subtype", Name.get("FallbackType"));
4857 dict.set("Encoding", Name.get("WinAnsiEncoding"));
4858
4859 return shadow(this, "fallbackFontDict", dict);
4860 }
4861}
4862