From 4ff0e68e5b6579ace1bfae1d74cc249e5c78e538 Mon Sep 17 00:00:00 2001 From: Alexis G Date: Thu, 7 Dec 2023 12:52:54 +0100 Subject: [PATCH 01/18] refactor(mespapiers): Simplify the `getAttributesFromOcr` params The rotated file is saved in the "formData" state only once we move on to the next step, and not at each rotation. The state of the "formData" is therefore not up to date if the last file added has been rotated. --- .../ScanResult/ScanResultDialog.jsx | 6 +- .../src/components/ModelSteps/helpers.js | 34 +++++++----- .../src/components/ModelSteps/helpers.spec.js | 55 ++++++++++++++++++- 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx index 276bbeb2e5..2ac26cab04 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx @@ -22,6 +22,7 @@ import { useStepperDialog } from '../../Hooks/useStepperDialog' import StepperDialogTitle from '../../StepperDialog/StepperDialogTitle' import { getAttributesFromOcr, + getFormDataFilesForOcr, makeFileFromBase64, makeMetadataFromOcr } from '../helpers' @@ -75,11 +76,10 @@ const ScanResultDialog = ({ const OcrActivated = await isFlagshipOCRAvailable(webviewIntent) if (OcrActivated) { setOcrProcessing(true) + const files = getFormDataFilesForOcr(formData, currentFileRotated) const attributesFound = await getAttributesFromOcr({ - formData, + files, ocrAttributes: currentDefinition.ocrAttributes, - currentFile, - currentFileRotated, webviewIntent }) const metadataFromOcr = makeMetadataFromOcr(attributesFound) diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js index ce615f13fc..b5bdaef875 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js @@ -283,6 +283,22 @@ const _getAttributesFromOcr = async ({ return attributesFound } +/** + * Get formData files for OCR + * @param {Object} formData - State of the FormDataProvider + * @param {File} lastFileRotated - File object + * @returns {File[]} - Files to send to OCR + */ +export const getFormDataFilesForOcr = (formData, lastFileRotated) => { + return formData.data.length > 1 + ? [ + formData.data.find(data => data.fileMetadata.page === 'front')?.file, + lastFileRotated || + formData.data.find(data => data.fileMetadata.page === 'back')?.file + ].filter(Boolean) + : [lastFileRotated || formData.data[0]?.file].filter(Boolean) +} + /** * Get attributes from OCR * @param {Object} options @@ -294,29 +310,21 @@ const _getAttributesFromOcr = async ({ * @returns {Promise} - Attributes found */ export const getAttributesFromOcr = async ({ - formData, + files, ocrAttributes, - currentFile, - currentFileRotated, webviewIntent }) => { try { - const frontFile = formData.data.find( - data => data.fileMetadata.page === 'front' - ) - const backFile = formData.data.find( - data => data.fileMetadata.page === 'back' - ) - const isDoubleSidedFile = frontFile && backFile + const isDoubleSidedFile = files.length > 1 if (isDoubleSidedFile) { const attributesFrontFound = await _getAttributesFromOcr({ - file: frontFile.file, + file: files[0], ocrAttributes: ocrAttributes.front, webviewIntent }) const attributesBackFound = await _getAttributesFromOcr({ - file: currentFileRotated || backFile.file, + file: files[1], ocrAttributes: ocrAttributes.back, webviewIntent }) @@ -324,7 +332,7 @@ export const getAttributesFromOcr = async ({ } const attributesFound = await _getAttributesFromOcr({ - file: currentFileRotated || currentFile, + file: files[0], ocrAttributes: ocrAttributes.front, webviewIntent }) diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js index 0608c59a1f..4afa5397f7 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js @@ -1,7 +1,8 @@ import { isFileAlreadySelected, makeBase64FromFile, - makeFileFromBase64 + makeFileFromBase64, + getFormDataFilesForOcr } from './helpers' describe('ModalSteps helpers', () => { @@ -138,4 +139,56 @@ describe('ModalSteps helpers', () => { await expect(makeBase64FromFile(invalidFile)).rejects.toThrow() }) }) + + describe('getFilesForOcr', () => { + it('should return an empty array if no files', () => { + const formData = { + metadata: {}, + data: [], + contacts: [] + } + const filesForOcr = getFormDataFilesForOcr(formData, undefined) + expect(filesForOcr).toEqual([]) + }) + it('should return an array with a file', () => { + const formData = { + metadata: {}, + data: [ + { file: { name: '001.pdf' }, fileMetadata: { page: undefined } } + ], + contacts: [] + } + const filesForOcr = getFormDataFilesForOcr(formData, undefined) + expect(filesForOcr).toEqual([{ name: '001.pdf' }]) + }) + it('should return an array with two file', () => { + const formData = { + metadata: {}, + data: [ + { file: { name: '001.pdf' }, fileMetadata: { page: 'front' } }, + { file: { name: '002.pdf' }, fileMetadata: { page: 'back' } } + ], + contacts: [] + } + const filesForOcr = getFormDataFilesForOcr(formData) + expect(filesForOcr).toEqual([{ name: '001.pdf' }, { name: '002.pdf' }]) + }) + it('should return an array with the last file rotated', () => { + const formData = { + metadata: {}, + data: [ + { file: { name: '001.pdf' }, fileMetadata: { page: 'front' } }, + { file: { name: '002.pdf' }, fileMetadata: { page: 'back' } } + ], + contacts: [] + } + const filesForOcr = getFormDataFilesForOcr(formData, { + name: '002_rotated.pdf' + }) + expect(filesForOcr).toEqual([ + { name: '001.pdf' }, + { name: '002_rotated.pdf' } + ]) + }) + }) }) From 56a964e150f6b7333c10c29b94b980ce8d23b6b2 Mon Sep 17 00:00:00 2001 From: Alexis G Date: Mon, 11 Dec 2023 11:07:21 +0100 Subject: [PATCH 02/18] feat(mespapiers): Add `xsmall` size to `CompositeHeaderImage` component --- .../components/CompositeHeader/CompositeHeaderImage.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cozy-mespapiers-lib/src/components/CompositeHeader/CompositeHeaderImage.jsx b/packages/cozy-mespapiers-lib/src/components/CompositeHeader/CompositeHeaderImage.jsx index fa034c575b..362ba01a4c 100644 --- a/packages/cozy-mespapiers-lib/src/components/CompositeHeader/CompositeHeaderImage.jsx +++ b/packages/cozy-mespapiers-lib/src/components/CompositeHeader/CompositeHeaderImage.jsx @@ -76,6 +76,9 @@ const images = { const useStyles = makeStyles(() => ({ image: { + '&--xsmall': { + height: '3rem' + }, '&--small': { height: '4rem' }, @@ -104,7 +107,7 @@ const isSupportedBitmapExtension = filename => { * @param {Object} props * @param {string} props.icon - Icon name (with extension) * @param {object} props.fallbackIcon - SVG filetype - * @param {string} props.iconSize - Icon size (small, medium, large(default)) + * @param {string} props.iconSize - Icon size (xsmall, small, medium, large(default)) * @returns {ReactElement|null} * @example * @@ -160,7 +163,7 @@ const CompositeHeaderImage = ({ icon, fallbackIcon, iconSize = 'large' }) => { CompositeHeaderImage.propTypes = { icon: iconPropType, fallbackIcon: PropTypes.object, - iconSize: PropTypes.oneOf(['small', 'medium', 'large']) + iconSize: PropTypes.oneOf(['xsmall', 'small', 'medium', 'large']) } export default CompositeHeaderImage From eb87e1311a028218ee782829639271b794471e3e Mon Sep 17 00:00:00 2001 From: Alexis G Date: Tue, 12 Dec 2023 11:09:27 +0100 Subject: [PATCH 03/18] refactor(mespapiers): Move the logic of the OCR process into its modal --- .../ScanResult/OcrProcessingDialog.jsx | 46 ++++++++++++++++++- .../ScanResult/ScanResultDialog.jsx | 43 ++++++----------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx index 7c73b73f38..179e31110c 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx @@ -1,13 +1,50 @@ -import React from 'react' +import PropTypes from 'prop-types' +import React, { useEffect } from 'react' +import { useWebviewIntent } from 'cozy-intent' import { Dialog } from 'cozy-ui/transpiled/react/CozyDialogs' import Empty from 'cozy-ui/transpiled/react/Empty' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import OcrProcessingIcon from '../../../assets/icons/OcrProcessing.svg' +import { useFormData } from '../../Hooks/useFormData' +import { useStepperDialog } from '../../Hooks/useStepperDialog' +import { + getAttributesFromOcr, + getFormDataFilesForOcr, + makeMetadataFromOcr +} from '../helpers' -const OcrProcessingDialog = ({ onBack }) => { +const OcrProcessingDialog = ({ onBack, rotatedFile }) => { const { t } = useI18n() + const { setFormData, formData } = useFormData() + const webviewIntent = useWebviewIntent() + const { currentDefinition, nextStep } = useStepperDialog() + const { ocrAttributes } = currentDefinition + + useEffect(() => { + const init = async () => { + const files = getFormDataFilesForOcr(formData, rotatedFile) + const attributesFound = await getAttributesFromOcr({ + files, + ocrAttributes, + webviewIntent + }) + + const metadataFromOcr = makeMetadataFromOcr(attributesFound) + setFormData(prev => ({ + ...prev, + metadata: { + ...prev.metadata, + ...metadataFromOcr + } + })) + + nextStep() + } + init() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) return ( { ) } +OcrProcessingDialog.propTypes = { + onBack: PropTypes.func.isRequired, + rotatedFile: PropTypes.object +} + export default OcrProcessingDialog diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx index 2ac26cab04..e91e8f8ce8 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.jsx @@ -17,15 +17,9 @@ import { FLAGSHIP_SCAN_TEMP_FILENAME, KEYS } from '../../../constants/const' import { isFlagshipOCRAvailable } from '../../../helpers/isFlagshipOCRAvailable' import { isSomePaperStepsCompliantWithOCR } from '../../../helpers/isSomePaperStepsCompliantWithOCR' import CompositeHeaderImage from '../../CompositeHeader/CompositeHeaderImage' -import { useFormData } from '../../Hooks/useFormData' import { useStepperDialog } from '../../Hooks/useStepperDialog' import StepperDialogTitle from '../../StepperDialog/StepperDialogTitle' -import { - getAttributesFromOcr, - getFormDataFilesForOcr, - makeFileFromBase64, - makeMetadataFromOcr -} from '../helpers' +import { makeFileFromBase64 } from '../helpers' const ScanResultDialog = ({ currentStep, @@ -37,21 +31,24 @@ const ScanResultDialog = ({ }) => { const { illustration, multipage, page = 'default', tooltip } = currentStep const { t } = useI18n() - const { currentStepIndex } = useStepperDialog() - const { setFormData, formData } = useFormData() const webviewIntent = useWebviewIntent() const [searchParams] = useSearchParams() const imageRef = useRef(null) const [rotationImage, setRotationImage] = useState(0) const [ocrProcessing, setOcrProcessing] = useState(false) - const { nextStep, isLastStep, allCurrentSteps, currentDefinition } = - useStepperDialog() + const { + currentStepIndex, + nextStep, + isLastStep, + allCurrentSteps, + currentDefinition + } = useStepperDialog() const fromFlagshipUpload = searchParams.get('fromFlagshipUpload') + let currentFileRotated const onValid = async addPage => { - let currentFileRotated // If the image has changed rotation, the process has changed it to base64, so we need to transform it back into File before saving it in the formData if (rotationImage % 360 !== 0) { currentFileRotated = makeFileFromBase64({ @@ -76,24 +73,10 @@ const ScanResultDialog = ({ const OcrActivated = await isFlagshipOCRAvailable(webviewIntent) if (OcrActivated) { setOcrProcessing(true) - const files = getFormDataFilesForOcr(formData, currentFileRotated) - const attributesFound = await getAttributesFromOcr({ - files, - ocrAttributes: currentDefinition.ocrAttributes, - webviewIntent - }) - const metadataFromOcr = makeMetadataFromOcr(attributesFound) - - setFormData(prev => ({ - ...prev, - metadata: { - ...prev.metadata, - ...metadataFromOcr - } - })) } + } else { + nextStep() } - nextStep() } } @@ -107,7 +90,9 @@ const ScanResultDialog = ({ useEventListener(window, 'keydown', handleKeyDown) if (ocrProcessing) { - return + return ( + + ) } return ( From 8ff576959460919d37382a162ae3472d5fbdfabb Mon Sep 17 00:00:00 2001 From: Alexis G Date: Wed, 13 Dec 2023 17:13:00 +0100 Subject: [PATCH 04/18] refactor(mespapiers): Rework OCR process helpers In view of future developments, it is preferable to separate the responsibilities of each helper. It was also discussed with @paultranvan to change the type of the `ocrAttributes` attribute in the config file, for more flexibility. --- .../docs/papersDefinitions.md | 2 +- .../ScanResult/OcrProcessingDialog.jsx | 13 +- .../ScanResult/ScanResultDialog.spec.jsx | 4 +- .../src/components/ModelSteps/helpers.js | 98 ++- .../src/components/ModelSteps/helpers.spec.js | 19 +- .../src/constants/papersDefinitions.json | 606 +++++++++--------- 6 files changed, 368 insertions(+), 374 deletions(-) diff --git a/packages/cozy-mespapiers-lib/docs/papersDefinitions.md b/packages/cozy-mespapiers-lib/docs/papersDefinitions.md index 8971b25a12..7728a54911 100644 --- a/packages/cozy-mespapiers-lib/docs/papersDefinitions.md +++ b/packages/cozy-mespapiers-lib/docs/papersDefinitions.md @@ -25,7 +25,7 @@ - [`[information]`](#step-information) {object} Step to get more informations about this file. - [`contact`](#step-contact) {object} Step to select one or more contacts linked to this file. - [`note`](#step-note) {object} Step to add / edit a note. - - `[ocrAttributes]`: {object} Contains all the parameters necessary for the OCR process. + - `[ocrAttributes]`: {object\[]} Contains all the parameters necessary for the OCR process. *** diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx index 179e31110c..f94fbb77d3 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/OcrProcessingDialog.jsx @@ -12,6 +12,7 @@ import { useStepperDialog } from '../../Hooks/useStepperDialog' import { getAttributesFromOcr, getFormDataFilesForOcr, + getOcrFromFlagship, makeMetadataFromOcr } from '../helpers' @@ -24,12 +25,12 @@ const OcrProcessingDialog = ({ onBack, rotatedFile }) => { useEffect(() => { const init = async () => { - const files = getFormDataFilesForOcr(formData, rotatedFile) - const attributesFound = await getAttributesFromOcr({ - files, - ocrAttributes, - webviewIntent - }) + const fileSides = getFormDataFilesForOcr(formData, rotatedFile) + const ocrFromFlagship = await getOcrFromFlagship(fileSides, webviewIntent) + const attributesFound = getAttributesFromOcr( + ocrFromFlagship, + ocrAttributes[0] + ) const metadataFromOcr = makeMetadataFromOcr(attributesFound) setFormData(prev => ({ diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.spec.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.spec.jsx index 138dc9ef46..7861d2e018 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.spec.jsx +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/ScanResultDialog.spec.jsx @@ -210,7 +210,7 @@ describe('AcquisitionResult component:', () => { currentFile: mockFile({ name: 'test.pdf' }), mockIsFlagshipOCRAvailable: true, isLastStep: jest.fn(() => true), - currentDefinition: { ocrAttributes: {} }, + currentDefinition: { ocrAttributes: [] }, allCurrentSteps: [{ isDisplayed: 'ocr' }] }) const btn = getByTestId('next-button') @@ -228,7 +228,7 @@ describe('AcquisitionResult component:', () => { mockIsFlagshipOCRAvailable: true, isLastStep: jest.fn(() => true), mockGetAttributesFromOcr, - currentDefinition: { ocrAttributes: {} }, + currentDefinition: { ocrAttributes: [] }, allCurrentSteps: [{ isDisplayed: 'ocr' }] }) const btn = getByTestId('next-button') diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js index b5bdaef875..9e150ec292 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.js @@ -263,80 +263,60 @@ export const getFirstFileFromNative = async webviewIntent => { return fileToHandle } -const _getAttributesFromOcr = async ({ - file, - ocrAttributes, - webviewIntent -}) => { - const cleanB64FrontFile = await makeBase64FromFile(file, { - prefix: false - }) - const ocrFromFlagshipResult = await webviewIntent.call( - 'ocr', - cleanB64FrontFile - ) - const { attributes: attributesFound } = findAttributes( - ocrFromFlagshipResult.OCRResult, - ocrFromFlagshipResult.imgSize, - ocrAttributes - ) - return attributesFound +/** + * Get OCR from flagship + * @param {{ front: File, back?: File }} fileSides - File object + * @param {Object} webviewIntent - Webview intent + * @returns {Promise<{ front: Object, back?: Object }[]>} - OCR result + */ +export const getOcrFromFlagship = async (fileSides, webviewIntent) => { + return Object.keys(fileSides).reduce(async (acc, side) => { + const cleanB64File = await makeBase64FromFile(fileSides[side], { + prefix: false + }) + const ocrFromFlagshipResult = await webviewIntent.call('ocr', cleanB64File) + const prevAcc = await acc + return { ...prevAcc, [side]: ocrFromFlagshipResult } + }, Promise.resolve({})) } /** * Get formData files for OCR * @param {Object} formData - State of the FormDataProvider * @param {File} lastFileRotated - File object - * @returns {File[]} - Files to send to OCR + * @returns {{ front: File, back?: File }} - Files to send to OCR */ export const getFormDataFilesForOcr = (formData, lastFileRotated) => { - return formData.data.length > 1 - ? [ - formData.data.find(data => data.fileMetadata.page === 'front')?.file, - lastFileRotated || - formData.data.find(data => data.fileMetadata.page === 'back')?.file - ].filter(Boolean) - : [lastFileRotated || formData.data[0]?.file].filter(Boolean) + const front = + formData.data.find(data => data.fileMetadata.page === 'front')?.file || + formData.data[0]?.file + const back = + lastFileRotated || + formData.data.find(data => data.fileMetadata.page === 'back')?.file + + return { + ...(front && { front }), + ...(back && { back }) + } } /** * Get attributes from OCR - * @param {Object} options - * @param {Object} options.formData - State of the FormDataProvider - * @param {Object} options.ocrAttributes - OCR attributes config of current definition of paper - * @param {File} options.currentFile - File object - * @param {File} options.currentFileRotated - File object - * @param {Object} options.webviewIntent - Webview intent - * @returns {Promise} - Attributes found + * @param {{front: Object, back?: Object}[]} ocrFromFlagshipResult - OCR result + * @param {Object[]} ocrAttributes - Attributes to find + * @returns {Object[]} - Attributes found */ -export const getAttributesFromOcr = async ({ - files, - ocrAttributes, - webviewIntent -}) => { +export const getAttributesFromOcr = (ocrFromFlagshipResult, ocrAttributes) => { try { - const isDoubleSidedFile = files.length > 1 - - if (isDoubleSidedFile) { - const attributesFrontFound = await _getAttributesFromOcr({ - file: files[0], - ocrAttributes: ocrAttributes.front, - webviewIntent - }) - const attributesBackFound = await _getAttributesFromOcr({ - file: files[1], - ocrAttributes: ocrAttributes.back, - webviewIntent - }) - return attributesFrontFound.concat(attributesBackFound) - } - - const attributesFound = await _getAttributesFromOcr({ - file: files[0], - ocrAttributes: ocrAttributes.front, - webviewIntent + return Object.keys(ocrFromFlagshipResult).flatMap(ocrFromFlagship => { + const ocrFromFlagshipSide = ocrFromFlagshipResult[ocrFromFlagship] + const { attributes } = findAttributes( + ocrFromFlagshipSide.OCRResult, + ocrFromFlagshipSide.imgSize, + ocrAttributes[ocrFromFlagship] + ) + return attributes }) - return attributesFound } catch (error) { log( 'error', diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js index 4afa5397f7..1b39b6e5a0 100644 --- a/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/helpers.spec.js @@ -148,9 +148,9 @@ describe('ModalSteps helpers', () => { contacts: [] } const filesForOcr = getFormDataFilesForOcr(formData, undefined) - expect(filesForOcr).toEqual([]) + expect(filesForOcr).toEqual({}) }) - it('should return an array with a file', () => { + it('should return an object with a file', () => { const formData = { metadata: {}, data: [ @@ -159,7 +159,7 @@ describe('ModalSteps helpers', () => { contacts: [] } const filesForOcr = getFormDataFilesForOcr(formData, undefined) - expect(filesForOcr).toEqual([{ name: '001.pdf' }]) + expect(filesForOcr).toEqual({ front: { name: '001.pdf' } }) }) it('should return an array with two file', () => { const formData = { @@ -171,7 +171,10 @@ describe('ModalSteps helpers', () => { contacts: [] } const filesForOcr = getFormDataFilesForOcr(formData) - expect(filesForOcr).toEqual([{ name: '001.pdf' }, { name: '002.pdf' }]) + expect(filesForOcr).toEqual({ + back: { name: '002.pdf' }, + front: { name: '001.pdf' } + }) }) it('should return an array with the last file rotated', () => { const formData = { @@ -185,10 +188,10 @@ describe('ModalSteps helpers', () => { const filesForOcr = getFormDataFilesForOcr(formData, { name: '002_rotated.pdf' }) - expect(filesForOcr).toEqual([ - { name: '001.pdf' }, - { name: '002_rotated.pdf' } - ]) + expect(filesForOcr).toEqual({ + back: { name: '002_rotated.pdf' }, + front: { name: '001.pdf' } + }) }) }) }) diff --git a/packages/cozy-mespapiers-lib/src/constants/papersDefinitions.json b/packages/cozy-mespapiers-lib/src/constants/papersDefinitions.json index f23dace7ff..e222989a8b 100644 --- a/packages/cozy-mespapiers-lib/src/constants/papersDefinitions.json +++ b/packages/cozy-mespapiers-lib/src/constants/papersDefinitions.json @@ -166,30 +166,32 @@ "text": "PaperJSON.generic.owner.text" } ], - "ocrAttributes": { - "front": { - "referenceBox": { - "text": "IBAN" - }, - "attributesRegex": [ - { - "name": "number", - "regex": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$" + "ocrAttributes": [ + { + "front": { + "referenceBox": { + "text": "IBAN" }, - { - "name": "bicNumber", - "regex": "^[A-Z]{4}([A-Z]{2})[A-Z0-9]{2}([A-Z0-9]{3})?$", - "oneWord": true, - "validationRules": [ - { - "regexGroupIdx": 1, - "validationFn": "checkCountryCode" - } - ] - } - ] + "attributesRegex": [ + { + "name": "number", + "regex": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$" + }, + { + "name": "bicNumber", + "regex": "^[A-Z]{4}([A-Z]{2})[A-Z0-9]{2}([A-Z0-9]{3})?$", + "oneWord": true, + "validationRules": [ + { + "regexGroupIdx": 1, + "validationFn": "checkCountryCode" + } + ] + } + ] + } } - } + ] }, { "label": "bank_statement", @@ -420,153 +422,155 @@ "text": "PaperJSON.generic.owner.text" } ], - "ocrAttributes": { - "front": { - "size": { - "width": 415, - "height": 254 - }, - "textBounding": { - "bottom": 237, - "left": 9, - "right": 409, - "top": 4 - }, - "referenceBox": { - "text": "PERMISDECONDUIRE" - }, - "attributesBoxes": [ - { - "type": "string", - "name": "lastName", - "enabled": true, - "fullLine": true, - "bounding": { - "height": 18, - "left": 130, - "top": 26 - }, - "postTextRules": [ - {"regex": "^1."}, - {"regex": "^[\\^4n]"}, - {"regex": "^\\d", "flag": "g"}, - {"regex": "[Aa]$"}, - {"regex": "^a|a$", "flag": "g"} - ] + "ocrAttributes": [ + { + "front": { + "size": { + "width": 415, + "height": 254 }, - { - "type": "string", - "name": "firstName", - "enabled": true, - "bounding": { - "height": 13, - "left": 130, - "top": 52 - }, - "postTextRules": [ - {"regex": "^[\\^4n]"}, - {"regex": "^\\d", "flag": "g"}, - {"regex": "^a|a$", "flag": "g"} - ] + "textBounding": { + "bottom": 237, + "left": 9, + "right": 409, + "top": 4 }, - { - "type": "date", - "name": "issueDate", - "dateFormat": "ddMMyyyy", - "enabled": true, - "bounding": { - "height": 13, - "left": 129, - "top": 96, - "width": 57 - }, - "postTextRules": [{"regex": "\\^4a"}, {"regex": "\\^."}] + "referenceBox": { + "text": "PERMISDECONDUIRE" }, - { - "type": "date", - "name": "expirationDate", - "dateFormat": "ddMMyyyy", - "enabled": true, - "fixedSize": true, - "bounding": { - "height": 16, - "left": 129, - "top": 120, - "width": 71 + "attributesBoxes": [ + { + "type": "string", + "name": "lastName", + "enabled": true, + "fullLine": true, + "bounding": { + "height": 18, + "left": 130, + "top": 26 + }, + "postTextRules": [ + {"regex": "^1."}, + {"regex": "^[\\^4n]"}, + {"regex": "^\\d", "flag": "g"}, + {"regex": "[Aa]$"}, + {"regex": "^a|a$", "flag": "g"} + ] }, - "postTextRules": [{"regex": "^4b"}, {"regex": "^\\."}] - }, - { - "type": "string", - "name": "bottomTest", - "enabled": false, - "fullLine": true, - "fixedSize": true, - "bounding": { - "height": 94, - "left": 129, - "top": 767, - "width": 2404 - } - } - ] - }, - "back": { - "size": { - "width": 401, - "height": 253 - }, - "textBounding": {"bottom": 217, "left": 3, "right": 370, "top": 6}, - "attributesBoxes": [ - { - "type": "string", - "name": "cardNumberLine1", - "enabled": true, - "fixedSize": true, - "group": { - "name": "number", - "order": 1 + { + "type": "string", + "name": "firstName", + "enabled": true, + "bounding": { + "height": 13, + "left": 130, + "top": 52 + }, + "postTextRules": [ + {"regex": "^[\\^4n]"}, + {"regex": "^\\d", "flag": "g"}, + {"regex": "^a|a$", "flag": "g"} + ] }, - "bounding": { - "left": 26, - "top": 24 - } - }, - { - "type": "string", - "name": "cardNumberLine2", - "enabled": true, - "fixedSize": true, - "group": { - "name": "number", - "order": 2 + { + "type": "date", + "name": "issueDate", + "dateFormat": "ddMMyyyy", + "enabled": true, + "bounding": { + "height": 13, + "left": 129, + "top": 96, + "width": 57 + }, + "postTextRules": [{"regex": "\\^4a"}, {"regex": "\\^."}] }, - "bounding": { - "left": 26, - "top": 42 - } - }, - { - "type": "date", - "name": "issueDateA", - "bounding": { - "left": 163, - "top": 51, - "width": 32, - "height": 7 + { + "type": "date", + "name": "expirationDate", + "dateFormat": "ddMMyyyy", + "enabled": true, + "fixedSize": true, + "bounding": { + "height": 16, + "left": 129, + "top": 120, + "width": 71 + }, + "postTextRules": [{"regex": "^4b"}, {"regex": "^\\."}] + }, + { + "type": "string", + "name": "bottomTest", + "enabled": false, + "fullLine": true, + "fixedSize": true, + "bounding": { + "height": 94, + "left": 129, + "top": 767, + "width": 2404 + } } + ] + }, + "back": { + "size": { + "width": 401, + "height": 253 }, - { - "type": "date", - "name": "issueDateB", - "bounding": { - "left": 163, - "top": 75 + "textBounding": {"bottom": 217, "left": 3, "right": 370, "top": 6}, + "attributesBoxes": [ + { + "type": "string", + "name": "cardNumberLine1", + "enabled": true, + "fixedSize": true, + "group": { + "name": "number", + "order": 1 + }, + "bounding": { + "left": 26, + "top": 24 + } + }, + { + "type": "string", + "name": "cardNumberLine2", + "enabled": true, + "fixedSize": true, + "group": { + "name": "number", + "order": 2 + }, + "bounding": { + "left": 26, + "top": 42 + } + }, + { + "type": "date", + "name": "issueDateA", + "bounding": { + "left": 163, + "top": 51, + "width": 32, + "height": 7 + } + }, + { + "type": "date", + "name": "issueDateB", + "bounding": { + "left": 163, + "top": 75 + } } - } - ] + ] + } } - } + ] }, { "label": "driver_license", @@ -950,67 +954,69 @@ "text": "PaperJSON.generic.owner.text" } ], - "ocrAttributes": { - "front": { - "size": { - "width": 1333, - "height": 909 - }, - "textBounding": { - "bottom": 845, - "left": 41, - "right": 1312, - "top": 16 - }, - "referenceBox": { - "text": "CARTENATIONALE" - }, - "attributesRegex": [ - { - "name": "number", - "regex": "^(\\d{12})" - } - ], - "attributesBoxes": [ - { - "type": "string", - "name": "number", - "enabled": true, - "fixedSize": true, - "bounding": { - "height": 39, - "left": 598, - "top": 96, - "width": 252 + "ocrAttributes": [ + { + "front": { + "size": { + "width": 1333, + "height": 909 + }, + "textBounding": { + "bottom": 845, + "left": 41, + "right": 1312, + "top": 16 + }, + "referenceBox": { + "text": "CARTENATIONALE" + }, + "attributesRegex": [ + { + "name": "number", + "regex": "^(\\d{12})" } - } - ] - }, - "back": { - "size": { - "width": 596, - "height": 410 - }, - "textBounding": { - "bottom": 304, - "left": 12, - "right": 429, - "top": 126 + ], + "attributesBoxes": [ + { + "type": "string", + "name": "number", + "enabled": true, + "fixedSize": true, + "bounding": { + "height": 39, + "left": 598, + "top": 96, + "width": 252 + } + } + ] }, - "attributesBoxes": [ - { - "type": "date", - "dateFormat": "ddMMyyyy", - "name": "expirationDate", - "bounding": { - "left": 174, - "top": 172 - }, - "fullLine": true - } - ] + "back": { + "size": { + "width": 596, + "height": 410 + }, + "textBounding": { + "bottom": 304, + "left": 12, + "right": 429, + "top": 126 + }, + "attributesBoxes": [ + { + "type": "date", + "dateFormat": "ddMMyyyy", + "name": "expirationDate", + "bounding": { + "left": 174, + "top": 172 + }, + "fullLine": true + } + ] + } } - } + ] }, { "label": "personal_sporting_licence", @@ -1780,55 +1786,57 @@ "text": "PaperJSON.generic.owner.text" } ], - "ocrAttributes": { - "front": { - "size": { - "width": 614, - "height": 390 - }, - "textBounding": { - "bottom": 337, - "left": 207, - "right": 577, - "top": 27 - }, - "referenceBox": { - "text": "TITREDESEJOUR" - }, - "attributesBoxes": [ - { - "type": "date", - "dateFormat": "ddMMyyyy", - "name": "expirationDate", - "bounding": { - "left": 214, - "top": 133 + "ocrAttributes": [ + { + "front": { + "size": { + "width": 614, + "height": 390 + }, + "textBounding": { + "bottom": 337, + "left": 207, + "right": 577, + "top": 27 + }, + "referenceBox": { + "text": "TITREDESEJOUR" + }, + "attributesBoxes": [ + { + "type": "date", + "dateFormat": "ddMMyyyy", + "name": "expirationDate", + "bounding": { + "left": 214, + "top": 133 + } } - } - ] - }, - "back": { - "size": { - "width": 615, - "height": 384 + ] }, - "referenceBox": { - "text": "TITREDESEJOUR" - }, - "attributesRegex": [ - { - "name": "number", - "regex": "<[0-9]{10}<", - "postTextRules": [{"regex": "<", "flag": "g"}] + "back": { + "size": { + "width": 615, + "height": 384 }, - { - "name": "country", - "regex": "[0-9][A-Z]{3}<", - "postTextRules": [{"regex": "^[0-9]"}, {"regex": "<$"}] - } - ] + "referenceBox": { + "text": "TITREDESEJOUR" + }, + "attributesRegex": [ + { + "name": "number", + "regex": "<[0-9]{10}<", + "postTextRules": [{"regex": "<", "flag": "g"}] + }, + { + "name": "country", + "regex": "[0-9][A-Z]{3}<", + "postTextRules": [{"regex": "^[0-9]"}, {"regex": "<$"}] + } + ] + } } - } + ] }, { "label": "school_attendance_certificate", @@ -2234,44 +2242,46 @@ "text": "PaperJSON.generic.owner.text" } ], - "ocrAttributes": { - "front": { - "size": { - "width": 1607, - "height": 1149 - }, - "textBounding": { - "bottom": 1045, - "left": 54, - "right": 1511, - "top": 18 - }, - "referenceBox": { - "text": "PASSEPORT" - }, - "attributesBoxes": [ - { - "type": "string", - "name": "number", - "bounding": { - "left": 1122, - "top": 129 - }, - "validationRules": [{"regex": "^\\d{2}[A-Za-z]{2}\\d{5}"}] + "ocrAttributes": [ + { + "front": { + "size": { + "width": 1607, + "height": 1149 }, - { - "type": "date", - "name": "expirationDate", - "fullLine": true, - "bounding": { - "left": 506, - "top": 771 + "textBounding": { + "bottom": 1045, + "left": 54, + "right": 1511, + "top": 18 + }, + "referenceBox": { + "text": "PASSEPORT" + }, + "attributesBoxes": [ + { + "type": "string", + "name": "number", + "bounding": { + "left": 1122, + "top": 129 + }, + "validationRules": [{"regex": "^\\d{2}[A-Za-z]{2}\\d{5}"}] + }, + { + "type": "date", + "name": "expirationDate", + "fullLine": true, + "bounding": { + "left": 506, + "top": 771 + } } - } - ] - }, - "back": {} - } + ] + }, + "back": {} + } + ] }, { "label": "resume", From e6ff9dad5ed54d1576b8dba0e43ba824fd7effc5 Mon Sep 17 00:00:00 2001 From: Alexis G Date: Thu, 14 Dec 2023 10:51:05 +0100 Subject: [PATCH 05/18] feat(mespapiers): Add SelectPaperFormat component This modal comes after the animated OCR processing modal, to ask the user to confirm the recognized version. --- .../ScanResult/SelectPaperFormat.jsx | 111 ++++++++++++++++++ .../cozy-mespapiers-lib/src/locales/en.json | 3 + .../cozy-mespapiers-lib/src/locales/fr.json | 3 + 3 files changed, 117 insertions(+) create mode 100644 packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/SelectPaperFormat.jsx diff --git a/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/SelectPaperFormat.jsx b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/SelectPaperFormat.jsx new file mode 100644 index 0000000000..430ca7b25d --- /dev/null +++ b/packages/cozy-mespapiers-lib/src/components/ModelSteps/ScanResult/SelectPaperFormat.jsx @@ -0,0 +1,111 @@ +import PropTypes from 'prop-types' +import React, { useState } from 'react' + +import Box from 'cozy-ui/transpiled/react/Box' +import Button from 'cozy-ui/transpiled/react/Buttons' +import { Dialog } from 'cozy-ui/transpiled/react/CozyDialogs' +import List from 'cozy-ui/transpiled/react/List' +import ListItem from 'cozy-ui/transpiled/react/ListItem' +import ListItemSecondaryAction from 'cozy-ui/transpiled/react/ListItemSecondaryAction' +import ListItemText from 'cozy-ui/transpiled/react/ListItemText' +import Radio from 'cozy-ui/transpiled/react/Radios' +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' + +import CompositeHeader from '../../CompositeHeader/CompositeHeader' +import CompositeHeaderImage from '../../CompositeHeader/CompositeHeaderImage' +import { useFormData } from '../../Hooks/useFormData' +import { useStepperDialog } from '../../Hooks/useStepperDialog' +import { getAttributesFromOcr, makeMetadataFromOcr } from '../helpers' + +const SelectPaperFormat = ({ onBack, ocrFromFlagship }) => { + const { t } = useI18n() + const { setFormData } = useFormData() + const { currentDefinition, nextStep } = useStepperDialog() + const [selectedVersion, setSelectedVersion] = useState() + const { ocrAttributes } = currentDefinition + + const handleClick = () => { + const attributesSelected = ocrAttributes.find( + attr => attr.version === selectedVersion + ) + const attributesFound = getAttributesFromOcr( + ocrFromFlagship, + attributesSelected + ) + + const metadataFromOcr = makeMetadataFromOcr(attributesFound) + setFormData(prev => ({ + ...prev, + metadata: { + ...prev.metadata, + ...metadataFromOcr + } + })) + + nextStep() + } + + const handleCheck = version => { + setSelectedVersion(version) + } + + return ( + ( + + + handleCheck(attr.version)} + > + + + + handleCheck(attr.version)} + value={attr.version} + name={attr.version} + /> + + + + + ))} + /> + } + actions={ +