Skip to content

Commit

Permalink
[QuickSignPDF] Add Font and Stroke Sizes and Colors (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkhatib authored Dec 15, 2022
1 parent 71f99c5 commit e971857
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 86 deletions.
227 changes: 141 additions & 86 deletions src/QuickSignPDF/QuickSignPDF.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
import {
ArrowLeftCircleIcon,
ArrowRightCircleIcon,
PhotoIcon,
ArrowUpTrayIcon,
LanguageIcon,
MagnifyingGlassMinusIcon,
MagnifyingGlassPlusIcon,
PaintBrushIcon,
TrashIcon,
} from '@heroicons/react/24/solid';
import fabric from 'fabric';
import { FabricJSEditor } from 'fabricjs-react';
import { saveAs } from 'file-saver';
import { PDFDocument, rgb } from 'pdf-lib';
import { LineCapStyle, PDFDocument, rgb } from 'pdf-lib';
import { getDocument } from 'pdfjs-dist';
import { PDFDocumentProxy } from 'pdfjs-dist/types/display/api';
import { useCallback, useEffect, useRef, useState } from 'react';
import UploadButton from '../components/UploadButton';
import FabricPagePreview from '../PDFViewer/FabricPagePreview';
import PagePreview from '../PDFViewer/PagePreview';
import ColorPickerButton, { hexToRgb } from '../ui/ColorPickerButton';

async function createPDF(files: File[]) {
const pdfDoc = await PDFDocument.create();
Expand All @@ -34,6 +40,7 @@ async function createPDF(files: File[]) {
};
}

const DEFAULT_COLOR = { red: 0, green: 0, blue: 0 };
async function drawFabricObjectsOnAllPages(
pdfDoc: PDFDocument,
fabricObjects: { [index: number]: any }
Expand All @@ -46,6 +53,8 @@ async function drawFabricObjectsOnAllPages(
newPdfDoc.addPage(page);
const json = fabricObjects[index + 1];
json?.objects.forEach((object: any) => {
const fill = object.fill ? hexToRgb(object.fill) : DEFAULT_COLOR;
const stroke = object.stroke ? hexToRgb(object.stroke) : DEFAULT_COLOR;
switch (object.type) {
// TODO: Maybe add more annotations tools (circle/rect...);
case 'text':
Expand All @@ -58,18 +67,27 @@ async function drawFabricObjectsOnAllPages(
// page.getHeight() is used here to transform the y location
// since PDF coordinate space top-bottom is 0,0 vs fabric canvas
// which has top-left 0,0
y: page.getHeight() - object.top - object.height + 4,
lineHeight: object.lineHeight,
color: rgb(0, 0, 0),
y:
page.getHeight() -
object.top -
object.height +
object.fontSize / 4,
lineHeight: 100 * object.lineHeight * object.fontSize,
color: rgb(fill.red, fill.green, fill.blue),
size: object.fontSize * object.scaleX,
});
break;
case 'path':
console.log(object);
page.moveTo(0, page.getHeight());
const path = object.path
.map((commandArr: (string | number)[]) => commandArr.join(' '))
.join(' ');
page.drawSvgPath(path);
page.drawSvgPath(path, {
borderLineCap: LineCapStyle.Round,
borderColor: rgb(stroke.red, stroke.green, stroke.blue),
borderWidth: object.strokeWidth,
});
break;
}
});
Expand All @@ -81,13 +99,34 @@ async function drawFabricObjectsOnAllPages(
};
}

const FONT_SIZES = [
1,
2,
4,
8,
12,
16,
24,
32,
36,
40,
48,
60,
64,
80,
96,
128,
160,
];

const QuickSignPDF = (): JSX.Element => {
const [pdf, setPDF] = useState<PDFDocumentProxy>();
const [doc, setDoc] = useState<PDFDocument>();
const [activePage, setActivePage] = useState<number>(1);
const [scale, setScale] = useState(1);
const [isDrawingMode, setIsDrawingMode] = useState<boolean>(false);

const [fontSize, setFontSize] = useState<number>(16);
const [color, setColor] = useState<string>('#000');
// Tracks all pages drawables so we can burn them to the PDF once the user
// click Sign & Download. This also allows humans to continue editing different
// pages.
Expand Down Expand Up @@ -184,11 +223,14 @@ const QuickSignPDF = (): JSX.Element => {
}
editor.canvas.on('mouse:up', onMouseUp);
editor.canvas.isDrawingMode = isDrawingMode;
editor.canvas.freeDrawingBrush.width = fontSize;
editor.canvas.freeDrawingBrush.color = color;

return () => {
if (!editor) return;
editor.canvas.off('mouse:up', onMouseUp);
};
}, [isDrawingMode, updatePageObjects]);
}, [color, fontSize, isDrawingMode, updatePageObjects]);

return (
<div className="h-full flex flex-col">
Expand All @@ -202,113 +244,126 @@ const QuickSignPDF = (): JSX.Element => {
{pdf && (
<div className="flex flex-col flex-grow">
{/* Toolbar */}
<div className="flex p-3">
<div className="flex items-center p-3">
<div className="h-10 w-48 mr-2">
<UploadButton onDrop={onDrop} accept=".pdf" fullSized={false}>
<span className="text-base">Upload New File</span>
<div className="flex items-center text-center">
<ArrowUpTrayIcon className="h-5 mr-2" />
<span className="text-base">Sign a New File</span>
</div>
</UploadButton>
</div>
<>
<div>
<button
className="h-10 self-end bg-gray-500 text-white px-3 py-2 hover:bg-green-700 mr-2"
onClick={() => {
if (!editorRef.current) return;
setIsDrawingMode(false);
const text = new fabric.fabric.Textbox('Hello', {
fontFamily: 'Helvetica',
fontSize: 16,
hasControls: false,
width: 400,
});
editorRef.current.canvas.add(text);
<div>
<select
className="border border-gray-100"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
>
{FONT_SIZES.map((size) => (
<option value={size}>{size}</option>
))}
</select>
</div>
<div className="ml-2">
<ColorPickerButton
onChange={(color) => setColor(color)}
color={color}
/>
</div>
<button
className=" text-gray-500 px-3 py-2 hover:text-green-700 mr-2"
onClick={() => {
if (!editorRef.current) return;
setIsDrawingMode(false);
const text = new fabric.fabric.Textbox('Hello', {
fontFamily: 'Helvetica',
fontSize,
hasControls: false,
width: 400,
fill: color,
});
editorRef.current.canvas.add(text);

updatePageObjects();
}}
>
Add Text
</button>
</div>
<div>
<button
className="h-10 self-end bg-gray-500 text-white px-3 py-2 hover:bg-green-700 mr-2"
onClick={() => {
if (!editorRef.current) return;
setIsDrawingMode((isDrawingMode) => !isDrawingMode);
}}
>
{isDrawingMode ? 'Stop Drawing' : 'Start Drawing'}
</button>
</div>
<div>
<button
className="h-10 self-end bg-gray-500 text-white px-3 py-2 hover:bg-green-700 mr-2"
onClick={() => {
editorRef.current?.deleteAll();
updatePageObjects();
}}
>
Clear
</button>
</div>
<div>
<button
className="h-10 self-end bg-gray-500 text-white px-3 py-2 hover:bg-green-700 mr-2"
onClick={() => {
editorRef.current?.deleteSelected();
updatePageObjects();
}}
>
Delete Selected
</button>
</div>
</>
updatePageObjects();
}}
>
<LanguageIcon className="w-5" />
</button>
<button
className=" text-gray-500 px-1 hover:text-green-700 mr-2"
onClick={() => {
if (!editorRef.current) return;
setIsDrawingMode((isDrawingMode) => !isDrawingMode);
}}
>
{isDrawingMode ? (
<PaintBrushIcon className="w-5" fill="text-green-500" />
) : (
<PaintBrushIcon className="w-5" />
)}
</button>
<button
className="flex text-gray-500 px-1 hover:text-green-700 mr-2"
onClick={() => {
editorRef.current?.deleteAll();
updatePageObjects();
}}
>
<TrashIcon className="w-5" />
<span>All</span>
</button>
<button
className="flex text-gray-500 px-1 hover:text-green-700 mr-2"
onClick={() => {
editorRef.current?.deleteSelected();
updatePageObjects();
}}
>
<TrashIcon className="w-5" />
<span>Selected</span>
</button>

<div className="flex-grow"></div>
<div className="flex items-center">
<button
disabled={!pdf || activePage > (doc?.getPageCount() || 1)}
className={` text-gray-500 px-3 py-2 ${
className={` text-gray-500 px-1 ${
activePage <= 1
? 'cursor-not-allowed '
: 'hover:text-green-700'
}`}
onClick={prevPage}
>
<ArrowLeftCircleIcon className="h-8" />
<ArrowLeftCircleIcon className="w-5" />
</button>
<span className="px-2 text-gray-500">
Page ({activePage} of {doc?.getPageCount()})
</span>
<button
disabled={!pdf || activePage >= (doc?.getPageCount() || 1)}
className={` text-gray-500 px-3 py-2 ${
className={` text-gray-500 px-1 ${
activePage >= doc!.getPageCount()
? 'cursor-not-allowed '
: 'hover:text-green-700'
}`}
onClick={nextPage}
>
<ArrowRightCircleIcon className="h-8" />
<ArrowRightCircleIcon className="w-5" />
</button>
<button
className="h-8 text-gray-500 px-1 hover:text-green-700"
onClick={() => setScale((scale) => scale / 1.2)}
disabled={!pdf}
>
<MagnifyingGlassMinusIcon className="w-5" />
</button>
<button
className="h-8 text-gray-500 px-1 hover:text-green-700"
onClick={() => setScale((scale) => scale * 1.2)}
disabled={!pdf}
>
<MagnifyingGlassPlusIcon className="w-5" />
</button>
</div>
<div>
<div className="flex justify-start align-top items-end ml-2">
<button
className="h-5 bg-gray-500 text-white px-1 hover:bg-green-700 mr-2"
onClick={() => setScale((scale) => scale / 1.2)}
disabled={!pdf}
>
<PhotoIcon className="w-3" />
</button>
<button
className="h-8 bg-gray-500 text-white px-1 hover:bg-green-700"
onClick={() => setScale((scale) => scale * 1.2)}
disabled={!pdf}
>
<PhotoIcon className="w-5" />
</button>
</div>{' '}
</div>
</div>
{pdf && (
Expand Down
65 changes: 65 additions & 0 deletions src/ui/ColorPickerButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState } from 'react';
import { ColorResult, SketchPicker } from 'react-color';

interface Props {
onChange: (value: string) => void;
color: string;
}

export const hexToRgb = (
hex: string
): { red: number; green: number; blue: number } => {
// Parse the hexadecimal string to a number
const int = parseInt(hex.replace('#', ''), 16);

// Extract the red, green, and blue values using bitwise operators
const r = (int >> 16) & 0xff;
const g = (int >> 8) & 0xff;
const b = int & 0xff;

// Divide the values by 255 to convert them to floating-point numbers between 0 and 1
const red = r / 255;
const green = g / 255;
const blue = b / 255;

// Return the RGB values as an object
return { red, green, blue };
};

const ColorPickerButton = ({ onChange, color }: Props): JSX.Element => {
const [displayColorPicker, setDisplayColorPicker] = useState(false);
const [selectedColor, setSelectedColor] = useState(color);

const handleClick = () => {
setDisplayColorPicker(!displayColorPicker);
};

const handleChange = (color: ColorResult) => {
setSelectedColor(color.hex);
if (onChange) {
onChange(color.hex);
}
};

return (
<div className="relative flex">
<button onClick={handleClick}>
<div
style={{
background: selectedColor,
width: 25,
height: 25,
}}
/>
</button>

{displayColorPicker ? (
<div className="absolute z-30 top-7">
<SketchPicker color={selectedColor} onChange={handleChange} />
</div>
) : null}
</div>
);
};

export default ColorPickerButton;

0 comments on commit e971857

Please sign in to comment.