From bbf92ffc4481bb102c0bafc3fa76c509d08b061b Mon Sep 17 00:00:00 2001 From: ngyngcphu Date: Tue, 5 Dec 2023 23:23:49 +0700 Subject: [PATCH] feat(edit-pdf): use util pdf-editor for configuration from back-end --- package.json | 2 + .../order/desktop/ConfirmOrderDesktop.tsx | 5 +- .../order/desktop/OrderSuccessDesktop.tsx | 199 ++++++------ .../order/desktop/UploadAndPreviewDesktop.tsx | 42 ++- .../order/mobile/ConfirmOrderForm.tsx | 21 +- .../order/mobile/PreviewDocument.tsx | 10 + .../order/mobile/UploadDocumentForm.tsx | 35 ++- src/hooks/usePrintingRequestMutation.hook.ts | 15 +- src/pages/HomePage.tsx | 8 +- src/services/printingRequest.service.ts | 6 + src/utils/editPdf.ts | 283 ++++++++++++++++++ src/utils/index.ts | 1 + yarn.lock | 49 ++- 13 files changed, 566 insertions(+), 110 deletions(-) create mode 100644 src/utils/editPdf.ts diff --git a/package.json b/package.json index 8fca390..664c81b 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^5.8.4", "@tanstack/react-table": "^8.10.7", + "buffer": "^6.0.3", "moment": "^2.29.4", "openapi-fetch": "^0.8.1", + "pdf-lib": "^1.17.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.4", diff --git a/src/components/order/desktop/ConfirmOrderDesktop.tsx b/src/components/order/desktop/ConfirmOrderDesktop.tsx index 86ad9dd..a4ac121 100644 --- a/src/components/order/desktop/ConfirmOrderDesktop.tsx +++ b/src/components/order/desktop/ConfirmOrderDesktop.tsx @@ -25,6 +25,7 @@ import { usePrintingRequestQuery } from '@hooks'; import { useOrderPrintStore, useOrderWorkflowStore } from '@states'; import { formatFileSize } from '@utils'; import { usePreviewDocumentDesktop } from './PreviewDocumentDesktop'; +import { useOrderSuccessDesktop } from './OrderSuccessDesktop'; export const ConfirmOrderDektop: Component<{ initialTotalCost: MutableRefObject }> = ({ initialTotalCost @@ -37,6 +38,7 @@ export const ConfirmOrderDektop: Component<{ initialTotalCost: MutableRefObject< } = usePrintingRequestQuery(); const { openPreviewDocumentDesktop, PreviewDocumentDesktop } = usePreviewDocumentDesktop(); + const { openOrderSuccessDesktop, OrderSuccessDesktop } = useOrderSuccessDesktop(); const { totalCost, setTotalCost } = useOrderPrintStore(); const { setDesktopOrderStep } = useOrderWorkflowStore(); @@ -240,7 +242,7 @@ export const ConfirmOrderDektop: Component<{ initialTotalCost: MutableRefObject< ? 'blue' : 'gray' } - onClick={() => setDesktopOrderStep(4)} + onClick={openOrderSuccessDesktop} disabled={!remainCoins || remainCoins < totalCost + (serviceFee ?? 0)} > Confirm Order @@ -249,6 +251,7 @@ export const ConfirmOrderDektop: Component<{ initialTotalCost: MutableRefObject< {} + {} ); }; diff --git a/src/components/order/desktop/OrderSuccessDesktop.tsx b/src/components/order/desktop/OrderSuccessDesktop.tsx index d98c6ec..e9b8b92 100644 --- a/src/components/order/desktop/OrderSuccessDesktop.tsx +++ b/src/components/order/desktop/OrderSuccessDesktop.tsx @@ -1,41 +1,45 @@ -import { Button, Card, CardBody, Typography } from '@material-tailwind/react'; -import { CheckIcon } from '@heroicons/react/24/outline'; -import { DocumentChartBarIcon } from '@heroicons/react/24/outline'; -import coin from '@assets/coin.png'; +import { MutableRefObject, useState } from 'react'; +import { + Button, + Card, + CardBody, + Dialog, + DialogBody, + DialogHeader, + IconButton, + Typography +} from '@material-tailwind/react'; +import { CheckIcon, DocumentChartBarIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import coinImage from '@assets/coin.png'; +import { useOrderPrintStore, useOrderWorkflowStore } from '@states'; -export function OrderSuccessDesktop() { - // const { setOrderStep } = useOrderWorkflowStore(); - const detail_order = [ - { - name: 'Order number', - detail: '#1234-5678' - }, - { - name: 'Pick-up location', - detail: 'Tiệm in thư viện H3, tầng 1' - }, - { - name: 'Print cost', - detail: '2.400', - coin: true - }, - { - name: 'Service cost', - detail: '2', - coin: true - }, - { - name: 'Total', - detail: '2.402', - coin: true - } - ]; - return ( - <> -
- -
-
+export function useOrderSuccessDesktop() { + const [openDialog, setOpenDialog] = useState(false); + + const OrderSuccessDesktop: Component<{ + initialTotalCost: MutableRefObject; + serviceFee?: number; + }> = ({ initialTotalCost, serviceFee }) => { + const { totalCost, setTotalCost, setIsFileUploadSuccess } = useOrderPrintStore(); + const { setDesktopOrderStep } = useOrderWorkflowStore(); + + const handleExistOrderSuccessForm = () => { + setIsFileUploadSuccess(false); + setTotalCost(0); + initialTotalCost.current = 0; + setDesktopOrderStep(0); + }; + + return ( + setOpenDialog(false)}> + + setOpenDialog(false)}> + + + + +
+
@@ -45,62 +49,77 @@ export function OrderSuccessDesktop() {
- - - Order details - + +

Order details

-
- -
-
- {detail_order.map((item, index) => ( -
- - {`${item.name}:`} - -
- {item.coin && ( - - )} - - {item.detail} - -
+
+
+
+

Order number:

+

{`#1234-5678`}

+
+
+

Pick-up location:

+

Tiệm in thư viện H3, tầng 1

- ))} +
+
    +
  • + + Print cost: + +

    + coinImage + {totalCost} +

    +
  • +
  • + + Service cost: + +

    + coinImage + {serviceFee ?? 0} +

    +
  • +
  • + + Total cost: + +

    + coinImage + + {totalCost + (serviceFee ?? 0)} + +

    +
  • +
+
-
- - - -
- -
- - ); +
+ + +
+ +
+ ); + }; + + return { + openOrderSuccessDesktop: () => setOpenDialog(true), + OrderSuccessDesktop: OrderSuccessDesktop + }; } diff --git a/src/components/order/desktop/UploadAndPreviewDesktop.tsx b/src/components/order/desktop/UploadAndPreviewDesktop.tsx index 6bbf9b3..9f37a46 100644 --- a/src/components/order/desktop/UploadAndPreviewDesktop.tsx +++ b/src/components/order/desktop/UploadAndPreviewDesktop.tsx @@ -1,6 +1,7 @@ -import { ChangeEvent, MutableRefObject, useCallback } from 'react'; +import { ChangeEvent, MutableRefObject, useCallback, useEffect } from 'react'; import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { Buffer } from 'buffer'; import { Button, IconButton, Input, Option, Radio, Select } from '@material-tailwind/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { ExclamationCircleIcon } from '@heroicons/react/24/solid'; @@ -14,6 +15,8 @@ import { import { LAYOUT_SIDE, FILE_CONFIG, PAGES_SPECIFIC, PAGES_PER_SHEET, PAGE_SIDE } from '@constants'; import { usePrintingRequestMutation, emitEvent } from '@hooks'; import { useOrderPrintStore, useOrderWorkflowStore } from '@states'; +import { editPdf } from '@utils'; +import type { PagePerSheet } from '@utils'; export const UploadAndPreviewDesktop: Component<{ initialTotalCost: MutableRefObject; @@ -27,6 +30,7 @@ export const UploadAndPreviewDesktop: Component<{ queryFn: () => fileIdCurrent ? queryClient.getQueryData(['fileMetadata', fileIdCurrent]) : null }); + const fileBuffer = queryClient.getQueryData(['fileBuffer']); const { openLayoutSide, LayoutSide } = useLayoutSide(); const { openCloseForm, CloseForm } = useCloseForm(); @@ -50,6 +54,32 @@ export const UploadAndPreviewDesktop: Component<{ clearSpecificPageAndPageBothSide } = useOrderPrintStore(); + const { editPdfPrinting } = editPdf; + + useEffect(() => { + const handleEditPdfPrinting = async () => { + if (fileBuffer) { + const fileEditedBuffer = await editPdfPrinting( + fileBuffer, + fileConfig.pageSide, + fileConfig.pages, + fileConfig.layout, + parseInt(fileConfig.pagesPerSheet) as PagePerSheet + ); + queryClient.setQueryData(['fileURL'], URL.createObjectURL(new Blob([fileEditedBuffer]))); + } + }; + handleEditPdfPrinting(); + }, [ + fileBuffer, + fileConfig.layout, + fileConfig.pageSide, + fileConfig.pages, + fileConfig.pagesPerSheet, + queryClient, + editPdfPrinting + ]); + const handlePageBothSide = useCallback( (event: string) => { setPageBothSide(event); @@ -177,6 +207,16 @@ export const UploadAndPreviewDesktop: Component<{ queryKey: ['fileURL'], queryFn: () => queryClient.getQueryData(['fileURL']) }); + + useEffect(() => { + return () => { + const revokeURL = queryClient.getQueryData(['fileURL']); + if (revokeURL) { + URL.revokeObjectURL(revokeURL); + } + }; + }, []); + const PreviewBody = () => { return ( { const queryClient = useQueryClient(); const remainCoins = queryClient.getQueryData(['/api/user/remain-coins']); + const printingRequestId = queryClient.getQueryData(['printingRequestId']); + const { listFiles: { data: listFiles, isFetching, isError }, serviceFee: { data: serviceFee } } = usePrintingRequestQuery(); + const { executePrintingRequest } = usePrintingRequestMutation(); const { mobileOrderStep, setMobileOrderStep, setDesktopOrderStep } = useOrderWorkflowStore(); const { totalCost, setIsOrderUpdate, setTotalCost } = useOrderPrintStore(); @@ -46,6 +49,15 @@ export const ConfirmOrderForm: Component<{ initialTotalCost: MutableRefObject { + if (!printingRequestId) return; + await executePrintingRequest.mutateAsync(printingRequestId.id); + setMobileOrderStep({ + current: 5, + prev: 3 + }); + }; + const ConfirmOrderItem: Component<{ fileExtraMetadata: FileExtraMetadata }> = useMemo( () => ({ fileExtraMetadata }) => { @@ -217,12 +229,7 @@ export const ConfirmOrderForm: Component<{ initialTotalCost: MutableRefObject - setMobileOrderStep({ - current: 5, - prev: 3 - }) - } + onClick={handleExecutePrintingRequest} disabled={!remainCoins || remainCoins < totalCost + (serviceFee ?? 0)} > Confirm diff --git a/src/components/order/mobile/PreviewDocument.tsx b/src/components/order/mobile/PreviewDocument.tsx index a7eb7d8..ce5b12d 100644 --- a/src/components/order/mobile/PreviewDocument.tsx +++ b/src/components/order/mobile/PreviewDocument.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import DocViewer, { DocViewerRenderers, IHeaderOverride } from '@cyntler/react-doc-viewer'; import { IconButton } from '@material-tailwind/react'; @@ -9,6 +10,15 @@ export function PreviewDocument() { const fileURL = queryClient.getQueryData(['fileURL']); const { mobileOrderStep, setMobileOrderStep } = useOrderWorkflowStore(); + useEffect(() => { + return () => { + const revokeURL = queryClient.getQueryData(['fileURL']); + if (revokeURL) { + URL.revokeObjectURL(revokeURL); + } + }; + }, [queryClient]); + const MyHeader: IHeaderOverride = (state) => { return (
Promise; @@ -31,6 +34,8 @@ export const UploadDocumentForm: Component<{ queryKey: ['fileMetadata', fileIdCurrent], queryFn: () => queryClient.getQueryData(['fileMetadata', fileIdCurrent]) }); + const fileBuffer = queryClient.getQueryData(['fileBuffer']); + const { uploadFileConfig, deleteFile } = usePrintingRequestMutation(); const { setMobileOrderStep, setDesktopOrderStep } = useOrderWorkflowStore(); const { @@ -51,6 +56,32 @@ export const UploadDocumentForm: Component<{ const { openLayoutSide, LayoutSide } = useLayoutSide(); const { openCloseForm, CloseForm } = useCloseForm(); + const { editPdfPrinting } = editPdf; + + useEffect(() => { + const handleEditPdfPrinting = async () => { + if (fileBuffer) { + const fileEditedBuffer = await editPdfPrinting( + fileBuffer, + fileConfig.pageSide, + fileConfig.pages, + fileConfig.layout, + parseInt(fileConfig.pagesPerSheet) as PagePerSheet + ); + queryClient.setQueryData(['fileURL'], URL.createObjectURL(new Blob([fileEditedBuffer]))); + } + }; + handleEditPdfPrinting(); + }, [ + fileBuffer, + fileConfig.layout, + fileConfig.pageSide, + fileConfig.pages, + fileConfig.pagesPerSheet, + queryClient, + editPdfPrinting + ]); + const handlePageBothSide = useCallback( (event: string) => { setPageBothSide(event); @@ -145,7 +176,7 @@ export const UploadDocumentForm: Component<{ } }; - const handleLayoutChange = (e: ChangeEvent) => { + const handleLayoutChange = async (e: ChangeEvent) => { setPageBothSide( e.target.value === LAYOUT_SIDE.portrait ? PAGE_SIDE.both.portrait[0]!.value diff --git a/src/hooks/usePrintingRequestMutation.hook.ts b/src/hooks/usePrintingRequestMutation.hook.ts index ef232c9..f09bfc0 100644 --- a/src/hooks/usePrintingRequestMutation.hook.ts +++ b/src/hooks/usePrintingRequestMutation.hook.ts @@ -1,4 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Buffer } from 'buffer'; import { printingRequestService, buyCoinService, userService } from '@services'; import { retryQueryFn } from '@utils'; @@ -17,6 +18,11 @@ export function usePrintingRequestMutation() { mutationKey: ['uploadFile'], mutationFn: ({ printingRequestId, file }: { printingRequestId: string; file: File }) => printingRequestService.uploadFile(printingRequestId, file), + onMutate: async ({ file }) => { + const fileArrayBuffer = await file.arrayBuffer(); + const fileBuffer = Buffer.from(fileArrayBuffer); + queryClient.setQueryData(['fileBuffer'], fileBuffer); + }, onSuccess: (data) => { queryClient.setQueryData(['fileIdCurrent'], data.fileId); queryClient.setQueryData(['fileURL'], data.fileURL); @@ -72,6 +78,12 @@ export function usePrintingRequestMutation() { } }); + const executePrintingRequest = useMutation({ + mutationKey: ['executePrintingRequest'], + mutationFn: (printingRequestId: string) => + printingRequestService.executePrintingRequest(printingRequestId) + }); + return { createPrintingRequest: createPrintingRequest, uploadFile: uploadFile, @@ -80,6 +92,7 @@ export function usePrintingRequestMutation() { updateAmountFile: updateAmountFile, cancelPrintingRequest: cancelPrintingRequest, createPayPalOrder: createPayPalOrder, - approvePayPalOrder: approvePayPalOrder + approvePayPalOrder: approvePayPalOrder, + executePrintingRequest: executePrintingRequest }; } diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 5ee98e5..77f2fb2 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -2,11 +2,7 @@ import { useRef } from 'react'; import { ArrowRightIcon, PrinterIcon } from '@heroicons/react/24/outline'; import { Orders, Slides, useChooseFileBox } from '@components/home'; import { TopupWallet } from '@components/order/common'; -import { - OrderListDesktop, - ConfirmOrderDektop, - OrderSuccessDesktop -} from '@components/order/desktop'; +import { OrderListDesktop, ConfirmOrderDektop } from '@components/order/desktop'; import { usePrintingRequestMutation } from '@hooks'; import { useOrderWorkflowStore } from '@states'; @@ -59,8 +55,6 @@ export function HomePage() { return ; } else if (desktopOrderStep === 3) { return ; - } else if (desktopOrderStep === 4) { - return ; } }; diff --git a/src/services/printingRequest.service.ts b/src/services/printingRequest.service.ts index b2feb43..eacd776 100644 --- a/src/services/printingRequest.service.ts +++ b/src/services/printingRequest.service.ts @@ -52,5 +52,11 @@ export const printingRequestService = { headers: { 'Content-Type': 'text/plain' }, params: { path: { printingRequestId } } }) + ), + executePrintingRequest: (printingRequestId: string) => + invoke( + apiClient.POST('/api/printRequest/execute', { + body: { printingRequestId } + }) ) }; diff --git a/src/utils/editPdf.ts b/src/utils/editPdf.ts new file mode 100644 index 0000000..57a7da1 --- /dev/null +++ b/src/utils/editPdf.ts @@ -0,0 +1,283 @@ +import { degrees, PDFDocument } from 'pdf-lib'; +import { Buffer } from 'buffer'; + +// Bring this together when reuse this file +export type PageSide = 'one' | 'both'; +export type EdgeBinding = 'long' | 'short'; +export type PageSideEdge = 'one' | EdgeBinding; +export type KeepPages = string; +export type Orientation = 'portrait' | 'landscape'; +export type PagePerSheet = 1 | 2 | 4 | 6 | 9 | 16; + +const setPageSide: (pdfByte: Buffer, option: PageSide) => Promise = async ( + pdfByte, + option +) => { + try { + const pdfDoc = await PDFDocument.load(pdfByte); + + if (option === 'one') { + const pageCount = pdfDoc.getPageCount(); + + for (let i = 0; i < pageCount * 2; i = i + 2) { + pdfDoc.insertPage(i + 1); + } + } + + const uint8Array = await pdfDoc.save(); + const buffer = Buffer.from(uint8Array); + + return buffer; + } catch (error) { + throw error; + } +}; + +const setKeepPages: (pdfByte: Buffer, option: KeepPages) => Promise = async ( + pdfByte, + option +) => { + try { + if (option === 'all') return pdfByte; + + const pdfDoc = await PDFDocument.load(pdfByte); + + const pageCount = pdfDoc.getPageCount(); + const keepingPageNumbers = new Set(); + + if (option === 'odd' || option === 'even') { + const start = option === 'odd' ? 1 : 0; + for (let i = start; i < pageCount; i += 2) { + keepingPageNumbers.add(i); + } + } else { + const optionArray = option.replace(/\s/g, '').split(','); + for (const pageOption of optionArray) { + const isValid = /^(\d+)-(\d+)$/.test(pageOption) || /^\d+$/.test(pageOption); + if (!isValid) throw new Error(`Invalid page option: ${pageOption}`); + + const [start, end] = pageOption.split('-').map(Number); + if (start !== undefined && end !== undefined) { + if (start > end || start < 1 || end > pageCount) + throw new Error(`Invalid range or page option: ${pageOption}`); + + for (let i = start; i <= (end || start); i++) { + keepingPageNumbers.add(i - 1); + } + } + } + } + + const sortedKeepingPageNumbers = Array.from(keepingPageNumbers).sort((a, b) => a - b); + + const newPdfDoc = await PDFDocument.create(); + + for (const pageNum of sortedKeepingPageNumbers) { + const newPage = newPdfDoc.addPage(); + + const embedPage = await newPdfDoc.embedPage(pdfDoc.getPage(pageNum)); + + newPage.drawPage(embedPage, { + x: 0, + y: 0 + }); + } + + const uint8Array = await newPdfDoc.save(); + const buffer = Buffer.from(uint8Array); + + return buffer; + } catch (err) { + throw err; + } +}; + +const convertToPortraitOrLandscape: ( + pdfByte: Buffer, + orientation: Orientation, + pagePerSheet: PagePerSheet +) => Promise = async (pdfByte, orientation, pagePerSheet) => { + try { + if (orientation === 'portrait' && pagePerSheet === 1) return pdfByte; + if (orientation === 'landscape' && pagePerSheet === 1) + throw new Error("Can't create landscape pages with one page per sheet"); + const newPdfDoc = await PDFDocument.create(); + const pdfDoc = await PDFDocument.load(pdfByte); + const pageCount = pdfDoc.getPageCount(); + + if (orientation === 'portrait') { + const AMOUNT_ROW_OF_NEW_PAGE_CONVENTION: { [key in typeof pagePerSheet]: number } = { + 1: 1, + 2: 2, + 4: 2, + 6: 3, + 9: 3, + 16: 4 + }; + + const amountRowOfNewPage = AMOUNT_ROW_OF_NEW_PAGE_CONVENTION[pagePerSheet]; + const amountColumnOfNewPage = pagePerSheet / amountRowOfNewPage; + for (let pageNum = 0; pageNum < pageCount; pageNum += pagePerSheet) { + const newPage = newPdfDoc.addPage(); + + const scaleRatio = 1 / amountRowOfNewPage; + const cellDims: { x: number; y: number } = { + x: newPage.getWidth() / amountColumnOfNewPage, + y: newPage.getHeight() / amountRowOfNewPage + }; + + for (let rowNum = 0; rowNum < amountRowOfNewPage; rowNum++) + for (let colNum = 0; colNum < amountColumnOfNewPage; colNum++) { + const embedOrder = rowNum * amountColumnOfNewPage + colNum; + const embedPageNum = pageNum + embedOrder; + if (embedPageNum >= pageCount) break; + + const embedPage = await newPdfDoc.embedPage(pdfDoc.getPage(embedPageNum)); + + const embedPageDims = embedPage.scale(scaleRatio); + const centerMove: { x: number; y: number } = { + x: (cellDims.x - embedPageDims.width) / 2, + y: (cellDims.y - embedPageDims.height) / 2 + }; + + newPage.drawPage(embedPage, { + ...embedPageDims, + x: cellDims.x * colNum + centerMove.x, + y: newPage.getHeight() - cellDims.y * (rowNum + 1) - centerMove.y + }); + } + } + } + + if (orientation === 'landscape') { + const AMOUNT_ROW_OF_NEW_PAGE_CONVENTION: { [key in typeof pagePerSheet]: number } = { + 1: 1, + 2: 1, + 4: 2, + 6: 2, + 9: 3, + 16: 4 + }; + + const amountRowOfNewPage = AMOUNT_ROW_OF_NEW_PAGE_CONVENTION[pagePerSheet]; + const amountColumnOfNewPage = pagePerSheet / amountRowOfNewPage; + for (let pageNum = 0; pageNum < pageCount; pageNum += pagePerSheet) { + const newPage = newPdfDoc.addPage(); + + const scaleRatio = newPage.getWidth() / newPage.getHeight() / amountRowOfNewPage; + const cellDims: { x: number; y: number } = { + x: newPage.getHeight() / amountColumnOfNewPage, + y: newPage.getWidth() / amountRowOfNewPage + }; + + for (let rowNum = 0; rowNum < amountRowOfNewPage; rowNum++) + for (let colNum = 0; colNum < amountColumnOfNewPage; colNum++) { + const embedOrder = rowNum * amountColumnOfNewPage + colNum; + const embedPageNum = pageNum + embedOrder; + if (embedPageNum >= pageCount) break; + + const embedPage = await newPdfDoc.embedPage(pdfDoc.getPage(embedPageNum)); + + const embedPageDims = embedPage.scale(scaleRatio); + const centerMove: { x: number; y: number } = { + x: (cellDims.y - embedPageDims.height) / 2, + y: (cellDims.x - embedPageDims.width) / 2 + }; + + newPage.drawPage(embedPage, { + ...embedPageDims, + x: cellDims.y * (amountRowOfNewPage - rowNum - 1) + centerMove.x, + y: newPage.getHeight() - cellDims.x * colNum - centerMove.y, + rotate: degrees(-90) + }); + } + } + } + + const uint8Array = await newPdfDoc.save(); + const buffer = Buffer.from(uint8Array); + + return buffer; + } catch (error) { + throw error; + } +}; + +const setTwoSideShortLongEdge: ( + pdfByte: Buffer, + orientation: Orientation, + edgeBinding?: EdgeBinding +) => Promise = async (pdfByte, orientation, edgeBinding) => { + try { + if (!edgeBinding) return pdfByte; + if (orientation === 'portrait' && edgeBinding === 'long') return pdfByte; + if (orientation === 'landscape' && edgeBinding === 'short') return pdfByte; + const newPdfDoc = await PDFDocument.create(); + const pdfDoc = await PDFDocument.load(pdfByte); + const pageCount = pdfDoc.getPageCount(); + + for (let pageNum = 0; pageNum < pageCount; pageNum++) { + const newPage = newPdfDoc.addPage(); + + const embedPage = await newPdfDoc.embedPage(pdfDoc.getPage(pageNum)); + + const adjustCoefficient = pageNum % 2 ? 1 : 0; + + newPage.drawPage(embedPage, { + rotate: degrees(180 * adjustCoefficient), + x: embedPage.width * adjustCoefficient, + y: embedPage.height * adjustCoefficient + }); + } + + const uint8Array = await newPdfDoc.save(); + const buffer = Buffer.from(uint8Array); + + return buffer; + } catch (error) { + throw error; + } +}; + +/** + * + * @param pdfByte A buffer of the PDF file. + * Note that this file must have configuration is pageSideEdge='long' keepPages = 'all', orientation = 'portrait', pagePerSheet = 1. + * @param pageSideEdge 'one' | 'long' | 'short' + * @param keepPages 'all' | 'odd' | 'even' | string[] + * Example: '9, 3-5, 1' + * @param orientation 'portrait' | 'landscape' + * @param pagePerSheet 1 | 2 | 4 | 6 | 9 | 16 + * @param edgeBinding 'long' | 'short' + * @returns An edited pdf file + * @example editPdfPrinting(pdfBuffer, 'long', '9, 3-5, 1', 'landscape', 6); + */ +const editPdfPrinting = async ( + pdfByte: Buffer, + pageSideEdge: PageSideEdge, + keepPages: KeepPages, + orientation: Orientation, + pagePerSheet: PagePerSheet +): Promise => { + const withKeepPages = await setKeepPages(pdfByte, keepPages); + const withOrientation = await convertToPortraitOrLandscape( + withKeepPages, + orientation, + pagePerSheet + ); + const withEdgeBinding = + pageSideEdge === 'one' + ? withOrientation + : await setTwoSideShortLongEdge(withOrientation, orientation, pageSideEdge); + const withPageSide = await setPageSide(withEdgeBinding, pageSideEdge === 'one' ? 'one' : 'both'); + + return withPageSide; +}; + +export const editPdf = { + setPageSide, + setKeepPages, + convertToPortraitOrLandscape, + setTwoSideShortLongEdge, + editPdfPrinting +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index db6fa14..664dedf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,5 +2,6 @@ * @file Automatically generated by barrelsby. */ +export * from './editPdf'; export * from './formatFileSize'; export * from './retryQueryFn'; diff --git a/yarn.lock b/yarn.lock index 5a64cb1..2afb032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1667,6 +1667,20 @@ dependencies: hi-base32 "^0.5.0" +"@pdf-lib/standard-fonts@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f" + integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA== + dependencies: + pako "^1.0.6" + +"@pdf-lib/upng@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb" + integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ== + dependencies: + pako "^1.0.10" + "@react-oauth/google@^0.12.1": version "0.12.1" resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.12.1.tgz#b76432c3a525e9afe076f787d2ded003fcc1bee9" @@ -2254,6 +2268,11 @@ barrelsby@^2.8.0: signale "^1.4.0" yargs "^17.4.1" +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2291,6 +2310,14 @@ browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1: node-releases "^2.0.13" update-browserslist-db "^1.0.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3231,6 +3258,11 @@ husky@^8.0.3: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" @@ -4020,6 +4052,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@^1.0.10, pako@^1.0.11, pako@^1.0.6: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + papaparse@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127" @@ -4090,6 +4127,16 @@ path2d-polyfill@^2.0.1: resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== +pdf-lib@^1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f" + integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw== + dependencies: + "@pdf-lib/standard-fonts" "^1.0.0" + "@pdf-lib/upng" "^1.0.1" + pako "^1.0.11" + tslib "^1.11.1" + pdfjs-dist@3.11.174: version "3.11.174" resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz#5ff47b80f2d58c8dd0d74f615e7c6a7e7e704c4b" @@ -4930,7 +4977,7 @@ tsconfck@^2.1.0: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-2.1.2.tgz#f667035874fa41d908c1fe4d765345fcb1df6e35" integrity sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg== -tslib@^1.8.1: +tslib@^1.11.1, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==