文章目录前言一、为扫描页面准备输入和保存选项二、让用户完成编辑再接收返回结果三、把返回资源保存到应用目录四、分别处理图片预览和文档文件五、让失败后的页面仍然有明确状态总结核心关键代码扫描与文件接收Index.ets前言会议通知、纸质表格或课堂资料需要放进应用时用户通常会先拍一张照片。照片里可能带有桌面纸张边缘也可能倾斜。等到需要把它作为附件发送或保存时用户还得回到图库裁剪再决定使用图片还是文档格式。VisionKit的DocumentScanner可以提供拍摄、选择图片和文档编辑界面。用户完成裁剪或调整后应用再接收扫描结果把文件保存到自己的目录。接入时需要把界面中的保存选择和回调中的文件类型对应起来这样图片预览和 PDF 附件才能进入各自合适的处理流程。一、为扫描页面准备输入和保存选项页面提供两个入口。点击拍摄或图库时扫描界面负责获取图片。点击固定文档时应用先准备一张已有图片再把它交给扫描控件。两种入口最终都进入同一个编辑流程用户可以在保存前确认文档边缘和文字内容。已有图片放在entry/src/main/resources/rawfile中名称为ocr.png。应用资源适合随程序一起分发但扫描控件接收的是可访问的文件 URI资源名称本身不能直接作为文件地址使用。因此页面先读取资源内容将它复制到应用文件目录再使用fileUri.getUriFromPath()把文件路径转换为 URI。资源管理器读取的是打包在应用中的原始文件内容context.filesDir则指向应用可以管理文件的位置。页面完成复制后新的文件才有一个可以转换的沙箱路径。扫描控件拿到 URI 时读取的是这份文件因此需要等写入和关闭完成再开始显示扫描界面。复制完成以后页面把这个 URI 放入originalUris再显示扫描控件。用户选择拍摄或图库入口时页面将originalUris设为空让扫描流程自行取得内容。每次打开扫描页面都重新设置输入可以避免上一次准备的固定图片继续留在配置中。通过originalUris传入的图片还需要满足扫描控件的尺寸要求单边长度为 224 至 8000 像素宽高乘积不超过 4800 万长边与短边的比例不超过 3。控件会过滤不符合条件的图片。如果固定文档没有进入编辑页面可以先检查复制后的图片是否满足这些条件。扫描配置同时指定处理类型和保存选项。当前页面使用普通文档类型并提供 JPEG 和 PDF 两种输出。用户在扫描界面完成编辑后可以按接下来的用途选择图片或文档文件。privatescannerConfig:DocumentScannerConfig{supportType:[DocType.DOC],saveOptions:[SaveOption.JPG,SaveOption.PDF],isShareable:false};supportType限定扫描页面提供的文档类型saveOptions决定保存时可以选择哪些格式。页面把isShareable设为false让本次操作先回到应用接收文件。要调整支持的选项应同时检查回调处理是否认识新的返回类型避免界面开放了一种格式应用却没有对应的接收方式。配置只是告诉控件允许哪些操作用户还要在扫描界面中完成选择。应用创建控件时同时传入配置和onResult回调才能在用户结束操作后知道选择了哪种保存方式。页面不需要在用户编辑的每一步读取图片接收逻辑集中在结果返回以后执行。二、让用户完成编辑再接收返回结果页面使用scanning控制当前显示内容。打开扫描流程时页面显示DocumentScanner用户可以在系统提供的编辑界面里查看图片并调整文档。应用自己的结果区域等到回调返回后再显示因此不会因为扫描界面已经打开就提前把文件标记为保存成功。编辑时需要结合材料内容判断是否调整。裁剪可以去掉文档外的桌面但边缘靠得太近时也可能切掉页码或批注。滤镜能够改变对比度带有印章、彩色图表的材料则需要确认颜色和文字仍然清晰。用户在保存前检查这些内容应用收到的才是已经确认过的结果。用户选择保存以后控件通过onResult把返回码、保存类型和地址数组交给页面。返回码说明当前操作是否成功保存类型告诉应用结果属于 JPEG 还是 PDF地址数组则提供需要读取的资源。页面收到回调后先退出扫描界面再判断返回码和地址是否满足接收条件。地址指向的是扫描流程交出的资源并不等于文件内容已经存进应用的附件目录。页面取得地址后还要逐项打开并复制复制成功以后才能记录目标路径。如果只把返回地址显示在列表中应用自己的文件管理就缺少了明确的保存位置。当前代码在返回码为 200 且地址数组非空时开始复制。没有取得可用地址时页面显示本次状态不创建空附件。扫描页面的取消、返回和失败都可能让应用没有可接收的文件因此页面应以实际回调为依据而不是只根据用户是否打开过扫描界面判断结果。三、把返回资源保存到应用目录扫描控件返回资源地址以后应用还需要决定文件由谁持有。当前页面逐项打开返回的 URI使用fileIo.copyFileSync()把内容复制到context.filesDir。复制完成后页面保存目标文件路径后续展示和附件处理都可以从应用自己的文件目录继续。打开返回地址时使用只读模式写入目标位置由复制接口完成。每个地址都对应一次文件打开操作代码把关闭描述符放在finally中。这样某一项复制失败时已经打开的文件仍会经过关闭路径页面也能进入错误处理不会继续把这次操作显示为全部成功。文件描述符可以理解为应用打开源资源后取得的操作入口。复制接口通过它读取内容目标路径则指出数据要写入哪里。当前实现逐个处理地址只有一次复制完成后才继续下一项因此文件与结果数组中的顺序能够保持对应。输出文件名由返回类型和数组下标组成。JPEG 使用.jpgPDF 使用.pdf页面根据同一个保存类型选择文件名和后续分支。扩展名有助于应用识别文件用途真正决定内容格式的是扫描控件返回的类型单纯修改后缀不能完成图片与 PDF 之间的转换。地址数组表示本次返回的文件资源。应用遍历数组后把成功接收的目标路径加入scannedFiles再更新页面中的文件列表。PDF 文件本身可以包含页面返回地址数量与文档页数并不是同一个概念。需要展示页数时应用应读取文档内容不能直接拿地址数量充当页数。四、分别处理图片预览和文档文件两种输出进入应用以后后续操作不同。JPEG 可以解码为像素图页面可以直接显示预览也可以把图片交给其他图像处理能力。PDF 适合按照文档文件交给附件或文档查看流程页面需要保留文件路径和格式信息才能在后续打开时选择合适的处理方式。当前页面复制 JPEG 后使用图像源解码第一份图片再把返回的像素图交给Image组件。PDF 分支保留已接收文件的信息不会把 PDF 文件直接传给图片解码器。用户先保存 JPEG、又保存 PDF 时页面会清理原来的图片预览让结果区域对应最近一次选择的输出类型。JPEG 文件保存的是编码后的图片数据页面需要创建图像源并解码才能得到用于显示的像素图。图像源和打开的文件在解码结束后关闭像素图继续交由页面持有。再次接收结果时页面先释放上一次预览再根据新的文件类型决定是否创建图片。一次回调如果返回多个 JPEG 地址复制循环会逐项保存预览代码则只解码paths[0]。因此文件列表可以包含多张图片预览区域显示的仍然只有第一张。如果应用需要让用户逐页检查可以基于已经保存的路径增加切换入口再按选中的文件创建预览。页面目前没有这个切换操作。文件名、大小和预览可以帮助确认应用接收了什么但它们分别回答不同问题。文件名和扩展名帮助区分附件类型图片预览用于检查解码后的内容PDF 是否能够打开还需要交给文档查看能力检查。应用若要支持搜索文档中的文字也需要单独处理文字内容不能仅凭保存格式判断已经具备全文检索。文件列表中的大小由fileIo.statSync(path).size读取数值来自复制后的目标文件。它有助于检查文件是否写入了数据图片内容是否完整还要结合解码和预览判断。当前命名方式使用scan-0.jpg或scan-0.pdf这样的路径。再次保存同一种格式时数组下标会重新从零开始同名文件可能被覆盖。路径列表保留的是保存位置重复出现同一路径也不代表已经保留多份独立文件。需要长期保存多次扫描结果的应用应为每次操作分配独立文件名并将路径写入自己的附件记录。五、让失败后的页面仍然有明确状态文件接收可能发生在不同阶段。返回地址无法打开时页面还没有取得源文件描述符可以先检查扫描结果和资源访问是否正常。如果文件已经打开但复制失败则需要继续检查目标目录、文件写入和可用空间。把错误码和消息显示出来能够帮助开发者从发生问题的步骤开始排查。页面在复制所有返回资源后才把目标路径加入列表。如果复制过程中途出错已经写入目录的文件可能仍然存在但本次路径尚未全部加入列表。面向长期附件管理时可以补充逐项记录或失败清理避免磁盘中的文件和附件列表脱节。当前页面保留了失败提示没有把这些后续管理动作写成已经完成。JPEG 预览发生在文件接收之后。若文件已经复制成功而图片解码失败应用目录中的文件仍然存在页面会显示错误信息。检查时需要区分保存文件与生成预览两个步骤前者决定附件是否已进入应用目录后者决定能否在当前页面直接显示。页面离开时文件复制或图片解码可能还在进行。代码先标记页面已经离开等待当前页面任务结束再释放持有的预览像素图。如果解码完成时页面已经销毁新得到的像素图会直接释放不再交给界面显示。这样用户离开以后尚未结束的文件处理也有明确的收尾位置。页面还用activeJob记录是否有操作正在执行。用户触发接收文件以后入口会拦住重复操作直到当前任务结束才允许再次进入避免多次点击同时写入相同名称。扫描界面出现期间原页面的按钮不再显示扫描控件通过回调将本次操作结果交回应用。总结文档扫描从准备图片或打开拍摄入口开始用户在编辑界面确认内容再选择 JPEG 或 PDF。应用根据回调中的返回码、类型和资源地址接收文件并把它们复制到自己的文件目录。DocumentScanner负责扫描交互originalUris用于传入已有图片onResult把保存结果交还应用。文件接口负责读取和复制资源图片解码器用于展示 JPEGPDF 则作为文档文件保留。大家可以先准备一张包含文字的图片使用固定文档入口完成编辑分别尝试保存 JPEG 和 PDF。检查应用接收到的文件名与预览再根据自己的附件需求调整保存位置和命名方式。核心关键代码截图中的页面带有功能目录。下面的代码将文档扫描整理为独立页面启动后可以选择拍摄或读取工程中的固定图片。在自己的工程中准备ocr.png放入entry/src/main/resources/rawfile。将页面放在entry/src/main/ets/pages/Index.ets并在entry/src/main/resources/base/profile/main_pages.json的src中包含pages/Index。入口 Ability 的loadContent()需要加载这个页面。扫描与文件接收Index.ets页面包含扫描配置、已有图片的 URI 准备、结果回调和 JPEG 预览。关键操作旁的注释说明了文件关闭与页面退出时的处理顺序。import{image}fromkit.ImageKit;import{common}fromkit.AbilityKit;import{BusinessError}fromkit.BasicServicesKit;import{fileIo,fileUri}fromkit.CoreFileKit;import{DocumentScanner,DocumentScannerConfig,DocType,SaveOption}fromkit.VisionKit;EntryComponentstruct Index{Statescanning:booleanfalse;Statebusy:booleanfalse;Statestatus:string选择文档来源;Statedetail:string;Stateoutput:image.PixelMap|undefinedundefined;StatescannedFiles:string[][];privatealive:booleantrue;privateactiveJob:booleanfalse;privatepagePending:Promisevoid|undefinedundefined;privatecontext:common.UIAbilityContextthis.getUIContext().getHostContext()ascommon.UIAbilityContext;privatescannerConfig:DocumentScannerConfig{supportType:[DocType.DOC],saveOptions:[SaveOption.JPG,SaveOption.PDF],isShareable:false};aboutToAppear():void{this.alivetrue;}aboutToDisappear():void{this.alivefalse;this.dispose();}privatelaunch(action:()Promisevoid):void{if(this.activeJob||!this.alive){return;}this.activeJobtrue;this.pagePendingaction().catch((err:BusinessError){if(this.alive){this.fail(err);}}).finally((){this.activeJobfalse;});}privateasyncdispose():Promisevoid{// 文件复制和图片解码结束后再释放页面持有的预览图。try{awaitthis.pagePending;}catch(_){/* 任务入口已处理错误。 */}this.output?.release();this.outputundefined;}privatefail(err:BusinessError):void{if(!this.alive){return;}this.status处理失败可检查文件后重试;this.detail错误码(err.code??-1)\nerr.message;}privateasynccopyRaw(name:string):Promisestring{constbytesawaitthis.context.resourceManager.getRawFileContent(name);constpaththis.context.filesDir/name;constfilefileIo.openSync(path,fileIo.OpenMode.CREATE|fileIo.OpenMode.READ_WRITE|fileIo.OpenMode.TRUNC);try{fileIo.writeSync(file.fd,bytes.bufferasArrayBuffer);}finally{fileIo.closeSync(file);}returnpath;}privateasyncscanner(fixed:boolean):Promisevoid{if(this.busy){return;}this.busytrue;try{// 已有图片先复制到应用目录扫描控件接收文件 URI。this.scannerConfig.originalUrisfixed?[fileUri.getUriFromPath(awaitthis.copyRaw(ocr.png))]:[];if(!this.alive){return;}this.scanningtrue;}catch(err){this.fail(errasBusinessError);}finally{if(this.alive){this.busyfalse;}}}privateasyncscannerResult(code:number,type:SaveOption,uris:string[]):Promisevoid{this.scanningfalse;// 只有扫描成功并返回资源地址时才复制文件。if(code!200||uris.length0){this.status扫描未返回可保存结果code;return;}this.busytrue;try{constpaths:string[][];for(leti0;iuris.length;i){constfilefileIo.openSync(uris[i],fileIo.OpenMode.READ_ONLY);try{constpaththis.context.filesDir/scan-i(typeSaveOption.PDF?.pdf:.jpg);fileIo.copyFileSync(file.fd,path);paths.push(path);}finally{fileIo.closeSync(file);}}this.scannedFilesthis.scannedFiles.concat(paths);this.status已接收并保存 uris.length 份(typeSaveOption.PDF? PDF: JPEG);this.detailthis.scannedFiles.map((path)path.substring(path.lastIndexOf(/)1) · fileIo.statSync(path).size 字节).join(\n);this.output?.release();this.outputundefined;if(typeSaveOption.JPG){// 只把图片结果交给图像解码器PDF 保留为文档文件。constfilefileIo.openSync(paths[0],fileIo.OpenMode.READ_ONLY);letsource:image.ImageSource|undefinedundefined;try{sourceimage.createImageSource(file.fd);constpmawaitsource.createPixelMap();if(!this.alive){pm.release();return;}this.outputpm;}finally{source?.release();fileIo.closeSync(file);}}}catch(err){this.fail(errasBusinessError);}finally{if(this.alive){this.busyfalse;}}}build(){Column({space:12}){if(this.scanning){DocumentScanner({scannerConfig:this.scannerConfig,onResult:(code:number,type:SaveOption,uris:string[]){this.launch(()this.scannerResult(code,type,uris));}}).width(100%).height(100%)}else{Text(文档扫描与导出).fontSize(23).fontWeight(FontWeight.Bold).width(100%)Row({space:8}){Button(拍摄或图库).enabled(!this.busy).onClick((){this.launch(()this.scanner(false));})Button(固定文档).enabled(!this.busy).onClick((){this.launch(()this.scanner(true));})}if(this.output){Image(this.output).height(330).objectFit(ImageFit.Contain)}Text(this.status).fontColor(#145897).width(100%)Text(this.detail).fontSize(14).width(100%)}}.width(100%).height(100%).padding({top:16,left:18,right:18,bottom:16}).backgroundColor(#F1F4F9)}}
阅读完成 · 觉得有帮助?