ng2-pdfjs-viewer 26.1.1 → 26.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,33 +1,3 @@
1
- # Apache License, Version 2.0 with Commons Clause License Condition v 1.0
2
-
3
- ===============================================================================
4
-
5
- COMMONS CLAUSE LICENSE CONDITION
6
-
7
- The Software is provided to you by the Licensor under the License, as defined
8
- below, subject to the following condition.
9
-
10
- Without limiting other conditions in the License, the grant of rights under
11
- the License will not include, and the License does not grant to you, the right
12
- to Sell the Software. For purposes of the foregoing, "Sell" means practicing
13
- any or all of the rights granted to you under the License to provide to third
14
- parties, for a fee or other consideration (including without limitation fees
15
- for hosting or consulting/ support services related to the Software), a product
16
- or service whose value derives, entirely or substantially, from the Software.
17
- Any license notice or attribution required by the License must also include
18
- this Commons Clause License Condition notice.
19
-
20
- Software: ng2-pdfjs-viewer
21
- Copyright (c) 2018-2025 Aneesh Goapalakrishnan
22
- Licensor: Aneesh Goapalakrishnan <codehippie1@gmail.com>
23
-
24
- This software is dual-licensed under the Apache License 2.0 and the Commons
25
- Clause License Condition. The Commons Clause modifies the Apache License 2.0
26
- to prevent commercial competitors from selling products or services that are
27
- substantially derived from this software.
28
-
29
- ===============================================================================
30
-
31
1
  Apache License
32
2
  Version 2.0, January 2004
33
3
  http://www.apache.org/licenses/
@@ -216,7 +186,7 @@ substantially derived from this software.
216
186
  same "printed page" as the copyright notice for easier
217
187
  identification within third-party archives.
218
188
 
219
- Copyright 2018-2025 Aneesh Goapalakrishnan
189
+ Copyright 2018-2026 Aneesh Goapalakrishnan
220
190
 
221
191
  Licensed under the Apache License, Version 2.0 (the "License");
222
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -14,10 +14,14 @@
14
14
  [![CodeQL](https://github.com/intbot/ng2-pdfjs-viewer/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/intbot/ng2-pdfjs-viewer/security/code-scanning)
15
15
  [![Angular](https://img.shields.io/badge/Angular-%3E%3D10-red?style=flat-square&logo=angular)](https://angular.dev)
16
16
  [![PDF.js](https://img.shields.io/badge/PDF.js-6.0.227-green?style=flat-square&logo=mozilla)](https://github.com/mozilla/pdf.js)
17
- [![license](https://img.shields.io/badge/license-Apache--2.0%20%28Commons%20Clause%29-blue?style=flat-square)](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE)
17
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE)
18
18
  [![stars](https://img.shields.io/github/stars/intbot/ng2-pdfjs-viewer?style=flat-square&logo=github)](https://github.com/intbot/ng2-pdfjs-viewer)
19
+ [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-1389FD?style=flat-square&logo=stackblitz&logoColor=white)](https://stackblitz.com/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
20
+ [![Edit on CodeSandbox](https://img.shields.io/badge/Edit%20on-CodeSandbox-151515?style=flat-square&logo=codesandbox&logoColor=white)](https://codesandbox.io/p/sandbox/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
19
21
 
20
- [**Live demo**](https://demo.angularpdf.com/) · [**Documentation**](https://angularpdf.com/) · [**API reference**](https://angularpdf.com/docs/api/component-inputs) · [**Changelog**](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CHANGELOG.md)
22
+ [**Documentation**](https://angularpdf.com/) · [**API reference**](https://angularpdf.com/docs/api/component-inputs) · [**Live demo**](https://demo.angularpdf.com/) · [**Showcase**](https://angularpdf.com/showcase) · [**Changelog**](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CHANGELOG.md)
23
+
24
+ ⭐ **Find it useful? [Star it on GitHub](https://github.com/intbot/ng2-pdfjs-viewer)** — it helps more Angular developers discover it.
21
25
 
22
26
  </div>
23
27
 
@@ -31,6 +35,9 @@ AI assistant — all driven by typed `@Input()`s and `@Output()` events, no ifra
31
35
  Shipping since **2018**, **8.3+ million downloads**, mobile-first, and built & verified on **Angular 22**
32
36
  while keeping a wide `>=10` peer range so existing apps upgrade without churn.
33
37
 
38
+ From France's data-protection regulator to Switzerland's federal tech institute, it's in
39
+ production on five continents — [see who's using it ↓](#-used-in-production).
40
+
34
41
  ```bash
35
42
  npm install ng2-pdfjs-viewer
36
43
  ```
@@ -41,6 +48,22 @@ npm install ng2-pdfjs-viewer
41
48
 
42
49
  That's the whole integration. [Wire up the assets](#-quick-start) and you have a full viewer.
43
50
 
51
+ ## 🌍 Used in production
52
+
53
+ National regulators, public universities, research infrastructure, and fintech platforms render
54
+ PDFs with ng2-pdfjs-viewer — on five continents. Among them:
55
+
56
+ | | |
57
+ |---|---|
58
+ | <img src="https://flagcdn.com/20x15/ch.png" width="20" alt="Switzerland"> **EPFL** | Switzerland's federal institute of technology — the Infoscience research portal |
59
+ | <img src="https://flagcdn.com/20x15/fr.png" width="20" alt="France"> **CNIL** | France's national data-protection authority |
60
+ | <img src="https://flagcdn.com/20x15/fi.png" width="20" alt="Finland"> **Finnish National Agency for Education** | the country's open learning-materials library (AOE) |
61
+ | <img src="https://flagcdn.com/20x15/au.png" width="20" alt="Australia"> **AuScope** | Australia's national geoscience research infrastructure |
62
+ | <img src="https://flagcdn.com/20x15/es.png" width="20" alt="Spain"> **Spain's Ministry of Culture** | the Travesía cultural-heritage platform |
63
+ | <img src="https://flagcdn.com/20x15/us.png" width="20" alt="United States"> **University of Virginia** | the Supporting Transformative Autism Research (DRIVE) program |
64
+
65
+ Part of **8.3M+ installs** worldwide. [See the full showcase →](https://angularpdf.com/showcase)
66
+
44
67
  ## ✨ Highlights
45
68
 
46
69
  | | |
@@ -209,16 +232,15 @@ test.bat # build the lib, link it, and serve the demo on http://localho
209
232
  See [CONTRIBUTING.md](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CONTRIBUTING.md) for the full setup, and look for
210
233
  [`good first issue`](https://github.com/intbot/ng2-pdfjs-viewer/labels/good%20first%20issue) to get started.
211
234
 
212
- ## Star history
235
+ ## 🏗️ Showcase
236
+
237
+ Shipped something with ng2-pdfjs-viewer? [Add it to the showcase](https://angularpdf.com/showcase) — submitted projects are listed next to other production apps using the viewer.
213
238
 
214
- <a href="https://star-history.com/#intbot/ng2-pdfjs-viewer&Date">
215
- <img src="https://api.star-history.com/svg?repos=intbot/ng2-pdfjs-viewer&type=Date" alt="Star history chart for ng2-pdfjs-viewer" width="640" />
216
- </a>
239
+ Using it somewhere that won't show up in public code — an internal tool, a hospital system, a government portal? I'd like to hear about it: email **codehippie1@gmail.com** with a line about what you're building. Teams in healthcare, finance, education, and public-sector software already have. (Bugs and feature requests are best filed as [an issue](https://github.com/intbot/ng2-pdfjs-viewer/issues).)
217
240
 
218
241
  ## 📄 License
219
242
 
220
- [Apache-2.0 (Commons Clause)](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE). Free to use, modify, and self-host; the Commons
221
- Clause restricts selling the software itself as a hosted/commercial product.
243
+ [Apache-2.0](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE). Use, modify, self-host, or ship it inside a commercial product — the Apache 2.0 grant is perpetual, irrevocable, and carries an express patent license.
222
244
 
223
245
  ## 🙏 Acknowledgments
224
246
 
@@ -40,7 +40,7 @@ class PdfAiAssistant {
40
40
  * of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns
41
41
  * for multi-turn chat.
42
42
  */
43
- async ask(question, documentText, history = [], signal) {
43
+ async ask(question, documentText, history = [], signal, onToken) {
44
44
  const context = this.buildContext(documentText);
45
45
  const messages = [
46
46
  {
@@ -53,14 +53,20 @@ class PdfAiAssistant {
53
53
  ...history,
54
54
  { role: "user", content: question },
55
55
  ];
56
- return this.complete(messages, signal);
56
+ return this.complete(messages, signal, onToken);
57
57
  }
58
58
  /** One-shot document summary. */
59
59
  async summarize(documentText) {
60
60
  return this.ask("Summarize this document concisely. Lead with what it is, then the key points.", documentText);
61
61
  }
62
- /** Raw chat-completions call for custom prompting. */
63
- async complete(messages, signal) {
62
+ /**
63
+ * Raw chat-completions call for custom prompting. Pass `onToken` to stream the
64
+ * answer token-by-token (the callback receives the running full text and the
65
+ * latest delta); the Promise still resolves to the complete text. Streaming is
66
+ * requested only when `onToken` is given and `config.stream !== false`, and it
67
+ * falls back to a single JSON response if the endpoint doesn't stream.
68
+ */
69
+ async complete(messages, signal, onToken) {
64
70
  const headers = {
65
71
  "Content-Type": "application/json",
66
72
  ...(this.config.headers ?? {}),
@@ -68,6 +74,7 @@ class PdfAiAssistant {
68
74
  if (this.config.apiKey) {
69
75
  headers["Authorization"] = `Bearer ${this.config.apiKey}`;
70
76
  }
77
+ const wantStream = !!onToken && this.config.stream !== false;
71
78
  const response = await fetch(this.config.endpoint, {
72
79
  method: "POST",
73
80
  headers,
@@ -76,19 +83,81 @@ class PdfAiAssistant {
76
83
  model: this.config.model,
77
84
  temperature: this.config.temperature ?? 0.2,
78
85
  messages,
86
+ ...(wantStream ? { stream: true } : {}),
79
87
  }),
80
88
  });
81
89
  if (!response.ok) {
82
90
  const body = await response.text().catch(() => "");
83
91
  throw new Error(`AI endpoint returned ${response.status}: ${body.slice(0, 300)}`);
84
92
  }
93
+ const contentType = response.headers?.get?.("Content-Type") ?? "";
94
+ if (wantStream && response.body && contentType.includes("text/event-stream")) {
95
+ return this.readStream(response, onToken);
96
+ }
97
+ // Non-streaming response, or the endpoint ignored `stream`: one JSON body.
85
98
  const json = await response.json();
86
99
  const content = json?.choices?.[0]?.message?.content;
87
100
  if (typeof content !== "string") {
88
101
  throw new Error("AI endpoint returned an unexpected response shape");
89
102
  }
103
+ // Emit once so callers wired for streaming still receive their update.
104
+ if (onToken)
105
+ onToken(content, content);
90
106
  return content;
91
107
  }
108
+ /**
109
+ * Read an OpenAI-style Server-Sent Events stream, accumulating
110
+ * choices[0].delta.content and emitting each delta through onToken. Returns the
111
+ * full concatenated text. Tolerates chunk boundaries that split SSE lines, and
112
+ * skips frames it can't parse (keep-alive comments, non-`data:` lines) rather
113
+ * than failing the whole stream.
114
+ */
115
+ async readStream(response, onToken) {
116
+ const reader = response.body.getReader();
117
+ const decoder = new TextDecoder();
118
+ let buffer = "";
119
+ let full = "";
120
+ const handleLine = (raw) => {
121
+ const line = raw.trim();
122
+ if (!line.startsWith("data:"))
123
+ return false;
124
+ const data = line.slice(5).trim();
125
+ if (data === "[DONE]")
126
+ return true;
127
+ try {
128
+ const delta = JSON.parse(data)?.choices?.[0]?.delta?.content;
129
+ if (typeof delta === "string" && delta) {
130
+ full += delta;
131
+ onToken(full, delta);
132
+ }
133
+ }
134
+ catch {
135
+ // Ignore an unparseable frame rather than aborting the stream.
136
+ }
137
+ return false;
138
+ };
139
+ try {
140
+ for (;;) {
141
+ const { done, value } = await reader.read();
142
+ if (done)
143
+ break;
144
+ buffer += decoder.decode(value, { stream: true });
145
+ let nl;
146
+ while ((nl = buffer.indexOf("\n")) !== -1) {
147
+ const line = buffer.slice(0, nl);
148
+ buffer = buffer.slice(nl + 1);
149
+ if (handleLine(line))
150
+ return full; // [DONE]
151
+ }
152
+ }
153
+ if (buffer.trim())
154
+ handleLine(buffer); // trailing line without a newline
155
+ }
156
+ finally {
157
+ reader.releaseLock();
158
+ }
159
+ return full;
160
+ }
92
161
  buildContext(documentText) {
93
162
  const max = this.config.maxContextChars ?? 100_000;
94
163
  let out = "";
@@ -0,0 +1,74 @@
1
+ import { computed } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
3
+
4
+ // Secondary entry point: ng2-pdfjs-viewer/signals
5
+ //
6
+ // Read-only Angular signals projected from the viewer's @Output() events, for
7
+ // zoneless and OnPush apps that prefer reading state from signals over wiring up
8
+ // a handful of (event)="..." bindings and EventEmitter subscriptions.
9
+ //
10
+ // It reads the same outputs the component already emits - it adds no new viewer
11
+ // behaviour and sends nothing anywhere. Each signal holds the latest value from
12
+ // its source output and starts as `undefined` until that output first fires
13
+ // (`loaded`/`totalPages` are derived from page initialization).
14
+ //
15
+ // Requires Angular 16+ (signals + `@angular/core/rxjs-interop`). The base package
16
+ // keeps its `>=10` peer range; only this entry point needs 16+, so import it from
17
+ // the subpath and apps on older Angular never load it:
18
+ //
19
+ // import { pdfViewerSignals } from "ng2-pdfjs-viewer/signals";
20
+ //
21
+ // A @ViewChild viewer is only populated in ngAfterViewInit, which is NOT an
22
+ // injection context, so pass an Injector captured earlier:
23
+ //
24
+ // @ViewChild('viewer') viewer!: PdfJsViewerComponent;
25
+ // private injector = inject(Injector);
26
+ // signals!: PdfViewerSignals;
27
+ //
28
+ // ngAfterViewInit() {
29
+ // this.signals = pdfViewerSignals(this.viewer, { injector: this.injector });
30
+ // }
31
+ // // template: {{ signals.page() }} / {{ signals.totalPages() }} / {{ signals.loaded() }}
32
+ //
33
+ // Called from a constructor or field initializer (where an injection context
34
+ // exists), the `injector` option can be omitted. Subscriptions are torn down with
35
+ // the injector's DestroyRef - i.e. when the host component is destroyed.
36
+ /**
37
+ * Project the viewer's outputs into read-only signals. See the file header for
38
+ * usage and the injection-context rules. The returned signals are live: each
39
+ * updates when its source output next fires.
40
+ */
41
+ function pdfViewerSignals(viewer, options = {}) {
42
+ const { injector } = options;
43
+ const latest = (source) => toSignal(source, { initialValue: undefined, injector });
44
+ const pages = latest(viewer.onPagesInit);
45
+ return {
46
+ page: latest(viewer.onPageChange),
47
+ scale: latest(viewer.onScaleChange),
48
+ totalPages: computed(() => pages()?.pagesCount),
49
+ loaded: computed(() => pages() !== undefined),
50
+ error: latest(viewer.onDocumentError),
51
+ rotation: latest(viewer.onRotationChange),
52
+ findMatches: latest(viewer.onUpdateFindMatchesCount),
53
+ readAloud: latest(viewer.onReadAloudStateChange),
54
+ annotationEditor: latest(viewer.onAnnotationEditorStateChange),
55
+ annotationEditorMode: latest(viewer.annotationEditorChange),
56
+ sidebar: latest(viewer.onSidebarViewChanged),
57
+ metadata: latest(viewer.onMetadataLoaded),
58
+ outline: latest(viewer.onOutlineLoaded),
59
+ formData: latest(viewer.formDataChange),
60
+ presentationMode: latest(viewer.onPresentationModeChanged),
61
+ zoom: latest(viewer.zoomChange),
62
+ cursor: latest(viewer.cursorChange),
63
+ scroll: latest(viewer.scrollChange),
64
+ spread: latest(viewer.spreadChange),
65
+ pageMode: latest(viewer.pageModeChange),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Generated bundle index. Do not edit.
71
+ */
72
+
73
+ export { pdfViewerSignals };
74
+ //# sourceMappingURL=ng2-pdfjs-viewer-signals.mjs.map
@@ -150,6 +150,32 @@ class ActionQueueManager {
150
150
  }
151
151
 
152
152
  // Property normalization between component inputs and PDF.js viewer values
153
+ // Mode/name lists shared by the to- and from-viewer transforms. PDF.js encodes
154
+ // scroll and spread modes as integer enums, so for those the array index is the
155
+ // enum value and the order here doubles as the numeric->name map (keep it in
156
+ // sync with PDF.js). Declaring each list once keeps the input whitelist and the
157
+ // index map from drifting apart, and hoisting them to module scope avoids
158
+ // re-allocating the array on every viewer state-sync event.
159
+ const ZOOM_NAMES = [
160
+ "auto",
161
+ "page-fit",
162
+ "page-width",
163
+ "page-actual",
164
+ ];
165
+ const CURSOR_MODES = ["select", "hand", "zoom"];
166
+ const SCROLL_MODES = [
167
+ "vertical",
168
+ "horizontal",
169
+ "wrapped",
170
+ "page",
171
+ ];
172
+ const SPREAD_MODES = ["none", "odd", "even"];
173
+ const PAGE_MODES = [
174
+ "none",
175
+ "thumbs",
176
+ "bookmarks",
177
+ "attachments",
178
+ ];
153
179
  // Lowercase + whitelist with fallback
154
180
  const pick = (value, allowed, fallback) => {
155
181
  const v = value ? value.toLowerCase() : "";
@@ -162,9 +188,7 @@ class PropertyTransformers {
162
188
  return "auto";
163
189
  const v = zoom.toLowerCase();
164
190
  // Named zooms normalize to lowercase; numeric strings pass through
165
- return ["auto", "page-fit", "page-width", "page-actual"].includes(v)
166
- ? v
167
- : zoom;
191
+ return ZOOM_NAMES.includes(v) ? v : zoom;
168
192
  },
169
193
  fromViewer: (viewerZoom) => {
170
194
  if (typeof viewerZoom === "string")
@@ -180,31 +204,29 @@ class PropertyTransformers {
180
204
  fromViewer: (viewerRotation) => typeof viewerRotation === "number" ? viewerRotation : 0,
181
205
  };
182
206
  static transformCursor = {
183
- toViewer: (cursor) => pick(cursor, ["select", "hand", "zoom"], "select"),
207
+ toViewer: (cursor) => pick(cursor, CURSOR_MODES, "select"),
184
208
  fromViewer: (viewerCursor) => typeof viewerCursor === "string" ? viewerCursor : "select",
185
209
  };
186
210
  static transformScroll = {
187
- toViewer: (scroll) => pick(scroll, ["vertical", "horizontal", "wrapped", "page"], "vertical"),
211
+ toViewer: (scroll) => pick(scroll, SCROLL_MODES, "vertical"),
188
212
  fromViewer: (viewerScroll) => {
189
- const modes = ["vertical", "horizontal", "wrapped", "page"];
190
213
  if (typeof viewerScroll === "number") {
191
- return modes[viewerScroll] || "vertical";
214
+ return SCROLL_MODES[viewerScroll] || "vertical";
192
215
  }
193
216
  return typeof viewerScroll === "string" ? viewerScroll : "vertical";
194
217
  },
195
218
  };
196
219
  static transformSpread = {
197
- toViewer: (spread) => pick(spread, ["none", "odd", "even"], "none"),
220
+ toViewer: (spread) => pick(spread, SPREAD_MODES, "none"),
198
221
  fromViewer: (viewerSpread) => {
199
- const modes = ["none", "odd", "even"];
200
222
  if (typeof viewerSpread === "number") {
201
- return modes[viewerSpread] || "none";
223
+ return SPREAD_MODES[viewerSpread] || "none";
202
224
  }
203
225
  return typeof viewerSpread === "string" ? viewerSpread : "none";
204
226
  },
205
227
  };
206
228
  static transformPageMode = {
207
- toViewer: (pageMode) => pick(pageMode, ["none", "thumbs", "bookmarks", "attachments"], "none"),
229
+ toViewer: (pageMode) => pick(pageMode, PAGE_MODES, "none"),
208
230
  fromViewer: (viewerPageMode) => typeof viewerPageMode === "string" ? viewerPageMode : "none",
209
231
  };
210
232
  }
@@ -1702,6 +1724,18 @@ class PdfJsViewerComponent {
1702
1724
  if (this.diagnosticLogs)
1703
1725
  console.debug("PdfJsViewer: The document has now been loaded!");
1704
1726
  this.onDocumentLoad.emit();
1727
+ // Project onPagesInit here. PDF.js fires the real 'pagesinit' once,
1728
+ // BEFORE this handler map finishes registering (it lands between
1729
+ // 'pagesinit' and 'scalechanging'), and the postMessage wrapper's own
1730
+ // pagesInit relay is wired on 'documentloaded' too late for the
1731
+ // one-shot - so onPagesInit never fired and the signals entry point's
1732
+ // loaded()/totalPages() stayed empty. 'documentloaded' is reliably
1733
+ // caught (it drives onDocumentLoad) and pagesCount is set by now.
1734
+ const app = this.PDFViewerApplication;
1735
+ const pagesCount = app?.pagesCount ?? app?.pdfDocument?.numPages;
1736
+ if (typeof pagesCount === "number" && pagesCount > 0) {
1737
+ this.onPagesInit.emit({ pagesCount });
1738
+ }
1705
1739
  // Queue auto-actions with the property values current at THIS load
1706
1740
  this.queueAutoActionsForDocumentLoad();
1707
1741
  // Execute all queued auto-actions
@@ -1980,11 +2014,15 @@ class PdfJsViewerComponent {
1980
2014
  // Embedded views for pageOverlayTpl, keyed by page number. Views are
1981
2015
  // attached to ApplicationRef so bindings inside stay live.
1982
2016
  overlayViews = new Map();
1983
- mountPageOverlay(pageNumber) {
2017
+ mountPageOverlay(pageNumber, knownPageEl) {
1984
2018
  if (!this.pageOverlayTpl || this.externalWindow)
1985
2019
  return;
1986
2020
  const doc = this.iframe?.nativeElement?.contentDocument;
1987
- const pageEl = doc?.querySelector(`.pdfViewer .page[data-page-number="${pageNumber}"]`);
2021
+ // Callers iterating already-rendered pages pass the element they hold, so
2022
+ // we skip re-finding it by selector (saves one DOM query per page on large
2023
+ // documents); the per-page render path passes nothing and looks it up.
2024
+ const pageEl = knownPageEl ??
2025
+ doc?.querySelector(`.pdfViewer .page[data-page-number="${pageNumber}"]`);
1988
2026
  if (!pageEl || pageEl.querySelector(":scope > .ng2-page-overlay")) {
1989
2027
  return;
1990
2028
  }
@@ -2026,7 +2064,7 @@ class PdfJsViewerComponent {
2026
2064
  .forEach((el) => {
2027
2065
  const pageNumber = Number(el.getAttribute("data-page-number"));
2028
2066
  if (pageNumber > 0) {
2029
- this.mountPageOverlay(pageNumber);
2067
+ this.mountPageOverlay(pageNumber, el);
2030
2068
  }
2031
2069
  });
2032
2070
  }
@@ -2100,6 +2138,9 @@ class PdfJsViewerComponent {
2100
2138
  this.aiAbort = new AbortController();
2101
2139
  const signal = this.aiAbort.signal;
2102
2140
  this.aiMessages.push({ role: "user", content: q, parts: [{ text: q }] });
2141
+ // Placeholder assistant turn we stream tokens into as they arrive.
2142
+ const assistant = { role: "assistant", content: "", parts: [] };
2143
+ this.aiMessages.push(assistant);
2103
2144
  this.cdr.markForCheck();
2104
2145
  try {
2105
2146
  if (!this.aiClient || this.aiClientConfig !== config) {
@@ -2110,29 +2151,28 @@ class PdfJsViewerComponent {
2110
2151
  this.aiDocText = await this.getDocumentText();
2111
2152
  }
2112
2153
  const history = this.aiMessages
2113
- .slice(0, -1)
2154
+ .slice(0, -2)
2114
2155
  .filter((m) => !m.error)
2115
2156
  .map((m) => ({ role: m.role, content: m.content }));
2116
- const answer = await this.aiClient.ask(q, this.aiDocText, history, signal);
2157
+ const answer = await this.aiClient.ask(q, this.aiDocText, history, signal, (full) => {
2158
+ if (generation !== this.aiGeneration) {
2159
+ return; // stale stream - ignore late tokens
2160
+ }
2161
+ assistant.content = full;
2162
+ assistant.parts = this.parseAiCitations(full);
2163
+ this.cdr.markForCheck();
2164
+ });
2117
2165
  if (generation !== this.aiGeneration) {
2118
2166
  return; // document changed mid-flight - stale answer
2119
2167
  }
2120
- this.aiMessages.push({
2121
- role: "assistant",
2122
- content: answer,
2123
- parts: this.parseAiCitations(answer),
2124
- });
2168
+ assistant.content = answer;
2169
+ assistant.parts = this.parseAiCitations(answer);
2125
2170
  }
2126
2171
  catch (e) {
2127
2172
  if (generation !== this.aiGeneration) {
2128
2173
  return; // aborted by invalidation - already cleaned up
2129
2174
  }
2130
- this.aiMessages.push({
2131
- role: "assistant",
2132
- content: "",
2133
- error: e?.message || "AI request failed",
2134
- parts: [],
2135
- });
2175
+ assistant.error = e?.message || "AI request failed";
2136
2176
  }
2137
2177
  finally {
2138
2178
  if (generation === this.aiGeneration) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ng2-pdfjs-viewer",
3
- "version": "26.1.1",
3
+ "version": "26.3.0",
4
4
  "description": "The most comprehensive Angular PDF viewer, powered by Mozilla PDF.js 6 — view, annotate, sign, fill forms, search, and read aloud from one component. 8.3M+ downloads, mobile-first, production-ready.",
5
5
  "author": {
6
6
  "name": "Aneesh Goapalakrishnan",
@@ -54,7 +54,7 @@
54
54
  "accessibility",
55
55
  "open-source"
56
56
  ],
57
- "license": "Apache-2.0 (Commons Clause)",
57
+ "license": "Apache-2.0",
58
58
  "readme": "README.md",
59
59
  "exports": {
60
60
  "./package.json": {
@@ -67,6 +67,10 @@
67
67
  "./ai": {
68
68
  "types": "./types/ng2-pdfjs-viewer-ai.d.ts",
69
69
  "default": "./fesm2022/ng2-pdfjs-viewer-ai.mjs"
70
+ },
71
+ "./signals": {
72
+ "types": "./types/ng2-pdfjs-viewer-signals.d.ts",
73
+ "default": "./fesm2022/ng2-pdfjs-viewer-signals.mjs"
70
74
  }
71
75
  },
72
76
  "sideEffects": false,
@@ -9,6 +9,7 @@ interface PdfAiAssistantConfig {
9
9
  headers?: Record<string, string>;
10
10
  maxContextChars?: number;
11
11
  temperature?: number;
12
+ stream?: boolean;
12
13
  }
13
14
  interface PdfAiMessage {
14
15
  role: "system" | "user" | "assistant";
@@ -35,11 +36,25 @@ declare class PdfAiAssistant {
35
36
  * of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns
36
37
  * for multi-turn chat.
37
38
  */
38
- ask(question: string, documentText: PdfPageText[], history?: PdfAiMessage[], signal?: AbortSignal): Promise<string>;
39
+ ask(question: string, documentText: PdfPageText[], history?: PdfAiMessage[], signal?: AbortSignal, onToken?: (full: string, delta: string) => void): Promise<string>;
39
40
  /** One-shot document summary. */
40
41
  summarize(documentText: PdfPageText[]): Promise<string>;
41
- /** Raw chat-completions call for custom prompting. */
42
- complete(messages: PdfAiMessage[], signal?: AbortSignal): Promise<string>;
42
+ /**
43
+ * Raw chat-completions call for custom prompting. Pass `onToken` to stream the
44
+ * answer token-by-token (the callback receives the running full text and the
45
+ * latest delta); the Promise still resolves to the complete text. Streaming is
46
+ * requested only when `onToken` is given and `config.stream !== false`, and it
47
+ * falls back to a single JSON response if the endpoint doesn't stream.
48
+ */
49
+ complete(messages: PdfAiMessage[], signal?: AbortSignal, onToken?: (full: string, delta: string) => void): Promise<string>;
50
+ /**
51
+ * Read an OpenAI-style Server-Sent Events stream, accumulating
52
+ * choices[0].delta.content and emitting each delta through onToken. Returns the
53
+ * full concatenated text. Tolerates chunk boundaries that split SSE lines, and
54
+ * skips frames it can't parse (keep-alive comments, non-`data:` lines) rather
55
+ * than failing the whole stream.
56
+ */
57
+ private readStream;
43
58
  private buildContext;
44
59
  }
45
60
 
@@ -0,0 +1,77 @@
1
+ import { EventEmitter, Signal, Injector } from '@angular/core';
2
+ import { ChangedRotation, PagesInfo, DocumentError, FindMatchesCount, ReadAloudState, AnnotationEditorState, AnnotationEditorMode, SidebarViewChange, DocumentMetadata, DocumentOutline, FormDataMap, PresentationMode } from 'ng2-pdfjs-viewer';
3
+
4
+ interface PdfViewerSignalSource {
5
+ onPageChange: EventEmitter<number>;
6
+ onScaleChange: EventEmitter<number>;
7
+ onRotationChange: EventEmitter<ChangedRotation>;
8
+ onPagesInit: EventEmitter<PagesInfo>;
9
+ onDocumentError: EventEmitter<DocumentError>;
10
+ onUpdateFindMatchesCount: EventEmitter<FindMatchesCount>;
11
+ onReadAloudStateChange: EventEmitter<ReadAloudState>;
12
+ onAnnotationEditorStateChange: EventEmitter<AnnotationEditorState>;
13
+ annotationEditorChange: EventEmitter<AnnotationEditorMode>;
14
+ onSidebarViewChanged: EventEmitter<SidebarViewChange>;
15
+ onMetadataLoaded: EventEmitter<DocumentMetadata>;
16
+ onOutlineLoaded: EventEmitter<DocumentOutline>;
17
+ formDataChange: EventEmitter<FormDataMap>;
18
+ onPresentationModeChanged: EventEmitter<PresentationMode>;
19
+ zoomChange: EventEmitter<string>;
20
+ cursorChange: EventEmitter<string>;
21
+ scrollChange: EventEmitter<string>;
22
+ spreadChange: EventEmitter<string>;
23
+ pageModeChange: EventEmitter<string>;
24
+ }
25
+ interface PdfViewerSignals {
26
+ /** Current 1-based page number; undefined until the first page change. */
27
+ page: Signal<number | undefined>;
28
+ /** Current zoom scale factor (1 = 100%); undefined until the first scale change. */
29
+ scale: Signal<number | undefined>;
30
+ /** Total page count, available once the document's pages initialize. */
31
+ totalPages: Signal<number | undefined>;
32
+ /** True once the document's pages have initialized. */
33
+ loaded: Signal<boolean>;
34
+ /** Last document-load error, or undefined if none. */
35
+ error: Signal<DocumentError | undefined>;
36
+ /** Page rotation state ({ rotation, page }). */
37
+ rotation: Signal<ChangedRotation | undefined>;
38
+ /** Live find/search match counts ({ current, total }). */
39
+ findMatches: Signal<FindMatchesCount | undefined>;
40
+ /** Read-aloud progress ({ status, page, sentence? }). */
41
+ readAloud: Signal<ReadAloudState | undefined>;
42
+ /** Annotation editor undo/empty/selection state. */
43
+ annotationEditor: Signal<AnnotationEditorState | undefined>;
44
+ /** Active annotation editor tool (highlight / ink / freetext / ...). */
45
+ annotationEditorMode: Signal<AnnotationEditorMode | undefined>;
46
+ /** Current sidebar panel ({ view }). */
47
+ sidebar: Signal<SidebarViewChange | undefined>;
48
+ /** Document metadata (title / author / ...). */
49
+ metadata: Signal<DocumentMetadata | undefined>;
50
+ /** Document outline / bookmarks ({ items?, hasOutline }). */
51
+ outline: Signal<DocumentOutline | undefined>;
52
+ /** Current AcroForm field values. */
53
+ formData: Signal<FormDataMap | undefined>;
54
+ /** Presentation (fullscreen) mode ({ active }). */
55
+ presentationMode: Signal<PresentationMode | undefined>;
56
+ /** Zoom mode string (e.g. 'auto', 'page-fit', '125'). */
57
+ zoom: Signal<string | undefined>;
58
+ /** Cursor tool ('select' | 'hand'). */
59
+ cursor: Signal<string | undefined>;
60
+ /** Scroll mode ('vertical' | 'horizontal' | 'wrapped' | 'page'). */
61
+ scroll: Signal<string | undefined>;
62
+ /** Spread mode ('none' | 'odd' | 'even'). */
63
+ spread: Signal<string | undefined>;
64
+ /** Page (view) mode. */
65
+ pageMode: Signal<string | undefined>;
66
+ }
67
+ /**
68
+ * Project the viewer's outputs into read-only signals. See the file header for
69
+ * usage and the injection-context rules. The returned signals are live: each
70
+ * updates when its source output next fires.
71
+ */
72
+ declare function pdfViewerSignals(viewer: PdfViewerSignalSource, options?: {
73
+ injector?: Injector;
74
+ }): PdfViewerSignals;
75
+
76
+ export { pdfViewerSignals };
77
+ export type { PdfViewerSignalSource, PdfViewerSignals };