From c6797fd97bff15481e30427d2c9c0e3dcbbdbefa Mon Sep 17 00:00:00 2001 From: wangyu Date: Wed, 18 Mar 2026 19:03:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=95=BF=E5=9B=BE=E6=8B=BC?= =?UTF-8?q?=E6=8E=A5=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91=E8=BD=AC?= =?UTF-8?q?=E6=96=87=E5=AD=97(=E5=BE=85=E5=AE=8C=E5=96=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entry/libs/qcloudfileflash.har | Bin 0 -> 3654 bytes entry/oh-package-lock.json5 | 7 + entry/oh-package.json5 | 3 +- entry/src/main/ets/common/Constants.ets | 5 + entry/src/main/ets/common/RouterUrls.ets | 10 + .../ets/entity/VoiceRecognizeResultEntity.ets | 27 ++ .../main/ets/manager/LocalMediaManager.ets | 2 +- entry/src/main/ets/manager/MediaManager.ets | 4 +- .../src/main/ets/pages/main/home/HomePage.ets | 5 +- .../pages/main/home/tools/AddAudioPage.ets | 2 + .../main/home/tools/AddWatermarkPage.ets | 1 + .../pages/main/home/tools/AudioToTextPage.ets | 6 + .../pages/main/home/tools/ClipVideoPage.ets | 1 + .../pages/main/home/tools/ImageMergePage.ets | 279 ++++++++----- .../pages/main/home/tools/MD5ResetPage.ets | 1 + .../pages/main/home/tools/RemoveAudioPage.ets | 1 + .../main/home/tools/RemoveWatermarkPage.ets | 1 + .../pages/main/home/tools/VideoMirrorPage.ets | 1 + .../main/home/tools/VideoReversePage.ets | 1 + .../main/home/tools/VideoToAudioPage.ets | 3 +- .../pages/main/home/tools/VideoToTextPage.ets | 386 ++++++++++++++++++ .../ets/pages/main/mine/tool/ToolsPage.ets | 2 + .../main/ets/pages/photo/PhotoViewPage.ets | 33 +- entry/src/main/ets/utils/ImageUtils.ets | 53 +++ entry/src/main/ets/utils/MediaUtils.ets | 2 + entry/src/main/ets/utils/SaveUtils.ets | 30 ++ entry/src/main/ets/view/RecordItemView.ets | 23 +- .../resources/base/media/ic_copy_text.webp | Bin 0 -> 398 bytes .../main/resources/base/media/ic_star.webp | Bin 0 -> 1086 bytes .../resources/base/profile/main_pages.json | 2 + oh-package-lock.json5 | 8 + oh-package.json5 | 3 +- 32 files changed, 788 insertions(+), 114 deletions(-) create mode 100644 entry/libs/qcloudfileflash.har create mode 100644 entry/src/main/ets/entity/VoiceRecognizeResultEntity.ets create mode 100644 entry/src/main/ets/pages/main/home/tools/AudioToTextPage.ets create mode 100644 entry/src/main/ets/pages/main/home/tools/VideoToTextPage.ets create mode 100644 entry/src/main/ets/utils/ImageUtils.ets create mode 100644 entry/src/main/resources/base/media/ic_copy_text.webp create mode 100644 entry/src/main/resources/base/media/ic_star.webp diff --git a/entry/libs/qcloudfileflash.har b/entry/libs/qcloudfileflash.har new file mode 100644 index 0000000000000000000000000000000000000000..cfc58c5ce4de877a4e52fb88853317237ad3c5d1 GIT binary patch literal 3654 zcmV-M4!Q9kiwFP!000001MOV@Q`vp$0d!6>)leW9F=epoY`_Uz7C@~k1J!!8z zR#mwlpy+d|f7c8898o5~9r*V_Kw}@wz=+Whn9dew-c|@P*+PPEy}|){6+k{dphPel z1c;gYn}3;R>+)D7D%t<_;~W3C+5fiNUfchwWo`fKvaDqGAK)S1`c7jcCf=QI|MGj; z{-0%DB75OVAROh%N2%9$9pAyI_UGct&2*anQnm@=bh z7`72$4-qa!cpgD-jGlGUu_8o#L_CbR(Yu@3hL}e3tD%Lek`)tp1U>O?8c80SACn^^ z0evW-(YrG^jhTneC>7l3T^I$xF)nAe;n6UH0uKXp?oU2sn!BA!OFGJH$U;h{r{{*9 zkBEGuCs>STKNT#>>|ajC_Zs6H&i|dg_WJx^wLHZC=V<>s$no&?-`R0@*6Y7&`KI-k z(cUA=@$mKUcG`RE^T zxxtK*5D^Iw!NsLrOi0#C&eZ0{%--BEl$~VrMpA`qC_2$D?LCG}Rru%zdkNEIq|J?_ zZUo~%Bm{Coh5^bRI+(b|=EjDSh@nq0;^Hl0A?8xuoQIS|lZB2cccPrkIXZcFcy@C5 zPO{pV<@_+=0tP_>nO`v^yN+PMky=m8L=1yt=#4Q^Q@|!}$7wsAY+=*6Jyx5+Ks=q9 zR8t+rgNw_}joIeUg^>TZOXK-pBzcd*C|LPca7p~Xw^u#?JFe^Qt^NP1r)5o9prm4-3pqEaOj2&&4XgbNmXf-(ztdVmXt$(0SR zweJ({d!T&}J3Vj=qkm-2>JxkgBo2p&{b~c!>4E2r!Rd>5G(xPU#!TJ4)#`z_jE0z_ zl*O-QF+GN$K!cbCs{RTIAQf-ySa{(3*#D}xf{X3H+uf<&|8?5k&i2~=S1k{+|9_pI zo}3P^ktZ}CAS>Ss9B^}g^P!$1rT5r067&^=A-bh(l6LxM0!}#cVur=ksSp*thlwea zI;pCHBo;U*5y*<7c`QVvnhn)VkZ@$Bkvp?SCw5n}_{avIY`|>5Z6ItAw^ABe#29lY zbzz_nOmn}|lsmFTtrIkDY)+h7V@u)3KpzlvD_xA$s!~a(Cc8@{uQ0tp#6v_l9t~?e zrN2wLY3|5s*VyLjMvJulv`|?#Pgi~~RYgsxRQkBa7xD?{tB2gDDxz8P7xF3b3U{k7 zE}LpkvIIzg0Q3R8g;*$_h5yy(KO+u7KiP>X*=r%9CNh$$Rv2TIiZ~nz%Y2?B(D9c9 zwVO5wF5JszO1Z5O5zl0_VcNiiQ50a6Am5^%K;jt}48hQ>bjKrLX)EFch+K(rt1=PV zK>vC6?%>%wNo(5rT2}bZYD&|5BEx)Tf{XU0-7r>&#F+X$HJ4|K_&BL^-kzSnvuhp3 z2>OWi0C8wKpLH7aSupq#3fR;3kTaYL^hcsJ==VVz{03s;qY);^2RdO{yM|dT7SoAI z2pJ#4z@iRAJPt(5xssFTQ6HHt@V7o_H%+k^GkTlsj2fUifBEK+G0Lo3OHwUkbm#o= z?8D*N;PCA1^vnkN(2Ev^ArUi@(-x!!nFob82G*o1u+(`1lCY9^h>qsVJYUtxBH31) zjg!N7f1aMb$);oCgotx^g={csRrSJH||!}q>ilFZ%^+$=bB<-nSVE1 zvyWwlBhatHmeg+_L(ekLUq5%viVRnCyPQwMH)#4hB8WkWBFxvv&kw5I$GHoywo4(`b&lqz&S4@>E)RJBGt1}5+%b=!hiXS_y3-MfLy(`P+V+2c^5*39&l7d3fCI|{@J?I;CG=?^~(lR>Z&TnN7n)8t!LP{gEFAeg@W=)Wssw9e<1xy5DBe^2A z)R`>O(agyV&58&XOb*EvCdrip=mP^KPlM!QVqYiwK(U8jB`@ztx*Pd8P%rb83C|-0 zCy2eoFpEMYJy*2@zo;&EdrieHW&V+b9L88Z46t7&I*7TTVcK7jdU{o@!z&yJDGL)a zq%=m!H3Jfdl5b^t|CG9u zk&Ji@r8*hW>d1_XgE1ro1*arP6=GZrJXJZMB&eu$jz|*FAH&aC!~zVSRt`e=d7z@V z#rG;`su2`*VZe^|IzDkgzmzF=Tdrq;Cg9LZpViuBQ z4v$1*YdEjJYHsfPJi>3~oLFDn!jZw8Q=X2_Jemr0OZ;;d)0{4^=}FIv}QI6-nR{fn zZ~ox`c1w-jh{@uAC<tk7j@!$Gjkw)x!0qL3I&L=$++HTC<94%( z+shrwxZTuodlO-X+f5y}HxZ_|-PCYPm+00_V}3u%CPNStglx1=>hezX~mrHC>5TC0cwn z3TjqfLV~J(Xq8bx5OO{6#*9(Nx=lFW5d_}ObOvHxhY>|{Qc)`oX zREv!7DQ6N9-BZv~sMsv23X{eQq|El%c&UJC$hAzkN~qc>R~}xCTpL_9N+&Pau3ULp zIPd%h4OWm`5T#7(HZ!Z$pn#+Mvi4Dc1hPi=#uVLFP`ai2kJ{~^T{DT&y}VtU0~`3&38-w(hQNHj zydR(5r6Iv=@bs<}Ea*3GU@j`k(nx(UOf7KMvTi{i+-PZO36S;Y3?R>1=^b!Fk$%k@ z|FV$mhbxWofBxE+<2#S=|A3af|GQKF{m))^x4Vx2S1sRg|D!OV_AX!+R_d>=YRz98 z5`RNV0|ZH$0{k^~5ZQ|_l}ujtR!&dzBNvm5=CUcVSnw|dN!^3`NZnNNvdyiYsnk2I zvxrOV|55%S(6Zlu)$V`Yz3rX#@4r?q53v75c3(B%`a2J@pQ8*6kET}ovYX5T9umPY z;t5b`*}#fj8{D`p(Eqi98zhvwarL*x3SkytjE13Z7Gs#S{!4EBvzr|G z28ippPRFSK@GBuK1$B}fdR}Mby2d)h`bo-C`+o!kxXk|Ds{OlryF2UnZ{_k3{I|^R Y*Y>_H>#{EE^1oI73xOV}8~|Pb09g+}BLDyZ literal 0 HcmV?d00001 diff --git a/entry/oh-package-lock.json5 b/entry/oh-package-lock.json5 index 52698da..2f481bb 100644 --- a/entry/oh-package-lock.json5 +++ b/entry/oh-package-lock.json5 @@ -12,6 +12,7 @@ "cmccssosdk@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/quick_login_hm_1.0.2.har": "cmccssosdk@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/quick_login_hm_1.0.2.har", "ctaccount@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/ctaccount_v1.1.2.har": "ctaccount@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/ctaccount_v1.1.2.har", "dljson@../oh_modules/.ohpm/ctaccount@qccjk9bmoqtng+2vpbi+2wqjznsjx4thqhodhlvlvn0=/oh_modules/ctaccount/library/dlJson.har": "dljson@../oh_modules/.ohpm/ctaccount@qccjk9bmoqtng+2vpbi+2wqjznsjx4thqhodhlvlvn0=/oh_modules/ctaccount/library/dlJson.har", + "qcloudfileflash@libs/qcloudfileflash.har": "qcloudfileflash@libs/qcloudfileflash.har", "unicom_login_harmony@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/unicom_login_harmony_v1.0.4AR001B0214.har": "unicom_login_harmony@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/unicom_login_harmony_v1.0.4AR001B0214.har" }, "packages": { @@ -62,6 +63,12 @@ "resolved": "../oh_modules/.ohpm/ctaccount@qccjk9bmoqtng+2vpbi+2wqjznsjx4thqhodhlvlvn0=/oh_modules/ctaccount/library/dlJson.har", "registryType": "local" }, + "qcloudfileflash@libs/qcloudfileflash.har": { + "name": "qcloudfileflash", + "version": "1.0.0", + "resolved": "libs/qcloudfileflash.har", + "registryType": "local" + }, "unicom_login_harmony@../oh_modules/.ohpm/@getui+gysdk@1.0.10/oh_modules/@getui/gysdk/libs/unicom_login_harmony_v1.0.4AR001B0214.har": { "name": "unicom_login_harmony", "version": "1.0.4", diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 index 3b94d70..15fd6e3 100644 --- a/entry/oh-package.json5 +++ b/entry/oh-package.json5 @@ -7,7 +7,8 @@ "license": "", "dependencies": { "@ohos/axios": "^2.2.6", - "@getui/gysdk": "1.0.10" + "@getui/gysdk": "1.0.10", + "qcloudfileflash": "file:./libs/qcloudfileflash.har" } } diff --git a/entry/src/main/ets/common/Constants.ets b/entry/src/main/ets/common/Constants.ets index 5bc2755..d1813d9 100644 --- a/entry/src/main/ets/common/Constants.ets +++ b/entry/src/main/ets/common/Constants.ets @@ -16,4 +16,9 @@ export class Constants { // static readonly ENCRYPT = "wE8x4EnIHgyGOyjnoluzI2vk60wz5eNI" static readonly SIGNATURE = "hfLLOtXRjd0e1Ac7O6sAXrECH2E828S9" + + //腾讯云 + static readonly QCLOUD_APP_ID = "1366199074" + static readonly QCLOUD_SECRET_ID = "AKIDYGvVCi06ycDk8ZprfFclgNpFer4D9sPi" + static readonly QCLOUD_SECRET_KEY = "72iBPPBj390d2PipqhMmyve9QSFpBKEu" } \ No newline at end of file diff --git a/entry/src/main/ets/common/RouterUrls.ets b/entry/src/main/ets/common/RouterUrls.ets index 676e53f..1ef6ac0 100644 --- a/entry/src/main/ets/common/RouterUrls.ets +++ b/entry/src/main/ets/common/RouterUrls.ets @@ -89,6 +89,16 @@ export class RouterUrls { */ static readonly IMAGE_MERGE_PAGE = "pages/main/home/tools/ImageMergePage" + /** + * 视频转文字页 + */ + static readonly VIDEO_TO_TEXT_PAGE = "pages/main/home/tools/VideoToTextPage" + + /** + * 语音转文字页 + */ + static readonly AUDIO_TO_TEXT_PAGE = "pages/main/home/tools/AudioToTextPage" + /** * 素材详情页 */ diff --git a/entry/src/main/ets/entity/VoiceRecognizeResultEntity.ets b/entry/src/main/ets/entity/VoiceRecognizeResultEntity.ets new file mode 100644 index 0000000..63c05c7 --- /dev/null +++ b/entry/src/main/ets/entity/VoiceRecognizeResultEntity.ets @@ -0,0 +1,27 @@ +import { Type } from "class-transformer"; +import "reflect-metadata"; + +export class VoiceRecognizeResultEntity { + code: number = 0; + message: string = ''; + request_id: string = ''; + @Type(() => FlashResult) + flash_result: Array = []; + audio_duration: number = 0; +} + +export class FlashResult { + channel_id: number = 0; + @Type(() => Sentence) + sentence_list: Array = []; + text: string = ''; +} + +export class Sentence { + emotional_energy: number = 0; + end_time: number = 0; + speaker_id: number = 0; + speech_speed: number = 0; + start_time: number = 0; + text: string = ''; +} \ No newline at end of file diff --git a/entry/src/main/ets/manager/LocalMediaManager.ets b/entry/src/main/ets/manager/LocalMediaManager.ets index b25dfac..5c4fc3e 100644 --- a/entry/src/main/ets/manager/LocalMediaManager.ets +++ b/entry/src/main/ets/manager/LocalMediaManager.ets @@ -47,7 +47,7 @@ export class LocalMediaManager { static getAllImages(): Array { let array = PrefUtils.getStringArray('local_record') - return array.filter(item => item.endsWith('.jpeg')) + return array.filter(item => item.endsWith('.jpeg') || item.endsWith('.jpg') || item.endsWith('.png')) } static deleteAllImages() { diff --git a/entry/src/main/ets/manager/MediaManager.ets b/entry/src/main/ets/manager/MediaManager.ets index 6718ece..f68de1b 100644 --- a/entry/src/main/ets/manager/MediaManager.ets +++ b/entry/src/main/ets/manager/MediaManager.ets @@ -46,6 +46,7 @@ export class MediaManager { } else { console.error('Create AVImageGenerator failed!'); } + FileUtil.closeSync(file) mediaList.push(record) } catch (e) { console.error(e) @@ -69,9 +70,10 @@ export class MediaManager { for (let i = 0; i < imageUri.length; i++) { try { let uri = imageUri[i] - let file = FileUtil.openSync(uri) + let file = FileUtil.openSync(uri) //判断图片是否存在 FileUtil.access()无效 let record = new MediaRecordEntity(uri) record.name = FileUtil.getFileName(uri) + FileUtil.closeSync(file) mediaList.push(record) } catch (e) { console.error(e) diff --git a/entry/src/main/ets/pages/main/home/HomePage.ets b/entry/src/main/ets/pages/main/home/HomePage.ets index f57c87a..c9b1906 100644 --- a/entry/src/main/ets/pages/main/home/HomePage.ets +++ b/entry/src/main/ets/pages/main/home/HomePage.ets @@ -144,13 +144,15 @@ export struct HomePage { break; } case 'check_Task': { - + break } case 'course': { this.getUIContext().getRouter().pushUrl({url: RouterUrls.COURSE_PAGE}) + break } case 'web_link': { WantUtil.toWebBrowser(Constants.WEB_URL) + break } } }) @@ -348,6 +350,7 @@ export struct HomePage { break } case 'videoToText': { + this.getUIContext().getRouter().pushUrl({url: RouterUrls.VIDEO_TO_TEXT_PAGE}) break } case 'longImageMerge': { diff --git a/entry/src/main/ets/pages/main/home/tools/AddAudioPage.ets b/entry/src/main/ets/pages/main/home/tools/AddAudioPage.ets index 304edf6..0677cef 100644 --- a/entry/src/main/ets/pages/main/home/tools/AddAudioPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/AddAudioPage.ets @@ -34,6 +34,7 @@ struct AddAudioPage { let videoFile = FileUtil.openSync(this.videoUri!!, fileIo.OpenMode.READ_ONLY) // 复制视频文件到缓存目录下 FileUtil.copyFileSync(videoFile.fd, cacheVideoPath) + FileUtil.closeSync(videoFile) let cacheAudioPath = FileUtil.getCacheDirPath() + FileUtil.separator + `cache_${systemDateTime.getTime()}.mp3` if (FileUtil.accessSync(cacheAudioPath)) { @@ -42,6 +43,7 @@ struct AddAudioPage { let audioFile = FileUtil.openSync(this.audioUri!!, fileIo.OpenMode.READ_ONLY) // 复制音频文件到缓存目录下 FileUtil.copyFileSync(audioFile.fd, cacheAudioPath) + FileUtil.closeSync(audioFile) let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp4` let cmd = `ffmpeg -i ${cacheVideoPath} -stream_loop -1 -i ${cacheAudioPath} -c:v copy -c:a aac -shortest -map 0:v -map 1:a ${outputPath}` diff --git a/entry/src/main/ets/pages/main/home/tools/AddWatermarkPage.ets b/entry/src/main/ets/pages/main/home/tools/AddWatermarkPage.ets index eb5ebfe..619734b 100644 --- a/entry/src/main/ets/pages/main/home/tools/AddWatermarkPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/AddWatermarkPage.ets @@ -48,6 +48,7 @@ struct AddWatermarkPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheVideoPath) + FileUtil.closeSync(file) let imageX = (vp2px(this.rect.x) * this.videoSize.width!!) / this.playerSize.width!! let imageY = (vp2px(this.rect.y) * this.videoSize.height!!) / this.playerSize.height!! diff --git a/entry/src/main/ets/pages/main/home/tools/AudioToTextPage.ets b/entry/src/main/ets/pages/main/home/tools/AudioToTextPage.ets new file mode 100644 index 0000000..0d40db3 --- /dev/null +++ b/entry/src/main/ets/pages/main/home/tools/AudioToTextPage.ets @@ -0,0 +1,6 @@ +@ComponentV2 +@Entry +struct AudioToTextPage { + build() { + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/main/home/tools/ClipVideoPage.ets b/entry/src/main/ets/pages/main/home/tools/ClipVideoPage.ets index 2f00f9c..3f9844c 100644 --- a/entry/src/main/ets/pages/main/home/tools/ClipVideoPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/ClipVideoPage.ets @@ -43,6 +43,7 @@ struct ClipVideoPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheVideoPath) + FileUtil.closeSync(file) let clipWidth: number = 0 let clipHeight: number = 0 diff --git a/entry/src/main/ets/pages/main/home/tools/ImageMergePage.ets b/entry/src/main/ets/pages/main/home/tools/ImageMergePage.ets index afe99bc..80cc73b 100644 --- a/entry/src/main/ets/pages/main/home/tools/ImageMergePage.ets +++ b/entry/src/main/ets/pages/main/home/tools/ImageMergePage.ets @@ -1,8 +1,8 @@ import { PhotoHelper } from '@pura/picker_utils' import { TitleBar } from '../../../../view/TitleBar' import { photoAccessHelper } from '@kit.MediaLibraryKit' -import { BusinessError, systemDateTime } from '@kit.BasicServicesKit' -import { AppUtil, FileUtil } from '@pura/harmony-utils' +import { BusinessError } from '@kit.BasicServicesKit' +import { AppUtil, DisplayUtil, FileUtil } from '@pura/harmony-utils' import { ToastUtils } from '../../../../utils/ToastUtils' import { fileIo } from '@kit.CoreFileKit' import { SaveUtils } from '../../../../utils/SaveUtils' @@ -10,23 +10,74 @@ import { LoadingDialog } from '../../../../dialog/LoadingDialog' import { DownloadDialog, DownloadStatus } from '../../../../dialog/DownloadDialog' import { EventConstants } from '../../../../common/EventConstants' import { TipDialog } from '../../../../dialog/TipDialog' -import { avSessionManager } from '../../../../manager/AVSessionManager' +import { image } from '@kit.ImageKit' +import { ImageUtils } from '../../../../utils/ImageUtils' +import { Luban } from '@ark/luban' @Entry @ComponentV2 struct ImageMergePage { - @Local uri?: string = undefined + @Local pixelMap?: image.PixelMap = undefined @Local selectedImage?: string = undefined @Local imageUris: Array = [] - @Local currentTime: number = 0 - @Local durationTime: number = 0 - @Local isPlaying: boolean = false @Local isSuccess: boolean = false private selectedImages: Array = [] - mergeImage() { + async mergeImage() { + LoadingDialog.show(this.getUIContext()) + try { + let pixelArray: Array = [] + let maxWidth = DisplayUtil.getWidth() + for (let i = 0;i < this.imageUris.length;i++) { + let compressedUri = await Luban.with(this.imageUris[i]).ignoreBy(100).get() + const imageSource: image.ImageSource = image.createImageSource(compressedUri[0]) + let decodingOptions: image.DecodingOptions = { + editable: true, + desiredPixelFormat: image.PixelMapFormat.RGB_565, + } + let pixelMap = imageSource.createPixelMapSync(decodingOptions) + if (pixelMap) { + let imageInfo = pixelMap.getImageInfoSync() + maxWidth = Math.max(imageInfo.size.width) + pixelArray.push(pixelMap) + } else { + ToastUtils.show('处理失败') + return + } + } + let totalHeight = 0 + let newPixelArray: Array = [] + for (let i = 0;i < pixelArray.length;i++) { + let pixelMap = pixelArray[i] + let imageInfo = pixelMap.getImageInfoSync() + let height = Math.trunc(imageInfo.size.height * maxWidth / imageInfo.size.width) + let newPixel = await ImageUtils.resizeImage(pixelMap, maxWidth, height) + newPixelArray.push(newPixel) + totalHeight += height + } + + let offScreenCanvas = new OffscreenCanvas(maxWidth, totalHeight) + let OffScreenContext = offScreenCanvas.getContext('2d') + let top = 0 + newPixelArray.forEach((pixelMap) => { + let imageInfo = pixelMap.getImageInfoSync() + OffScreenContext.drawImage(pixelMap, 0, top, imageInfo.size.width, imageInfo.size.height) + top += imageInfo.size.height + }) + this.pixelMap = OffScreenContext.getPixelMap(0, 0, maxWidth, totalHeight) + + this.selectedImage = undefined + this.imageUris = [] + this.selectedImages = [] + this.isSuccess = true + ToastUtils.show('处理成功') + } catch (e) { + console.error(e) + ToastUtils.show('处理失败') + } + LoadingDialog.dismiss() } selectPhotos() { @@ -34,11 +85,12 @@ struct ImageMergePage { MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: 9, preselectedUris: this.selectedImages, - isOriginalSupported: true, + isOriginalSupported: false, }) .then((result: photoAccessHelper.PhotoSelectResult) => { if (result.photoUris.length != 0) { this.isSuccess = false + this.pixelMap = undefined this.selectedImages = result.photoUris this.imageUris = result.photoUris this.selectedImage = result.photoUris[0] @@ -75,110 +127,126 @@ struct ImageMergePage { TitleBar({ title: '长图拼接' }) Stack() { - Stack() { - Stack() { - Column() { - Image($r('app.media.ic_add_image')).width(40).height(40) - Text('请上传图片').fontColor($r('app.color.color_466afd')).fontSize(15).fontWeight(FontWeight.Medium).margin({ top: 8}) - } - } - .width('100%') - .height('100%') - .onClick(() => { - this.selectPhotos() - }) - .visibility(this.imageUris.length !== 0 ? Visibility.None : Visibility.Visible) - - Image(this.selectedImage).width('100%').height('100%') - .borderRadius(20) - .visibility(this.imageUris.length === 0 ? Visibility.None : Visibility.Visible) + Scroll() { + Image(this.pixelMap).width('100%').height('auto') } - .width('100%') - .aspectRatio(1) - .borderRadius(20) - .backgroundColor(Color.White) - .shadow({radius: 10, color: '#1a9399a1'}) - } - .width('100%') - .height('auto') - .padding({left: 32, right: 32}) - .margin({top: 40}) + .height('100%') + .scrollBar(BarState.Off) + .visibility(this.pixelMap ? Visibility.Visible : Visibility.None) - Blank().layoutWeight(1) - - Scroll() { - List({space: 8}) { - ForEach(this.imageUris, (item: string, index) => { - ListItem() { - RelativeContainer() { - Stack() { - Image(item).width('100%').height('100%').borderRadius(6) - Text(`${index + 1}`) - .width(20) - .height(20) - .textAlign(TextAlign.Center) - .fontColor(Color.White) - .fontSize(14) - .borderRadius(10) - .backgroundColor('#99000000') - } - .width(80) - .height(80) - .margin({top: 11, right: 11}) - - Image($r('app.media.ic_delete_image')).width(22).height(22) - .alignRules({ - right: {anchor: '__container__', align: HorizontalAlign.End} - }) - .onClick(() => { - this.imageUris.splice(index, 1) - if (this.imageUris.length === 0) { - this.selectedImage = undefined - } else { - if (item !== this.selectedImage) { - this.selectedImage = this.imageUris[0] - } - } - }) - } - .height('100%') - .aspectRatio(1) - } - .onClick(() => { - this.selectedImage = item - }) - }) - - if (this.imageUris.length > 0 && this.imageUris.length < 9) { - ListItem() { + Column() { + Stack() { + Stack() { Stack() { Column() { - Image($r('app.media.ic_add_image')).width(24).height(24) - Text('请上传图片').fontColor($r('app.color.color_466afd')).fontSize(10).margin({top: 4}) + Image($r('app.media.ic_add_image')).width(40).height(40) + Text('请上传图片').fontColor($r('app.color.color_466afd')).fontSize(15).fontWeight(FontWeight.Medium).margin({ top: 8}) } } - .width(80) - .height(80) - .borderRadius(6) - .backgroundColor(Color.White) - .margin({top: 11, right: 11}) + .width('100%') + .height('100%') .onClick(() => { this.selectPhotos() }) - } - } - } - .width('100%') - .scrollBar(BarState.Off) - .listDirection(Axis.Horizontal) - .padding({left: 32, right: 32}) - } - .width('100%') - .height(92) - .scrollBar(BarState.Off) - .scrollable(ScrollDirection.Horizontal) - .margin({bottom: 20}) + .visibility(this.imageUris.length !== 0 ? Visibility.None : Visibility.Visible) + Image(this.selectedImage).width('100%').height('100%') + .borderRadius(20) + .visibility(this.imageUris.length === 0 ? Visibility.None : Visibility.Visible) + } + .width('100%') + .aspectRatio(1) + .borderRadius(20) + .backgroundColor(Color.White) + .shadow({radius: 10, color: '#1a9399a1'}) + } + .width('100%') + .height('auto') + .padding({left: 32, right: 32}) + .margin({top: 40}) + + Blank().layoutWeight(1) + + Scroll() { + List({space: 8}) { + ForEach(this.imageUris, (item: string, index) => { + ListItem() { + RelativeContainer() { + Stack() { + Image(item).width('100%').height('100%').borderRadius(6) + Text(`${index + 1}`) + .width(20) + .height(20) + .textAlign(TextAlign.Center) + .fontColor(Color.White) + .fontSize(14) + .borderRadius(10) + .backgroundColor('#99000000') + } + .width(80) + .height(80) + .margin({top: 11, right: 11}) + .borderWidth(1) + .borderRadius(6) + .borderColor(item === this.selectedImage ? $r('app.color.color_466afd') : Color.Transparent) + + Image($r('app.media.ic_delete_image')).width(22).height(22) + .alignRules({ + right: {anchor: '__container__', align: HorizontalAlign.End} + }) + .onClick(() => { + this.imageUris.splice(index, 1) + if (this.imageUris.length === 0) { + this.selectedImage = undefined + } else { + if (item !== this.selectedImage) { + this.selectedImage = this.imageUris[0] + } + } + }) + } + .height('100%') + .aspectRatio(1) + } + .margin({left: index === 0 ? 32 : 0, right: index === 8 ? 32 : 0}) + .onClick(() => { + this.selectedImage = item + }) + }) + + if (this.imageUris.length > 0 && this.imageUris.length < 9) { + ListItem() { + Stack() { + Column() { + Image($r('app.media.ic_add_image')).width(24).height(24) + Text('请上传图片').fontColor($r('app.color.color_466afd')).fontSize(10).margin({top: 4}) + } + } + .width(80) + .height(80) + .borderRadius(6) + .backgroundColor(Color.White) + .margin({top: 11, right: 11}) + } + .margin({right: 32}) + .onClick(() => { + this.selectPhotos() + }) + } + } + .width('100%') + .scrollBar(BarState.Off) + .listDirection(Axis.Horizontal) + } + .width('100%') + .height(92) + .scrollBar(BarState.Off) + .scrollable(ScrollDirection.Horizontal) + .margin({bottom: 20}) + } + .visibility(this.pixelMap ? Visibility.None : Visibility.Visible) + } + .layoutWeight(1) Stack() { Button('确认处理', { type: ButtonType.Capsule, stateEffect: true }) @@ -225,10 +293,13 @@ struct ImageMergePage { .layoutWeight(1) .backgroundColor($r('app.color.color_466afd')) .onClick(() => { - SaveUtils.saveImageVideoToAlbumDialog([this.uri!!]) + SaveUtils.savePixelMapToAlbum(this.pixelMap!!) .then((saved) => { if (saved) { + this.pixelMap = undefined + this.selectedImage = undefined this.imageUris = [] + this.selectedImages = [] this.showDownloadDialog() } else { ToastUtils.show('保存失败') diff --git a/entry/src/main/ets/pages/main/home/tools/MD5ResetPage.ets b/entry/src/main/ets/pages/main/home/tools/MD5ResetPage.ets index 60495ee..e76d08d 100644 --- a/entry/src/main/ets/pages/main/home/tools/MD5ResetPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/MD5ResetPage.ets @@ -33,6 +33,7 @@ struct MD5ResetPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, outputPath) + FileUtil.closeSync(file) if (FileUtil.accessSync(outputPath)) { this.uri = FileUtil.getUriFromPath(outputPath) diff --git a/entry/src/main/ets/pages/main/home/tools/RemoveAudioPage.ets b/entry/src/main/ets/pages/main/home/tools/RemoveAudioPage.ets index 7848aca..78d8a14 100644 --- a/entry/src/main/ets/pages/main/home/tools/RemoveAudioPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/RemoveAudioPage.ets @@ -33,6 +33,7 @@ struct RemoveAudioPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cachePath) + FileUtil.closeSync(file) let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp4` let cmd = `ffmpeg -i ${cachePath} -an -c:v copy ${outputPath}` diff --git a/entry/src/main/ets/pages/main/home/tools/RemoveWatermarkPage.ets b/entry/src/main/ets/pages/main/home/tools/RemoveWatermarkPage.ets index a8ba4ab..d41a99b 100644 --- a/entry/src/main/ets/pages/main/home/tools/RemoveWatermarkPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/RemoveWatermarkPage.ets @@ -47,6 +47,7 @@ struct RemoveWatermarkPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheVideoPath) + FileUtil.closeSync(file) let rectX = (vp2px(this.rect.x) * this.videoSize.width!!) / this.playerSize.width!! let rectY = (vp2px(this.rect.y) * this.videoSize.height!!) / this.playerSize.height!! diff --git a/entry/src/main/ets/pages/main/home/tools/VideoMirrorPage.ets b/entry/src/main/ets/pages/main/home/tools/VideoMirrorPage.ets index 4c8c12b..345a10d 100644 --- a/entry/src/main/ets/pages/main/home/tools/VideoMirrorPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/VideoMirrorPage.ets @@ -36,6 +36,7 @@ struct VideoMirrorPage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheVideoPath) + FileUtil.closeSync(file) let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp4` let cmd = `ffmpeg -i ${cacheVideoPath} -vf ${this.orientation === 1 ? "hflip" : "vflip"} -c:v h264 -pix_fmt yuv420p -y ${outputPath}` diff --git a/entry/src/main/ets/pages/main/home/tools/VideoReversePage.ets b/entry/src/main/ets/pages/main/home/tools/VideoReversePage.ets index 4c6a269..71922a1 100644 --- a/entry/src/main/ets/pages/main/home/tools/VideoReversePage.ets +++ b/entry/src/main/ets/pages/main/home/tools/VideoReversePage.ets @@ -34,6 +34,7 @@ struct VideoReversePage { let file = FileUtil.openSync(this.uri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheVideoPath) + FileUtil.closeSync(file) let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp4` let cmd = `ffmpeg -i ${cacheVideoPath} -vf reverse -af areverse -c:v h264 -pix_fmt yuv420p -y ${outputPath}` diff --git a/entry/src/main/ets/pages/main/home/tools/VideoToAudioPage.ets b/entry/src/main/ets/pages/main/home/tools/VideoToAudioPage.ets index 231d7b1..49ca687 100644 --- a/entry/src/main/ets/pages/main/home/tools/VideoToAudioPage.ets +++ b/entry/src/main/ets/pages/main/home/tools/VideoToAudioPage.ets @@ -17,11 +17,11 @@ import { avSessionManager } from '../../../../manager/AVSessionManager' struct VideoToAudioPage { private controller: VideoController = new VideoController() @Local videoUri?: string + @Local audioUri?: string @Local currentTime: number = 0 @Local durationTime: number = 0 @Local isPlaying: boolean = false @Local isSuccess: boolean = false - @Local audioUri?: string videoToAudio() { LoadingDialog.show(this.getUIContext()) @@ -33,6 +33,7 @@ struct VideoToAudioPage { let file = FileUtil.openSync(this.videoUri!!, fileIo.OpenMode.READ_ONLY) // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cachePath) + FileUtil.closeSync(file) let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp3` let cmd = `ffmpeg -i ${cachePath} -vn -c:a mp3 ${outputPath}` diff --git a/entry/src/main/ets/pages/main/home/tools/VideoToTextPage.ets b/entry/src/main/ets/pages/main/home/tools/VideoToTextPage.ets new file mode 100644 index 0000000..99cee31 --- /dev/null +++ b/entry/src/main/ets/pages/main/home/tools/VideoToTextPage.ets @@ -0,0 +1,386 @@ +import { PhotoHelper } from '@pura/picker_utils' +import { TitleBar } from '../../../../view/TitleBar' +import { photoAccessHelper } from '@kit.MediaLibraryKit' +import { BusinessError, systemDateTime } from '@kit.BasicServicesKit' +import { AppUtil, FileUtil } from '@pura/harmony-utils' +import { ToastUtils } from '../../../../utils/ToastUtils' +import { fileIo } from '@kit.CoreFileKit' +import { SaveUtils } from '../../../../utils/SaveUtils' +import { LoadingDialog } from '../../../../dialog/LoadingDialog' +import { DownloadDialog, DownloadStatus } from '../../../../dialog/DownloadDialog' +import { EventConstants } from '../../../../common/EventConstants' +import { TipDialog } from '../../../../dialog/TipDialog' +import { avSessionManager } from '../../../../manager/AVSessionManager' +import { MP4Parser } from '@ohos/mp4parser' +import { QCloud } from 'qcloudfileflash' +import { Constants } from '../../../../common/Constants' +import { VoiceRecognizeResultEntity } from '../../../../entity/VoiceRecognizeResultEntity' +import { plainToInstance } from 'class-transformer' + +@Entry +@ComponentV2 +struct VideoToTextPage { + private controller: VideoController = new VideoController() + @Local resultText?: string + @Local videoUri?: string + @Local currentTime: number = 0 + @Local durationTime: number = 0 + @Local isPlaying: boolean = false + @Local isSuccess: boolean = false + + videoToAudio() { + LoadingDialog.show(this.getUIContext()) + this.isSuccess = false + let cachePath = FileUtil.getCacheDirPath() + FileUtil.separator + `cache_${systemDateTime.getTime()}.mp4` + if (FileUtil.accessSync(cachePath)) { + FileUtil.unlinkSync(cachePath) + } + let file = FileUtil.openSync(this.videoUri!!, fileIo.OpenMode.READ_ONLY) + // 复制文件到缓存目录下 + FileUtil.copyFileSync(file.fd, cachePath) + FileUtil.closeSync(file) + + let outputPath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.mp3` + let cmd = `ffmpeg -i ${cachePath} -vn -c:a mp3 ${outputPath}` + MP4Parser.ffmpegCmd(cmd, { + callBackResult: (code: number) => { + if (code === 0) { + this.recognizeAudio(outputPath) + .then((result: VoiceRecognizeResultEntity) => { + this.resultText = result.flash_result[0].text + this.isSuccess = true + this.isPlaying = false + ToastUtils.show('处理成功') + LoadingDialog.dismiss() + }) + .catch((e: BusinessError) => { + console.log(e.message) + ToastUtils.show('处理失败') + LoadingDialog.dismiss() + }) + } else { + ToastUtils.show('处理失败') + LoadingDialog.dismiss() + } + } + }) + } + + async recognizeAudio(uri: string): Promise { + let builder = new QCloud.FileFlash.Builder() + builder.appID = Constants.QCLOUD_APP_ID + builder.secretID = Constants.QCLOUD_SECRET_ID + builder.secretKey = Constants.QCLOUD_SECRET_KEY + // builder.token = this._token + builder.setApiParam(QCloud.FileFlash.kEngineType, '16k_zh') + builder.setApiParam(QCloud.FileFlash.kVoiceFormat, 'mp3') + builder.setApiParam(QCloud.FileFlash.kFilterDirty, 0) + builder.setApiParam(QCloud.FileFlash.kFilterModal, 0) + builder.setApiParam(QCloud.FileFlash.kFilterPunc, 0) + builder.setApiParam(QCloud.FileFlash.kConvertNumMode, 1) + builder.setApiParam(QCloud.FileFlash.kWordInfo, 0) + builder.setApiParam(QCloud.FileFlash.kSpeakerDiarization, 1) + try { + let file = FileUtil.openSync(uri, fileIo.OpenMode.READ_ONLY) + const stat = FileUtil.lstatSync(uri) + const buffer = new ArrayBuffer(stat.size) + FileUtil.readSync(file.fd, buffer) + FileUtil.closeSync(file) + let result = await builder.build(buffer).task + const voiceResult = plainToInstance(VoiceRecognizeResultEntity, result) + return Promise.resolve(voiceResult) + } catch (e) { + console.error(e) + return Promise.reject(e) + } + } + + selectVideo() { + PhotoHelper.selectEasy({ + MIMEType: photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE, + maxSelectNumber: 1, + isPhotoTakingSupported: false, + isEditSupported: false, + isOriginalSupported: false + }) + .then((uris) => { + if (uris.length != 0) { + this.isSuccess = false + this.videoUri = uris[0] + } + }) + } + + showDownloadDialog() { + DownloadDialog.show(this.getUIContext(), { status: DownloadStatus.COMPLETED, totalSize: 0, progress: 0, totalCount: 1, index: 0, callback: { + confirm: () => { + AppUtil.getContext().eventHub.emit(EventConstants.JumpToRecordEvent, 0) + this.getUIContext().getRouter().back() + } + } }) + } + + formatTime(time: number): string { + let minute: number = 0 + let second: number = 0 + if (time > 60) { + minute = Math.trunc(time / 60) + second = time % 60 + if (minute < 10) { + if (second < 10) { + return `0${minute}:0${second}` + } else { + return `0${minute}:${second}` + } + } else { + if (second < 10) { + return `${minute}:0${second}` + } else { + return `${minute}:${second}` + } + } + } else { + second = time + if (second < 10) { + return `00:0${second}` + } else { + return `00:${second}` + } + } + } + + onBackPress(): boolean | void { + if (this.isSuccess) { + TipDialog.show(this.getUIContext(), {title:'温馨提示', content:'文本尚未保存,是否确定退出?', callback: { + confirm: () => { + this.getUIContext().getRouter().back() + } + }}) + return true + } + return false + } + + build() { + Column() { + TitleBar({ title: '视频转文字' }) + + Stack() { + Stack() { + Stack() { + Column() { + Image($r('app.media.ic_add_video')).width(40).height(40) + Text('请上传视频').fontColor($r('app.color.color_466afd')).fontSize(15).fontWeight(FontWeight.Medium).margin({ top: 8}) + } + } + .width('100%') + .height('100%') + .onClick(() => { + this.selectVideo() + }) + .visibility(this.videoUri ? Visibility.None : Visibility.Visible) + + RelativeContainer() { + Video({ + src: this.videoUri, // 设置视频源 + controller: this.controller, //设置视频控制器,可以控制视频的播放状态 + posterOptions: { showFirstFrame: true } + }) + .width('100%') + .height('100%') + .borderRadius(20) + .backgroundColor(Color.White) + .controls(false) // 设置是否显示默认控制条 + .autoPlay(false) // 设置是否自动播放 + .loop(false) // 设置是否循环播放 + .objectFit(ImageFit.Contain) // 设置视频填充模式 + .onPrepared((event) => { + if (event) { + this.durationTime = event.duration + } + }) + .onUpdate((event) => { + if (event) { + this.currentTime = event.time + } + }) + .onStart(() => { + this.isPlaying = true + }) + .onPause(() => { + this.isPlaying = false + avSessionManager.deactivate() + }) + .onStop(() => { + this.isPlaying = false + avSessionManager.deactivate() + }) + .onFinish(() => { + this.isPlaying = false + avSessionManager.deactivate() + }) + .onError(() => { + this.isPlaying = false + avSessionManager.deactivate() + }) + .onDisAppear(() => { + avSessionManager.deactivate() + }) + + Image($r('app.media.ic_play_video')) + .width(50) + .height(50) + .visibility(this.isPlaying ? Visibility.None : Visibility.Visible) + .onClick(async () => { + await avSessionManager.activate() + this.controller.start() + }) + .alignRules({ + left: { anchor: '__container__', align: HorizontalAlign.Start }, + top: { anchor: '__container__', align: VerticalAlign.Top }, + right: { anchor: '__container__', align: HorizontalAlign.End }, + bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, + }) + + Row() { + Image(this.isPlaying ? $r('app.media.ic_player_controls_pause') : $r('app.media.ic_player_controls_play')) + .width(20) + .height(20) + .margin({ right: 20 }) + .onClick(async () => { + if (this.isPlaying) { + this.controller.pause() + } else { + await avSessionManager.activate() + this.controller.start() + } + }) + Text(this.formatTime(this.currentTime)).width(35).fontColor(Color.White).fontSize(12) + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .blockColor(Color.White) + .trackColor($r('app.color.color_60ffffff')) + .onChange((value: number, mode: SliderChangeMode) => { + this.controller.setCurrentTime(value); // 设置视频播放的进度跳转到value处 + }) + .layoutWeight(1) + Text(this.formatTime(this.durationTime)).width(35).fontColor(Color.White).fontSize(12) + } + .opacity(0.8) + .width("100%") + .borderRadius({bottomLeft: 20, bottomRight: 20}) + .backgroundColor('#1A000000') + .padding({ left: 30, right: 30 }) + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom } + }) + } + .width('100%') + .height('100%') + .visibility(this.videoUri ? Visibility.Visible : Visibility.None) + } + .width('100%') + .aspectRatio(1) + .borderRadius(20) + .backgroundColor(Color.White) + .shadow({radius: 10, color: '#1a9399a1'}) + } + .width('100%') + .height('auto') + .padding({left: 32, right: 32}) + .margin({top: 40}) + + Column() { + Row() { + Image($r('app.media.ic_star')).width(22).height(22) + Text('文本结果').fontColor($r('app.color.color_212226')).fontSize(15).fontWeight(FontWeight.Medium).margin({left: 4}) + } + Divider().strokeWidth(1).color($r('app.color.color_eeeeee')).margin({top: 12}) + Text(this.resultText).width('100%').height('auto').fontColor($r('app.color.color_212226')).fontSize(14).margin({top: 14}) + } + .width('90%') + .layoutWeight(1) + .borderRadius(10) + .borderWidth(1) + .borderColor('#DADEE5') + .backgroundColor(Color.White) + .margin({top: 30, bottom: 20}) + .padding(12) + .visibility(this.resultText ? Visibility.Visible : Visibility.None) + + Blank().layoutWeight(1).visibility(this.resultText ? Visibility.None : Visibility.Visible) + + Stack() { + Button('确认处理', { type: ButtonType.Capsule, stateEffect: true }) + .width('100%') + .height(46) + .fontColor(Color.White) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .backgroundColor($r('app.color.color_466afd')) + .onClick(() => { + if (this.videoUri) { + this.videoToAudio() + } else { + ToastUtils.show('请上传视频') + } + }) + .visibility(!this.isSuccess ? Visibility.Visible : Visibility.None) + + Row() { + Button({ type: ButtonType.Capsule, stateEffect: true }) { + Row() { + Image($r('app.media.ic_reupload')).width(20).height(20) + Text('重新上传').fontColor($r('app.color.color_466afd')).fontSize(15).fontWeight(FontWeight.Medium) + } + } + .height(46) + .layoutWeight(1) + .borderWidth(1) + .borderColor($r('app.color.color_466afd')) + .backgroundColor(Color.Transparent) + .onClick(() => { + this.controller.stop() + this.selectVideo() + }) + + Blank().width(9) + + Button({ type: ButtonType.Capsule, stateEffect: true }) { + Row() { + Image($r('app.media.ic_copy_text')).width(20).height(20) + Text('复制文本').fontColor(Color.White).fontSize(15).fontWeight(FontWeight.Medium) + } + } + .height(46) + .layoutWeight(1) + .backgroundColor($r('app.color.color_466afd')) + .onClick(() => { + this.controller.stop() + SaveUtils.saveImageVideoToAlbumDialog([this.videoUri!!]) + .then((saved) => { + if (saved) { + this.videoUri = undefined + this.showDownloadDialog() + } else { + ToastUtils.show('保存失败') + } + }) + .catch((e: BusinessError) => { + ToastUtils.show('保存失败:' + e.message) + }) + }) + } + .visibility(this.isSuccess ? Visibility.Visible : Visibility.None) + } + .padding({left: 16, top: 9, right: 16, bottom: 30 }) + .backgroundColor(Color.White) + } + .width('100%') + .height('100%') + .backgroundColor($r('app.color.window_background')) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/main/mine/tool/ToolsPage.ets b/entry/src/main/ets/pages/main/mine/tool/ToolsPage.ets index ccd3961..8ebdae3 100644 --- a/entry/src/main/ets/pages/main/mine/tool/ToolsPage.ets +++ b/entry/src/main/ets/pages/main/mine/tool/ToolsPage.ets @@ -21,6 +21,7 @@ export struct ToolsPage { break } case 'videoToText': { + this.getUIContext().getRouter().pushUrl({url: RouterUrls.VIDEO_TO_TEXT_PAGE}) break } case 'videoToAudio': { @@ -28,6 +29,7 @@ export struct ToolsPage { break } case 'audioToText': { + this.getUIContext().getRouter().pushUrl({url: RouterUrls.AUDIO_TO_TEXT_PAGE}) break } case 'addWatermark': { diff --git a/entry/src/main/ets/pages/photo/PhotoViewPage.ets b/entry/src/main/ets/pages/photo/PhotoViewPage.ets index 745e572..4254204 100644 --- a/entry/src/main/ets/pages/photo/PhotoViewPage.ets +++ b/entry/src/main/ets/pages/photo/PhotoViewPage.ets @@ -1,4 +1,9 @@ +import { DisplayUtil } from '@pura/harmony-utils'; +import { ImagePreview } from '@rv/image-preview'; import { TitleBar } from '../../view/TitleBar' +import { media } from '@kit.MediaKit'; +import image from '@ohos.multimedia.image'; +import { fileIo } from '@kit.CoreFileKit'; @Entry @ComponentV2 @@ -6,8 +11,11 @@ struct PhotoViewPage { @Local title: string = '' @Local uri?: string = '' + private imageSize: media.PixelMapParams = {} + aboutToAppear(): void { this.initParams() + this.initImageSize() } initParams() { @@ -18,12 +26,31 @@ struct PhotoViewPage { } } + initImageSize() { + let file = fileIo.openSync(this.uri, fileIo.OpenMode.READ_ONLY) + let imageSource = image.createImageSource(file.fd) + let imageInfo = imageSource.getImageInfoSync() + this.imageSize.width = imageInfo.size.width + this.imageSize.height = imageInfo.size.height + } + build() { Column() { TitleBar({title: this.title}).width('100%') - Image(this.uri).width('100%').layoutWeight(1) - .margin({bottom: 50}) - .objectFit(ImageFit.Contain) + Stack() { + ImagePreview() { + Image(this.uri) + .width('100%') + .objectFit(ImageFit.Contain) + .draggable(false) + .sourceSize({ + width: px2vp(DisplayUtil.getWidth()), + height: px2vp(Math.round(this.imageSize.height!! * DisplayUtil.getWidth() / this.imageSize.width!!)) + }) + } + } + .layoutWeight(1) + .margin({bottom: 50}) } .width('100%') .height('100%') diff --git a/entry/src/main/ets/utils/ImageUtils.ets b/entry/src/main/ets/utils/ImageUtils.ets new file mode 100644 index 0000000..7b1a159 --- /dev/null +++ b/entry/src/main/ets/utils/ImageUtils.ets @@ -0,0 +1,53 @@ +import { common2D, drawing } from "@kit.ArkGraphics2D"; +import { image } from "@kit.ImageKit"; + +export class ImageUtils { + + /** + * 调整图片分辨率 + * @param bitmap + * @param w + * @param h + * @returns + */ + static async resizeImage(bitmap: PixelMap, w: number, h: number): Promise { + // 获取图像像素信息 + const imageInfo = await bitmap.getImageInfo(); + const width: number = imageInfo.size.width; + const height: number = imageInfo.size.height; + const scaleWidth: number = w / width; + const scaleHeight: number = h / height; + const pixelMapColor: ArrayBuffer = new ArrayBuffer(w * h * 2); + const options: image.InitializationOptions = { + editable: true, + pixelFormat: image.PixelMapFormat.RGB_565, + size: { height: h, width: w } + }; + // 采用RGB_565格式创建画布PixelMap + const canvasPixelMap = await image.createPixelMap(pixelMapColor, options); + // 创建一个以PixelMap作为绘制目标的Canvas对象 + const canvas = new drawing.Canvas(canvasPixelMap); + // 构造矩阵对象 + const matrix = new drawing.Matrix(); + // 构造画笔对象 + let pen = new drawing.Pen(); + // 绑定画笔到画布上,在画布上进行绘制时,将使用画笔的样式去绘制图形形状的轮廓 + canvas.attachPen(pen); + // 矩形区域 + let rect: common2D.Rect = { + left: 0, + top: 0, + right: w, + bottom: h + }; + // 将图片绘制到画布的指定区域上 + canvas.drawImageRect(bitmap, rect); + // 将矩阵设置为矩阵右乘围绕轴心点按一定缩放系数缩放后的单位矩阵后得到的矩阵 + matrix.postScale(scaleWidth, scaleHeight, 0, 0); + // 设置矩阵对象参数 + canvas.setMatrix(matrix); + // 将画笔与画布解绑 + canvas.detachPen(); + return Promise.resolve(canvasPixelMap) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/MediaUtils.ets b/entry/src/main/ets/utils/MediaUtils.ets index b11ae96..ba232ab 100644 --- a/entry/src/main/ets/utils/MediaUtils.ets +++ b/entry/src/main/ets/utils/MediaUtils.ets @@ -13,6 +13,7 @@ export class MediaUtils { let metadata = await avMetaDataExtractor.fetchMetadata() videoSize.width = parseInt(metadata.videoWidth as string); videoSize.height = parseInt(metadata.videoHeight as string); + FileUtil.closeSync(file) return Promise.resolve(videoSize) } catch (e) { let cacheFilePath = FileUtil.getCacheDirPath() + '/' + FileUtil.getFileName(uri) @@ -20,6 +21,7 @@ export class MediaUtils { let file = FileUtil.openSync(uri, fileIo.OpenMode.READ_ONLY); // 复制文件到缓存目录下 FileUtil.copyFileSync(file.fd, cacheFilePath) + FileUtil.closeSync(file) avMetaDataExtractor.fdSrc = FileUtil.openSync(cacheFilePath); let metadata = await avMetaDataExtractor.fetchMetadata() diff --git a/entry/src/main/ets/utils/SaveUtils.ets b/entry/src/main/ets/utils/SaveUtils.ets index fc7d0ca..9e76d1f 100644 --- a/entry/src/main/ets/utils/SaveUtils.ets +++ b/entry/src/main/ets/utils/SaveUtils.ets @@ -6,9 +6,39 @@ import { EventConstants } from '../common/EventConstants'; import { MediaAction, MediaType } from '../manager/MediaManager'; import { LocalMediaManager } from '../manager/LocalMediaManager'; import { systemDateTime } from '@kit.BasicServicesKit'; +import { image } from '@kit.ImageKit'; export class SaveUtils { + /** + * 保存pixelMap到相册 + * @param pixelMap + * @returns + */ + static async savePixelMapToAlbum(pixelMap: image.PixelMap): Promise { + try { + const packOptions: image.PackingOption = { + format: 'image/jpeg', + quality: 80 + } + let buffer = await image.createImagePacker().packToData(pixelMap, packOptions) + // 应用沙箱路径 + let cachePath = FileUtil.getCacheDirPath() + FileUtil.separator + `scmf_${systemDateTime.getTime()}.jpeg` + // 在沙箱新建并打开文件 + let file = fileIo.openSync(cachePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE) + // 写入pixelMap图片内容 + fileIo.writeSync(file.fd, buffer) + // 关闭文件 + fileIo.closeSync(file.fd) + // 使用showAssetsCreationDialog保存沙箱中的图片 + let saved = await SaveUtils.saveImageVideoToAlbumDialog([cachePath]) + return Promise.resolve(saved) + } catch (e) { + console.error(e) + return Promise.resolve(false) + } + } + /** * 保存视频和图片到相册, 弹窗授权 * @param path diff --git a/entry/src/main/ets/view/RecordItemView.ets b/entry/src/main/ets/view/RecordItemView.ets index f8af4cc..bd10562 100644 --- a/entry/src/main/ets/view/RecordItemView.ets +++ b/entry/src/main/ets/view/RecordItemView.ets @@ -1,12 +1,15 @@ import { RouterUrls } from '../common/RouterUrls'; import { MediaRecordEntity } from '../entity/MediaRecordEntity'; import { router } from '@kit.ArkUI'; -import { AppUtil, DateUtil, StrUtil } from '@pura/harmony-utils'; +import { AppUtil, DateUtil, DisplayUtil, StrUtil } from '@pura/harmony-utils'; import { ShareManager } from '../manager/ShareManager'; import { Want } from '@kit.AbilityKit'; import { SimpleTipDialog } from '../dialog/SimpleTipDialog'; import { PrefUtils } from '../utils/PrefUtils'; import { WantUtils } from '../utils/WantUtils'; +import { media } from '@kit.MediaKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { image } from '@kit.ImageKit'; @ComponentV2 export struct VideoRecordItemView { @@ -85,6 +88,20 @@ export struct ImageRecordItemView { @Param media?: MediaRecordEntity = undefined; @Param rowCount: number = 1; + private imageSize: media.PixelMapParams = {} + + aboutToAppear(): void { + this.initImageSize() + } + + initImageSize() { + let file = fileIo.openSync(this.media?.uri, fileIo.OpenMode.READ_ONLY) + let imageSource = image.createImageSource(file.fd) + let imageInfo = imageSource.getImageInfoSync() + this.imageSize.width = imageInfo.size.width + this.imageSize.height = imageInfo.size.height + } + build() { RelativeContainer() { Image(this.media?.uri) @@ -92,6 +109,10 @@ export struct ImageRecordItemView { .height('100%') .borderRadius(6) .backgroundColor($r('app.color.color_222222')) + .sourceSize({ + width: px2vp(DisplayUtil.getWidth() / 2), + height: px2vp(Math.round(this.imageSize.height!! * DisplayUtil.getWidth() / 2 / this.imageSize.width!!)) + }) .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: RouterUrls.PHOTO_VIEW_PAGE, params: { uri : this.media?.uri } }) }) diff --git a/entry/src/main/resources/base/media/ic_copy_text.webp b/entry/src/main/resources/base/media/ic_copy_text.webp new file mode 100644 index 0000000000000000000000000000000000000000..ac7b42369a346755113d0df440828047644e799c GIT binary patch literal 398 zcmV;90df9PNk&G70RRA3MM6+kP&il$0000G0000x0027x06|PpNEZPB00DrjZQCK| zU#pF6t#{8xWsGB3mq9tzqMvKJsP2$W*i@_zW z+yTJBh)H{;(<)w{Ezw>WZ(fhuv{wMmv$0q#HdrIHrvMv4@p@6L`~I&z07)G~aD%1o z6j)Bi=(FcQp00cz>}rTk-+RQRPL|RMd-vSQ1H}+%h7D{`GS&BNu6{G zMvFHiD~#Dtsrc0g09H^qAV>fJ0MG*fodGI506YLbaVn2Tq@tlAJK&HG326v$00ETc sq6npxWHkKn0RF>fpXdxF3E%Pwe)+mp6d?(hzJuqxSfnVP(supM8(I{L(_5S36i{X{bYK4ev!6#6h z6Gnw`_7hNj(>9EN)F+_+ybQl&%Uun;E*_efk@v6*?PqPnaI&i2fS+%shG1jH8u=AI zpD!4aN0=&m;s4E{Nf;RE*1tlK&iPs)1G1C$dWE3B?8q4*03&gyv*7qt6rJtaRc%1Ly9b$N><=XnM#E>h`F776og?Y4bdo4LL&-!>n6B_~u+_L`);Lx#9Zr zX>Mkd3)#7k`tqo9B;p)t$49-Jl}+LuXWU1fqj_!O9(%jSy}Tqa(tcOvd1XTaWxS-) z!z7Vb!?#|m!jjO4OFXV*AhGH1|KelUkaDPz`_rj7BsltJ0ADJTWV7B|V7V=dq8!AN zEF`@6`-$PCnT<1$c+-&gA3Ok7P&gp&0RRAy4FH`1DnbB406vjGm`J6hA|Wi)+c2;a z2|*43F-(T}PT&Wj&9?h?8|gdFSOZ_qeu~0(yCRVP;I`Kf?=m@i*&a{}$mriAUlw{n z+{}YG(zp+N{ACy2D8VQ}2N4!Jt_%m{Md|@*2=OlI!6v63w9_R30RGKzFT1j~F3}&o z7-+SB0B*}ZCnU*to=kjOt5&c z*Xust{LpcN{$l@L8;c&