#atom

Subtitle:

A React-based custom plugin for rendering elements at precise coordinates on PDF pages


Core Idea:

The Coordinate Marker Plugin leverages React PDF Viewer's plugin architecture to render React components at exact coordinates within PDF pages, enabling annotation, highlighting, or interactive elements at specific positions in documents.


Key Principles:

  1. React-Centric Implementation:
    • Uses React components and portals instead of direct DOM manipulation
  2. Coordinate-Based Positioning:
    • Places elements at precise (x,y) coordinates relative to PDF page dimensions
  3. Page-Focused Navigation:
    • Automatically navigates to specific pages and scrolls to relevant coordinates

Why It Matters:


How to Implement:

  1. Define Plugin Structure:
    • Create a TypeScript interface for the plugin and its data types
    • Implement a store for managing coordinates and rendering state
  2. Use Proper Rendering Methods:
    • Leverage the renderPageLayer hook for consistent rendering
    • Ensure components respect the PDF scaling and rotation
  3. Handle Coordinate Rendering:
    • Position elements using percentage-based positioning for scale independence
    • Update positions when document scale or rotation changes

Example:

import * as React from 'react';
import { createStore, Plugin, PluginFunctions, PluginOnPageRender, PluginRenderPageLayer } from '@react-pdf-viewer/core';

// Define marker data type
interface Coordinate {
  x0: number;
  y0: number;
  x1: number;
  y1: number;
  className?: string;
}

// Define store properties
interface StoreProps {
  coordinates: Coordinate[];
  scale: number;
  rotation: number;
  isMatch: boolean;
  jumpToDestination?: (destination: {
    pageIndex: number;
    bottomOffset?: (viewportWidth: number, viewportHeight: number) => number;
    leftOffset?: (viewportWidth: number, viewportHeight: number) => number;
    scaleTo?: number;
  }) => void;
}

// Define plugin interface
interface CoordinateMarkerPlugin extends Plugin {
  setCoordinates: (page: number, coordinates: Coordinate[], isMatch: boolean) => void;
  jumpToCoordinate: (page: number, coordinate: Coordinate) => void;
}

interface CoordinateMarkerPluginOptions {
	page?: number;
	coordinates?: Coordinate[];
	isMatch?: boolean;
}

// Create the marker component that will be rendered on each page
const CoordinateMarkers: React.FC<{
  pageIndex: number;
  coordinates: Coordinate[];
  isMatch: boolean;
}> = ({ pageIndex, coordinates, isMatch  }) => {
  if (!coordinates || coordinates.length === 0) {
    return null;
  }
  
  return (
    <>
      {coordinates.map((coordinate, index) => (
        <div
          key={`${pageIndex}-marker-${index}`}
          className={`rpv-coordinate-marker ${coordinate.className || ''}`}
          style={{
            position: 'absolute',
            left: `${(coordinate.x0 * 100) / 612}%`,
            top: `${(coordinate.y0 * 100) / 792}%`,
            width: `${((coordinate.x1 - coordinate.x0) * 100)/612}%`,
            height: `${((coordinate.x1 - coordinate.x0) * 100)/792}%`,
            border: `2px solid ${isMatch ? "rgba(0, 255, 0, 0.5)" : "rgba(255, 0, 0, 0.5)"}`,
            pointerEvents: "none"
          }}
        >
        </div>
      ))}
    </>
  );
};

// Create the plugin
const CoordinateMarkerPlugin = (options: CoordinateMarketPluginOptions = {}): CoordinateMarkerPlugin => {
	const { page = -1, coordinates = [], isMatch = true } = options;
  const store = React.useMemo(() => createStore<StoreProps>({
    coordinates,
    pageIndex: page,
    isMatch,
    scale: 1,
    rotation: 0,
  }), []);
  
  const jumpToCoordinate = (page: number, coordinate: Coordinate) => {
    const jumpToDestination = store.get('jumpToDestination');
    
    if (!jumpToDestination) {
      return;
    }
    
    jumpToDestination({
      pageIndex: page,
      bottomOffset: (_, viewportHeight) => viewportHeight - coordinate.y0,
      leftOffset: (_, __) => coordinate.x0,
    });
  };

  return {
    install: (pluginFunctions: PluginFunctions) => {
      store.update('jumpToDestination', pluginFunctions.jumpToDestination);
    },
    
    onPageRender: (e: PluginOnPageRender) => {
      store.update('scale', e.scale);
      store.update('rotation', e.rotation);
    },
    
    // Use renderPageLayer instead of renderViewer with portals
    renderPageLayer: (props: PluginRenderPageLayer) => (
	    if (props.pageIndex !== store.get('pageIndex')) { return <></>; }
      <CoordinateMarkers
        pageIndex={props.pageIndex}
        coordinates={store.get('coordinates')}
        isMatch={store.get('isMatch')}
      />
    ),
    
    setCoordinates: (page: number, coordinates: Coordinate[], isMatch: bool) => {
	  if (page < 0) {
		  return;
	  }
      store.update('coordinates', coordinates);
      store.update('pageIndex', page);
      store.update('isMatch', isMatch);
      // If there are coordinates, jump to the first one
      if (coordinates.length > 0) {
        jumpToCoordinate(page, coordinates[0]);
      }
    },
    
    jumpToCoordinate,
  };
};

export default coordinateMarkerPlugin;
export type { Coordinate, CoordinateMarkerPlugin, CoordinateMarkerPluginOptions };

Connections:


References:

  1. Primary Source:
    • React PDF Viewer Documentation: Plugin Development Guide
  2. Additional Resources:

Tags:

#react #pdf #coordinates #markers #plugin #portals #annotation #custom-plugin


Connections:


Sources: