Skip to content

Commit

Permalink
Implement ink annotation drawing and editing
Browse files Browse the repository at this point in the history
  • Loading branch information
mrtcode committed May 11, 2023
1 parent 0ce42ee commit 25fbd14
Show file tree
Hide file tree
Showing 18 changed files with 556 additions and 79 deletions.
2 changes: 1 addition & 1 deletion demo/pdf/annotations.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added res/icons/darwin/eraser-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/eraser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/ink-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/ink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/icons/darwin/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/common/components/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ function Toolbar(props) {
<span className="button-background"/>
</button>
)}
{props.type === 'pdf' && (
<button
tabIndex={-1}
className={cx('toolbarButton ink', { toggled: props.tool.type === 'ink' })}
title={intl.formatMessage({ id: 'pdfReader.selectArea' })}
onClick={() => handleToolClick('ink')}
>
<span className="button-background"/>
</button>
)}
{props.type === 'pdf' && (
<button
tabIndex={-1}
className={cx('toolbarButton eraser', { toggled: props.tool.type === 'eraser' })}
title={intl.formatMessage({ id: 'pdfReader.selectArea' })}
onClick={() => handleToolClick('eraser')}
>
<span className="button-background"/>
</button>
)}
<button
tabIndex={-1}
className="toolbarButton global-color"
Expand Down
6 changes: 4 additions & 2 deletions src/common/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class Reader {
tool: {
type: 'pointer',
color: ANNOTATION_COLORS[0][1],
pathWidth: 2,
eraserSize: 16
},
thumbnails: [],
outline: [],
Expand Down Expand Up @@ -1110,7 +1112,7 @@ class Reader {
if (annotations.length > 1) {
setMultiDragPreview(dataTransfer);
}
annotations = annotations.filter(x => x.type !== 'ink');
// annotations = annotations.filter(x => x.type !== 'ink');
let plainText = annotations.map((annotation) => {
let formatted = '';
if (annotation.text) {
Expand Down Expand Up @@ -1151,7 +1153,7 @@ class Reader {
// which also prevents word processors from using `text/plain`, and
// results to dumped base64 content (LibreOffice) or image (Google Docs)
dataTransfer.clearData();
dataTransfer.setData('text/plain', plainText);
dataTransfer.setData('text/plain', plainText || ' ');
this._onSetDataTransferAnnotations(dataTransfer, annotations, fromText);
}

Expand Down
16 changes: 16 additions & 0 deletions src/common/stylesheets/components/_icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@
@include icon($text-tool-icon-active);
}
}

.ink {
@include icon($ink-tool-icon);

&.toggled {
@include icon($ink-tool-icon-active);
}
}

.eraser {
@include icon($eraser-tool-icon);

&.toggled {
@include icon($eraser-tool-icon-active);
}
}
}

// Toolbar sidebar
Expand Down
4 changes: 4 additions & 0 deletions src/common/stylesheets/themes/_light-darwin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ $area-tool-icon: "darwin/area";
$area-tool-icon-active: "darwin/area-white";
$text-tool-icon: "darwin/text";
$text-tool-icon-active: "darwin/text-white";
$ink-tool-icon: "darwin/ink";
$ink-tool-icon-active: "darwin/ink-white";
$eraser-tool-icon: "darwin/eraser";
$eraser-tool-icon-active: "darwin/eraser-white";

// Search
$search-margin: 6px 13px;
Expand Down
6 changes: 4 additions & 2 deletions src/pdf/lib/extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getLines,
extractLinks, getRangeByHighlight
} from './text';
import { getPositionBoundingRect } from './utilities';

export class Extractor {
constructor(pdfViewer, getAnnotations) {
Expand Down Expand Up @@ -131,9 +132,10 @@ export class Extractor {
getSortIndex(position) {
let chars = this.getPageCharsSync(position.pageIndex);
let page = position.pageIndex;
let offset = chars.length && getClosestOffset(chars, position.rects[0]) || 0;
let rect = getPositionBoundingRect(position);
let offset = chars.length && getClosestOffset(chars, rect) || 0;
let pageHeight = this.pdfViewer._pages[position.pageIndex].viewport.viewBox[3];
let top = pageHeight - position.rects[0][3];
let top = pageHeight - rect[3];
if (top < 0) {
top = 0;
}
Expand Down
194 changes: 194 additions & 0 deletions src/pdf/lib/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@

// Calculates the Euclidean distance between two points.
function euclideanDistance(p1, p2) {
const dx = p1[0] - p2[0];
const dy = p1[1] - p2[1];
return Math.sqrt(dx * dx + dy * dy);
}

// Returns a point between two given points with a specified ratio.
// Rounds the coordinates to a maximum of 3 decimal places.
function interpolatePoint(p1, p2, ratio) {
return [
parseFloat((p1[0] + (p2[0] - p1[0]) * ratio).toFixed(3)),
parseFloat((p1[1] + (p2[1] - p1[1]) * ratio).toFixed(3))
];
}

// Filters out points that are too close.
export function filterClosePoints(points, minThreshold) {
const filteredPoints = [points[0], points[1]];

for (let i = 2; i < points.length; i += 2) {
const prevPoint = [filteredPoints[filteredPoints.length - 2], filteredPoints[filteredPoints.length - 1]];
const currentPoint = [points[i], points[i + 1]];
const distance = euclideanDistance(prevPoint, currentPoint);

if (distance >= minThreshold) {
filteredPoints.push(currentPoint[0], currentPoint[1]);
}
}

return filteredPoints;
}

// Inserts additional points if the distance between two consecutive points
// is larger than the maxThreshold value.
export function insertMissingPoints(points, maxThreshold) {
const processedPoints = [points[0], points[1]];

for (let i = 2; i < points.length; i += 2) {
const prevPoint = [processedPoints[processedPoints.length - 2], processedPoints[processedPoints.length - 1]];
const currentPoint = [points[i], points[i + 1]];
const distance = euclideanDistance(prevPoint, currentPoint);

if (distance > maxThreshold) {
const numPointsToInsert = Math.ceil(distance / maxThreshold) - 1;
for (let j = 0; j < numPointsToInsert; j++) {
const ratio = (j + 1) / (numPointsToInsert + 1);
const interpolatedPoint = interpolatePoint(prevPoint, currentPoint, ratio);
processedPoints.push(interpolatedPoint[0], interpolatedPoint[1]);
}
}
processedPoints.push(currentPoint[0], currentPoint[1]);
}

return processedPoints;
}

// Smoothens path edges using the Catmull-Rom Spline technique.
export function catmullRomSpline(points, segments) {
function calculatePoint(p0, p1, p2, p3, t) {
const t2 = t * t;
const t3 = t2 * t;

const x = 0.5 * ((2 * p1[0]) +
(-p0[0] + p2[0]) * t +
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);
const y = 0.5 * ((2 * p1[1]) +
(-p0[1] + p2[1]) * t +
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);

return [x, y];
}

const smoothedPoints = [];
const numPoints = points.length / 2;

for (let i = 0; i < numPoints - 1; i++) {
const p0 = (i === 0) ? [points[0], points[1]] : [points[(i - 1) * 2], points[(i - 1) * 2 + 1]];
const p1 = [points[i * 2], points[i * 2 + 1]];
const p2 = [points[(i + 1) * 2], points[(i + 1) * 2 + 1]];
const p3 = (i === numPoints - 2) ? [points[numPoints * 2 - 2], points[numPoints * 2 - 1]] : [points[(i + 2) * 2], points[(i + 2) * 2 + 1]];

for (let j = 0; j < segments; j++) {
const t = j / segments;
const point = calculatePoint(p0, p1, p2, p3, t);
smoothedPoints.push(point[0], point[1]);
}
}

// Add the last point
smoothedPoints.push(points[numPoints * 2 - 2], points[numPoints * 2 - 1]);

return smoothedPoints;
}


export function addPointToPath(path, newPoint) {
const minThreshold = 5;
const maxThreshold = 20;
const segments = 10;

// Add the new point to the path
path.push(newPoint[0], newPoint[1]);

// Filter close points
const filteredPath = filterClosePoints(path, minThreshold);

// Insert missing points
const interpolatedPath = insertMissingPoints(filteredPath, maxThreshold);

// Smooth the path using Catmull-Rom Spline
const smoothedPath = catmullRomSpline(interpolatedPath, segments);

return smoothedPath;
}

export function applyTransformationMatrixToInkPosition(matrix, position) {
const { paths, width } = position;
const a = matrix[0], b = matrix[1], c = matrix[2], d = matrix[3], e = matrix[4], f = matrix[5];
const transformedPaths = paths.map(path => {
const transformedPath = [];
for (let i = 0; i < path.length; i += 2) {
const x = path[i];
const y = path[i + 1];
const newX = a * x + c * y + e;
const newY = b * x + d * y + f;
transformedPath.push(newX, newY);
}
return transformedPath;
});
let scaleFactor = Math.sqrt(Math.abs(a * d - b * c));
const transformedWidth = width * scaleFactor;
return {
...position,
paths: transformedPaths,
width: transformedWidth
};
}

function removeIntersectingPoints(circleCenterX, circleCenterY, circleWidth, position) {
const isPointInsideCircle = (x, y, centerX, centerY, radius) => {
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= radius;
};

const newPathList = [];
let currentPath = [];

position.paths.forEach(path => {
for (let i = 0; i < path.length; i += 2) {
const x = path[i];
const y = path[i + 1];

if (!isPointInsideCircle(x, y, circleCenterX, circleCenterY, circleWidth / 2)) {
currentPath.push(x, y);
} else {
if (currentPath.length > 0) {
newPathList.push(currentPath);
currentPath = [];
}
}
}

if (currentPath.length > 0) {
newPathList.push(currentPath);
currentPath = [];
}
});

position = { ...position, paths: newPathList };
return position;
}

export function eraseInk(circleCenterX, circleCenterY, circleWidth, annotations) {
const modifiedAnnotations = [];

annotations.forEach(annotation => {
const originalTotalPoints = annotation.position.paths.reduce((total, path) => total + path.length, 0);
let { position } = annotation;
position = removeIntersectingPoints(circleCenterX, circleCenterY, circleWidth, position);
const newTotalPoints = position.paths.reduce((total, path) => total + path.length, 0);

if (newTotalPoints !== originalTotalPoints) {
modifiedAnnotations.push({ ...annotation, position });
}
});

return modifiedAnnotations;
}
39 changes: 39 additions & 0 deletions src/pdf/lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,42 @@ export function setCaretPosition(event) {
console.error('Your browser does not support caret position from point.');
}
}

export function distanceBetweenRects(rect1, rect2) {
const [x1A, y1A, x2A, y2A] = rect1;
const [x1B, y1B, x2B, y2B] = rect2;
// Calculate the horizontal distance (dx, dy) between the two rectangles
// If rectangles overlap horizontally, dx, dy is set to 0
const dx = Math.max(x1A, x1B) > Math.min(x2A, x2B) ? Math.max(x1A, x1B) - Math.min(x2A, x2B) : 0;
const dy = Math.max(y1A, y1B) > Math.min(y2A, y2B) ? Math.max(y1A, y1B) - Math.min(y2A, y2B) : 0;
return Math.hypot(dx, dy);
}

export function getTransformFromRects(rect1, rect2) {
const x1 = rect1[0];
const y1 = rect1[1];
const x2 = rect1[2];
const y2 = rect1[3];

const x1Prime = rect2[0];
const y1Prime = rect2[1];
const x2Prime = rect2[2];
const y2Prime = rect2[3];

// Calculate scaling factors
const scaleX = (x2Prime - x1Prime) / (x2 - x1);
const scaleY = (y2Prime - y1Prime) / (y2 - y1);

// Calculate translation factors
const translateX = x1Prime - x1 * scaleX;
const translateY = y1Prime - y1 * scaleY;

// Create the transformation matrix for PDF
const matrix = [
scaleX, 0,
0, scaleY,
translateX, translateY
];

return matrix;
}
Loading

0 comments on commit 25fbd14

Please sign in to comment.