From e971857d5301559035d913e6806dfd1bb4832fd3 Mon Sep 17 00:00:00 2001 From: Mohammad Khatib Date: Thu, 15 Dec 2022 12:18:42 -0800 Subject: [PATCH] [QuickSignPDF] Add Font and Stroke Sizes and Colors (#72) --- src/QuickSignPDF/QuickSignPDF.tsx | 227 +++++++++++++++++++----------- src/ui/ColorPickerButton.tsx | 65 +++++++++ 2 files changed, 206 insertions(+), 86 deletions(-) create mode 100644 src/ui/ColorPickerButton.tsx diff --git a/src/QuickSignPDF/QuickSignPDF.tsx b/src/QuickSignPDF/QuickSignPDF.tsx index a0611f5..5e53ca3 100644 --- a/src/QuickSignPDF/QuickSignPDF.tsx +++ b/src/QuickSignPDF/QuickSignPDF.tsx @@ -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(); @@ -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 } @@ -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': @@ -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; } }); @@ -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(); const [doc, setDoc] = useState(); const [activePage, setActivePage] = useState(1); const [scale, setScale] = useState(1); const [isDrawingMode, setIsDrawingMode] = useState(false); - + const [fontSize, setFontSize] = useState(16); + const [color, setColor] = useState('#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. @@ -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 (
@@ -202,113 +244,126 @@ const QuickSignPDF = (): JSX.Element => { {pdf && (
{/* Toolbar */} -
+
- Upload New File +
+ + Sign a New File +
- <> -
- -
-
- -
-
- -
-
- -
- + updatePageObjects(); + }} + > + + + + +
Page ({activePage} of {doc?.getPageCount()}) + + -
-
-
- - -
{' '}
{pdf && ( diff --git a/src/ui/ColorPickerButton.tsx b/src/ui/ColorPickerButton.tsx new file mode 100644 index 0000000..54484dc --- /dev/null +++ b/src/ui/ColorPickerButton.tsx @@ -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 ( +
+ + + {displayColorPicker ? ( +
+ +
+ ) : null} +
+ ); +}; + +export default ColorPickerButton;