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 +1 -31
- package/README.md +30 -8
- package/fesm2022/ng2-pdfjs-viewer-ai.mjs +73 -4
- package/fesm2022/ng2-pdfjs-viewer-signals.mjs +74 -0
- package/fesm2022/ng2-pdfjs-viewer.mjs +67 -27
- package/package.json +6 -2
- package/types/ng2-pdfjs-viewer-ai.d.ts +18 -3
- package/types/ng2-pdfjs-viewer-signals.d.ts +77 -0
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-
|
|
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
|
[](https://github.com/intbot/ng2-pdfjs-viewer/security/code-scanning)
|
|
15
15
|
[](https://angular.dev)
|
|
16
16
|
[](https://github.com/mozilla/pdf.js)
|
|
17
|
-
[](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE)
|
|
18
18
|
[](https://github.com/intbot/ng2-pdfjs-viewer)
|
|
19
|
+
[](https://stackblitz.com/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
|
|
20
|
+
[](https://codesandbox.io/p/sandbox/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
|
|
19
21
|
|
|
20
|
-
[**
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
63
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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, -
|
|
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
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
42
|
-
|
|
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 };
|