diff --git a/package.json b/package.json index 501384a67..70c05d4f1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "chart.js": "^2.9.3", "exif-js": "^2.3.0", "file-type": "^14.6.2", + "jszip": "^3.5.0", "lodash": "^4.17.20", "ol": "^5.3.3", "promise.allsettled": "^1.0.2", diff --git a/src/common/utils.ts b/src/common/utils.ts index 156b8dd72..d4722568c 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -7,6 +7,7 @@ import { encryptObject, decryptObject, encrypt, decrypt } from "./crypto"; import UTIF from "utif"; import {constants} from "./constants"; import _ from "lodash"; +import JsZip from 'jszip'; // tslint:disable-next-line:no-var-requires const tagColors = require("../react/components/common/tagColors.json"); @@ -404,15 +405,32 @@ export function poll(func, timeout, interval): Promise { * @param fileName * @param prefix */ -export function downloadAsJsonFile(data: any, fileName: string, prefix?: string): void { - const predictionData = JSON.stringify(data); - const fileURL = window.URL.createObjectURL(new Blob([predictionData])); +export function downloadFile(data: any, fileName: string, prefix?: string): void { + const fileURL = window.URL.createObjectURL(new Blob([data])); const fileLink = document.createElement("a"); - const fileBaseName = fileName.split(".")[0]; - const downloadFileName = prefix + "Result-" + fileBaseName + ".json"; + const downloadFileName = prefix + "Result-" + fileName ; fileLink.href = fileURL; fileLink.setAttribute("download", downloadFileName); document.body.appendChild(fileLink); fileLink.click(); } + +export type zipData = { + fileName: string; + data: any; +} + +export function downloadZipFile(data: zipData[], fileName: string): void { + const zip = new JsZip(); + data.forEach(item => { + zip.file(item.fileName, item.data); + }) + zip.generateAsync({type: "blob"}).then(content => { + const fileLink = document.createElement("a"); + fileLink.href = window.URL.createObjectURL(content); + fileLink.setAttribute("download", fileName+".zip"); + document.body.appendChild(fileLink); + fileLink.click(); + }); +} diff --git a/src/react/components/common/common.scss b/src/react/components/common/common.scss index e066267bf..9899ecc47 100644 --- a/src/react/components/common/common.scss +++ b/src/react/components/common/common.scss @@ -162,6 +162,10 @@ .keep-button-80px { max-width: 80px; } +.keep-button-120px { + min-width: 120px; + width: 120px; +} .container-items-end { display: flex; diff --git a/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx b/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx index 012829b5e..db196adca 100644 --- a/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx +++ b/src/react/components/pages/prebuiltPredict/layoutPredictPage.tsx @@ -9,7 +9,9 @@ import { Separator, Spinner, SpinnerSize, - TooltipHost + TooltipHost, + ContextualMenu, + IContextualMenuProps } from "@fluentui/react"; import Fill from "ol/style/Fill"; import Stroke from "ol/style/Stroke"; @@ -22,7 +24,7 @@ import url from "url"; import {constants} from "../../../../common/constants"; import {interpolate, strings} from "../../../../common/strings"; import {getPrimaryGreenTheme, getPrimaryWhiteTheme} from "../../../../common/themes"; -import {downloadAsJsonFile, poll} from "../../../../common/utils"; +import {downloadFile, poll, zipData, downloadZipFile} from "../../../../common/utils"; import { ErrorCode, IApplicationState, @@ -42,6 +44,7 @@ import {TableView} from "../editorPage/tableView"; import {ILayoutHelper, LayoutHelper} from "./layoutHelper"; import {ILoadFileHelper, LoadFileHelper} from "./LoadFileHelper"; import {ITableHelper, ITableState, TableHelper} from "./tableHelper"; +import _ from "lodash"; interface ILayoutPredictPageProps extends RouteComponentProps { prebuiltSettings: IPrebuiltSettings; @@ -174,7 +177,21 @@ export class LayoutPredictPage extends React.Component this.onJsonDownloadClick() + }, + { + key: 'Table', + text: 'Table', + onClick: () => this.onCSVDownloadClick() + } + ] + } return ( <>
{strings.layoutPredict.layoutResults}
} @@ -285,10 +304,48 @@ export class LayoutPredictPage extends React.Component { + onJsonDownloadClick = () => { const {layoutData} = this.state; if (layoutData) { - downloadAsJsonFile(layoutData, this.state.fileLabel, "Layout-"); + downloadFile(JSON.stringify(layoutData), this.state.fileLabel+".json", "Layout-"); + } + } + onCSVDownloadClick = () => { + const {layoutData} = this.state; + if (layoutData) { + const analyzeResult = layoutData.analyzeResult; + const ocrPageResults = analyzeResult["pageResults"]; + const data: zipData[] = []; + for (let i = 0; i < ocrPageResults.length; i++) { + const currentPageResult = ocrPageResults[i]; + if (currentPageResult?.tables) { + currentPageResult.tables.forEach((table, index) => { + if (table.cells && table.columns && table.rows) { + let tableContent = ""; + let rowIndex = 0; + table.cells.forEach(cell => { + if (cell.rowIndex === rowIndex) { + tableContent += `"${cell.text}"${cell.columnSpan ? _.repeat(',', cell.columnSpan) : ','}`; + } + else { + tableContent += "\n"; + tableContent += `"${cell.text}"${cell.columnSpan ? _.repeat(',', cell.columnSpan) : ','}`; + rowIndex = cell.rowIndex; + } + }); + if (tableContent.length > 0) { + data.push({ + fileName: `Layout-page-${i + 1}-table-${index + 1}.csv`, + data: tableContent + }); + } + } + }); + } + } + if (data.length > 0) { + downloadZipFile(data, this.state.fileLabel + "tables"); + } } } @@ -585,4 +642,8 @@ export class LayoutPredictPage extends React.Component; + } } diff --git a/src/react/components/pages/predict/predictPage.tsx b/src/react/components/pages/predict/predictPage.tsx index 64ed21ded..e6e4e54b3 100644 --- a/src/react/components/pages/predict/predictPage.tsx +++ b/src/react/components/pages/predict/predictPage.tsx @@ -373,6 +373,7 @@ export default class PredictPage extends React.Component 0 && this.props.project && p1.displayOrder - p2.displayOrder); - + const menuProps: IContextualMenuProps = { + className: "keep-button-120px", + items: [ + { + key: 'JSON', + text: 'JSON', + onClick: () => this.triggerJSONDownload() + }, + { + key: 'CSV', + text: 'CSV', + onClick: () => this.triggerCSVDownload() + } + ] + } return (
@@ -58,12 +74,13 @@ export default class PredictResult extends React.Component }
{this.props.children} @@ -78,6 +95,10 @@ export default class PredictResult extends React.Component; + } + private renderItem = (item: any, key: any) => { const postProcessedValue = this.getPostProcessedValue(item); const style: any = { @@ -151,18 +172,70 @@ export default class PredictResult extends React.Component { + + private triggerJSONDownload = (): void => { const {analyzeResult} = this.props; const predictionData = JSON.stringify(analyzeResult); - const fileURL = window.URL.createObjectURL(new Blob([predictionData])); - const fileLink = document.createElement("a"); - const fileBaseName = this.props.downloadResultLabel.split(".")[0]; - const downloadFileName = this.props.downloadPrefix + "Result-" + fileBaseName + ".json"; - - fileLink.href = fileURL; - fileLink.setAttribute("download", downloadFileName); - document.body.appendChild(fileLink); - fileLink.click(); + downloadFile(predictionData, this.props.downloadResultLabel + ".json", this.props.downloadPrefix); + } + + private triggerCSVDownload = (): void => { + const data: zipData[] = []; + const items = this.getItems(); + let csvContent: string = `Key,Value,Confidence,Page,Bounding Box`; + items.forEach(item => { + csvContent += `\n"${item.fieldName}","${item.text ?? ""}",${isNaN(item.confidence)? "NaN":(item.confidence * 100).toFixed(2) + "%"},${item.page},"[${item.boundingBox}]"`; + }); + data.push({ + fileName: `${this.props.downloadPrefix}${this.props.downloadResultLabel}-keyvalues.csv`, + data: csvContent + }); + + let tableContent: string = ""; + const itemNames=["fieldName","text","confidence","page","boundingBox"]; + const getValue=(item:any, fieldName:string)=>{ + switch(fieldName){ + case "fieldName": + return `"${item[fieldName]}"`; + case "text": + return `"${item[fieldName]}"`; + case "confidence": + return isNaN(item.confidence)? "NaN":(item.confidence * 100).toFixed(2) + "%"; + case "page": + return item[fieldName]; + case "boundingBox": + return `"[${item.boundingBox}]"`; + default: + return ""; + } + } + itemNames.forEach(name=>{ + tableContent+=(name+","); + items.forEach(item=>{ + tableContent+=(getValue(item,name)+","); + }) + tableContent+="\n"; + }) + data.push({ + fileName: `${this.props.downloadPrefix}${this.props.downloadResultLabel}-table.csv`, + data: tableContent + }); + downloadZipFile(data, this.props.downloadResultLabel); + } + + private getItems() { + const {tags, predictions} = this.props; + const tagsDisplayOrder = tags.map((tag) => tag.name); + for (const name of Object.keys(predictions)) { + const prediction = predictions[name]; + if (prediction != null) { + prediction.fieldName = name; + prediction.displayOrder = tagsDisplayOrder.indexOf(name); + } + } + // not sure if we decide to filter item by the page + const items = Object.values(predictions).filter(Boolean).sort((p1, p2) => p1.displayOrder - p2.displayOrder); + return items; } private toPercentage = (x: number): string => { diff --git a/yarn.lock b/yarn.lock index 364d00519..832750438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6637,6 +6637,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" @@ -7887,6 +7892,16 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3: array-includes "^3.1.1" object.assign "^4.1.0" +jszip@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" + integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -7997,6 +8012,13 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -9269,7 +9291,7 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -pako@^1.0.5, pako@~1.0.5: +pako@^1.0.5, pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -11800,6 +11822,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"