stimulus-pdf-viewer-rails 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/app/assets/javascripts/stimulus-pdf-viewer.esm.js +196 -57
- data/app/assets/javascripts/stimulus-pdf-viewer.js +195 -56
- data/lib/stimulus_pdf_viewer/rails/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 651ed5f4fcb8152f9716cdee8eaa0e69481c9ac0f44534331fa6650fbbf30a76
|
|
4
|
+
data.tar.gz: c045dc496a804a77b19f6d5ef81b9657cc1f5321c857bc04a87103ed0431d9a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28a1ad29124fd3cc7d30a470e9774dc0d043aac56b0df4d81f1ae187ea474b9009db8cbd2dedee4b73f4a38edb9ff97ff0ecfe021f402b37df23a0a73694e797
|
|
7
|
+
data.tar.gz: 28302f66b536695dd0fca27167a72d6b111e3290dd5fb2f102db0b710bb07a059353d9bad7cac1580ca68729eef02f337a96c3f20c4d0da72124f58391c6bf61
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Updated stimulus-pdf-viewer to 0.6.0
|
|
9
|
+
|
|
10
|
+
Annotated downloads now include everything shown on screen. Underlines were
|
|
11
|
+
never exported, because the download code expected an `annotation_type` of
|
|
12
|
+
`underline` while the underline tool created `line`. Comments on highlights,
|
|
13
|
+
underlines, and drawings are now exported too. Curly quotes, em dashes, and
|
|
14
|
+
non-Latin text in comments and notes no longer come out garbled. See the
|
|
15
|
+
upstream
|
|
16
|
+
[0.6.0 release notes](https://github.com/jhubert/stimulus-pdf-viewer/releases/tag/v0.6.0)
|
|
17
|
+
for details.
|
|
18
|
+
|
|
19
|
+
### Upgrade notes
|
|
20
|
+
- New underlines are saved with `annotation_type: "underline"` instead of
|
|
21
|
+
`"line"`. Make sure your `Annotation` model accepts `underline`; with an
|
|
22
|
+
integer enum, rename the `line` key in place. Stored `"line"` records keep
|
|
23
|
+
working, but they are deprecated. The upstream release notes give the SQL to
|
|
24
|
+
migrate them.
|
|
25
|
+
|
|
5
26
|
## [0.5.0] - 2026-08-11
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
3
3
|
import { FetchRequest } from '@rails/request.js';
|
|
4
|
-
import { PDFDocument, StandardFonts, degrees, rgb, PDFName, PDFString,
|
|
4
|
+
import { PDFDocument, StandardFonts, degrees, rgb, PDFName, PDFArray, PDFString, PDFHexString } from 'pdf-lib';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* EventBus - Internal event system for the PDF viewer.
|
|
@@ -1694,6 +1694,115 @@ class MemoryAnnotationStore extends AnnotationStore {
|
|
|
1694
1694
|
}
|
|
1695
1695
|
}
|
|
1696
1696
|
|
|
1697
|
+
// Canonical annotation vocabulary.
|
|
1698
|
+
//
|
|
1699
|
+
// `annotation_type` travels over the REST API and is stored in the consuming
|
|
1700
|
+
// application's database, so inbound records may still carry legacy values that
|
|
1701
|
+
// this library no longer emits. Records are normalized once on the way in (see
|
|
1702
|
+
// AnnotationManager); everything downstream may assume the canonical names.
|
|
1703
|
+
//
|
|
1704
|
+
// These are the annotation types, not the editing modes -- see ToolMode in
|
|
1705
|
+
// index.js, which adds SELECT and otherwise uses the same strings.
|
|
1706
|
+
|
|
1707
|
+
const AnnotationType = {
|
|
1708
|
+
HIGHLIGHT: "highlight",
|
|
1709
|
+
UNDERLINE: "underline",
|
|
1710
|
+
NOTE: "note",
|
|
1711
|
+
INK: "ink"
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
// Legacy `annotation_type` values accepted on read and rewritten to canonical
|
|
1715
|
+
// names. "line" was the original spelling of underline; it is also actively
|
|
1716
|
+
// misleading, naming the PDF spec's Line annotation (subtype 4) -- a different
|
|
1717
|
+
// annotation that this viewer does not support.
|
|
1718
|
+
const LEGACY_TYPE_ALIASES = {
|
|
1719
|
+
line: AnnotationType.UNDERLINE
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// Marks an ink annotation drawn with the highlighter rather than the pen. This
|
|
1723
|
+
// lives on `subject` rather than being its own type because both are genuinely
|
|
1724
|
+
// Ink annotations once exported to PDF.
|
|
1725
|
+
const FREE_HIGHLIGHT_SUBJECT = "Free Highlight";
|
|
1726
|
+
|
|
1727
|
+
// PDF annotation subtype each canonical type is written as by DownloadManager
|
|
1728
|
+
// when baking annotations into the file. See PDF 32000-1:2008 section 12.5.6.
|
|
1729
|
+
const PDF_SUBTYPES = {
|
|
1730
|
+
[AnnotationType.HIGHLIGHT]: "Highlight",
|
|
1731
|
+
[AnnotationType.UNDERLINE]: "Underline",
|
|
1732
|
+
[AnnotationType.NOTE]: "Text",
|
|
1733
|
+
[AnnotationType.INK]: "Ink"
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// Legacy values already warned about, so a document full of them produces one
|
|
1737
|
+
// warning per type rather than one per annotation.
|
|
1738
|
+
const warnedLegacyTypes = new Set();
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Map a possibly-legacy annotation type onto the canonical vocabulary.
|
|
1742
|
+
*
|
|
1743
|
+
* Warns once per legacy value encountered. The aliases exist only to carry
|
|
1744
|
+
* records written by earlier versions, and are slated for removal; the warning
|
|
1745
|
+
* is how a consuming application finds out it still has rows to migrate while
|
|
1746
|
+
* that support is still in place.
|
|
1747
|
+
*
|
|
1748
|
+
* @param {string} type
|
|
1749
|
+
* @returns {string}
|
|
1750
|
+
*/
|
|
1751
|
+
function normalizeAnnotationType(type) {
|
|
1752
|
+
const canonical = LEGACY_TYPE_ALIASES[type];
|
|
1753
|
+
if (!canonical) return type
|
|
1754
|
+
|
|
1755
|
+
if (!warnedLegacyTypes.has(type)) {
|
|
1756
|
+
warnedLegacyTypes.add(type);
|
|
1757
|
+
console.warn(
|
|
1758
|
+
`[stimulus-pdf-viewer] Deprecated annotation_type "${type}" was read and ` +
|
|
1759
|
+
`treated as "${canonical}". Support for the old value will be removed in a ` +
|
|
1760
|
+
`future release. Migrate stored annotations: ` +
|
|
1761
|
+
`UPDATE annotations SET annotation_type = '${canonical}' WHERE annotation_type = '${type}'.`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
return canonical
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Rewrite a record's annotation_type in place. Mutates rather than clones so
|
|
1770
|
+
* that references already held elsewhere (selectedAnnotation, rendered sidebar
|
|
1771
|
+
* rows) observe the same canonical value.
|
|
1772
|
+
* @param {Object} annotation
|
|
1773
|
+
* @returns {Object} the same annotation
|
|
1774
|
+
*/
|
|
1775
|
+
function normalizeAnnotation(annotation) {
|
|
1776
|
+
if (annotation && annotation.annotation_type) {
|
|
1777
|
+
annotation.annotation_type = normalizeAnnotationType(annotation.annotation_type);
|
|
1778
|
+
}
|
|
1779
|
+
return annotation
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/** An ink annotation drawn with the highlighter tool. */
|
|
1783
|
+
function isFreeHighlight(annotation) {
|
|
1784
|
+
return annotation?.annotation_type === AnnotationType.INK &&
|
|
1785
|
+
annotation?.subject === FREE_HIGHLIGHT_SUBJECT
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** Highlights and highlighter-drawn ink both render as highlights. */
|
|
1789
|
+
function isHighlightLike(annotation) {
|
|
1790
|
+
return annotation?.annotation_type === AnnotationType.HIGHLIGHT ||
|
|
1791
|
+
isFreeHighlight(annotation)
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/** Ink drawn with the pen, as opposed to the highlighter. */
|
|
1795
|
+
function isDrawing(annotation) {
|
|
1796
|
+
return annotation?.annotation_type === AnnotationType.INK &&
|
|
1797
|
+
!isFreeHighlight(annotation)
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Types whose contents are editable as a comment via the note dialog. */
|
|
1801
|
+
function supportsComment(annotation) {
|
|
1802
|
+
return [AnnotationType.HIGHLIGHT, AnnotationType.UNDERLINE, AnnotationType.INK]
|
|
1803
|
+
.includes(annotation?.annotation_type)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1697
1806
|
// Custom event types for error handling
|
|
1698
1807
|
const AnnotationErrorType = {
|
|
1699
1808
|
LOAD_FAILED: "load_failed",
|
|
@@ -1767,6 +1876,7 @@ class AnnotationManager {
|
|
|
1767
1876
|
this.annotationsByPage.clear();
|
|
1768
1877
|
|
|
1769
1878
|
for (const annotation of annotationsData) {
|
|
1879
|
+
normalizeAnnotation(annotation);
|
|
1770
1880
|
this.annotations.set(this._key(annotation.id), annotation);
|
|
1771
1881
|
|
|
1772
1882
|
if (!this.annotationsByPage.has(annotation.page)) {
|
|
@@ -1868,6 +1978,8 @@ class AnnotationManager {
|
|
|
1868
1978
|
}
|
|
1869
1979
|
|
|
1870
1980
|
_addAnnotation(annotation) {
|
|
1981
|
+
normalizeAnnotation(annotation);
|
|
1982
|
+
|
|
1871
1983
|
this.annotations.set(this._key(annotation.id), annotation);
|
|
1872
1984
|
|
|
1873
1985
|
if (!this.annotationsByPage.has(annotation.page)) {
|
|
@@ -1877,6 +1989,8 @@ class AnnotationManager {
|
|
|
1877
1989
|
}
|
|
1878
1990
|
|
|
1879
1991
|
_updateAnnotation(annotation) {
|
|
1992
|
+
normalizeAnnotation(annotation);
|
|
1993
|
+
|
|
1880
1994
|
const oldAnnotation = this.getAnnotation(annotation.id);
|
|
1881
1995
|
if (!oldAnnotation) {
|
|
1882
1996
|
this._addAnnotation(annotation);
|
|
@@ -2065,16 +2179,16 @@ class DownloadManager {
|
|
|
2065
2179
|
_applyAnnotationsToPage(pdfDoc, page, annotations, pageHeight) {
|
|
2066
2180
|
for (const annotation of annotations) {
|
|
2067
2181
|
switch (annotation.annotation_type) {
|
|
2068
|
-
case
|
|
2182
|
+
case AnnotationType.HIGHLIGHT:
|
|
2069
2183
|
this._applyHighlight(pdfDoc, page, annotation, pageHeight);
|
|
2070
2184
|
break
|
|
2071
|
-
case
|
|
2185
|
+
case AnnotationType.UNDERLINE:
|
|
2072
2186
|
this._applyUnderline(pdfDoc, page, annotation, pageHeight);
|
|
2073
2187
|
break
|
|
2074
|
-
case
|
|
2188
|
+
case AnnotationType.INK:
|
|
2075
2189
|
this._applyInk(pdfDoc, page, annotation, pageHeight);
|
|
2076
2190
|
break
|
|
2077
|
-
case
|
|
2191
|
+
case AnnotationType.NOTE:
|
|
2078
2192
|
this._applyNote(pdfDoc, page, annotation, pageHeight);
|
|
2079
2193
|
break
|
|
2080
2194
|
}
|
|
@@ -2113,7 +2227,7 @@ class DownloadManager {
|
|
|
2113
2227
|
|
|
2114
2228
|
const annotationDict = pdfDoc.context.obj({
|
|
2115
2229
|
Type: PDFName.of("Annot"),
|
|
2116
|
-
Subtype: PDFName.of(
|
|
2230
|
+
Subtype: PDFName.of(PDF_SUBTYPES[AnnotationType.HIGHLIGHT]),
|
|
2117
2231
|
Rect: [minX, minY, maxX, maxY],
|
|
2118
2232
|
QuadPoints: quadPoints,
|
|
2119
2233
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2154,7 +2268,7 @@ class DownloadManager {
|
|
|
2154
2268
|
|
|
2155
2269
|
const annotationDict = pdfDoc.context.obj({
|
|
2156
2270
|
Type: PDFName.of("Annot"),
|
|
2157
|
-
Subtype: PDFName.of(
|
|
2271
|
+
Subtype: PDFName.of(PDF_SUBTYPES[AnnotationType.UNDERLINE]),
|
|
2158
2272
|
Rect: [minX, minY, maxX, maxY],
|
|
2159
2273
|
QuadPoints: quadPoints,
|
|
2160
2274
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2167,7 +2281,7 @@ class DownloadManager {
|
|
|
2167
2281
|
|
|
2168
2282
|
_applyInk(pdfDoc, page, annotation, pageHeight) {
|
|
2169
2283
|
// Freehand highlights need different rendering (thick, semi-transparent strokes)
|
|
2170
|
-
if (annotation
|
|
2284
|
+
if (isFreeHighlight(annotation)) {
|
|
2171
2285
|
this._applyFreehandHighlight(pdfDoc, page, annotation, pageHeight);
|
|
2172
2286
|
return
|
|
2173
2287
|
}
|
|
@@ -2239,7 +2353,7 @@ class DownloadManager {
|
|
|
2239
2353
|
|
|
2240
2354
|
const annotationDict = pdfDoc.context.obj({
|
|
2241
2355
|
Type: PDFName.of("Annot"),
|
|
2242
|
-
Subtype: PDFName.of(
|
|
2356
|
+
Subtype: PDFName.of(PDF_SUBTYPES[AnnotationType.INK]),
|
|
2243
2357
|
Rect: [minX, minY, maxX, maxY],
|
|
2244
2358
|
InkList: inkList,
|
|
2245
2359
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2335,7 +2449,7 @@ class DownloadManager {
|
|
|
2335
2449
|
|
|
2336
2450
|
const annotationDict = pdfDoc.context.obj({
|
|
2337
2451
|
Type: PDFName.of("Annot"),
|
|
2338
|
-
Subtype: PDFName.of(
|
|
2452
|
+
Subtype: PDFName.of(PDF_SUBTYPES[AnnotationType.INK]),
|
|
2339
2453
|
Rect: [minX, minY, maxX, maxY],
|
|
2340
2454
|
InkList: inkList,
|
|
2341
2455
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2361,9 +2475,8 @@ class DownloadManager {
|
|
|
2361
2475
|
|
|
2362
2476
|
const annotationDict = pdfDoc.context.obj({
|
|
2363
2477
|
Type: PDFName.of("Annot"),
|
|
2364
|
-
Subtype: PDFName.of(
|
|
2478
|
+
Subtype: PDFName.of(PDF_SUBTYPES[AnnotationType.NOTE]),
|
|
2365
2479
|
Rect: [x, pdfY - iconSize, x + iconSize, pdfY],
|
|
2366
|
-
Contents: PDFString.of(contents),
|
|
2367
2480
|
C: [rgba.r, rgba.g, rgba.b],
|
|
2368
2481
|
Name: PDFName.of("Comment"),
|
|
2369
2482
|
Open: false,
|
|
@@ -2427,6 +2540,13 @@ class DownloadManager {
|
|
|
2427
2540
|
metadata.T = PDFString.of(this.userName);
|
|
2428
2541
|
}
|
|
2429
2542
|
|
|
2543
|
+
// Contents - a note's body, or the comment on any other annotation type.
|
|
2544
|
+
// fromText encodes as UTF-16BE when needed; PDFString.of only covers
|
|
2545
|
+
// PDFDocEncoding and mangles curly quotes, dashes, and the like.
|
|
2546
|
+
if (annotation.contents) {
|
|
2547
|
+
metadata.Contents = PDFHexString.fromText(annotation.contents);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2430
2550
|
// Modification date (M) - use annotation's updated_at or created_at
|
|
2431
2551
|
const dateStr = annotation.updated_at || annotation.created_at;
|
|
2432
2552
|
if (dateStr) {
|
|
@@ -2487,12 +2607,17 @@ class DownloadManager {
|
|
|
2487
2607
|
.replace(/\s+/g, " ") // Normalize whitespace
|
|
2488
2608
|
.trim();
|
|
2489
2609
|
|
|
2610
|
+
// Check for emptiness before appending the extension. A name made only of
|
|
2611
|
+
// illegal characters is empty at this point, and appending first would
|
|
2612
|
+
// produce a hidden ".pdf" file instead of falling back.
|
|
2613
|
+
if (!sanitized) return "document.pdf"
|
|
2614
|
+
|
|
2490
2615
|
// Ensure it ends with .pdf
|
|
2491
2616
|
if (!sanitized.toLowerCase().endsWith(".pdf")) {
|
|
2492
2617
|
sanitized += ".pdf";
|
|
2493
2618
|
}
|
|
2494
2619
|
|
|
2495
|
-
return sanitized
|
|
2620
|
+
return sanitized
|
|
2496
2621
|
}
|
|
2497
2622
|
|
|
2498
2623
|
_triggerDownload(bytes, filename) {
|
|
@@ -2822,8 +2947,8 @@ class AnnotationEditToolbar {
|
|
|
2822
2947
|
}
|
|
2823
2948
|
} else if (e.key === "c" || e.key === "C") {
|
|
2824
2949
|
// Comment shortcut for highlight/underline/ink annotations
|
|
2825
|
-
const
|
|
2826
|
-
if (
|
|
2950
|
+
const canComment = supportsComment(this.currentAnnotation);
|
|
2951
|
+
if (canComment && this.onComment) {
|
|
2827
2952
|
e.preventDefault();
|
|
2828
2953
|
this.onComment(this.currentAnnotation);
|
|
2829
2954
|
}
|
|
@@ -2885,14 +3010,14 @@ class AnnotationEditToolbar {
|
|
|
2885
3010
|
|
|
2886
3011
|
// Show/hide buttons based on annotation type
|
|
2887
3012
|
const isNote = annotation.annotation_type === "note";
|
|
2888
|
-
const
|
|
3013
|
+
const canComment = supportsComment(annotation);
|
|
2889
3014
|
|
|
2890
3015
|
// Comment button for highlight/underline/ink, edit button for notes
|
|
2891
|
-
this.commentBtn.classList.toggle("hidden", !
|
|
3016
|
+
this.commentBtn.classList.toggle("hidden", !canComment);
|
|
2892
3017
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2893
3018
|
|
|
2894
3019
|
// Update comment button title based on whether contents exists
|
|
2895
|
-
if (
|
|
3020
|
+
if (canComment) {
|
|
2896
3021
|
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2897
3022
|
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2898
3023
|
}
|
|
@@ -3098,13 +3223,13 @@ class AnnotationDetailPanel {
|
|
|
3098
3223
|
this.onDelete(this.currentAnnotation);
|
|
3099
3224
|
}
|
|
3100
3225
|
} else if (e.key === "e" || e.key === "E") {
|
|
3101
|
-
if (this.currentAnnotation?.annotation_type ===
|
|
3226
|
+
if (this.currentAnnotation?.annotation_type === AnnotationType.NOTE && this.onEdit) {
|
|
3102
3227
|
e.preventDefault();
|
|
3103
3228
|
this.onEdit(this.currentAnnotation);
|
|
3104
3229
|
}
|
|
3105
3230
|
} else if (e.key === "c" || e.key === "C") {
|
|
3106
|
-
const
|
|
3107
|
-
if (
|
|
3231
|
+
const canComment = supportsComment(this.currentAnnotation);
|
|
3232
|
+
if (canComment && this.onComment) {
|
|
3108
3233
|
e.preventDefault();
|
|
3109
3234
|
this.onComment(this.currentAnnotation);
|
|
3110
3235
|
}
|
|
@@ -3156,10 +3281,10 @@ class AnnotationDetailPanel {
|
|
|
3156
3281
|
|
|
3157
3282
|
_getTypeLabel(annotationType) {
|
|
3158
3283
|
const labels = {
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3284
|
+
[AnnotationType.HIGHLIGHT]: "Highlight",
|
|
3285
|
+
[AnnotationType.UNDERLINE]: "Underline",
|
|
3286
|
+
[AnnotationType.NOTE]: "Note",
|
|
3287
|
+
[AnnotationType.INK]: "Drawing"
|
|
3163
3288
|
};
|
|
3164
3289
|
return labels[annotationType] || "Annotation"
|
|
3165
3290
|
}
|
|
@@ -3188,11 +3313,11 @@ class AnnotationDetailPanel {
|
|
|
3188
3313
|
|
|
3189
3314
|
// Show/hide buttons based on annotation type
|
|
3190
3315
|
const isNote = annotation.annotation_type === "note";
|
|
3191
|
-
const
|
|
3192
|
-
this.commentBtn.classList.toggle("hidden", !
|
|
3316
|
+
const canComment = supportsComment(annotation);
|
|
3317
|
+
this.commentBtn.classList.toggle("hidden", !canComment);
|
|
3193
3318
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
3194
3319
|
|
|
3195
|
-
if (
|
|
3320
|
+
if (canComment) {
|
|
3196
3321
|
const hasComment = annotation.contents && annotation.contents.trim();
|
|
3197
3322
|
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
3198
3323
|
}
|
|
@@ -4014,7 +4139,7 @@ const ANNOTATION_ICONS = {
|
|
|
4014
4139
|
ink: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4015
4140
|
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>
|
|
4016
4141
|
</svg>`,
|
|
4017
|
-
|
|
4142
|
+
underline: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4018
4143
|
<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/>
|
|
4019
4144
|
<line x1="4" y1="21" x2="20" y2="21" stroke-width="3"/>
|
|
4020
4145
|
</svg>`
|
|
@@ -4301,13 +4426,13 @@ class AnnotationSidebar {
|
|
|
4301
4426
|
|
|
4302
4427
|
switch (this.filterType) {
|
|
4303
4428
|
case FilterType.HIGHLIGHT:
|
|
4304
|
-
return
|
|
4429
|
+
return isHighlightLike(annotation)
|
|
4305
4430
|
case FilterType.NOTE:
|
|
4306
|
-
return type ===
|
|
4431
|
+
return type === AnnotationType.NOTE
|
|
4307
4432
|
case FilterType.DRAWING:
|
|
4308
|
-
return
|
|
4433
|
+
return isDrawing(annotation)
|
|
4309
4434
|
case FilterType.UNDERLINE:
|
|
4310
|
-
return type ===
|
|
4435
|
+
return type === AnnotationType.UNDERLINE
|
|
4311
4436
|
default:
|
|
4312
4437
|
return true
|
|
4313
4438
|
}
|
|
@@ -4462,23 +4587,23 @@ class AnnotationSidebar {
|
|
|
4462
4587
|
const type = annotation.annotation_type;
|
|
4463
4588
|
let icon, label, typeLabel;
|
|
4464
4589
|
|
|
4465
|
-
if (
|
|
4590
|
+
if (isHighlightLike(annotation)) {
|
|
4466
4591
|
icon = ANNOTATION_ICONS.highlight;
|
|
4467
4592
|
typeLabel = "Highlight";
|
|
4468
4593
|
// Extract highlighted text if available
|
|
4469
4594
|
label = annotation.title || annotation.contents || "Freehand Highlight";
|
|
4470
4595
|
label = this._truncate(label, 80);
|
|
4471
|
-
} else if (type ===
|
|
4596
|
+
} else if (type === AnnotationType.NOTE) {
|
|
4472
4597
|
icon = ANNOTATION_ICONS.note;
|
|
4473
4598
|
typeLabel = "Note";
|
|
4474
4599
|
label = annotation.contents || "Empty note";
|
|
4475
4600
|
label = this._truncate(label, 80);
|
|
4476
|
-
} else if (type ===
|
|
4601
|
+
} else if (type === AnnotationType.INK) {
|
|
4477
4602
|
icon = ANNOTATION_ICONS.ink;
|
|
4478
4603
|
typeLabel = "Drawing";
|
|
4479
4604
|
label = "Ink drawing";
|
|
4480
|
-
} else if (type ===
|
|
4481
|
-
icon = ANNOTATION_ICONS.
|
|
4605
|
+
} else if (type === AnnotationType.UNDERLINE) {
|
|
4606
|
+
icon = ANNOTATION_ICONS.underline;
|
|
4482
4607
|
typeLabel = "Underline";
|
|
4483
4608
|
label = annotation.title || "Underlined text";
|
|
4484
4609
|
label = this._truncate(label, 80);
|
|
@@ -4778,6 +4903,17 @@ class FindController {
|
|
|
4778
4903
|
// If new matches were added, re-sort and fix current index
|
|
4779
4904
|
if (this.matches.length > matchCountBefore) {
|
|
4780
4905
|
this._sortMatchesAndFixIndex();
|
|
4906
|
+
|
|
4907
|
+
// The first search on a document runs before any text has been
|
|
4908
|
+
// extracted, so find() finds nothing and leaves no current match.
|
|
4909
|
+
// Select the first match as soon as one appears; otherwise the UI
|
|
4910
|
+
// sits at "0 of N" with nothing highlighted until the user presses
|
|
4911
|
+
// Next.
|
|
4912
|
+
if (this.currentMatchIndex === -1) {
|
|
4913
|
+
this.currentMatchIndex = 0;
|
|
4914
|
+
this.state = FindState.FOUND;
|
|
4915
|
+
this._scrollToMatch(0);
|
|
4916
|
+
}
|
|
4781
4917
|
}
|
|
4782
4918
|
|
|
4783
4919
|
this._updateHighlights(pageNum);
|
|
@@ -6692,7 +6828,7 @@ class HighlightTool extends TextSelectionTool {
|
|
|
6692
6828
|
rect: [minX, minY, maxX - minX, maxY - minY],
|
|
6693
6829
|
color: colorWithAlpha,
|
|
6694
6830
|
thickness: this.freehandThickness / scale,
|
|
6695
|
-
subject:
|
|
6831
|
+
subject: FREE_HIGHLIGHT_SUBJECT
|
|
6696
6832
|
});
|
|
6697
6833
|
}
|
|
6698
6834
|
|
|
@@ -6724,7 +6860,7 @@ class UnderlineTool extends TextSelectionTool {
|
|
|
6724
6860
|
|
|
6725
6861
|
async createAnnotationFromSelection(selectedText, pageNumber, quads, rect) {
|
|
6726
6862
|
await this.annotationManager.createAnnotation({
|
|
6727
|
-
annotation_type:
|
|
6863
|
+
annotation_type: AnnotationType.UNDERLINE,
|
|
6728
6864
|
page: pageNumber,
|
|
6729
6865
|
quads: quads,
|
|
6730
6866
|
rect: rect,
|
|
@@ -7628,6 +7764,11 @@ class PdfViewer {
|
|
|
7628
7764
|
}
|
|
7629
7765
|
|
|
7630
7766
|
_setupEventListeners() {
|
|
7767
|
+
// _initializeComponents() bails out when .pdf-pages-container is missing,
|
|
7768
|
+
// having logged what the host got wrong. Without this guard the
|
|
7769
|
+
// constructor then throws a TypeError here that buries that message.
|
|
7770
|
+
if (!this.pagesContainer) return
|
|
7771
|
+
|
|
7631
7772
|
const signal = this._abortController.signal;
|
|
7632
7773
|
|
|
7633
7774
|
// Handle visibility change for time tracking
|
|
@@ -7928,15 +8069,14 @@ class PdfViewer {
|
|
|
7928
8069
|
|
|
7929
8070
|
_onAnnotationEdit(annotation) {
|
|
7930
8071
|
// For notes, show the edit popup
|
|
7931
|
-
if (annotation.annotation_type ===
|
|
8072
|
+
if (annotation.annotation_type === AnnotationType.NOTE) {
|
|
7932
8073
|
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7933
8074
|
}
|
|
7934
8075
|
}
|
|
7935
8076
|
|
|
7936
8077
|
_onAnnotationComment(annotation) {
|
|
7937
8078
|
// For highlight/underline/ink, use the note tool's edit dialog to edit contents
|
|
7938
|
-
|
|
7939
|
-
if (supportsComment) {
|
|
8079
|
+
if (supportsComment(annotation)) {
|
|
7940
8080
|
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7941
8081
|
}
|
|
7942
8082
|
}
|
|
@@ -7956,15 +8096,15 @@ class PdfViewer {
|
|
|
7956
8096
|
|
|
7957
8097
|
/**
|
|
7958
8098
|
* Get human-readable label for annotation type.
|
|
7959
|
-
* @param {string} type -
|
|
8099
|
+
* @param {string} type - Canonical annotation type (see AnnotationType)
|
|
7960
8100
|
* @returns {string} Human-readable label
|
|
7961
8101
|
*/
|
|
7962
8102
|
_getAnnotationTypeLabel(type) {
|
|
7963
8103
|
switch (type) {
|
|
7964
|
-
case
|
|
7965
|
-
case
|
|
7966
|
-
case
|
|
7967
|
-
case
|
|
8104
|
+
case AnnotationType.HIGHLIGHT: return "Highlight"
|
|
8105
|
+
case AnnotationType.NOTE: return "Note"
|
|
8106
|
+
case AnnotationType.INK: return "Drawing"
|
|
8107
|
+
case AnnotationType.UNDERLINE: return "Underline"
|
|
7968
8108
|
default: return "Annotation"
|
|
7969
8109
|
}
|
|
7970
8110
|
}
|
|
@@ -8029,9 +8169,8 @@ class PdfViewer {
|
|
|
8029
8169
|
|
|
8030
8170
|
// Render each annotation using percentage-based positioning
|
|
8031
8171
|
for (const annotation of annotations) {
|
|
8032
|
-
const isHighlight = annotation
|
|
8033
|
-
|
|
8034
|
-
const isUnderline = annotation.annotation_type === "line";
|
|
8172
|
+
const isHighlight = isHighlightLike(annotation);
|
|
8173
|
+
const isUnderline = annotation.annotation_type === AnnotationType.UNDERLINE;
|
|
8035
8174
|
|
|
8036
8175
|
if (isHighlight) {
|
|
8037
8176
|
// Render colored SVG in the highlight layer (has mix-blend-mode for text visibility)
|
|
@@ -8121,7 +8260,7 @@ class PdfViewer {
|
|
|
8121
8260
|
// Render highlight as SVG in the blend layer (for mix-blend-mode to work)
|
|
8122
8261
|
// Uses unscaled PDF coordinates - SVG viewBox handles scaling
|
|
8123
8262
|
_renderHighlightSvg(annotation, svgLayer) {
|
|
8124
|
-
if (annotation.annotation_type ===
|
|
8263
|
+
if (annotation.annotation_type === AnnotationType.INK) {
|
|
8125
8264
|
this._renderFreehandHighlightSvg(annotation, svgLayer);
|
|
8126
8265
|
return
|
|
8127
8266
|
}
|
|
@@ -8270,7 +8409,7 @@ class PdfViewer {
|
|
|
8270
8409
|
container.className = "annotation annotation-highlight";
|
|
8271
8410
|
container.dataset.annotationId = annotation.id;
|
|
8272
8411
|
|
|
8273
|
-
if (annotation.annotation_type ===
|
|
8412
|
+
if (annotation.annotation_type === AnnotationType.INK) {
|
|
8274
8413
|
// Freehand highlight bounds
|
|
8275
8414
|
const strokes = annotation.ink_strokes || [];
|
|
8276
8415
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -8316,11 +8455,11 @@ class PdfViewer {
|
|
|
8316
8455
|
|
|
8317
8456
|
_createAnnotationElement(annotation, pageWidth, pageHeight) {
|
|
8318
8457
|
switch (annotation.annotation_type) {
|
|
8319
|
-
case
|
|
8458
|
+
case AnnotationType.HIGHLIGHT:
|
|
8320
8459
|
return this._createHighlightElement(annotation, pageWidth, pageHeight)
|
|
8321
|
-
case
|
|
8460
|
+
case AnnotationType.NOTE:
|
|
8322
8461
|
return this._createNoteElement(annotation, pageWidth, pageHeight)
|
|
8323
|
-
case
|
|
8462
|
+
case AnnotationType.INK:
|
|
8324
8463
|
return this._createInkElement(annotation, pageWidth, pageHeight)
|
|
8325
8464
|
default:
|
|
8326
8465
|
return null
|
|
@@ -8603,7 +8742,7 @@ class PdfViewer {
|
|
|
8603
8742
|
async _onAnnotationColorChange(annotation, color) {
|
|
8604
8743
|
try {
|
|
8605
8744
|
// Preserve the existing opacity when changing color (default to 0.4 for highlights/ink, 1 for others)
|
|
8606
|
-
const defaultOpacity = (annotation.annotation_type ===
|
|
8745
|
+
const defaultOpacity = (annotation.annotation_type === AnnotationType.HIGHLIGHT || annotation.annotation_type === AnnotationType.INK) ? 0.4 : 1;
|
|
8607
8746
|
const opacity = annotation.opacity ?? defaultOpacity;
|
|
8608
8747
|
|
|
8609
8748
|
// Encode opacity into color string as alpha channel (#RRGGBBAA)
|
|
@@ -1714,6 +1714,115 @@
|
|
|
1714
1714
|
}
|
|
1715
1715
|
}
|
|
1716
1716
|
|
|
1717
|
+
// Canonical annotation vocabulary.
|
|
1718
|
+
//
|
|
1719
|
+
// `annotation_type` travels over the REST API and is stored in the consuming
|
|
1720
|
+
// application's database, so inbound records may still carry legacy values that
|
|
1721
|
+
// this library no longer emits. Records are normalized once on the way in (see
|
|
1722
|
+
// AnnotationManager); everything downstream may assume the canonical names.
|
|
1723
|
+
//
|
|
1724
|
+
// These are the annotation types, not the editing modes -- see ToolMode in
|
|
1725
|
+
// index.js, which adds SELECT and otherwise uses the same strings.
|
|
1726
|
+
|
|
1727
|
+
const AnnotationType = {
|
|
1728
|
+
HIGHLIGHT: "highlight",
|
|
1729
|
+
UNDERLINE: "underline",
|
|
1730
|
+
NOTE: "note",
|
|
1731
|
+
INK: "ink"
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
// Legacy `annotation_type` values accepted on read and rewritten to canonical
|
|
1735
|
+
// names. "line" was the original spelling of underline; it is also actively
|
|
1736
|
+
// misleading, naming the PDF spec's Line annotation (subtype 4) -- a different
|
|
1737
|
+
// annotation that this viewer does not support.
|
|
1738
|
+
const LEGACY_TYPE_ALIASES = {
|
|
1739
|
+
line: AnnotationType.UNDERLINE
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// Marks an ink annotation drawn with the highlighter rather than the pen. This
|
|
1743
|
+
// lives on `subject` rather than being its own type because both are genuinely
|
|
1744
|
+
// Ink annotations once exported to PDF.
|
|
1745
|
+
const FREE_HIGHLIGHT_SUBJECT = "Free Highlight";
|
|
1746
|
+
|
|
1747
|
+
// PDF annotation subtype each canonical type is written as by DownloadManager
|
|
1748
|
+
// when baking annotations into the file. See PDF 32000-1:2008 section 12.5.6.
|
|
1749
|
+
const PDF_SUBTYPES = {
|
|
1750
|
+
[AnnotationType.HIGHLIGHT]: "Highlight",
|
|
1751
|
+
[AnnotationType.UNDERLINE]: "Underline",
|
|
1752
|
+
[AnnotationType.NOTE]: "Text",
|
|
1753
|
+
[AnnotationType.INK]: "Ink"
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
// Legacy values already warned about, so a document full of them produces one
|
|
1757
|
+
// warning per type rather than one per annotation.
|
|
1758
|
+
const warnedLegacyTypes = new Set();
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* Map a possibly-legacy annotation type onto the canonical vocabulary.
|
|
1762
|
+
*
|
|
1763
|
+
* Warns once per legacy value encountered. The aliases exist only to carry
|
|
1764
|
+
* records written by earlier versions, and are slated for removal; the warning
|
|
1765
|
+
* is how a consuming application finds out it still has rows to migrate while
|
|
1766
|
+
* that support is still in place.
|
|
1767
|
+
*
|
|
1768
|
+
* @param {string} type
|
|
1769
|
+
* @returns {string}
|
|
1770
|
+
*/
|
|
1771
|
+
function normalizeAnnotationType(type) {
|
|
1772
|
+
const canonical = LEGACY_TYPE_ALIASES[type];
|
|
1773
|
+
if (!canonical) return type
|
|
1774
|
+
|
|
1775
|
+
if (!warnedLegacyTypes.has(type)) {
|
|
1776
|
+
warnedLegacyTypes.add(type);
|
|
1777
|
+
console.warn(
|
|
1778
|
+
`[stimulus-pdf-viewer] Deprecated annotation_type "${type}" was read and ` +
|
|
1779
|
+
`treated as "${canonical}". Support for the old value will be removed in a ` +
|
|
1780
|
+
`future release. Migrate stored annotations: ` +
|
|
1781
|
+
`UPDATE annotations SET annotation_type = '${canonical}' WHERE annotation_type = '${type}'.`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
return canonical
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Rewrite a record's annotation_type in place. Mutates rather than clones so
|
|
1790
|
+
* that references already held elsewhere (selectedAnnotation, rendered sidebar
|
|
1791
|
+
* rows) observe the same canonical value.
|
|
1792
|
+
* @param {Object} annotation
|
|
1793
|
+
* @returns {Object} the same annotation
|
|
1794
|
+
*/
|
|
1795
|
+
function normalizeAnnotation(annotation) {
|
|
1796
|
+
if (annotation && annotation.annotation_type) {
|
|
1797
|
+
annotation.annotation_type = normalizeAnnotationType(annotation.annotation_type);
|
|
1798
|
+
}
|
|
1799
|
+
return annotation
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** An ink annotation drawn with the highlighter tool. */
|
|
1803
|
+
function isFreeHighlight(annotation) {
|
|
1804
|
+
return annotation?.annotation_type === AnnotationType.INK &&
|
|
1805
|
+
annotation?.subject === FREE_HIGHLIGHT_SUBJECT
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/** Highlights and highlighter-drawn ink both render as highlights. */
|
|
1809
|
+
function isHighlightLike(annotation) {
|
|
1810
|
+
return annotation?.annotation_type === AnnotationType.HIGHLIGHT ||
|
|
1811
|
+
isFreeHighlight(annotation)
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** Ink drawn with the pen, as opposed to the highlighter. */
|
|
1815
|
+
function isDrawing(annotation) {
|
|
1816
|
+
return annotation?.annotation_type === AnnotationType.INK &&
|
|
1817
|
+
!isFreeHighlight(annotation)
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/** Types whose contents are editable as a comment via the note dialog. */
|
|
1821
|
+
function supportsComment(annotation) {
|
|
1822
|
+
return [AnnotationType.HIGHLIGHT, AnnotationType.UNDERLINE, AnnotationType.INK]
|
|
1823
|
+
.includes(annotation?.annotation_type)
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1717
1826
|
// Custom event types for error handling
|
|
1718
1827
|
const AnnotationErrorType = {
|
|
1719
1828
|
LOAD_FAILED: "load_failed",
|
|
@@ -1787,6 +1896,7 @@
|
|
|
1787
1896
|
this.annotationsByPage.clear();
|
|
1788
1897
|
|
|
1789
1898
|
for (const annotation of annotationsData) {
|
|
1899
|
+
normalizeAnnotation(annotation);
|
|
1790
1900
|
this.annotations.set(this._key(annotation.id), annotation);
|
|
1791
1901
|
|
|
1792
1902
|
if (!this.annotationsByPage.has(annotation.page)) {
|
|
@@ -1888,6 +1998,8 @@
|
|
|
1888
1998
|
}
|
|
1889
1999
|
|
|
1890
2000
|
_addAnnotation(annotation) {
|
|
2001
|
+
normalizeAnnotation(annotation);
|
|
2002
|
+
|
|
1891
2003
|
this.annotations.set(this._key(annotation.id), annotation);
|
|
1892
2004
|
|
|
1893
2005
|
if (!this.annotationsByPage.has(annotation.page)) {
|
|
@@ -1897,6 +2009,8 @@
|
|
|
1897
2009
|
}
|
|
1898
2010
|
|
|
1899
2011
|
_updateAnnotation(annotation) {
|
|
2012
|
+
normalizeAnnotation(annotation);
|
|
2013
|
+
|
|
1900
2014
|
const oldAnnotation = this.getAnnotation(annotation.id);
|
|
1901
2015
|
if (!oldAnnotation) {
|
|
1902
2016
|
this._addAnnotation(annotation);
|
|
@@ -2085,16 +2199,16 @@
|
|
|
2085
2199
|
_applyAnnotationsToPage(pdfDoc, page, annotations, pageHeight) {
|
|
2086
2200
|
for (const annotation of annotations) {
|
|
2087
2201
|
switch (annotation.annotation_type) {
|
|
2088
|
-
case
|
|
2202
|
+
case AnnotationType.HIGHLIGHT:
|
|
2089
2203
|
this._applyHighlight(pdfDoc, page, annotation, pageHeight);
|
|
2090
2204
|
break
|
|
2091
|
-
case
|
|
2205
|
+
case AnnotationType.UNDERLINE:
|
|
2092
2206
|
this._applyUnderline(pdfDoc, page, annotation, pageHeight);
|
|
2093
2207
|
break
|
|
2094
|
-
case
|
|
2208
|
+
case AnnotationType.INK:
|
|
2095
2209
|
this._applyInk(pdfDoc, page, annotation, pageHeight);
|
|
2096
2210
|
break
|
|
2097
|
-
case
|
|
2211
|
+
case AnnotationType.NOTE:
|
|
2098
2212
|
this._applyNote(pdfDoc, page, annotation, pageHeight);
|
|
2099
2213
|
break
|
|
2100
2214
|
}
|
|
@@ -2133,7 +2247,7 @@
|
|
|
2133
2247
|
|
|
2134
2248
|
const annotationDict = pdfDoc.context.obj({
|
|
2135
2249
|
Type: pdfLib.PDFName.of("Annot"),
|
|
2136
|
-
Subtype: pdfLib.PDFName.of(
|
|
2250
|
+
Subtype: pdfLib.PDFName.of(PDF_SUBTYPES[AnnotationType.HIGHLIGHT]),
|
|
2137
2251
|
Rect: [minX, minY, maxX, maxY],
|
|
2138
2252
|
QuadPoints: quadPoints,
|
|
2139
2253
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2174,7 +2288,7 @@
|
|
|
2174
2288
|
|
|
2175
2289
|
const annotationDict = pdfDoc.context.obj({
|
|
2176
2290
|
Type: pdfLib.PDFName.of("Annot"),
|
|
2177
|
-
Subtype: pdfLib.PDFName.of(
|
|
2291
|
+
Subtype: pdfLib.PDFName.of(PDF_SUBTYPES[AnnotationType.UNDERLINE]),
|
|
2178
2292
|
Rect: [minX, minY, maxX, maxY],
|
|
2179
2293
|
QuadPoints: quadPoints,
|
|
2180
2294
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2187,7 +2301,7 @@
|
|
|
2187
2301
|
|
|
2188
2302
|
_applyInk(pdfDoc, page, annotation, pageHeight) {
|
|
2189
2303
|
// Freehand highlights need different rendering (thick, semi-transparent strokes)
|
|
2190
|
-
if (annotation
|
|
2304
|
+
if (isFreeHighlight(annotation)) {
|
|
2191
2305
|
this._applyFreehandHighlight(pdfDoc, page, annotation, pageHeight);
|
|
2192
2306
|
return
|
|
2193
2307
|
}
|
|
@@ -2259,7 +2373,7 @@
|
|
|
2259
2373
|
|
|
2260
2374
|
const annotationDict = pdfDoc.context.obj({
|
|
2261
2375
|
Type: pdfLib.PDFName.of("Annot"),
|
|
2262
|
-
Subtype: pdfLib.PDFName.of(
|
|
2376
|
+
Subtype: pdfLib.PDFName.of(PDF_SUBTYPES[AnnotationType.INK]),
|
|
2263
2377
|
Rect: [minX, minY, maxX, maxY],
|
|
2264
2378
|
InkList: inkList,
|
|
2265
2379
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2355,7 +2469,7 @@
|
|
|
2355
2469
|
|
|
2356
2470
|
const annotationDict = pdfDoc.context.obj({
|
|
2357
2471
|
Type: pdfLib.PDFName.of("Annot"),
|
|
2358
|
-
Subtype: pdfLib.PDFName.of(
|
|
2472
|
+
Subtype: pdfLib.PDFName.of(PDF_SUBTYPES[AnnotationType.INK]),
|
|
2359
2473
|
Rect: [minX, minY, maxX, maxY],
|
|
2360
2474
|
InkList: inkList,
|
|
2361
2475
|
C: [rgba.r, rgba.g, rgba.b],
|
|
@@ -2381,9 +2495,8 @@
|
|
|
2381
2495
|
|
|
2382
2496
|
const annotationDict = pdfDoc.context.obj({
|
|
2383
2497
|
Type: pdfLib.PDFName.of("Annot"),
|
|
2384
|
-
Subtype: pdfLib.PDFName.of(
|
|
2498
|
+
Subtype: pdfLib.PDFName.of(PDF_SUBTYPES[AnnotationType.NOTE]),
|
|
2385
2499
|
Rect: [x, pdfY - iconSize, x + iconSize, pdfY],
|
|
2386
|
-
Contents: pdfLib.PDFString.of(contents),
|
|
2387
2500
|
C: [rgba.r, rgba.g, rgba.b],
|
|
2388
2501
|
Name: pdfLib.PDFName.of("Comment"),
|
|
2389
2502
|
Open: false,
|
|
@@ -2447,6 +2560,13 @@
|
|
|
2447
2560
|
metadata.T = pdfLib.PDFString.of(this.userName);
|
|
2448
2561
|
}
|
|
2449
2562
|
|
|
2563
|
+
// Contents - a note's body, or the comment on any other annotation type.
|
|
2564
|
+
// fromText encodes as UTF-16BE when needed; PDFString.of only covers
|
|
2565
|
+
// PDFDocEncoding and mangles curly quotes, dashes, and the like.
|
|
2566
|
+
if (annotation.contents) {
|
|
2567
|
+
metadata.Contents = pdfLib.PDFHexString.fromText(annotation.contents);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2450
2570
|
// Modification date (M) - use annotation's updated_at or created_at
|
|
2451
2571
|
const dateStr = annotation.updated_at || annotation.created_at;
|
|
2452
2572
|
if (dateStr) {
|
|
@@ -2507,12 +2627,17 @@
|
|
|
2507
2627
|
.replace(/\s+/g, " ") // Normalize whitespace
|
|
2508
2628
|
.trim();
|
|
2509
2629
|
|
|
2630
|
+
// Check for emptiness before appending the extension. A name made only of
|
|
2631
|
+
// illegal characters is empty at this point, and appending first would
|
|
2632
|
+
// produce a hidden ".pdf" file instead of falling back.
|
|
2633
|
+
if (!sanitized) return "document.pdf"
|
|
2634
|
+
|
|
2510
2635
|
// Ensure it ends with .pdf
|
|
2511
2636
|
if (!sanitized.toLowerCase().endsWith(".pdf")) {
|
|
2512
2637
|
sanitized += ".pdf";
|
|
2513
2638
|
}
|
|
2514
2639
|
|
|
2515
|
-
return sanitized
|
|
2640
|
+
return sanitized
|
|
2516
2641
|
}
|
|
2517
2642
|
|
|
2518
2643
|
_triggerDownload(bytes, filename) {
|
|
@@ -2842,8 +2967,8 @@
|
|
|
2842
2967
|
}
|
|
2843
2968
|
} else if (e.key === "c" || e.key === "C") {
|
|
2844
2969
|
// Comment shortcut for highlight/underline/ink annotations
|
|
2845
|
-
const
|
|
2846
|
-
if (
|
|
2970
|
+
const canComment = supportsComment(this.currentAnnotation);
|
|
2971
|
+
if (canComment && this.onComment) {
|
|
2847
2972
|
e.preventDefault();
|
|
2848
2973
|
this.onComment(this.currentAnnotation);
|
|
2849
2974
|
}
|
|
@@ -2905,14 +3030,14 @@
|
|
|
2905
3030
|
|
|
2906
3031
|
// Show/hide buttons based on annotation type
|
|
2907
3032
|
const isNote = annotation.annotation_type === "note";
|
|
2908
|
-
const
|
|
3033
|
+
const canComment = supportsComment(annotation);
|
|
2909
3034
|
|
|
2910
3035
|
// Comment button for highlight/underline/ink, edit button for notes
|
|
2911
|
-
this.commentBtn.classList.toggle("hidden", !
|
|
3036
|
+
this.commentBtn.classList.toggle("hidden", !canComment);
|
|
2912
3037
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
2913
3038
|
|
|
2914
3039
|
// Update comment button title based on whether contents exists
|
|
2915
|
-
if (
|
|
3040
|
+
if (canComment) {
|
|
2916
3041
|
const hasComment = annotation.contents && annotation.contents.trim();
|
|
2917
3042
|
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
2918
3043
|
}
|
|
@@ -3118,13 +3243,13 @@
|
|
|
3118
3243
|
this.onDelete(this.currentAnnotation);
|
|
3119
3244
|
}
|
|
3120
3245
|
} else if (e.key === "e" || e.key === "E") {
|
|
3121
|
-
if (this.currentAnnotation?.annotation_type ===
|
|
3246
|
+
if (this.currentAnnotation?.annotation_type === AnnotationType.NOTE && this.onEdit) {
|
|
3122
3247
|
e.preventDefault();
|
|
3123
3248
|
this.onEdit(this.currentAnnotation);
|
|
3124
3249
|
}
|
|
3125
3250
|
} else if (e.key === "c" || e.key === "C") {
|
|
3126
|
-
const
|
|
3127
|
-
if (
|
|
3251
|
+
const canComment = supportsComment(this.currentAnnotation);
|
|
3252
|
+
if (canComment && this.onComment) {
|
|
3128
3253
|
e.preventDefault();
|
|
3129
3254
|
this.onComment(this.currentAnnotation);
|
|
3130
3255
|
}
|
|
@@ -3176,10 +3301,10 @@
|
|
|
3176
3301
|
|
|
3177
3302
|
_getTypeLabel(annotationType) {
|
|
3178
3303
|
const labels = {
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3304
|
+
[AnnotationType.HIGHLIGHT]: "Highlight",
|
|
3305
|
+
[AnnotationType.UNDERLINE]: "Underline",
|
|
3306
|
+
[AnnotationType.NOTE]: "Note",
|
|
3307
|
+
[AnnotationType.INK]: "Drawing"
|
|
3183
3308
|
};
|
|
3184
3309
|
return labels[annotationType] || "Annotation"
|
|
3185
3310
|
}
|
|
@@ -3208,11 +3333,11 @@
|
|
|
3208
3333
|
|
|
3209
3334
|
// Show/hide buttons based on annotation type
|
|
3210
3335
|
const isNote = annotation.annotation_type === "note";
|
|
3211
|
-
const
|
|
3212
|
-
this.commentBtn.classList.toggle("hidden", !
|
|
3336
|
+
const canComment = supportsComment(annotation);
|
|
3337
|
+
this.commentBtn.classList.toggle("hidden", !canComment);
|
|
3213
3338
|
this.editBtn.classList.toggle("hidden", !isNote);
|
|
3214
3339
|
|
|
3215
|
-
if (
|
|
3340
|
+
if (canComment) {
|
|
3216
3341
|
const hasComment = annotation.contents && annotation.contents.trim();
|
|
3217
3342
|
this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
|
|
3218
3343
|
}
|
|
@@ -4034,7 +4159,7 @@
|
|
|
4034
4159
|
ink: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4035
4160
|
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>
|
|
4036
4161
|
</svg>`,
|
|
4037
|
-
|
|
4162
|
+
underline: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4038
4163
|
<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/>
|
|
4039
4164
|
<line x1="4" y1="21" x2="20" y2="21" stroke-width="3"/>
|
|
4040
4165
|
</svg>`
|
|
@@ -4321,13 +4446,13 @@
|
|
|
4321
4446
|
|
|
4322
4447
|
switch (this.filterType) {
|
|
4323
4448
|
case FilterType.HIGHLIGHT:
|
|
4324
|
-
return
|
|
4449
|
+
return isHighlightLike(annotation)
|
|
4325
4450
|
case FilterType.NOTE:
|
|
4326
|
-
return type ===
|
|
4451
|
+
return type === AnnotationType.NOTE
|
|
4327
4452
|
case FilterType.DRAWING:
|
|
4328
|
-
return
|
|
4453
|
+
return isDrawing(annotation)
|
|
4329
4454
|
case FilterType.UNDERLINE:
|
|
4330
|
-
return type ===
|
|
4455
|
+
return type === AnnotationType.UNDERLINE
|
|
4331
4456
|
default:
|
|
4332
4457
|
return true
|
|
4333
4458
|
}
|
|
@@ -4482,23 +4607,23 @@
|
|
|
4482
4607
|
const type = annotation.annotation_type;
|
|
4483
4608
|
let icon, label, typeLabel;
|
|
4484
4609
|
|
|
4485
|
-
if (
|
|
4610
|
+
if (isHighlightLike(annotation)) {
|
|
4486
4611
|
icon = ANNOTATION_ICONS.highlight;
|
|
4487
4612
|
typeLabel = "Highlight";
|
|
4488
4613
|
// Extract highlighted text if available
|
|
4489
4614
|
label = annotation.title || annotation.contents || "Freehand Highlight";
|
|
4490
4615
|
label = this._truncate(label, 80);
|
|
4491
|
-
} else if (type ===
|
|
4616
|
+
} else if (type === AnnotationType.NOTE) {
|
|
4492
4617
|
icon = ANNOTATION_ICONS.note;
|
|
4493
4618
|
typeLabel = "Note";
|
|
4494
4619
|
label = annotation.contents || "Empty note";
|
|
4495
4620
|
label = this._truncate(label, 80);
|
|
4496
|
-
} else if (type ===
|
|
4621
|
+
} else if (type === AnnotationType.INK) {
|
|
4497
4622
|
icon = ANNOTATION_ICONS.ink;
|
|
4498
4623
|
typeLabel = "Drawing";
|
|
4499
4624
|
label = "Ink drawing";
|
|
4500
|
-
} else if (type ===
|
|
4501
|
-
icon = ANNOTATION_ICONS.
|
|
4625
|
+
} else if (type === AnnotationType.UNDERLINE) {
|
|
4626
|
+
icon = ANNOTATION_ICONS.underline;
|
|
4502
4627
|
typeLabel = "Underline";
|
|
4503
4628
|
label = annotation.title || "Underlined text";
|
|
4504
4629
|
label = this._truncate(label, 80);
|
|
@@ -4798,6 +4923,17 @@
|
|
|
4798
4923
|
// If new matches were added, re-sort and fix current index
|
|
4799
4924
|
if (this.matches.length > matchCountBefore) {
|
|
4800
4925
|
this._sortMatchesAndFixIndex();
|
|
4926
|
+
|
|
4927
|
+
// The first search on a document runs before any text has been
|
|
4928
|
+
// extracted, so find() finds nothing and leaves no current match.
|
|
4929
|
+
// Select the first match as soon as one appears; otherwise the UI
|
|
4930
|
+
// sits at "0 of N" with nothing highlighted until the user presses
|
|
4931
|
+
// Next.
|
|
4932
|
+
if (this.currentMatchIndex === -1) {
|
|
4933
|
+
this.currentMatchIndex = 0;
|
|
4934
|
+
this.state = FindState.FOUND;
|
|
4935
|
+
this._scrollToMatch(0);
|
|
4936
|
+
}
|
|
4801
4937
|
}
|
|
4802
4938
|
|
|
4803
4939
|
this._updateHighlights(pageNum);
|
|
@@ -6712,7 +6848,7 @@
|
|
|
6712
6848
|
rect: [minX, minY, maxX - minX, maxY - minY],
|
|
6713
6849
|
color: colorWithAlpha,
|
|
6714
6850
|
thickness: this.freehandThickness / scale,
|
|
6715
|
-
subject:
|
|
6851
|
+
subject: FREE_HIGHLIGHT_SUBJECT
|
|
6716
6852
|
});
|
|
6717
6853
|
}
|
|
6718
6854
|
|
|
@@ -6744,7 +6880,7 @@
|
|
|
6744
6880
|
|
|
6745
6881
|
async createAnnotationFromSelection(selectedText, pageNumber, quads, rect) {
|
|
6746
6882
|
await this.annotationManager.createAnnotation({
|
|
6747
|
-
annotation_type:
|
|
6883
|
+
annotation_type: AnnotationType.UNDERLINE,
|
|
6748
6884
|
page: pageNumber,
|
|
6749
6885
|
quads: quads,
|
|
6750
6886
|
rect: rect,
|
|
@@ -7648,6 +7784,11 @@
|
|
|
7648
7784
|
}
|
|
7649
7785
|
|
|
7650
7786
|
_setupEventListeners() {
|
|
7787
|
+
// _initializeComponents() bails out when .pdf-pages-container is missing,
|
|
7788
|
+
// having logged what the host got wrong. Without this guard the
|
|
7789
|
+
// constructor then throws a TypeError here that buries that message.
|
|
7790
|
+
if (!this.pagesContainer) return
|
|
7791
|
+
|
|
7651
7792
|
const signal = this._abortController.signal;
|
|
7652
7793
|
|
|
7653
7794
|
// Handle visibility change for time tracking
|
|
@@ -7948,15 +8089,14 @@
|
|
|
7948
8089
|
|
|
7949
8090
|
_onAnnotationEdit(annotation) {
|
|
7950
8091
|
// For notes, show the edit popup
|
|
7951
|
-
if (annotation.annotation_type ===
|
|
8092
|
+
if (annotation.annotation_type === AnnotationType.NOTE) {
|
|
7952
8093
|
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7953
8094
|
}
|
|
7954
8095
|
}
|
|
7955
8096
|
|
|
7956
8097
|
_onAnnotationComment(annotation) {
|
|
7957
8098
|
// For highlight/underline/ink, use the note tool's edit dialog to edit contents
|
|
7958
|
-
|
|
7959
|
-
if (supportsComment) {
|
|
8099
|
+
if (supportsComment(annotation)) {
|
|
7960
8100
|
this.tools[ToolMode.NOTE].editNote(annotation);
|
|
7961
8101
|
}
|
|
7962
8102
|
}
|
|
@@ -7976,15 +8116,15 @@
|
|
|
7976
8116
|
|
|
7977
8117
|
/**
|
|
7978
8118
|
* Get human-readable label for annotation type.
|
|
7979
|
-
* @param {string} type -
|
|
8119
|
+
* @param {string} type - Canonical annotation type (see AnnotationType)
|
|
7980
8120
|
* @returns {string} Human-readable label
|
|
7981
8121
|
*/
|
|
7982
8122
|
_getAnnotationTypeLabel(type) {
|
|
7983
8123
|
switch (type) {
|
|
7984
|
-
case
|
|
7985
|
-
case
|
|
7986
|
-
case
|
|
7987
|
-
case
|
|
8124
|
+
case AnnotationType.HIGHLIGHT: return "Highlight"
|
|
8125
|
+
case AnnotationType.NOTE: return "Note"
|
|
8126
|
+
case AnnotationType.INK: return "Drawing"
|
|
8127
|
+
case AnnotationType.UNDERLINE: return "Underline"
|
|
7988
8128
|
default: return "Annotation"
|
|
7989
8129
|
}
|
|
7990
8130
|
}
|
|
@@ -8049,9 +8189,8 @@
|
|
|
8049
8189
|
|
|
8050
8190
|
// Render each annotation using percentage-based positioning
|
|
8051
8191
|
for (const annotation of annotations) {
|
|
8052
|
-
const isHighlight = annotation
|
|
8053
|
-
|
|
8054
|
-
const isUnderline = annotation.annotation_type === "line";
|
|
8192
|
+
const isHighlight = isHighlightLike(annotation);
|
|
8193
|
+
const isUnderline = annotation.annotation_type === AnnotationType.UNDERLINE;
|
|
8055
8194
|
|
|
8056
8195
|
if (isHighlight) {
|
|
8057
8196
|
// Render colored SVG in the highlight layer (has mix-blend-mode for text visibility)
|
|
@@ -8141,7 +8280,7 @@
|
|
|
8141
8280
|
// Render highlight as SVG in the blend layer (for mix-blend-mode to work)
|
|
8142
8281
|
// Uses unscaled PDF coordinates - SVG viewBox handles scaling
|
|
8143
8282
|
_renderHighlightSvg(annotation, svgLayer) {
|
|
8144
|
-
if (annotation.annotation_type ===
|
|
8283
|
+
if (annotation.annotation_type === AnnotationType.INK) {
|
|
8145
8284
|
this._renderFreehandHighlightSvg(annotation, svgLayer);
|
|
8146
8285
|
return
|
|
8147
8286
|
}
|
|
@@ -8290,7 +8429,7 @@
|
|
|
8290
8429
|
container.className = "annotation annotation-highlight";
|
|
8291
8430
|
container.dataset.annotationId = annotation.id;
|
|
8292
8431
|
|
|
8293
|
-
if (annotation.annotation_type ===
|
|
8432
|
+
if (annotation.annotation_type === AnnotationType.INK) {
|
|
8294
8433
|
// Freehand highlight bounds
|
|
8295
8434
|
const strokes = annotation.ink_strokes || [];
|
|
8296
8435
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -8336,11 +8475,11 @@
|
|
|
8336
8475
|
|
|
8337
8476
|
_createAnnotationElement(annotation, pageWidth, pageHeight) {
|
|
8338
8477
|
switch (annotation.annotation_type) {
|
|
8339
|
-
case
|
|
8478
|
+
case AnnotationType.HIGHLIGHT:
|
|
8340
8479
|
return this._createHighlightElement(annotation, pageWidth, pageHeight)
|
|
8341
|
-
case
|
|
8480
|
+
case AnnotationType.NOTE:
|
|
8342
8481
|
return this._createNoteElement(annotation, pageWidth, pageHeight)
|
|
8343
|
-
case
|
|
8482
|
+
case AnnotationType.INK:
|
|
8344
8483
|
return this._createInkElement(annotation, pageWidth, pageHeight)
|
|
8345
8484
|
default:
|
|
8346
8485
|
return null
|
|
@@ -8623,7 +8762,7 @@
|
|
|
8623
8762
|
async _onAnnotationColorChange(annotation, color) {
|
|
8624
8763
|
try {
|
|
8625
8764
|
// Preserve the existing opacity when changing color (default to 0.4 for highlights/ink, 1 for others)
|
|
8626
|
-
const defaultOpacity = (annotation.annotation_type ===
|
|
8765
|
+
const defaultOpacity = (annotation.annotation_type === AnnotationType.HIGHLIGHT || annotation.annotation_type === AnnotationType.INK) ? 0.4 : 1;
|
|
8627
8766
|
const opacity = annotation.opacity ?? defaultOpacity;
|
|
8628
8767
|
|
|
8629
8768
|
// Encode opacity into color string as alpha channel (#RRGGBBAA)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stimulus-pdf-viewer-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Baker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|