• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adnsistemas / pdf-lib / #5

01 Sep 2025 10:07PM UTC coverage: 70.146% (+0.6%) from 69.562%
#5

push

David N. Abdala
Fix in Size trailer info for StreamXref

2407 of 3895 branches covered (61.8%)

Branch coverage included in aggregate %.

6700 of 9088 relevant lines covered (73.72%)

77444.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.63
/src/core/structures/PDFCrossRefStream.ts
1
import PDFDict from '../objects/PDFDict';
2
import PDFName from '../objects/PDFName';
53✔
3
import PDFRef from '../objects/PDFRef';
53✔
4
import PDFContext from '../PDFContext';
5
import PDFFlateStream from './PDFFlateStream';
53✔
6
import { bytesFor, Cache, reverseArray, sizeInBytes, sum } from '../../utils';
53✔
7

8
export enum EntryType {
53✔
9
  Deleted = 0,
53✔
10
  Uncompressed = 1,
53✔
11
  Compressed = 2,
53✔
12
}
13

14
export interface DeletedEntry {
15
  type: EntryType.Deleted;
16
  ref: PDFRef;
17
  nextFreeObjectNumber: number;
18
}
19

20
export interface UncompressedEntry {
21
  type: EntryType.Uncompressed;
22
  ref: PDFRef;
23
  offset: number;
24
}
25

26
export interface CompressedEntry {
27
  type: EntryType.Compressed;
28
  ref: PDFRef;
29
  objectStreamRef: PDFRef;
30
  index: number;
31
}
32

33
export type Entry = DeletedEntry | UncompressedEntry | CompressedEntry;
34

35
export type EntryTuple = [number, number, number];
36

37
/**
38
 * Entries should be added using the [[addDeletedEntry]],
39
 * [[addUncompressedEntry]], and [[addCompressedEntry]] methods
40
 */
41
class PDFCrossRefStream extends PDFFlateStream {
42
  static create = (dict: PDFDict, encode = true) => {
53!
43
    const stream = new PDFCrossRefStream(dict, [], encode);
27✔
44
    stream.addDeletedEntry(PDFRef.of(0, 65535), 0);
27✔
45
    return stream;
27✔
46
  };
47

48
  static of = (dict: PDFDict, entries: Entry[], encode = true) =>
53!
49
    new PDFCrossRefStream(dict, entries, encode);
1✔
50

51
  private readonly entries: Entry[];
52
  private readonly entryTuplesCache: Cache<EntryTuple[]>;
53
  private readonly maxByteWidthsCache: Cache<[number, number, number]>;
54
  private readonly indexCache: Cache<number[]>;
55

56
  private constructor(dict: PDFDict, entries?: Entry[], encode = true) {
×
57
    super(dict, encode);
28✔
58

59
    this.entries = entries || [];
28!
60
    this.entryTuplesCache = Cache.populatedBy(this.computeEntryTuples);
28✔
61
    this.maxByteWidthsCache = Cache.populatedBy(this.computeMaxEntryByteWidths);
28✔
62
    this.indexCache = Cache.populatedBy(this.computeIndex);
28✔
63

64
    dict.set(PDFName.of('Type'), PDFName.of('XRef'));
28✔
65
  }
66

67
  private appendEntry(entry: Entry) {
68
    const eind = this.entries.findIndex(
1,308✔
69
      (e) => e.ref.objectNumber > entry.ref.objectNumber,
154,957✔
70
    );
71
    if (eind < 0 || eind > this.entries.length) this.entries.push(entry);
1,308!
72
    else this.entries.splice(eind, 0, entry);
×
73
  }
74

75
  addDeletedEntry(ref: PDFRef, nextFreeObjectNumber: number) {
76
    const type = EntryType.Deleted;
28✔
77
    this.appendEntry({ type, ref, nextFreeObjectNumber });
28✔
78
    this.entryTuplesCache.invalidate();
28✔
79
    this.maxByteWidthsCache.invalidate();
28✔
80
    this.indexCache.invalidate();
28✔
81
    this.contentsCache.invalidate();
28✔
82
  }
83

84
  addUncompressedEntry(ref: PDFRef, offset: number) {
85
    const type = EntryType.Uncompressed;
847✔
86
    this.appendEntry({ type, ref, offset });
847✔
87
    this.entryTuplesCache.invalidate();
847✔
88
    this.maxByteWidthsCache.invalidate();
847✔
89
    this.indexCache.invalidate();
847✔
90
    this.contentsCache.invalidate();
847✔
91
  }
92

93
  addCompressedEntry(ref: PDFRef, objectStreamRef: PDFRef, index: number) {
94
    const type = EntryType.Compressed;
433✔
95
    this.appendEntry({ type, ref, objectStreamRef, index });
433✔
96
    this.entryTuplesCache.invalidate();
433✔
97
    this.maxByteWidthsCache.invalidate();
433✔
98
    this.indexCache.invalidate();
433✔
99
    this.contentsCache.invalidate();
433✔
100
  }
101

102
  clone(context?: PDFContext): PDFCrossRefStream {
103
    const { dict, entries, encode } = this;
1✔
104
    return PDFCrossRefStream.of(dict.clone(context), entries.slice(), encode);
1✔
105
  }
106

107
  getContentsString(): string {
108
    const entryTuples = this.entryTuplesCache.access();
4✔
109
    const byteWidths = this.maxByteWidthsCache.access();
4✔
110
    let value = '';
4✔
111

112
    for (
4✔
113
      let entryIdx = 0, entriesLen = entryTuples.length;
4✔
114
      entryIdx < entriesLen;
115
      entryIdx++
116
    ) {
117
      const [first, second, third] = entryTuples[entryIdx];
17✔
118

119
      const firstBytes = reverseArray(bytesFor(first));
17✔
120
      const secondBytes = reverseArray(bytesFor(second));
17✔
121
      const thirdBytes = reverseArray(bytesFor(third));
17✔
122

123
      for (let idx = byteWidths[0] - 1; idx >= 0; idx--) {
17✔
124
        value += (firstBytes[idx] || 0).toString(2);
17✔
125
      }
126
      for (let idx = byteWidths[1] - 1; idx >= 0; idx--) {
17✔
127
        value += (secondBytes[idx] || 0).toString(2);
22✔
128
      }
129
      for (let idx = byteWidths[2] - 1; idx >= 0; idx--) {
17✔
130
        value += (thirdBytes[idx] || 0).toString(2);
34✔
131
      }
132
    }
133
    return value;
4✔
134
  }
135

136
  getUnencodedContents(): Uint8Array {
137
    const entryTuples = this.entryTuplesCache.access();
27✔
138
    const byteWidths = this.maxByteWidthsCache.access();
27✔
139
    const buffer = new Uint8Array(this.getUnencodedContentsSize());
27✔
140

141
    let offset = 0;
27✔
142
    for (
27✔
143
      let entryIdx = 0, entriesLen = entryTuples.length;
27✔
144
      entryIdx < entriesLen;
145
      entryIdx++
146
    ) {
147
      const [first, second, third] = entryTuples[entryIdx];
1,311✔
148

149
      const firstBytes = reverseArray(bytesFor(first));
1,311✔
150
      const secondBytes = reverseArray(bytesFor(second));
1,311✔
151
      const thirdBytes = reverseArray(bytesFor(third));
1,311✔
152

153
      for (let idx = byteWidths[0] - 1; idx >= 0; idx--) {
1,311✔
154
        buffer[offset++] = firstBytes[idx] || 0;
1,311✔
155
      }
156
      for (let idx = byteWidths[1] - 1; idx >= 0; idx--) {
1,311✔
157
        buffer[offset++] = secondBytes[idx] || 0;
3,720✔
158
      }
159
      for (let idx = byteWidths[2] - 1; idx >= 0; idx--) {
1,311✔
160
        buffer[offset++] = thirdBytes[idx] || 0;
2,622✔
161
      }
162
    }
163

164
    return buffer;
27✔
165
  }
166

167
  getUnencodedContentsSize(): number {
168
    const byteWidths = this.maxByteWidthsCache.access();
27✔
169
    const entryWidth = sum(byteWidths);
27✔
170
    return entryWidth * this.entries.length;
27✔
171
  }
172

173
  updateDict(): void {
174
    super.updateDict();
58✔
175

176
    const byteWidths = this.maxByteWidthsCache.access();
58✔
177
    const index = this.indexCache.access();
58✔
178

179
    const { context } = this.dict;
58✔
180
    this.dict.set(PDFName.of('W'), context.obj(byteWidths));
58✔
181
    this.dict.set(PDFName.of('Index'), context.obj(index));
58✔
182
  }
183

184
  // Returns an array of integer pairs for each subsection of the cross ref
185
  // section, where each integer pair represents:
186
  //   firstObjectNumber(OfSection), length(OfSection)
187
  private computeIndex = (): number[] => {
28✔
188
    const subsections: number[] = [];
27✔
189

190
    let subsectionLength = 0;
27✔
191
    for (let idx = 0, len = this.entries.length; idx < len; idx++) {
27✔
192
      const currEntry = this.entries[idx];
1,311✔
193
      const prevEntry = this.entries[idx - 1];
1,311✔
194

195
      if (idx === 0) {
1,311✔
196
        subsections.push(currEntry.ref.objectNumber);
27✔
197
      } else if (currEntry.ref.objectNumber - prevEntry.ref.objectNumber > 1) {
1,284✔
198
        subsections.push(subsectionLength);
108✔
199
        subsections.push(currEntry.ref.objectNumber);
108✔
200
        subsectionLength = 0;
108✔
201
      }
202

203
      subsectionLength += 1;
1,311✔
204
    }
205
    subsections.push(subsectionLength);
27✔
206

207
    return subsections;
27✔
208
  };
209

210
  private computeEntryTuples = (): EntryTuple[] => {
28✔
211
    const entryTuples: EntryTuple[] = new Array(this.entries.length);
27✔
212

213
    for (let idx = 0, len = this.entries.length; idx < len; idx++) {
27✔
214
      const entry = this.entries[idx];
1,311✔
215
      if (entry.type === EntryType.Deleted) {
1,311✔
216
        const { type, nextFreeObjectNumber, ref } = entry;
29✔
217
        entryTuples[idx] = [type, nextFreeObjectNumber, ref.generationNumber];
29✔
218
      }
219
      if (entry.type === EntryType.Uncompressed) {
1,311✔
220
        const { type, offset, ref } = entry;
848✔
221
        entryTuples[idx] = [type, offset, ref.generationNumber];
848✔
222
      }
223
      if (entry.type === EntryType.Compressed) {
1,311✔
224
        const { type, objectStreamRef, index } = entry;
434✔
225
        entryTuples[idx] = [type, objectStreamRef.objectNumber, index];
434✔
226
      }
227
    }
228

229
    return entryTuples;
27✔
230
  };
231

232
  private computeMaxEntryByteWidths = (): [number, number, number] => {
28✔
233
    const entryTuples = this.entryTuplesCache.access();
27✔
234
    const widths: [number, number, number] = [0, 0, 0];
27✔
235

236
    for (let idx = 0, len = entryTuples.length; idx < len; idx++) {
27✔
237
      const [first, second, third] = entryTuples[idx];
1,311✔
238

239
      const firstSize = sizeInBytes(first);
1,311✔
240
      const secondSize = sizeInBytes(second);
1,311✔
241
      const thirdSize = sizeInBytes(third);
1,311✔
242

243
      if (firstSize > widths[0]) widths[0] = firstSize;
1,311✔
244
      if (secondSize > widths[1]) widths[1] = secondSize;
1,311✔
245
      if (thirdSize > widths[2]) widths[2] = thirdSize;
1,311✔
246
    }
247

248
    return widths;
27✔
249
  };
250
}
251

252
export default PDFCrossRefStream;
53✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc