hazo_pdf 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,7 @@ A React component library for viewing and annotating PDF documents with support
6
6
 
7
7
  - 📄 **PDF Viewing** - Render PDF documents with customizable zoom levels
8
8
  - ✏️ **Annotations** - Square and FreeText annotation tools
9
+ - 🔍 **Programmatic Highlights** - Ref-based API for creating and managing highlights programmatically
9
10
  - 🎨 **Customizable Styling** - Extensive configuration options via INI file
10
11
  - ⏰ **Timestamp Support** - Automatic timestamp appending to annotations
11
12
  - 🏷️ **Custom Stamps** - Add quick-insert stamps via right-click menu
@@ -413,6 +414,204 @@ See `config/hazo_pdf_config.ini` in the project root for all available configura
413
414
 
414
415
  ---
415
416
 
417
+ ## Programmatic Highlight API
418
+
419
+ The PDF viewer exposes a ref-based API for programmatically creating, removing, and managing highlights. This allows external code to control highlights without user interaction.
420
+
421
+ ### Basic Usage
422
+
423
+ ```tsx
424
+ import { PdfViewer, PdfViewerRef } from 'hazo_pdf';
425
+ import { useRef } from 'react';
426
+ import 'hazo_pdf/styles.css';
427
+
428
+ function HighlightExample() {
429
+ const viewer_ref = useRef<PdfViewerRef>(null);
430
+
431
+ const add_highlight = () => {
432
+ // Highlight region on page 0 at PDF coordinates [100, 500, 300, 550]
433
+ const id = viewer_ref.current?.highlight_region(0, [100, 500, 300, 550], {
434
+ border_color: '#FF0000',
435
+ background_color: '#FFFF00',
436
+ background_opacity: 0.4
437
+ });
438
+ console.log('Created highlight:', id);
439
+ };
440
+
441
+ const remove_specific_highlight = (id: string) => {
442
+ const removed = viewer_ref.current?.remove_highlight(id);
443
+ console.log('Highlight removed:', removed);
444
+ };
445
+
446
+ const clear_all = () => {
447
+ viewer_ref.current?.clear_all_highlights();
448
+ };
449
+
450
+ return (
451
+ <div style={{ width: '100%', height: '800px' }}>
452
+ <button onClick={add_highlight}>Add Highlight</button>
453
+ <button onClick={clear_all}>Clear All Highlights</button>
454
+
455
+ <PdfViewer
456
+ ref={viewer_ref}
457
+ url="/document.pdf"
458
+ />
459
+ </div>
460
+ );
461
+ }
462
+ ```
463
+
464
+ ### PdfViewerRef Interface
465
+
466
+ The ref exposes three methods for highlight management:
467
+
468
+ #### `highlight_region(page_index, rect, options?)`
469
+
470
+ Creates a new highlight annotation on the specified page.
471
+
472
+ **Parameters:**
473
+ - `page_index` (number): Zero-based page index where the highlight should appear
474
+ - `rect` ([number, number, number, number]): Rectangle coordinates in PDF space [x1, y1, x2, y2]
475
+ - `options` (HighlightOptions, optional): Styling options
476
+
477
+ **Returns:** `string` - The unique ID of the created highlight annotation
478
+
479
+ **HighlightOptions:**
480
+ ```typescript
481
+ {
482
+ border_color?: string; // Hex color (e.g., "#FF0000")
483
+ background_color?: string; // Hex color (e.g., "#FFFF00")
484
+ background_opacity?: number; // 0-1 (e.g., 0.4)
485
+ }
486
+ ```
487
+
488
+ **Example:**
489
+ ```tsx
490
+ const id = viewer_ref.current?.highlight_region(
491
+ 0, // Page 0
492
+ [100, 500, 300, 550], // PDF coordinates
493
+ {
494
+ border_color: '#FF0000',
495
+ background_color: '#FFFF00',
496
+ background_opacity: 0.4
497
+ }
498
+ );
499
+ ```
500
+
501
+ #### `remove_highlight(id)`
502
+
503
+ Removes a specific highlight by its ID.
504
+
505
+ **Parameters:**
506
+ - `id` (string): The highlight ID returned from `highlight_region()`
507
+
508
+ **Returns:** `boolean` - `true` if the highlight was found and removed, `false` otherwise
509
+
510
+ **Example:**
511
+ ```tsx
512
+ const removed = viewer_ref.current?.remove_highlight('highlight-123');
513
+ if (removed) {
514
+ console.log('Highlight removed successfully');
515
+ }
516
+ ```
517
+
518
+ #### `clear_all_highlights()`
519
+
520
+ Removes all highlights created via the `highlight_region()` API. Does not affect user-created annotations.
521
+
522
+ **Example:**
523
+ ```tsx
524
+ viewer_ref.current?.clear_all_highlights();
525
+ ```
526
+
527
+ ### Advanced Example: Search Results Highlighting
528
+
529
+ A common use case is highlighting search results in a PDF:
530
+
531
+ ```tsx
532
+ import { useState, useRef } from 'react';
533
+ import { PdfViewer, PdfViewerRef } from 'hazo_pdf';
534
+ import 'hazo_pdf/styles.css';
535
+
536
+ interface SearchResult {
537
+ page: number;
538
+ rect: [number, number, number, number];
539
+ text: string;
540
+ }
541
+
542
+ function SearchableViewer() {
543
+ const viewer_ref = useRef<PdfViewerRef>(null);
544
+ const [highlight_ids, set_highlight_ids] = useState<string[]>([]);
545
+
546
+ // Simulated search results
547
+ const search_results: SearchResult[] = [
548
+ { page: 0, rect: [100, 500, 300, 550], text: 'Result 1' },
549
+ { page: 0, rect: [100, 400, 300, 450], text: 'Result 2' },
550
+ { page: 1, rect: [150, 600, 350, 650], text: 'Result 3' }
551
+ ];
552
+
553
+ const highlight_search_results = () => {
554
+ // Clear previous highlights
555
+ viewer_ref.current?.clear_all_highlights();
556
+
557
+ // Add new highlights
558
+ const ids = search_results.map(result =>
559
+ viewer_ref.current?.highlight_region(result.page, result.rect, {
560
+ border_color: '#3B82F6',
561
+ background_color: '#DBEAFE',
562
+ background_opacity: 0.3
563
+ })
564
+ ).filter(Boolean) as string[];
565
+
566
+ set_highlight_ids(ids);
567
+ };
568
+
569
+ const clear_search = () => {
570
+ viewer_ref.current?.clear_all_highlights();
571
+ set_highlight_ids([]);
572
+ };
573
+
574
+ return (
575
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
576
+ <div style={{ padding: '1rem', background: '#f0f0f0' }}>
577
+ <button onClick={highlight_search_results}>
578
+ Highlight Search Results ({search_results.length})
579
+ </button>
580
+ <button onClick={clear_search}>Clear Highlights</button>
581
+ <span>Highlighted: {highlight_ids.length} results</span>
582
+ </div>
583
+
584
+ <div style={{ flex: 1 }}>
585
+ <PdfViewer
586
+ ref={viewer_ref}
587
+ url="/document.pdf"
588
+ />
589
+ </div>
590
+ </div>
591
+ );
592
+ }
593
+ ```
594
+
595
+ ### Coordinate System Notes
596
+
597
+ - Highlights use PDF coordinate space (points), not screen pixels
598
+ - PDF coordinates start at the bottom-left corner (Y increases upward)
599
+ - The `rect` parameter format is `[x1, y1, x2, y2]` where:
600
+ - `x1, y1`: Bottom-left corner
601
+ - `x2, y2`: Top-right corner
602
+ - If you need to convert screen coordinates to PDF coordinates, you'll need to use the PDF page viewport (see the test app for examples)
603
+
604
+ ### Styling Defaults
605
+
606
+ If no `options` are provided to `highlight_region()`, the highlight uses default colors from the configuration file:
607
+ - `border_color`: From `highlight_border_color` config setting
608
+ - `background_color`: From `highlight_fill_color` config setting
609
+ - `background_opacity`: From `highlight_fill_opacity` config setting
610
+
611
+ You can override any or all of these on a per-highlight basis.
612
+
613
+ ---
614
+
416
615
  ## API Reference
417
616
 
418
617
  ### PdfViewer Props
@@ -573,6 +772,40 @@ interface PdfAnnotation {
573
772
  }
574
773
  ```
575
774
 
775
+ **Note:** Highlights created via the programmatic API (`PdfViewerRef.highlight_region()`) will have `type: 'Highlight'` and `flags: 'api_highlight'` to distinguish them from user-created annotations.
776
+
777
+ ### PdfViewerRef Interface
778
+
779
+ Interface for the ref exposed by `PdfViewer`. Use with `useRef<PdfViewerRef>()` to access programmatic highlight methods.
780
+
781
+ ```typescript
782
+ interface PdfViewerRef {
783
+ highlight_region: (
784
+ page_index: number,
785
+ rect: [number, number, number, number],
786
+ options?: HighlightOptions
787
+ ) => string;
788
+
789
+ remove_highlight: (id: string) => boolean;
790
+
791
+ clear_all_highlights: () => void;
792
+ }
793
+ ```
794
+
795
+ See [Programmatic Highlight API](#programmatic-highlight-api) for detailed usage.
796
+
797
+ ### HighlightOptions Interface
798
+
799
+ Options for customizing highlights created via the API.
800
+
801
+ ```typescript
802
+ interface HighlightOptions {
803
+ border_color?: string; // Hex format (e.g., "#FF0000")
804
+ background_color?: string; // Hex format (e.g., "#FFFF00")
805
+ background_opacity?: number; // 0-1 (e.g., 0.4)
806
+ }
807
+ ```
808
+
576
809
  ### PDFDocumentProxy
577
810
 
578
811
  Type from `pdfjs-dist`. Contains PDF metadata and page proxies.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
+ import React__default from 'react';
2
3
  import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
3
4
 
4
5
  /**
@@ -403,18 +404,48 @@ interface MetadataInput {
403
404
  /** Array of footer items */
404
405
  footer: MetadataFooterItem[];
405
406
  }
406
-
407
407
  /**
408
- * PDF Viewer Component
409
- * Main component for displaying and interacting with PDF documents
410
- * Integrates PDF rendering, annotation overlay, and layout management
408
+ * Options for programmatic highlight creation via PdfViewerRef
411
409
  */
410
+ interface HighlightOptions {
411
+ /** Border color in hex format (default: from config highlight_border_color) */
412
+ border_color?: string;
413
+ /** Background/fill color in hex format (default: from config highlight_fill_color) */
414
+ background_color?: string;
415
+ /** Background opacity 0-1 (default: from config highlight_fill_opacity) */
416
+ background_opacity?: number;
417
+ }
418
+ /**
419
+ * Ref interface for programmatic PDF viewer control
420
+ * Use with useRef<PdfViewerRef>() to get access to imperative methods
421
+ */
422
+ interface PdfViewerRef {
423
+ /**
424
+ * Create a highlight on a specific page region
425
+ * @param page_index - Zero-based page index
426
+ * @param rect - Rectangle coordinates in PDF space [x1, y1, x2, y2]
427
+ * @param options - Optional styling overrides (border_color, background_color, background_opacity)
428
+ * @returns The highlight annotation ID
429
+ */
430
+ highlight_region: (page_index: number, rect: [number, number, number, number], options?: HighlightOptions) => string;
431
+ /**
432
+ * Remove a specific highlight by ID
433
+ * @param id - The highlight annotation ID returned from highlight_region
434
+ * @returns true if highlight was found and removed, false otherwise
435
+ */
436
+ remove_highlight: (id: string) => boolean;
437
+ /**
438
+ * Remove all highlights created via the highlight_region API
439
+ * Does not affect user-created annotations
440
+ */
441
+ clear_all_highlights: () => void;
442
+ }
412
443
 
413
444
  /**
414
445
  * PDF Viewer Component
415
446
  * Main entry point for PDF viewing and annotation
416
447
  */
417
- declare const PdfViewer: React.FC<PdfViewerProps>;
448
+ declare const PdfViewer: React.ForwardRefExoticComponent<PdfViewerProps & React.RefAttributes<PdfViewerRef>>;
418
449
 
419
450
  /**
420
451
  * PDF Viewer Layout Component
@@ -431,7 +462,7 @@ interface PdfViewerLayoutProps {
431
462
  annotations: PdfAnnotation[];
432
463
  current_tool: 'Square' | 'Highlight' | 'FreeText' | 'CustomBookmark' | null;
433
464
  on_annotation_create: (annotation: PdfAnnotation) => void;
434
- on_context_menu: (e: React.MouseEvent, page_index: number, screen_x: number, screen_y: number, mapper: CoordinateMapper) => void;
465
+ on_context_menu: (e: React__default.MouseEvent, page_index: number, screen_x: number, screen_y: number, mapper: CoordinateMapper) => void;
435
466
  on_annotation_click: (annotation: PdfAnnotation, screen_x: number, screen_y: number, mapper: CoordinateMapper) => void;
436
467
  on_freetext_click?: (page_index: number, screen_x: number, screen_y: number, mapper: CoordinateMapper) => void;
437
468
  background_color?: string;
@@ -442,7 +473,7 @@ interface PdfViewerLayoutProps {
442
473
  * PDF Viewer Layout Component
443
474
  * Manages page rendering and annotation overlay coordination
444
475
  */
445
- declare const PdfViewerLayout: React.FC<PdfViewerLayoutProps>;
476
+ declare const PdfViewerLayout: React__default.FC<PdfViewerLayoutProps>;
446
477
 
447
478
  /**
448
479
  * PDF Page Renderer Component
@@ -471,7 +502,7 @@ interface PdfPageRendererProps {
471
502
  * PDF Page Renderer Component
472
503
  * Handles rendering of a single PDF page to canvas
473
504
  */
474
- declare const PdfPageRenderer: React.FC<PdfPageRendererProps>;
505
+ declare const PdfPageRenderer: React__default.FC<PdfPageRendererProps>;
475
506
 
476
507
  /**
477
508
  * Annotation Overlay Component
@@ -497,7 +528,7 @@ interface AnnotationOverlayProps {
497
528
  /** Callback when annotation is created */
498
529
  on_annotation_create?: (annotation: PdfAnnotation) => void;
499
530
  /** Callback when right-click occurs */
500
- on_context_menu?: (event: React.MouseEvent, screen_x: number, screen_y: number) => void;
531
+ on_context_menu?: (event: React__default.MouseEvent, screen_x: number, screen_y: number) => void;
501
532
  /** Callback when annotation is clicked */
502
533
  on_annotation_click?: (annotation: PdfAnnotation, screen_x: number, screen_y: number) => void;
503
534
  /** Callback when FreeText tool is active and user clicks on empty area */
@@ -511,7 +542,7 @@ interface AnnotationOverlayProps {
511
542
  * Annotation Overlay Component
512
543
  * Handles mouse interactions for creating annotations
513
544
  */
514
- declare const AnnotationOverlay: React.FC<AnnotationOverlayProps>;
545
+ declare const AnnotationOverlay: React__default.FC<AnnotationOverlayProps>;
515
546
 
516
547
  /**
517
548
  * PDF Worker Setup
@@ -729,4 +760,4 @@ declare function load_pdf_config(config_file?: string): PdfViewerConfig;
729
760
  */
730
761
  declare const default_config: PdfViewerConfig;
731
762
 
732
- export { AnnotationOverlay, type CoordinateMapper, type CustomStamp, type MetadataDataItem, type MetadataFooterItem, type MetadataFormatType, type MetadataHeaderItem, type MetadataInput, type PageDimensions, type PdfAnnotation, type PdfBookmark, type PdfDocument, type PdfPage, PdfPageRenderer, PdfViewer, type PdfViewerConfig, PdfViewerLayout, type PdfViewerProps, build_config_from_ini, calculate_rectangle_coords, create_coordinate_mapper, default_config, download_pdf, download_xfdf, export_annotations_to_xfdf, generate_xfdf, get_viewport_dimensions, is_rectangle_too_small, load_pdf_config, load_pdf_config_async, load_pdf_document, parse_color, parse_number, parse_opacity, parse_string, pdf_rect_to_rectangle, rectangle_to_pdf_rect, save_and_download_pdf, save_annotations_to_pdf };
763
+ export { AnnotationOverlay, type CoordinateMapper, type CustomStamp, type HighlightOptions, type MetadataDataItem, type MetadataFooterItem, type MetadataFormatType, type MetadataHeaderItem, type MetadataInput, type PageDimensions, type PdfAnnotation, type PdfBookmark, type PdfDocument, type PdfPage, PdfPageRenderer, PdfViewer, type PdfViewerConfig, PdfViewerLayout, type PdfViewerProps, type PdfViewerRef, build_config_from_ini, calculate_rectangle_coords, create_coordinate_mapper, default_config, download_pdf, download_xfdf, export_annotations_to_xfdf, generate_xfdf, get_viewport_dimensions, is_rectangle_too_small, load_pdf_config, load_pdf_config_async, load_pdf_document, parse_color, parse_number, parse_opacity, parse_string, pdf_rect_to_rectangle, rectangle_to_pdf_rect, save_and_download_pdf, save_annotations_to_pdf };
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-S3AJUZ7D.js";
9
9
 
10
10
  // src/components/pdf_viewer/pdf_viewer.tsx
11
- import { useState as useState6, useEffect as useEffect6, useRef as useRef7, useCallback as useCallback2 } from "react";
11
+ import { useState as useState6, useEffect as useEffect6, useRef as useRef7, useCallback as useCallback2, forwardRef, useImperativeHandle } from "react";
12
12
  import { Save, Undo2 as Undo22, Redo2, PanelRight, ZoomIn, ZoomOut, RotateCcw, Square, Type } from "lucide-react";
13
13
 
14
14
  // src/components/pdf_viewer/pdf_worker_setup.ts
@@ -725,18 +725,29 @@ var AnnotationOverlay = ({
725
725
  }
726
726
  const get_annotation_props = () => {
727
727
  switch (annotation.type) {
728
- case "Highlight":
729
- const highlight_color = annotation.color || highlight_config.highlight_fill_color;
728
+ case "Highlight": {
729
+ let custom_highlight_options = null;
730
+ if (annotation.subject) {
731
+ try {
732
+ custom_highlight_options = JSON.parse(annotation.subject);
733
+ } catch {
734
+ }
735
+ }
736
+ const highlight_fill_color = custom_highlight_options?.background_color || annotation.color || highlight_config.highlight_fill_color;
737
+ const highlight_fill_opacity = custom_highlight_options?.background_opacity ?? highlight_config.highlight_fill_opacity;
738
+ const highlight_border_color = custom_highlight_options?.border_color || highlight_config.highlight_border_color;
730
739
  return {
731
- fill: hex_to_rgba(highlight_color, highlight_config.highlight_fill_opacity),
732
- stroke: highlight_config.highlight_border_color
740
+ fill: hex_to_rgba(highlight_fill_color, highlight_fill_opacity),
741
+ stroke: highlight_border_color
733
742
  };
734
- case "Square":
743
+ }
744
+ case "Square": {
735
745
  const square_color = annotation.color || square_config.square_fill_color;
736
746
  return {
737
747
  fill: hex_to_rgba(square_color, square_config.square_fill_opacity),
738
748
  stroke: square_config.square_border_color
739
749
  };
750
+ }
740
751
  default:
741
752
  return {
742
753
  fill: "rgba(0, 0, 255, 0.2)",
@@ -2272,7 +2283,7 @@ function load_pdf_config(config_file) {
2272
2283
 
2273
2284
  // src/components/pdf_viewer/pdf_viewer.tsx
2274
2285
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2275
- var PdfViewer = ({
2286
+ var PdfViewer = forwardRef(({
2276
2287
  url,
2277
2288
  className = "",
2278
2289
  scale: initial_scale = 1,
@@ -2301,7 +2312,7 @@ var PdfViewer = ({
2301
2312
  show_metadata_button,
2302
2313
  show_annotate_button,
2303
2314
  on_close
2304
- }) => {
2315
+ }, ref) => {
2305
2316
  const [pdf_document, setPdfDocument] = useState6(null);
2306
2317
  const [loading, setLoading] = useState6(true);
2307
2318
  const [error, setError] = useState6(null);
@@ -2661,6 +2672,60 @@ ${suffix_line}`;
2661
2672
  on_annotation_delete(annotation_id);
2662
2673
  }
2663
2674
  };
2675
+ useImperativeHandle(ref, () => ({
2676
+ /**
2677
+ * Create a highlight on a specific page region
2678
+ * @param page_index - Zero-based page index
2679
+ * @param rect - Rectangle coordinates in PDF space [x1, y1, x2, y2]
2680
+ * @param options - Optional styling overrides
2681
+ * @returns The highlight annotation ID
2682
+ */
2683
+ highlight_region: (page_index, rect, options) => {
2684
+ const highlight_config = config_ref.current?.highlight_annotation || default_config.highlight_annotation;
2685
+ const annotation = {
2686
+ id: `highlight_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
2687
+ type: "Highlight",
2688
+ page_index,
2689
+ rect,
2690
+ author: "API",
2691
+ date: (/* @__PURE__ */ new Date()).toISOString(),
2692
+ contents: "",
2693
+ color: options?.background_color || highlight_config.highlight_fill_color,
2694
+ flags: "api_highlight"
2695
+ // Marker to identify API-created highlights
2696
+ };
2697
+ if (options) {
2698
+ annotation.subject = JSON.stringify({
2699
+ border_color: options.border_color,
2700
+ background_color: options.background_color,
2701
+ background_opacity: options.background_opacity
2702
+ });
2703
+ }
2704
+ handle_annotation_create(annotation);
2705
+ return annotation.id;
2706
+ },
2707
+ /**
2708
+ * Remove a specific highlight by ID
2709
+ * @param id - The highlight annotation ID
2710
+ * @returns true if highlight was found and removed
2711
+ */
2712
+ remove_highlight: (id) => {
2713
+ const annotation = annotations.find((a) => a.id === id);
2714
+ if (annotation) {
2715
+ handle_annotation_delete(id);
2716
+ return true;
2717
+ }
2718
+ return false;
2719
+ },
2720
+ /**
2721
+ * Remove all highlights created via the highlight_region API
2722
+ */
2723
+ clear_all_highlights: () => {
2724
+ const api_highlights = annotations.filter((a) => a.flags === "api_highlight");
2725
+ api_highlights.forEach((a) => handle_annotation_delete(a.id));
2726
+ }
2727
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2728
+ }), [annotations, handle_annotation_create, handle_annotation_delete]);
2664
2729
  const handle_undo = useCallback2(() => {
2665
2730
  if (history_index > 0) {
2666
2731
  history_ref.current.saving = true;
@@ -2742,9 +2807,10 @@ ${suffix_line}`;
2742
2807
  const output_filename = `${filename_without_ext}_annotated.pdf`;
2743
2808
  const { save_annotations_to_pdf: save_annotations_to_pdf2, download_pdf: download_pdf2 } = await import("./pdf_saver-P2MJN45S.js");
2744
2809
  const pdf_bytes = await save_annotations_to_pdf2(url, annotations, output_filename, config_ref.current);
2745
- download_pdf2(pdf_bytes, output_filename);
2746
2810
  if (on_save) {
2747
2811
  on_save(pdf_bytes, output_filename);
2812
+ } else {
2813
+ download_pdf2(pdf_bytes, output_filename);
2748
2814
  }
2749
2815
  } catch (error2) {
2750
2816
  console.error("PdfViewer: Error saving PDF:", error2);
@@ -3271,7 +3337,8 @@ ${suffix_line}`;
3271
3337
  }
3272
3338
  )
3273
3339
  ] });
3274
- };
3340
+ });
3341
+ PdfViewer.displayName = "PdfViewer";
3275
3342
 
3276
3343
  // src/utils/xfdf_generator.ts
3277
3344
  function format_pdf_date(date) {