Skip to content

Commit

Permalink
feat(Action): Print can now deal with multiple docs
Browse files Browse the repository at this point in the history
All the helpers code except `makePdfBlob` is from mespapiers-lib
  • Loading branch information
JF-Cozy committed Jan 25, 2024
1 parent 9b363e1 commit ee5695d
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 19 deletions.
162 changes: 162 additions & 0 deletions react/ActionsMenu/Actions/helpers.js
@@ -1,3 +1,9 @@
import { PDFDocument } from 'pdf-lib'
import { fetchBlobFileById } from 'cozy-client/dist/models/file'

// Should guarantee good resolution for different uses (printing, downloading, etc.)
const MAX_RESIZE_IMAGE_SIZE = 3840

/**
* Make array of actions for ActionsItems component
*
Expand Down Expand Up @@ -82,3 +88,159 @@ export const makeBase64FromFile = async file => {
}
})
}

/**
* @param {HTMLImageElement} image
* @param {number} [maxSizeInPixel] - Maximum size before being resized
* @returns {number}
*/
const getImageScaleRatio = (image, maxSize) => {
const longerSideSizeInPixel = Math.max(image.height, image.width)
let scaleRatio = 1
if (maxSize < longerSideSizeInPixel) {
scaleRatio = maxSize / longerSideSizeInPixel
}

return scaleRatio
}

/**
* @param {object} opts
* @param {string} opts.base64 - Base64 of image
* @param {string} opts.type - Type of image
* @param {number} opts.maxSize - Maximum size before being resized
* @returns {Promise<string>}
*/
const resizeImage = async ({
base64: fileDataUri,
type: fileType,
maxSize
}) => {
return new Promise((resolve, reject) => {
const newImage = new Image()
newImage.src = fileDataUri
newImage.onerror = reject
newImage.onload = () => {
const canvas = document.createElement('canvas')
const scaleRatio = getImageScaleRatio(newImage, maxSize)
const scaledWidth = scaleRatio * newImage.width
const scaledHeight = scaleRatio * newImage.height

canvas.width = scaledWidth
canvas.height = scaledHeight
canvas
.getContext('2d')
.drawImage(newImage, 0, 0, scaledWidth, scaledHeight)

resolve(canvas.toDataURL(fileType))
}
})
}

/**
* @param {File} file
* @returns {Promise<string>}
*/
const fileToDataUri = async file => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onerror = reject
reader.onload = e => resolve(e.target.result)
reader.readAsDataURL(file)
})
}

/**
* @param {PDFDocument} pdfDoc
* @param {File} file
* @returns {Promise<void>}
*/
const addImageToPdf = async (pdfDoc, file) => {
const fileDataUri = await fileToDataUri(file)
const resizedImage = await resizeImage({
base64: fileDataUri,
type: file.type,
maxSize: MAX_RESIZE_IMAGE_SIZE
})

let img
if (file.type === 'image/png') img = await pdfDoc.embedPng(resizedImage)
if (file.type === 'image/jpeg') img = await pdfDoc.embedJpg(resizedImage)

const page = pdfDoc.addPage([img.width, img.height])
const { width: pageWidth, height: pageHeight } = page.getSize()
page.drawImage(img, {
x: pageWidth / 2 - img.width / 2,
y: pageHeight / 2 - img.height / 2,
width: img.width,
height: img.height
})
}

/**
* @param {File} file
* @returns {Promise<ArrayBuffer>}
*/
const fileToArrayBuffer = async file => {
if ('arrayBuffer' in file) return await file.arrayBuffer()

return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onerror = reject
reader.onload = e => resolve(new Uint8Array(e.target.result))
reader.readAsArrayBuffer(file)
})
}

/**
* @param {PDFDocument} pdfDoc
* @param {File} file
* @returns {Promise<void>}
*/
const addPdfToPdf = async (pdfDoc, file) => {
const pdfToAdd = await fileToArrayBuffer(file)
const document = await PDFDocument.load(pdfToAdd)
const copiedPages = await pdfDoc.copyPages(
document,
document.getPageIndices()
)
copiedPages.forEach(page => pdfDoc.addPage(page))
}

/**
* @param {PDFDocument} pdfDoc - Instance of PDFDocument
* @param {File} file - File to add in pdf
* @returns {Promise<ArrayBuffer>} - Data of pdf generated
*/
export const addFileToPdf = async (pdfDoc, file) => {
if (file.type === 'application/pdf') {
await addPdfToPdf(pdfDoc, file)
} else {
await addImageToPdf(pdfDoc, file)
}
const pdfDocBytes = await pdfDoc.save()

return pdfDocBytes
}

/**
* Fetches file from docs list and return a blob of pdf
* @param {import('cozy-client/types/CozyClient').default} client - Instance of CozyClient
* @param {array} docs - Docs from an io.cozy.xxx doctypes
* @returns {Promise<object>} Blob of generated Pdf
*/
export const makePdfBlob = async (client, docs) => {
const pdfDoc = await PDFDocument.create()

for (const doc of docs) {
const blob = await fetchBlobFileById(client, doc._id)
await addFileToPdf(pdfDoc, blob)
}

const pdfBytes = await pdfDoc.save()

const bytes = new Uint8Array(pdfBytes)
const blob = new Blob([bytes], { type: 'application/pdf' })

return blob
}
45 changes: 26 additions & 19 deletions react/ActionsMenu/Actions/print.js
Expand Up @@ -3,7 +3,7 @@ import React, { forwardRef } from 'react'
import logger from 'cozy-logger'
import { fetchBlobFileById, isFile } from 'cozy-client/dist/models/file'

import { makeBase64FromFile } from './helpers'
import { makeBase64FromFile, makePdfBlob } from './helpers'
import PrinterIcon from '../../Icons/Printer'
import { getActionsI18n } from './locales/withActionsLocales'
import ActionsMenuItem from '../ActionsMenuItem'
Expand All @@ -21,33 +21,40 @@ export const print = () => {
icon,
label,
disabled: docs => docs.length === 0,
displayCondition: docs => isFile(docs[0]), // feature not yet supported for multi-files
displayCondition: docs => docs.every(doc => isFile(doc)),
action: async (docs, { client, webviewIntent }) => {
const doc = docs[0] // feature not yet supported for multi-files
const isSingleDoc = docs.length === 1
const firstDoc = docs[0]

if (webviewIntent) {
try {
const blob = await fetchBlobFileById(client, doc._id)
try {
// in flagship app
if (webviewIntent) {
const blob = isSingleDoc
? await fetchBlobFileById(client, firstDoc._id)
: await makePdfBlob(client, docs)
const base64 = await makeBase64FromFile(blob)

return webviewIntent.call('print', base64)
} catch (error) {
logger.error(
`Error trying to print document with Flagship App: ${JSON.stringify(
error
)}`
)
}
}

try {
const downloadURL = await client
.collection('io.cozy.files')
.getDownloadLinkById(doc._id, doc.name)
// not in flagship app
let docUrl = ''
if (isSingleDoc) {
docUrl = await client
.collection('io.cozy.files')
.getDownloadLinkById(firstDoc._id, firstDoc.name)
} else {
const blob = await makePdfBlob(client, docs)
docUrl = URL.createObjectURL(blob)
}

window.open(downloadURL, '_blank')
window.open(docUrl, '_blank')
} catch (error) {
logger.error(`Error trying to print document: ${JSON.stringify(error)}`)
logger.error(
`Error trying to print document ${
webviewIntent ? 'inside flagship appp' : 'outside flagship app'
}: ${JSON.stringify(error)}`
)
}
},
Component: forwardRef((props, ref) => {
Expand Down

0 comments on commit ee5695d

Please sign in to comment.