@anaralabs/lector 3.12.8 → 3.13.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.
@@ -132,6 +132,23 @@ declare interface AnnotationTooltipProps {
132
132
  }) => React.ReactNode;
133
133
  }
134
134
 
135
+ /**
136
+ * Recolors a 2D context at draw time: every fill/stroke whose style is a
137
+ * string is painted with `map(style)` and the original style is restored
138
+ * right after, so pdf.js readbacks (`ctx.fillStyle`, `copyCtxState`,
139
+ * save/restore) always observe original-document colors and nothing is ever
140
+ * mapped twice. Gradients are recolored per stop at creation. `drawImage`
141
+ * and `putImageData` are deliberately untouched — photos keep their pixels —
142
+ * with one exception: a drawImage that composes a luminosity soft mask
143
+ * (destination-in through a pdf.js `*_luminosity_map_*` filter) draws a
144
+ * luma-corrected copy of the mask so the recoloring doesn't invert the
145
+ * mask's alpha.
146
+ *
147
+ * Wrapping installs own properties on the context instance (the prototype is
148
+ * never modified). Returns a cleanup that restores the pristine context.
149
+ */
150
+ export declare function applyContextRecolor(ctx: CanvasRenderingContext2D, map: RenderColorMap): () => void;
151
+
135
152
  /**
136
153
  * Calculates the highlight rectangles for a given text match.
137
154
  *
@@ -151,6 +168,11 @@ export declare const cancellable: <T extends Promise<unknown>>(promise: T) => {
151
168
  cancel: () => void;
152
169
  };
153
170
 
171
+ declare interface CanvasAndContext {
172
+ canvas: HTMLCanvasElement | null;
173
+ context: CanvasRenderingContext2D | null;
174
+ }
175
+
154
176
  export declare const CanvasLayer: MemoExoticComponent<({ style, background, ...props }: HTMLProps<HTMLCanvasElement> & {
155
177
  background?: string;
156
178
  }) => JSX.Element>;
@@ -182,6 +204,49 @@ declare type ColoredHighlightLayerProps = {
182
204
  onHighlight?: (highlight: ColoredHighlight) => void;
183
205
  };
184
206
 
207
+ declare type ColorScheme = "light" | "dark";
208
+ export { ColorScheme }
209
+ export { ColorScheme as ColorScheme_alias_1 }
210
+
211
+ /**
212
+ * Builds a memoized color map that flips perceived lightness onto the
213
+ * background<->foreground ramp while preserving hue and chroma (OKLab).
214
+ * Position on the ramp follows gamma-encoded BT.709 luma — the same measure
215
+ * "luminance inversion" night modes use — so saturated accents move the way
216
+ * readers expect (red becomes pink, not cyan; blue links become light blue).
217
+ * Neutrals land exactly on the ramp: white maps to `background`, black to
218
+ * `foreground`, including the palette's tint.
219
+ */
220
+ declare function createDarkModeColorMap(colors?: DarkModeColors): RenderColorMap;
221
+ export { createDarkModeColorMap }
222
+ export { createDarkModeColorMap as createDarkModeColorMap_alias_1 }
223
+
224
+ /**
225
+ * pdf.js paints transparency groups, soft masks, tiling/shading patterns and
226
+ * image-mask fills on internal scratch canvases obtained from the document's
227
+ * CanvasFactory — that content never touches the context we hand to
228
+ * `page.render()`. Passing this factory via getDocument's `CanvasFactory`
229
+ * option extends dark-mode recoloring to those canvases, which is what makes
230
+ * the scheme complete (pages with a page-level transparency group otherwise
231
+ * render entirely on a scratch canvas).
232
+ *
233
+ * The map is captured per canvas at creation time from `mapRef`; scratch
234
+ * canvases only live for a single render task, so toggling the scheme simply
235
+ * takes effect on the next render.
236
+ */
237
+ export declare function createRecolorCanvasFactory(mapRef: RenderColorMapRef): {
238
+ new ({ ownerDocument, enableHWA, }?: {
239
+ ownerDocument?: Document;
240
+ enableHWA?: boolean;
241
+ }): {
242
+ "__#44@#document": Document;
243
+ "__#44@#enableHWA": boolean;
244
+ create(width: number, height: number): CanvasAndContext;
245
+ reset(canvasAndContext: CanvasAndContext, width: number, height: number): void;
246
+ destroy(canvasAndContext: CanvasAndContext): void;
247
+ };
248
+ };
249
+
185
250
  export declare const CurrentPage: ({ ...props }: HTMLProps<HTMLInputElement>) => JSX.Element;
186
251
 
187
252
  export declare const CurrentZoom: ({ ...props }: HTMLProps<HTMLInputElement>) => JSX.Element;
@@ -190,6 +255,19 @@ export declare const CustomLayer: MemoExoticComponent<({ children }: {
190
255
  children: (pageNumber: number) => JSX_2.Element;
191
256
  }) => JSX_2.Element>;
192
257
 
258
+ declare interface DarkModeColors {
259
+ /** Replaces the white paper background. Any CSS hex/rgb color. */
260
+ background?: string;
261
+ /** Replaces black text and line art. Any CSS hex/rgb color. */
262
+ foreground?: string;
263
+ }
264
+ export { DarkModeColors }
265
+ export { DarkModeColors as DarkModeColors_alias_1 }
266
+
267
+ declare const DEFAULT_DARK_MODE_COLORS: Required<DarkModeColors>;
268
+ export { DEFAULT_DARK_MODE_COLORS }
269
+ export { DEFAULT_DARK_MODE_COLORS as DEFAULT_DARK_MODE_COLORS_alias_1 }
270
+
193
271
  export declare const ensureAnnotationLayerStyles: () => void;
194
272
 
195
273
  export declare const firstMemo: <T>(first: boolean, memo: unknown, initializer: () => T) => T;
@@ -238,6 +316,9 @@ export declare interface InitialPDFState {
238
316
  zoom: number;
239
317
  isZoomFitWidth?: boolean;
240
318
  zoomOptions?: ZoomOptions;
319
+ colorScheme?: ColorScheme;
320
+ darkModeColors?: DarkModeColors;
321
+ renderColorMapRef?: RenderColorMapRef;
241
322
  }
242
323
 
243
324
  export declare class LinkService {
@@ -349,6 +430,14 @@ declare interface PDFState {
349
430
  renderedPages: Record<number, true>;
350
431
  markPageRendered: (pageNumber: number) => void;
351
432
  unmarkPageRendered: (pageNumber: number) => void;
433
+ colorScheme: ColorScheme;
434
+ darkModeColors: Required<DarkModeColors>;
435
+ setColorScheme: (colorScheme: ColorScheme, colors?: DarkModeColors) => void;
436
+ /**
437
+ * Shared with the document's pdf.js CanvasFactory so internal scratch
438
+ * canvases (transparency groups, soft masks, patterns) follow the scheme.
439
+ */
440
+ renderColorMapRef: RenderColorMapRef;
352
441
  }
353
442
 
354
443
  export declare const PDFStore: {
@@ -364,6 +453,27 @@ export declare type PDFVirtualizer = Virtualizer<any, any>;
364
453
 
365
454
  export declare const PreviousPage: () => void;
366
455
 
456
+ /**
457
+ * Removes any recolor wrapper installed on the context. For long-lived
458
+ * contexts (the visible canvas in the no-buffer fallback, thumbnails) that
459
+ * are about to render in the light scheme: a still-pending dark render's
460
+ * deferred restore could otherwise leave its wrapper active across the
461
+ * scheme switch.
462
+ */
463
+ export declare function removeContextRecolor(ctx: CanvasRenderingContext2D): void;
464
+
465
+ /**
466
+ * Maps a color from the original document to its dark-scheme equivalent.
467
+ * Unparseable and fully-transparent colors are returned unchanged.
468
+ */
469
+ declare type RenderColorMap = (color: string) => string;
470
+ export { RenderColorMap }
471
+ export { RenderColorMap as RenderColorMap_alias_1 }
472
+
473
+ export declare interface RenderColorMapRef {
474
+ current: RenderColorMap | null;
475
+ }
476
+
367
477
  export declare const Root: ForwardRefExoticComponent<Omit<Omit<HTMLProps<HTMLDivElement>, "onError"> & usePDFDocumentParams & {
368
478
  loader?: ReactNode;
369
479
  }, "ref"> & RefAttributes<unknown>>;
@@ -462,7 +572,7 @@ declare const usePdf: <T>(selector: (state: PDFState) => T) => T;
462
572
  export { usePdf }
463
573
  export { usePdf as usePdf_alias_1 }
464
574
 
465
- export declare const usePDFDocumentContext: ({ onDocumentLoad, onError, source, initialRotation, isZoomFitWidth, zoom, zoomOptions, documentOptions, }: usePDFDocumentParams) => {
575
+ export declare const usePDFDocumentContext: ({ onDocumentLoad, onError, source, initialRotation, isZoomFitWidth, zoom, zoomOptions, documentOptions, colorScheme, darkModeColors, }: usePDFDocumentParams) => {
466
576
  initialState: InitialPDFState | null | undefined;
467
577
  };
468
578
 
@@ -501,6 +611,19 @@ export declare interface usePDFDocumentParams {
501
611
  * Must be a stable reference (module-level constant or useMemo) to avoid reloading the document.
502
612
  */
503
613
  documentOptions?: Partial<DocumentInitParameters>;
614
+ /**
615
+ * Initial color scheme for page rendering. "dark" recolors the document at
616
+ * render time (native dark mode): perceived lightness is flipped onto the
617
+ * darkModeColors ramp while hue is preserved, and images keep their
618
+ * original pixels. Changing the prop after mount updates the scheme; the
619
+ * runtime equivalent is `usePdf((s) => s.setColorScheme)`.
620
+ */
621
+ colorScheme?: ColorScheme;
622
+ /**
623
+ * Palette for the dark scheme: `background` replaces white paper,
624
+ * `foreground` replaces black text. Defaults to #141210 / #eae6e0.
625
+ */
626
+ darkModeColors?: DarkModeColors;
504
627
  }
505
628
 
506
629
  export declare const usePdfJump: () => {
package/dist/index.d.ts CHANGED
@@ -42,3 +42,8 @@ export { useSelectionDimensions } from './_tsup-dts-rollup.js';
42
42
  export { ColoredHighlight } from './_tsup-dts-rollup.js';
43
43
  export { HighlightRect } from './_tsup-dts-rollup.js';
44
44
  export { usePdf } from './_tsup-dts-rollup.js';
45
+ export { ColorScheme } from './_tsup-dts-rollup.js';
46
+ export { createDarkModeColorMap } from './_tsup-dts-rollup.js';
47
+ export { DarkModeColors } from './_tsup-dts-rollup.js';
48
+ export { DEFAULT_DARK_MODE_COLORS } from './_tsup-dts-rollup.js';
49
+ export { RenderColorMap } from './_tsup-dts-rollup.js';
package/dist/index.js CHANGED
@@ -15,6 +15,209 @@ var clamp = (value, minimum, maximum) => {
15
15
  return Math.min(Math.max(value, minimum), maximum);
16
16
  };
17
17
 
18
+ // src/lib/dark-mode.ts
19
+ var DEFAULT_DARK_MODE_COLORS = {
20
+ background: "#141210",
21
+ foreground: "#eae6e0"
22
+ };
23
+ var NAMED_COLORS = {
24
+ white: [1, 1, 1, 1],
25
+ black: [0, 0, 0, 1],
26
+ transparent: [0, 0, 0, 0]
27
+ };
28
+ function clamp01(value) {
29
+ return value < 0 ? 0 : value > 1 ? 1 : value;
30
+ }
31
+ function parseColor(input) {
32
+ const str = input.trim().toLowerCase();
33
+ const named = Object.hasOwn(NAMED_COLORS, str) ? NAMED_COLORS[str] : void 0;
34
+ if (named) return named;
35
+ if (str.startsWith("#")) {
36
+ const hex = str.slice(1);
37
+ if (!/^[0-9a-f]{3,8}$/.test(hex)) return null;
38
+ if (hex.length === 3 || hex.length === 4) {
39
+ const r2 = Number.parseInt(hex[0], 16) / 15;
40
+ const g = Number.parseInt(hex[1], 16) / 15;
41
+ const b = Number.parseInt(hex[2], 16) / 15;
42
+ const a = hex.length === 4 ? Number.parseInt(hex[3], 16) / 15 : 1;
43
+ return [r2, g, b, a];
44
+ }
45
+ if (hex.length === 6 || hex.length === 8) {
46
+ const r2 = Number.parseInt(hex.slice(0, 2), 16) / 255;
47
+ const g = Number.parseInt(hex.slice(2, 4), 16) / 255;
48
+ const b = Number.parseInt(hex.slice(4, 6), 16) / 255;
49
+ const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1;
50
+ return [r2, g, b, a];
51
+ }
52
+ return null;
53
+ }
54
+ const fn = /^rgba?\(([^)]+)\)$/.exec(str);
55
+ if (fn) {
56
+ const parts = fn[1].split(/[,\s/]+/).filter(Boolean);
57
+ if (parts.length < 3) return null;
58
+ const channel = (part) => part.endsWith("%") ? Number.parseFloat(part) / 100 : Number.parseFloat(part) / 255;
59
+ const alpha = (part) => part.endsWith("%") ? Number.parseFloat(part) / 100 : Number.parseFloat(part);
60
+ const r2 = channel(parts[0]);
61
+ const g = channel(parts[1]);
62
+ const b = channel(parts[2]);
63
+ const a = parts[3] !== void 0 ? alpha(parts[3]) : 1;
64
+ if (Number.isNaN(r2) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(a))
65
+ return null;
66
+ return [clamp01(r2), clamp01(g), clamp01(b), clamp01(a)];
67
+ }
68
+ const normalized = normalizeCssColor(str);
69
+ if (normalized !== null && normalized !== str) return parseColor(normalized);
70
+ return null;
71
+ }
72
+ var normalizeCtx;
73
+ function normalizeCssColor(input) {
74
+ if (normalizeCtx === void 0) {
75
+ normalizeCtx = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d", {
76
+ willReadFrequently: true
77
+ });
78
+ }
79
+ const ctx = normalizeCtx;
80
+ if (!ctx) return null;
81
+ ctx.fillStyle = "#000000";
82
+ ctx.fillStyle = input;
83
+ const first = ctx.fillStyle;
84
+ ctx.fillStyle = "#ffffff";
85
+ ctx.fillStyle = input;
86
+ return first === ctx.fillStyle ? first : null;
87
+ }
88
+ function srgbToLinear(c) {
89
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
90
+ }
91
+ function linearToSrgb(c) {
92
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
93
+ }
94
+ function rgbToOklab(r2, g, b) {
95
+ const lr = srgbToLinear(r2);
96
+ const lg = srgbToLinear(g);
97
+ const lb = srgbToLinear(b);
98
+ const l = Math.cbrt(
99
+ 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
100
+ );
101
+ const m = Math.cbrt(
102
+ 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
103
+ );
104
+ const s = Math.cbrt(
105
+ 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
106
+ );
107
+ return [
108
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
109
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
110
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
111
+ ];
112
+ }
113
+ function oklabToLinearRgb(L, a, b) {
114
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
115
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
116
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
117
+ return [
118
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
119
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
120
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
121
+ ];
122
+ }
123
+ var GAMUT_EPSILON = 5e-4;
124
+ function oklabToSrgbExact(L, a, b) {
125
+ const [lr, lg, lb] = oklabToLinearRgb(L, a, b);
126
+ if (lr < -GAMUT_EPSILON || lr > 1 + GAMUT_EPSILON || lg < -GAMUT_EPSILON || lg > 1 + GAMUT_EPSILON || lb < -GAMUT_EPSILON || lb > 1 + GAMUT_EPSILON) {
127
+ return null;
128
+ }
129
+ return [
130
+ linearToSrgb(clamp01(lr)),
131
+ linearToSrgb(clamp01(lg)),
132
+ linearToSrgb(clamp01(lb))
133
+ ];
134
+ }
135
+ function oklabToSrgbGamutMapped(L, a, b) {
136
+ const exact = oklabToSrgbExact(L, a, b);
137
+ if (exact) return exact;
138
+ let lo = 0;
139
+ let hi = 1;
140
+ for (let i = 0; i < 12; i++) {
141
+ const mid = (lo + hi) / 2;
142
+ if (oklabToSrgbExact(L, a * mid, b * mid)) lo = mid;
143
+ else hi = mid;
144
+ }
145
+ const [lr, lg, lb] = oklabToLinearRgb(L, a * lo, b * lo);
146
+ return [
147
+ linearToSrgb(clamp01(lr)),
148
+ linearToSrgb(clamp01(lg)),
149
+ linearToSrgb(clamp01(lb))
150
+ ];
151
+ }
152
+ function toHexByte(channel) {
153
+ return Math.round(clamp01(channel) * 255).toString(16).padStart(2, "0");
154
+ }
155
+ function toCssColor(r2, g, b, a) {
156
+ const rgb = `#${toHexByte(r2)}${toHexByte(g)}${toHexByte(b)}`;
157
+ return a >= 1 ? rgb : `${rgb}${toHexByte(a)}`;
158
+ }
159
+ var NEUTRAL_CHROMA_THRESHOLD = 0.04;
160
+ var COLOR_CACHE_MAX = 4096;
161
+ var mapInstances = /* @__PURE__ */ new Map();
162
+ function createDarkModeColorMap(colors) {
163
+ const background = colors?.background ?? DEFAULT_DARK_MODE_COLORS.background;
164
+ const foreground = colors?.foreground ?? DEFAULT_DARK_MODE_COLORS.foreground;
165
+ const instanceKey = `${background}|${foreground}`;
166
+ const existing = mapInstances.get(instanceKey);
167
+ if (existing) {
168
+ mapInstances.delete(instanceKey);
169
+ mapInstances.set(instanceKey, existing);
170
+ return existing;
171
+ }
172
+ const bgRgba = parseColor(background) ?? parseColor(DEFAULT_DARK_MODE_COLORS.background);
173
+ const fgRgba = parseColor(foreground) ?? parseColor(DEFAULT_DARK_MODE_COLORS.foreground);
174
+ const bgLab = rgbToOklab(bgRgba[0], bgRgba[1], bgRgba[2]);
175
+ const fgLab = rgbToOklab(fgRgba[0], fgRgba[1], fgRgba[2]);
176
+ const cache = /* @__PURE__ */ new Map();
177
+ const backgroundCss = toCssColor(bgRgba[0], bgRgba[1], bgRgba[2], 1);
178
+ const foregroundCss = toCssColor(fgRgba[0], fgRgba[1], fgRgba[2], 1);
179
+ const seedPoles = () => {
180
+ for (const white of ["#ffffff", "#fff", "white"])
181
+ cache.set(white, backgroundCss);
182
+ for (const black of ["#000000", "#000", "black"])
183
+ cache.set(black, foregroundCss);
184
+ };
185
+ seedPoles();
186
+ const transform = (input) => {
187
+ const parsed = parseColor(input);
188
+ if (!parsed || parsed[3] === 0) return input;
189
+ const [r2, g, b, alpha] = parsed;
190
+ const [, labA, labB] = rgbToOklab(r2, g, b);
191
+ const luma = 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
192
+ const rampL = fgLab[0] + (bgLab[0] - fgLab[0]) * luma;
193
+ const rampA = fgLab[1] + (bgLab[1] - fgLab[1]) * luma;
194
+ const rampB = fgLab[2] + (bgLab[2] - fgLab[2]) * luma;
195
+ const chroma = Math.hypot(labA, labB);
196
+ const hueWeight = Math.min(1, chroma / NEUTRAL_CHROMA_THRESHOLD);
197
+ const outA = rampA + (labA - rampA) * hueWeight;
198
+ const outB = rampB + (labB - rampB) * hueWeight;
199
+ const [sr, sg, sb] = oklabToSrgbGamutMapped(rampL, outA, outB);
200
+ return toCssColor(sr, sg, sb, alpha);
201
+ };
202
+ const map = (input) => {
203
+ const hit = cache.get(input);
204
+ if (hit !== void 0) return hit;
205
+ const out = transform(input);
206
+ if (cache.size >= COLOR_CACHE_MAX) {
207
+ cache.clear();
208
+ seedPoles();
209
+ }
210
+ cache.set(input, out);
211
+ return out;
212
+ };
213
+ if (mapInstances.size >= 16) {
214
+ const oldest = mapInstances.keys().next().value;
215
+ if (oldest !== void 0) mapInstances.delete(oldest);
216
+ }
217
+ mapInstances.set(instanceKey, map);
218
+ return map;
219
+ }
220
+
18
221
  // src/lib/zoom.ts
19
222
  var getFitWidthZoom = (containerWidth, viewports, zoomOptions) => {
20
223
  const { minZoom, maxZoom } = zoomOptions;
@@ -39,6 +242,15 @@ var createZustandContext = (getStore) => {
39
242
  // src/internal.ts
40
243
  var PDFStore = createZustandContext(
41
244
  (initialState) => {
245
+ const initialColorScheme = initialState.colorScheme ?? "light";
246
+ const initialDarkModeColors = {
247
+ ...DEFAULT_DARK_MODE_COLORS,
248
+ ...initialState.darkModeColors
249
+ };
250
+ const renderColorMapRef = initialState.renderColorMapRef ?? {
251
+ current: null
252
+ };
253
+ renderColorMapRef.current = initialColorScheme === "dark" ? createDarkModeColorMap(initialDarkModeColors) : null;
42
254
  return createStore((set, get) => ({
43
255
  pdfDocumentProxy: initialState.pdfDocumentProxy,
44
256
  zoom: initialState.zoom,
@@ -131,6 +343,21 @@ var PDFStore = createZustandContext(
131
343
  (rect) => rect.uuid !== uuid
132
344
  )
133
345
  })),
346
+ colorScheme: initialColorScheme,
347
+ darkModeColors: initialDarkModeColors,
348
+ renderColorMapRef,
349
+ setColorScheme: (colorScheme, colors) => {
350
+ const prev = get();
351
+ const darkModeColors = {
352
+ background: colors?.background ?? prev.darkModeColors.background,
353
+ foreground: colors?.foreground ?? prev.darkModeColors.foreground
354
+ };
355
+ if (prev.colorScheme === colorScheme && prev.darkModeColors.background === darkModeColors.background && prev.darkModeColors.foreground === darkModeColors.foreground) {
356
+ return;
357
+ }
358
+ prev.renderColorMapRef.current = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
359
+ set({ colorScheme, darkModeColors });
360
+ },
134
361
  renderedPages: {},
135
362
  markPageRendered: (pageNumber) => {
136
363
  const current = get().renderedPages;
@@ -1165,6 +1392,129 @@ function clampScaleForPage(targetScale, pageWidth, pageHeight) {
1165
1392
  );
1166
1393
  return Math.max(safeScale, 0);
1167
1394
  }
1395
+
1396
+ // src/lib/recolor-context.ts
1397
+ var RECOLOR_CLEANUP = Symbol("lectorRecolorCleanup");
1398
+ var RECOLOR_PAINTED = Symbol("lectorRecolorPainted");
1399
+ var FILL_METHODS = ["fill", "fillRect", "fillText"];
1400
+ var STROKE_METHODS = ["stroke", "strokeRect", "strokeText"];
1401
+ var GRADIENT_METHODS = [
1402
+ "createLinearGradient",
1403
+ "createRadialGradient"
1404
+ ];
1405
+ function ntscLuma(r2, g, b) {
1406
+ return 0.3 * r2 + 0.59 * g + 0.11 * b;
1407
+ }
1408
+ function parseHexLuma(color) {
1409
+ if (!/^#[0-9a-f]{6}$/i.test(color)) return null;
1410
+ return ntscLuma(
1411
+ Number.parseInt(color.slice(1, 3), 16),
1412
+ Number.parseInt(color.slice(3, 5), 16),
1413
+ Number.parseInt(color.slice(5, 7), 16)
1414
+ );
1415
+ }
1416
+ function correctLuminosityMask(source, mappedWhiteLuma, mappedBlackLuma) {
1417
+ if (typeof document === "undefined") return null;
1418
+ if (!(typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) && !(typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas)) {
1419
+ return null;
1420
+ }
1421
+ const width = source.width;
1422
+ const height = source.height;
1423
+ if (!width || !height) return null;
1424
+ const span = mappedWhiteLuma - mappedBlackLuma;
1425
+ if (Math.abs(span) < 1) return null;
1426
+ const tmp = document.createElement("canvas");
1427
+ tmp.width = width;
1428
+ tmp.height = height;
1429
+ const tmpCtx = tmp.getContext("2d", { willReadFrequently: true });
1430
+ if (!tmpCtx) return null;
1431
+ tmpCtx.drawImage(source, 0, 0);
1432
+ let image;
1433
+ try {
1434
+ image = tmpCtx.getImageData(0, 0, width, height);
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ const data = image.data;
1439
+ for (let i = 0; i < data.length; i += 4) {
1440
+ const luma = ntscLuma(data[i], data[i + 1], data[i + 2]);
1441
+ const t = (luma - mappedBlackLuma) / span;
1442
+ const gray = t <= 0 ? 0 : t >= 1 ? 255 : Math.round(t * 255);
1443
+ data[i] = data[i + 1] = data[i + 2] = gray;
1444
+ }
1445
+ tmpCtx.putImageData(image, 0, 0);
1446
+ return tmp;
1447
+ }
1448
+ function applyContextRecolor(ctx, map) {
1449
+ const target = ctx;
1450
+ target[RECOLOR_CLEANUP]?.();
1451
+ const mappedWhiteLuma = parseHexLuma(map("#ffffff"));
1452
+ const mappedBlackLuma = parseHexLuma(map("#000000"));
1453
+ const withMaskCorrection = (original) => function(...args) {
1454
+ if (mappedWhiteLuma !== null && mappedBlackLuma !== null && this.globalCompositeOperation === "destination-in" && typeof this.filter === "string" && this.filter.includes("luminosity")) {
1455
+ const source = args[0];
1456
+ const sourcePainted = (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement || typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas) && source.getContext("2d")?.[RECOLOR_PAINTED] === true;
1457
+ const corrected = sourcePainted ? correctLuminosityMask(source, mappedWhiteLuma, mappedBlackLuma) : null;
1458
+ if (corrected) {
1459
+ const next = [corrected, ...args.slice(1)];
1460
+ return original.apply(this, next);
1461
+ }
1462
+ }
1463
+ return original.apply(this, args);
1464
+ };
1465
+ const withMappedStyle = (original, styleProp) => function(...args) {
1466
+ const style = this[styleProp];
1467
+ if (typeof style === "string") {
1468
+ const mapped = map(style);
1469
+ if (mapped !== style) {
1470
+ this[RECOLOR_PAINTED] = true;
1471
+ this[styleProp] = mapped;
1472
+ try {
1473
+ return original.apply(this, args);
1474
+ } finally {
1475
+ this[styleProp] = style;
1476
+ }
1477
+ }
1478
+ }
1479
+ return original.apply(this, args);
1480
+ };
1481
+ const withMappedStops = (original) => function(...args) {
1482
+ const gradient = original.apply(this, args);
1483
+ const addColorStop = gradient.addColorStop.bind(gradient);
1484
+ const context = this;
1485
+ gradient.addColorStop = (offset3, color) => {
1486
+ const mapped = map(color);
1487
+ if (mapped !== color) context[RECOLOR_PAINTED] = true;
1488
+ addColorStop(offset3, mapped);
1489
+ };
1490
+ return gradient;
1491
+ };
1492
+ const record = target;
1493
+ for (const name of FILL_METHODS)
1494
+ record[name] = withMappedStyle(target[name], "fillStyle");
1495
+ for (const name of STROKE_METHODS)
1496
+ record[name] = withMappedStyle(target[name], "strokeStyle");
1497
+ for (const name of GRADIENT_METHODS)
1498
+ record[name] = withMappedStops(target[name]);
1499
+ record.drawImage = withMaskCorrection(target.drawImage);
1500
+ const cleanup = () => {
1501
+ if (target[RECOLOR_CLEANUP] !== cleanup) return;
1502
+ for (const name of [
1503
+ ...FILL_METHODS,
1504
+ ...STROKE_METHODS,
1505
+ ...GRADIENT_METHODS,
1506
+ "drawImage"
1507
+ ])
1508
+ delete record[name];
1509
+ delete target[RECOLOR_CLEANUP];
1510
+ delete target[RECOLOR_PAINTED];
1511
+ };
1512
+ target[RECOLOR_CLEANUP] = cleanup;
1513
+ return cleanup;
1514
+ }
1515
+ function removeContextRecolor(ctx) {
1516
+ ctx[RECOLOR_CLEANUP]?.();
1517
+ }
1168
1518
  var useDpr = () => {
1169
1519
  const [dpr, setDPR] = useState(
1170
1520
  typeof window === "undefined" ? 1 : Math.min(window.devicePixelRatio, 2)
@@ -1208,16 +1558,16 @@ function clearBitmapCache(documentId) {
1208
1558
  cacheEntries.splice(i, 1);
1209
1559
  }
1210
1560
  }
1211
- function cacheKey(scale, background) {
1212
- const bg = background ?? "white";
1561
+ function cacheKey(scale, background, recolorKey) {
1562
+ const bg = `${background ?? "white"}|${recolorKey ?? ""}`;
1213
1563
  let hash = Math.round(scale * 1e4);
1214
1564
  for (let i = 0; i < bg.length; i++) {
1215
1565
  hash = hash * 31 + bg.charCodeAt(i) | 0;
1216
1566
  }
1217
1567
  return hash;
1218
1568
  }
1219
- function getCachedBitmap(proxy, scale, background) {
1220
- const key = cacheKey(scale, background);
1569
+ function getCachedBitmap(proxy, scale, background, recolorKey) {
1570
+ const key = cacheKey(scale, background, recolorKey);
1221
1571
  const bitmap = canvasBitmapCache.get(proxy)?.get(key);
1222
1572
  if (!bitmap) return null;
1223
1573
  const idx = cacheEntries.findIndex((e) => e.proxy === proxy && e.key === key);
@@ -1227,8 +1577,8 @@ function getCachedBitmap(proxy, scale, background) {
1227
1577
  }
1228
1578
  return bitmap;
1229
1579
  }
1230
- function setCachedBitmap(docId, proxy, scale, background, bitmap) {
1231
- const key = cacheKey(scale, background);
1580
+ function setCachedBitmap(docId, proxy, scale, background, recolorKey, bitmap) {
1581
+ const key = cacheKey(scale, background, recolorKey);
1232
1582
  let map = canvasBitmapCache.get(proxy);
1233
1583
  if (!map) {
1234
1584
  map = /* @__PURE__ */ new Map();
@@ -1255,8 +1605,9 @@ function setCachedBitmap(docId, proxy, scale, background, bitmap) {
1255
1605
  }
1256
1606
  var useCanvasLayer = ({ background }) => {
1257
1607
  const canvasRef = useRef(null);
1258
- const hasContentRef = useRef(false);
1608
+ const paintedKeyRef = useRef(null);
1259
1609
  const pageNumber = usePDFPageNumber();
1610
+ const store = PDFStore.useContext();
1260
1611
  const dpr = useDpr();
1261
1612
  const bouncyZoom = usePdf((state) => state.zoom);
1262
1613
  const isResizing = usePdf((state) => state.isResizing);
@@ -1264,6 +1615,10 @@ var useCanvasLayer = ({ background }) => {
1264
1615
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1265
1616
  const markPageRendered = usePdf((state) => state.markPageRendered);
1266
1617
  const unmarkPageRendered = usePdf((state) => state.unmarkPageRendered);
1618
+ const colorScheme = usePdf((state) => state.colorScheme);
1619
+ const darkModeColors = usePdf((state) => state.darkModeColors);
1620
+ const recolor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
1621
+ const recolorKey = recolor ? `dark:${darkModeColors.background},${darkModeColors.foreground}` : void 0;
1267
1622
  const [zoom] = useDebounce(bouncyZoom, 100);
1268
1623
  useLayoutEffect(() => {
1269
1624
  if (!canvasRef.current) return;
@@ -1271,13 +1626,14 @@ var useCanvasLayer = ({ background }) => {
1271
1626
  const baseViewport = pdfPageProxy.getViewport({ scale: 1 });
1272
1627
  const pageWidth = baseViewport.width;
1273
1628
  const pageHeight = baseViewport.height;
1274
- if (isResizing && hasContentRef.current) {
1629
+ const contentKey = `${background ?? "white"}|${recolorKey ?? ""}`;
1630
+ if (isResizing && paintedKeyRef.current === contentKey) {
1275
1631
  baseCanvas.style.visibility = "";
1276
1632
  baseCanvas.style.width = `${pageWidth}px`;
1277
1633
  baseCanvas.style.height = `${pageHeight}px`;
1278
1634
  return;
1279
1635
  }
1280
- hasContentRef.current = false;
1636
+ paintedKeyRef.current = null;
1281
1637
  const targetBaseScale = dpr * Math.min(zoom, 1);
1282
1638
  const baseScale = clampScaleForPage(targetBaseScale, pageWidth, pageHeight);
1283
1639
  baseCanvas.width = Math.floor(pageWidth * baseScale);
@@ -1290,15 +1646,20 @@ var useCanvasLayer = ({ background }) => {
1290
1646
  baseCanvas.style.transform = "translate(0px, 0px)";
1291
1647
  baseCanvas.style.zIndex = "0";
1292
1648
  baseCanvas.style.pointerEvents = "none";
1293
- baseCanvas.style.backgroundColor = background ?? "white";
1649
+ baseCanvas.style.backgroundColor = recolor ? recolor(background ?? "#ffffff") : background ?? "white";
1294
1650
  baseCanvas.style.visibility = "";
1295
1651
  const context = baseCanvas.getContext("2d");
1296
1652
  if (!context) return;
1297
- const cached = getCachedBitmap(pdfPageProxy, baseScale, background);
1653
+ const cached = getCachedBitmap(
1654
+ pdfPageProxy,
1655
+ baseScale,
1656
+ background,
1657
+ recolorKey
1658
+ );
1298
1659
  if (cached) {
1299
1660
  context.drawImage(cached, 0, 0);
1300
1661
  markPageRendered(pageNumber);
1301
- hasContentRef.current = true;
1662
+ paintedKeyRef.current = contentKey;
1302
1663
  return;
1303
1664
  }
1304
1665
  context.setTransform(1, 0, 0, 1, 0, 0);
@@ -1313,28 +1674,48 @@ var useCanvasLayer = ({ background }) => {
1313
1674
  const useBuffer = bufferCtx !== null;
1314
1675
  const renderCanvas = useBuffer ? buffer : baseCanvas;
1315
1676
  const renderCtx = bufferCtx ?? context;
1316
- const renderingTask = pdfPageProxy.render({
1317
- canvas: renderCanvas,
1318
- canvasContext: renderCtx,
1319
- viewport,
1320
- background
1321
- });
1677
+ let restoreRecolor = null;
1678
+ if (recolor) {
1679
+ restoreRecolor = applyContextRecolor(renderCtx, recolor);
1680
+ } else {
1681
+ removeContextRecolor(renderCtx);
1682
+ }
1683
+ let renderingTask;
1684
+ try {
1685
+ renderingTask = pdfPageProxy.render({
1686
+ canvas: renderCanvas,
1687
+ canvasContext: renderCtx,
1688
+ viewport,
1689
+ background
1690
+ });
1691
+ } catch (error) {
1692
+ restoreRecolor?.();
1693
+ throw error;
1694
+ }
1322
1695
  const releaseBuffer = () => {
1323
1696
  buffer.width = 0;
1324
1697
  buffer.height = 0;
1325
1698
  };
1326
- renderingTask.promise.then(() => {
1699
+ renderingTask.promise.finally(() => {
1700
+ restoreRecolor?.();
1701
+ }).then(() => {
1327
1702
  if (cancelled) {
1328
1703
  releaseBuffer();
1329
1704
  return;
1330
1705
  }
1706
+ const state = store.getState();
1707
+ const currentRecolorKey = state.colorScheme === "dark" ? `dark:${state.darkModeColors.background},${state.darkModeColors.foreground}` : void 0;
1708
+ if (currentRecolorKey !== recolorKey) {
1709
+ releaseBuffer();
1710
+ return;
1711
+ }
1331
1712
  if (useBuffer) {
1332
1713
  context.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
1333
1714
  context.drawImage(buffer, 0, 0);
1334
1715
  }
1335
1716
  baseCanvas.style.visibility = "";
1336
1717
  markPageRendered(pageNumber);
1337
- hasContentRef.current = true;
1718
+ paintedKeyRef.current = contentKey;
1338
1719
  if (typeof createImageBitmap !== "undefined") {
1339
1720
  createImageBitmap(renderCanvas).then((bitmap) => {
1340
1721
  if (cancelled) {
@@ -1346,6 +1727,7 @@ var useCanvasLayer = ({ background }) => {
1346
1727
  pdfPageProxy,
1347
1728
  baseScale,
1348
1729
  background,
1730
+ recolorKey,
1349
1731
  bitmap
1350
1732
  );
1351
1733
  }).catch(() => {
@@ -1372,7 +1754,10 @@ var useCanvasLayer = ({ background }) => {
1372
1754
  isResizing,
1373
1755
  pageNumber,
1374
1756
  markPageRendered,
1375
- docId
1757
+ docId,
1758
+ recolor,
1759
+ recolorKey,
1760
+ store
1376
1761
  ]);
1377
1762
  useEffect(() => {
1378
1763
  const canvas = canvasRef.current;
@@ -1467,6 +1852,11 @@ var useDetailCanvasLayer = ({
1467
1852
  const isPinching = usePdf((state) => state.isPinching);
1468
1853
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1469
1854
  const viewportRef = usePdf((state) => state.viewportRef);
1855
+ const colorScheme = usePdf((state) => state.colorScheme);
1856
+ const darkModeColors = usePdf((state) => state.darkModeColors);
1857
+ const recolor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
1858
+ const recolorKey = recolor ? `dark:${darkModeColors.background},${darkModeColors.foreground}` : "light";
1859
+ const paintedKeyRef = useRef(null);
1470
1860
  const [zoom] = useDebounce(bouncyZoom, 200);
1471
1861
  const pageDimsRef = useRef(null);
1472
1862
  if (!pageDimsRef.current || pageDimsRef.current.proxy !== pdfPageProxy) {
@@ -1489,7 +1879,8 @@ var useDetailCanvasLayer = ({
1489
1879
  }
1490
1880
  let renderingTask = null;
1491
1881
  let renderTimeoutId = null;
1492
- const bgColor = background ?? "white";
1882
+ const bgColor = recolor ? recolor(background ?? "#ffffff") : background ?? "white";
1883
+ const contentKey = `${background ?? "white"}|${recolorKey}`;
1493
1884
  const detailBaseStyle = `position:absolute;top:0;left:0;pointer-events:none;z-index:1;background-color:${bgColor}`;
1494
1885
  const hideDetailCanvas = () => {
1495
1886
  renderingTask?.cancel();
@@ -1569,6 +1960,7 @@ var useDetailCanvasLayer = ({
1569
1960
  const detailViewport = pdfPageProxy.getViewport({
1570
1961
  scale: effectiveScale
1571
1962
  });
1963
+ if (recolor) applyContextRecolor(bufferCtx, recolor);
1572
1964
  const currentRenderingTask = pdfPageProxy.render({
1573
1965
  canvas: buffer,
1574
1966
  canvasContext: bufferCtx,
@@ -1579,6 +1971,9 @@ var useDetailCanvasLayer = ({
1579
1971
  renderingTask = currentRenderingTask;
1580
1972
  currentRenderingTask.promise.then(() => {
1581
1973
  if (renderingTask !== currentRenderingTask) return;
1974
+ const state = store.getState();
1975
+ const currentRecolorKey = state.colorScheme === "dark" ? `dark:${state.darkModeColors.background},${state.darkModeColors.foreground}` : "light";
1976
+ if (currentRecolorKey !== recolorKey) return;
1582
1977
  detailCanvas.width = actualWidth;
1583
1978
  detailCanvas.height = actualHeight;
1584
1979
  detailCanvas.style.cssText = `${detailBaseStyle};display:block;opacity:1;width:${visibleWidth * zoom}px;height:${visibleHeight * zoom}px;transform-origin:center center;transform:translate(${visibleLeft * zoom}px,${visibleTop * zoom}px)`;
@@ -1587,6 +1982,7 @@ var useDetailCanvasLayer = ({
1587
1982
  if (ctx) {
1588
1983
  ctx.drawImage(buffer, 0, 0);
1589
1984
  }
1985
+ paintedKeyRef.current = contentKey;
1590
1986
  }).catch((error) => {
1591
1987
  if (error.name === "RenderingCancelledException") {
1592
1988
  return;
@@ -1617,6 +2013,10 @@ var useDetailCanvasLayer = ({
1617
2013
  if (zoom <= 1 || isPinching) {
1618
2014
  hideDetailCanvas();
1619
2015
  } else {
2016
+ if (paintedKeyRef.current !== null && paintedKeyRef.current !== contentKey) {
2017
+ hideDetailCanvas();
2018
+ paintedKeyRef.current = null;
2019
+ }
1620
2020
  scheduleRender(0);
1621
2021
  }
1622
2022
  return () => {
@@ -1634,7 +2034,9 @@ var useDetailCanvasLayer = ({
1634
2034
  dpr,
1635
2035
  viewportRef,
1636
2036
  baseCanvasRef,
1637
- store
2037
+ store,
2038
+ recolor,
2039
+ recolorKey
1638
2040
  ]);
1639
2041
  useEffect(() => {
1640
2042
  const canvas = detailCanvasRef.current;
@@ -2353,8 +2755,11 @@ var ColoredHighlightComponent = ({
2353
2755
  const deleteColoredHighlight = usePdf(
2354
2756
  (state) => state.deleteColoredHighlight
2355
2757
  );
2758
+ const colorScheme = usePdf((state) => state.colorScheme);
2759
+ const darkModeColors = usePdf((state) => state.darkModeColors);
2356
2760
  const [showButton, setShowButton] = useState(false);
2357
2761
  const pageNumber = usePDFPageNumber();
2762
+ const highlightColor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors)(selection.color) : selection.color;
2358
2763
  const pageRectangles = useMemo(
2359
2764
  () => selection.rectangles.filter((r2) => r2.pageNumber === pageNumber),
2360
2765
  [selection.rectangles, pageNumber]
@@ -2377,11 +2782,10 @@ var ColoredHighlightComponent = ({
2377
2782
  width: rect.width,
2378
2783
  cursor: "pointer",
2379
2784
  zIndex: 30,
2380
- backgroundColor: selection.color,
2381
- // mixBlendMode: "lighten", // changes the color of the text
2382
- mixBlendMode: "darken",
2383
- // best results
2384
- // mixBlendMode: "multiply", // works but coloring has some inconsistencies
2785
+ backgroundColor: highlightColor,
2786
+ // "darken" keeps dark text legible on a light page; on a dark
2787
+ // page the equivalent is "lighten" (keeps light text legible).
2788
+ mixBlendMode: colorScheme === "dark" ? "lighten" : "darken",
2385
2789
  borderRadius: "0.2rem"
2386
2790
  }
2387
2791
  },
@@ -2669,6 +3073,7 @@ var createTextSelectionManager = () => {
2669
3073
  var bindMouseEvents = createTextSelectionManager();
2670
3074
  var TEXT_BUILD_IDLE_MS = 300;
2671
3075
  var TEXT_SCROLL_SETTLE_MS = 160;
3076
+ var VIEWPORT_ATTACH_MAX_FRAMES = 300;
2672
3077
  var useTextLayer = () => {
2673
3078
  const textContainerRef = useRef(null);
2674
3079
  const textLayerRef = useRef(null);
@@ -2681,15 +3086,22 @@ var useTextLayer = () => {
2681
3086
  return;
2682
3087
  }
2683
3088
  let isCancelled = false;
2684
- const buildTimer = setTimeout(() => {
2685
- if (isCancelled || textContainerRef.current !== textContainer) return;
3089
+ let buildTimer = null;
3090
+ let unsubscribe = null;
3091
+ let built = false;
3092
+ let building = false;
3093
+ let buildGen = 0;
3094
+ const build = () => {
3095
+ building = true;
3096
+ const gen = ++buildGen;
2686
3097
  textContainer.innerHTML = "";
2687
3098
  if (textLayerRef.current) {
2688
3099
  textLayerRef.current.cancel();
2689
3100
  textLayerRef.current = null;
2690
3101
  }
2691
3102
  void import('pdfjs-dist/legacy/build/pdf.mjs').then(({ TextLayer: TextLayer2 }) => {
2692
- if (isCancelled || textContainerRef.current !== textContainer) return;
3103
+ if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer)
3104
+ return;
2693
3105
  const textLayer = new TextLayer2({
2694
3106
  textContentSource: pdfPageProxy.streamTextContent(),
2695
3107
  container: textContainer,
@@ -2698,22 +3110,61 @@ var useTextLayer = () => {
2698
3110
  textLayerRef.current = textLayer;
2699
3111
  return textLayer.render();
2700
3112
  }).then(() => {
2701
- if (isCancelled || textContainerRef.current !== textContainer) {
3113
+ if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer) {
2702
3114
  return;
2703
3115
  }
2704
3116
  const endOfContent = document.createElement("div");
2705
3117
  endOfContent.className = "endOfContent";
2706
3118
  textContainer.appendChild(endOfContent);
2707
3119
  bindMouseEvents(textContainer, endOfContent);
3120
+ built = true;
3121
+ building = false;
3122
+ unsubscribe?.();
3123
+ unsubscribe = null;
2708
3124
  }).catch((error) => {
3125
+ if (gen === buildGen) building = false;
2709
3126
  if (error instanceof Error && error.name !== "AbortException") {
2710
3127
  console.error("TextLayer rendering error:", error);
2711
3128
  }
2712
3129
  });
2713
- }, TEXT_BUILD_IDLE_MS);
3130
+ };
3131
+ const schedule = () => {
3132
+ if (built) return;
3133
+ if (building) {
3134
+ buildGen++;
3135
+ building = false;
3136
+ if (textLayerRef.current) {
3137
+ textLayerRef.current.cancel();
3138
+ textLayerRef.current = null;
3139
+ }
3140
+ textContainer.innerHTML = "";
3141
+ }
3142
+ if (buildTimer) clearTimeout(buildTimer);
3143
+ buildTimer = setTimeout(() => {
3144
+ buildTimer = null;
3145
+ if (isCancelled || textContainerRef.current !== textContainer) return;
3146
+ build();
3147
+ }, TEXT_BUILD_IDLE_MS);
3148
+ };
3149
+ let attachRaf = null;
3150
+ let attachAttempts = 0;
3151
+ const attachScrollSubscription = () => {
3152
+ attachRaf = null;
3153
+ const scrollViewport = viewportRef?.current;
3154
+ if (!scrollViewport) {
3155
+ if (++attachAttempts > VIEWPORT_ATTACH_MAX_FRAMES) return;
3156
+ attachRaf = requestAnimationFrame(attachScrollSubscription);
3157
+ return;
3158
+ }
3159
+ unsubscribe = subscribeToViewportInvalidation(scrollViewport, schedule);
3160
+ };
3161
+ attachScrollSubscription();
3162
+ schedule();
2714
3163
  return () => {
2715
3164
  isCancelled = true;
2716
- clearTimeout(buildTimer);
3165
+ if (buildTimer) clearTimeout(buildTimer);
3166
+ if (attachRaf !== null) cancelAnimationFrame(attachRaf);
3167
+ unsubscribe?.();
2717
3168
  if (textLayerRef.current) {
2718
3169
  textLayerRef.current.cancel();
2719
3170
  textLayerRef.current = null;
@@ -2723,11 +3174,10 @@ var useTextLayer = () => {
2723
3174
  delete textContainer._cleanupTextSelection;
2724
3175
  }
2725
3176
  };
2726
- }, [pdfPageProxy.streamTextContent, pdfPageProxy.getViewport]);
3177
+ }, [pdfPageProxy.streamTextContent, pdfPageProxy.getViewport, viewportRef]);
2727
3178
  useEffect(() => {
2728
3179
  const textContainer = textContainerRef.current;
2729
- const viewport = viewportRef?.current;
2730
- if (!textContainer || !viewport) return;
3180
+ if (!textContainer) return;
2731
3181
  let settleTimer = null;
2732
3182
  let pointerDown = false;
2733
3183
  let selecting = false;
@@ -2756,14 +3206,28 @@ var useTextLayer = () => {
2756
3206
  pointerDown = false;
2757
3207
  selecting = false;
2758
3208
  };
2759
- const unsubscribe = subscribeToViewportInvalidation(viewport, onScroll);
3209
+ let unsubscribe = null;
3210
+ let attachRaf = null;
3211
+ let attachAttempts = 0;
3212
+ const attachScrollSubscription = () => {
3213
+ attachRaf = null;
3214
+ const scrollViewport = viewportRef?.current;
3215
+ if (!scrollViewport) {
3216
+ if (++attachAttempts > VIEWPORT_ATTACH_MAX_FRAMES) return;
3217
+ attachRaf = requestAnimationFrame(attachScrollSubscription);
3218
+ return;
3219
+ }
3220
+ unsubscribe = subscribeToViewportInvalidation(scrollViewport, onScroll);
3221
+ };
3222
+ attachScrollSubscription();
2760
3223
  document.addEventListener("pointerdown", onPointerDown, true);
2761
3224
  document.addEventListener("pointerup", clearPointer, true);
2762
3225
  document.addEventListener("pointercancel", clearPointer, true);
2763
3226
  document.addEventListener("selectionchange", onSelectionChange);
2764
3227
  window.addEventListener("blur", clearPointer);
2765
3228
  return () => {
2766
- unsubscribe();
3229
+ if (attachRaf !== null) cancelAnimationFrame(attachRaf);
3230
+ unsubscribe?.();
2767
3231
  document.removeEventListener("pointerdown", onPointerDown, true);
2768
3232
  document.removeEventListener("pointerup", clearPointer, true);
2769
3233
  document.removeEventListener("pointercancel", clearPointer, true);
@@ -2978,6 +3442,9 @@ var Page = memo(
2978
3442
  }) => {
2979
3443
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
2980
3444
  const viewport = usePdf((state) => state.viewports[pageNumber - 1]);
3445
+ const colorScheme = usePdf((state) => state.colorScheme);
3446
+ const darkModeColors = usePdf((state) => state.darkModeColors);
3447
+ const pageBackground = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors)("#ffffff") : "white";
2981
3448
  const width = viewport?.width ?? (pdfPageProxy.view[2] ?? 0) - (pdfPageProxy.view[0] ?? 0);
2982
3449
  const height = viewport?.height ?? (pdfPageProxy.view[3] ?? 0) - (pdfPageProxy.view[1] ?? 0);
2983
3450
  return /* @__PURE__ */ jsx(PDFPageNumberContext.Provider, { value: pdfPageProxy.pageNumber, children: /* @__PURE__ */ jsx(
@@ -2992,7 +3459,7 @@ var Page = memo(
2992
3459
  style: {
2993
3460
  "--scale-factor": 1,
2994
3461
  "--total-scale-factor": 1,
2995
- backgroundColor: "white",
3462
+ backgroundColor: pageBackground,
2996
3463
  position: "relative",
2997
3464
  width,
2998
3465
  height,
@@ -5535,8 +6002,63 @@ var getDefaultPdfJsAssetUrls = (version) => {
5535
6002
  };
5536
6003
  };
5537
6004
 
6005
+ // src/lib/recolor-canvas-factory.ts
6006
+ function createRecolorCanvasFactory(mapRef) {
6007
+ return class RecolorCanvasFactory {
6008
+ #document;
6009
+ #enableHWA;
6010
+ constructor({
6011
+ ownerDocument = globalThis.document,
6012
+ enableHWA = false
6013
+ } = {}) {
6014
+ this.#document = ownerDocument;
6015
+ this.#enableHWA = enableHWA;
6016
+ }
6017
+ create(width, height) {
6018
+ if (width <= 0 || height <= 0) {
6019
+ throw new Error("Invalid canvas size");
6020
+ }
6021
+ const canvas = this.#document.createElement("canvas");
6022
+ canvas.width = width;
6023
+ canvas.height = height;
6024
+ const context = canvas.getContext("2d", {
6025
+ willReadFrequently: !this.#enableHWA
6026
+ });
6027
+ const map = mapRef.current;
6028
+ if (context && map) {
6029
+ applyContextRecolor(context, map);
6030
+ }
6031
+ return { canvas, context };
6032
+ }
6033
+ reset(canvasAndContext, width, height) {
6034
+ if (!canvasAndContext.canvas) {
6035
+ throw new Error("Canvas is not specified");
6036
+ }
6037
+ if (width <= 0 || height <= 0) {
6038
+ throw new Error("Invalid canvas size");
6039
+ }
6040
+ canvasAndContext.canvas.width = width;
6041
+ canvasAndContext.canvas.height = height;
6042
+ if (canvasAndContext.context) {
6043
+ const map = mapRef.current;
6044
+ if (map) applyContextRecolor(canvasAndContext.context, map);
6045
+ else removeContextRecolor(canvasAndContext.context);
6046
+ }
6047
+ }
6048
+ destroy(canvasAndContext) {
6049
+ if (!canvasAndContext.canvas) {
6050
+ throw new Error("Canvas is not specified");
6051
+ }
6052
+ canvasAndContext.canvas.width = 0;
6053
+ canvasAndContext.canvas.height = 0;
6054
+ canvasAndContext.canvas = null;
6055
+ canvasAndContext.context = null;
6056
+ }
6057
+ };
6058
+ }
6059
+
5538
6060
  // src/hooks/document/document.ts
5539
- function buildDocumentInitParams(source, pdfJsVersion, overrides) {
6061
+ function buildDocumentInitParams(source, pdfJsVersion, renderColorMapRef, overrides) {
5540
6062
  let params;
5541
6063
  if (typeof source === "string" || source instanceof URL) {
5542
6064
  params = { url: source };
@@ -5572,7 +6094,12 @@ function buildDocumentInitParams(source, pdfJsVersion, overrides) {
5572
6094
  cMapUrl: assetUrls.cMapUrl,
5573
6095
  cMapPacked: true,
5574
6096
  standardFontDataUrl: assetUrls.standardFontDataUrl,
5575
- iccUrl: assetUrls.iccUrl
6097
+ iccUrl: assetUrls.iccUrl,
6098
+ // Internal pdf.js scratch canvases (transparency groups, soft masks,
6099
+ // patterns) go through this factory so native dark mode can recolor
6100
+ // content that never touches the context passed to render(). A no-op
6101
+ // while the light scheme is active.
6102
+ CanvasFactory: createRecolorCanvasFactory(renderColorMapRef)
5576
6103
  };
5577
6104
  return { ...defaults, ...params, ...overrides };
5578
6105
  }
@@ -5584,15 +6111,24 @@ var usePDFDocumentContext = ({
5584
6111
  isZoomFitWidth,
5585
6112
  zoom = 1,
5586
6113
  zoomOptions,
5587
- documentOptions
6114
+ documentOptions,
6115
+ colorScheme,
6116
+ darkModeColors
5588
6117
  }) => {
5589
6118
  const [_, setProgress] = useState(0);
5590
6119
  const [initialState, setInitialState] = useState();
5591
6120
  const [rotation] = useState(initialRotation);
6121
+ const [renderColorMapRef] = useState(() => ({
6122
+ current: null
6123
+ }));
5592
6124
  const documentOptionsRef = useRef(documentOptions);
5593
6125
  documentOptionsRef.current = documentOptions;
5594
6126
  const onErrorRef = useRef(onError);
5595
6127
  onErrorRef.current = onError;
6128
+ const colorSchemeRef = useRef(colorScheme);
6129
+ colorSchemeRef.current = colorScheme;
6130
+ const darkModeColorsRef = useRef(darkModeColors);
6131
+ darkModeColorsRef.current = darkModeColors;
5596
6132
  useEffect(() => {
5597
6133
  const generateViewports = async (pdf) => {
5598
6134
  const pageProxies = [];
@@ -5620,7 +6156,10 @@ var usePDFDocumentContext = ({
5620
6156
  pageProxies: sortedPageProxies,
5621
6157
  pdfDocumentProxy: pdf,
5622
6158
  zoom,
5623
- zoomOptions
6159
+ zoomOptions,
6160
+ colorScheme: colorSchemeRef.current,
6161
+ darkModeColors: darkModeColorsRef.current,
6162
+ renderColorMapRef
5624
6163
  }));
5625
6164
  };
5626
6165
  const loadDocument = () => {
@@ -5636,6 +6175,7 @@ var usePDFDocumentContext = ({
5636
6175
  buildDocumentInitParams(
5637
6176
  source,
5638
6177
  version,
6178
+ renderColorMapRef,
5639
6179
  documentOptionsRef.current
5640
6180
  )
5641
6181
  );
@@ -5697,6 +6237,38 @@ var usePDFDocumentContext = ({
5697
6237
  initialState
5698
6238
  };
5699
6239
  };
6240
+ var ColorSchemeSync = ({
6241
+ colorScheme,
6242
+ darkModeColors
6243
+ }) => {
6244
+ const setColorScheme = usePdf((state) => state.setColorScheme);
6245
+ const storeColorScheme = usePdf((state) => state.colorScheme);
6246
+ const storeBackground = usePdf((state) => state.darkModeColors.background);
6247
+ const storeForeground = usePdf((state) => state.darkModeColors.foreground);
6248
+ const hasPalette = darkModeColors !== void 0;
6249
+ const background = darkModeColors?.background;
6250
+ const foreground = darkModeColors?.foreground;
6251
+ useEffect(() => {
6252
+ if (!colorScheme && !hasPalette) return;
6253
+ setColorScheme(
6254
+ colorScheme ?? storeColorScheme,
6255
+ hasPalette ? {
6256
+ background: background ?? DEFAULT_DARK_MODE_COLORS.background,
6257
+ foreground: foreground ?? DEFAULT_DARK_MODE_COLORS.foreground
6258
+ } : void 0
6259
+ );
6260
+ }, [
6261
+ colorScheme,
6262
+ storeColorScheme,
6263
+ storeBackground,
6264
+ storeForeground,
6265
+ hasPalette,
6266
+ background,
6267
+ foreground,
6268
+ setColorScheme
6269
+ ]);
6270
+ return null;
6271
+ };
5700
6272
  var Root = forwardRef(
5701
6273
  ({
5702
6274
  children,
@@ -5708,6 +6280,8 @@ var Root = forwardRef(
5708
6280
  zoom,
5709
6281
  zoomOptions,
5710
6282
  documentOptions,
6283
+ colorScheme,
6284
+ darkModeColors,
5711
6285
  ...props
5712
6286
  }, ref) => {
5713
6287
  const { initialState } = usePDFDocumentContext({
@@ -5717,7 +6291,9 @@ var Root = forwardRef(
5717
6291
  isZoomFitWidth,
5718
6292
  zoom,
5719
6293
  zoomOptions,
5720
- documentOptions
6294
+ documentOptions,
6295
+ colorScheme,
6296
+ darkModeColors
5721
6297
  });
5722
6298
  const linkService = useCreatePDFLinkService(
5723
6299
  initialState?.pdfDocumentProxy ?? null
@@ -5729,7 +6305,16 @@ var Root = forwardRef(
5729
6305
  clearBitmapCache(documentId);
5730
6306
  };
5731
6307
  }, [documentId]);
5732
- return /* @__PURE__ */ jsx(Primitive.div, { ref, ...props, children: initialState ? /* @__PURE__ */ jsx(PDFStore.Provider, { initialValue: initialState, children: /* @__PURE__ */ jsx(PDFLinkServiceContext.Provider, { value: linkService, children }) }) : loader ?? "Loading..." });
6308
+ return /* @__PURE__ */ jsx(Primitive.div, { ref, ...props, children: initialState ? /* @__PURE__ */ jsxs(PDFStore.Provider, { initialValue: initialState, children: [
6309
+ /* @__PURE__ */ jsx(
6310
+ ColorSchemeSync,
6311
+ {
6312
+ colorScheme,
6313
+ darkModeColors
6314
+ }
6315
+ ),
6316
+ /* @__PURE__ */ jsx(PDFLinkServiceContext.Provider, { value: linkService, children })
6317
+ ] }) : loader ?? "Loading..." });
5733
6318
  }
5734
6319
  );
5735
6320
  Root.displayName = "Root";
@@ -5776,6 +6361,9 @@ var useThumbnail = (pageNumber, config = {}) => {
5776
6361
  const { visible } = useVisibility({ elementRef: containerRef });
5777
6362
  const [debouncedVisible] = useDebounce(visible, 50);
5778
6363
  const dpr = useDpr();
6364
+ const colorScheme = usePdf((state) => state.colorScheme);
6365
+ const darkModeColors = usePdf((state) => state.darkModeColors);
6366
+ const recolor = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors) : null;
5779
6367
  const isVisible = isFirstPage || debouncedVisible;
5780
6368
  useEffect(() => {
5781
6369
  const renderThumbnail = async () => {
@@ -5792,13 +6380,23 @@ var useThumbnail = (pageNumber, config = {}) => {
5792
6380
  canvas2.height = scaledViewport.height;
5793
6381
  const context = canvas2.getContext("2d");
5794
6382
  if (!context) return;
5795
- const renderTask = pageProxy.render({
5796
- canvas: canvas2,
5797
- canvasContext: context,
5798
- viewport: scaledViewport
5799
- });
5800
- renderTaskRef.current = renderTask;
5801
- await renderTask.promise;
6383
+ let restoreRecolor = null;
6384
+ if (recolor) {
6385
+ restoreRecolor = applyContextRecolor(context, recolor);
6386
+ } else {
6387
+ removeContextRecolor(context);
6388
+ }
6389
+ try {
6390
+ const renderTask = pageProxy.render({
6391
+ canvas: canvas2,
6392
+ canvasContext: context,
6393
+ viewport: scaledViewport
6394
+ });
6395
+ renderTaskRef.current = renderTask;
6396
+ await renderTask.promise;
6397
+ } finally {
6398
+ restoreRecolor?.();
6399
+ }
5802
6400
  } catch (error) {
5803
6401
  if (error instanceof Error && error.name === "RenderingCancelledException") {
5804
6402
  console.log("Rendering cancelled");
@@ -5816,7 +6414,7 @@ var useThumbnail = (pageNumber, config = {}) => {
5816
6414
  canvas.height = 1;
5817
6415
  }
5818
6416
  };
5819
- }, [pageProxy, isVisible, dpr, maxHeight, maxWidth]);
6417
+ }, [pageProxy, isVisible, dpr, maxHeight, maxWidth, recolor]);
5820
6418
  return {
5821
6419
  canvasRef,
5822
6420
  containerRef,
@@ -6152,4 +6750,4 @@ function mergeAdjacentRects(rects) {
6152
6750
  // src/hooks/usePageRendered.ts
6153
6751
  var usePageRendered = (pageNumber) => usePdf((state) => !!state.renderedPages[pageNumber]);
6154
6752
 
6155
- export { AnnotationHighlightLayer, AnnotationLayer, AnnotationTooltip, AnnotationsStoreProvider, CanvasLayer, ColoredHighlightLayer, CurrentPage, CurrentZoom, CustomLayer, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, PDFLinkServiceContext, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, useAnnotations, useCreatePDFLinkService, usePDFLinkService, usePDFPageNumber, usePageRendered, usePdf, usePdfJump, useSearch, useSelectionDimensions };
6753
+ export { AnnotationHighlightLayer, AnnotationLayer, AnnotationTooltip, AnnotationsStoreProvider, CanvasLayer, ColoredHighlightLayer, CurrentPage, CurrentZoom, CustomLayer, DEFAULT_DARK_MODE_COLORS, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, PDFLinkServiceContext, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, createDarkModeColorMap, useAnnotations, useCreatePDFLinkService, usePDFLinkService, usePDFPageNumber, usePageRendered, usePdf, usePdfJump, useSearch, useSelectionDimensions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.12.8",
3
+ "version": "3.13.0",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",