Xamarin.Android实现App内版本更新
目录
- 1、具体的效果
- 2、代码实现
- 2.1 基本原理
- 2.2 开发环境
- 2.3 具体代码
- 2.3.1 基本设置
- 2.3.2 系统的权限授予
- 2.3.3 进度条的layout文件
- 2.3.4 核心的升级文件
- 3、代码下载
- 4、知识点
- 5、参考文献
1、具体的效果
有事需要在程序内集成自动更新的功能,网上找了下,改改适配下Xamarin.Android
,效果如下
2、代码实现
2.1 基本原理
这个功能本质上,就是使用一个Intent
打开一个apk
文件进行预览。Android
系统遇到预览apk
文件时,就会弹出“是否进行安装更新”这类的安装框。
2.2 开发环境
VS2022,.NET7,Xamarin.Android、实体手机的Android版本:11
2.3 具体代码
2.3.1 基本设置
1、允许访问http
为了安全,从Android 7.0之后,不允许直接访问http的资源,因为我们会把安装包放在http的网络环境中,因此需要进行一个设置:在AndroidManifest.xml
中application
节点中,直接添加android:usesCleartextTraffic="true"
即可。Android访问http的方案说明
2、设置FileProvider
同样,为了安全,在Android7.0之后,系统安装APP必须使用FileProvider
,因此需要在AndroidManifest.xml
中进行配置provider
3、权限设置
为了能够下载、存放、读取安装包,需要一系列的权限。需要在AndroidManifest.xml
中进行配置
因此最终的配置文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.2" package="com.updateapp" android:installLocation="auto"><uses-sdk android:minSdkVersion="28" android:targetSdkVersion="33" /><!--为了能够安装apk文件,需要下面的一系列授权--><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /><application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true"> <!--这句话是为了可以访问http的资源--><!--下面的配置,是为了设置FileProvider,其中用到了file_paths配置文件,具体如下--><provider android:name="androidx.core.content.FileProvider" android:authorities="com.updateapp.fileprovider" android:exported="false" android:grantUriPermissions="true"><meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data></provider></application>
</manifest>
在Resources
文件下创建xml
文件夹,并创建file_paths.xml
配置文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><!--安装包文件存储路径--><external-files-pathname="my_download"path="Download" /><external-pathname="."path="." />
</paths>
以上,就是第一步,程序的基本配置
2.3.2 系统的权限授予
除了AndroidManifest.xml
中进行配置权限外,还需要进行权限的程序判定及授权
protected override void OnCreate(Bundle savedInstanceState)
{base.OnCreate(savedInstanceState);Xamarin.Essentials.Platform.Init(this, savedInstanceState);SetContentView(Resource.Layout.activity_main);Toolbar toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);SetSupportActionBar(toolbar);FloatingActionButton fab = FindViewById<FloatingActionButton>(Resource.Id.fab);fab.Click += FabOnClick;//版本跟踪,这个是和Android不一样的地方VersionTracking.Track();//初始化自动升级的功能autoUpdater=new AutoUpdater(this);try{//6.0之后才能使用动态授权if(Build.VERSION.SdkInt>=BuildVersionCodes.M){string[] permissions ={Manifest.Permission.ReadExternalStorage,Manifest.Permission.WriteExternalStorage,Manifest.Permission.AccessWifiState,Manifest.Permission.Internet};List<string> permissionList = new List<string>();for (int i = 0; i < permissions.Length; i++){if(ActivityCompat.CheckSelfPermission(this, permissions[i])!=Permission.Granted){ permissionList.Add(permissions[i]);}}//if(permissionList.Count==0){//更新程序autoUpdater.checkUpdate();}else{//获取授权ActivityCompat.RequestPermissions(this, permissions, 100);}}}catch(Exception e){Toast.MakeText(this,"发生异常:"+e.Message,ToastLength.Long).Show();}
}public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);bool checkPermissionFlag = true;if (requestCode == 100){for(int i = 0; i < permissions.Length; i++){if (grantResults[i]== Permission.Granted){checkPermissionFlag = checkPermissionFlag && true;}else{checkPermissionFlag = checkPermissionFlag && false;}}if(!checkPermissionFlag){//授权程序Snackbar.Make(View.Inflate(this,Resource.Id.activity_main_layout,null),"需要授权",Snackbar.LengthIndefinite).SetAction("ok",new Action<View>(delegate (View obj){ActivityCompat.RequestPermissions(this, permissions, 100);})).Show();}else{//更新程序Toast.MakeText(this, "授权后,可以进行更新程序啦!", ToastLength.Long).Show();autoUpdater.checkUpdate();}}base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
2.3.3 进度条的layout文件
在layout文件夹中添加progress.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:id="@+id/titleBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TextViewandroid:id="@+id/txtStatus"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="状态"android:textSize="10sp"android:textStyle="normal" /><ProgressBarandroid:id="@+id/progress"style="?android:attr/progressBarStyleHorizontal"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_toLeftOf="@id/txtStatus" /></LinearLayout>
</LinearLayout>
2.3.4 核心的升级文件
using Android.App;
using Android.Content;
using Android.Net;
using Android.OS;
using Android.Runtime;
using Android.Systems;
using Android.Util;
using Android.Views;
using Android.Widget;
using Java.IO;
using Java.Net;
using Java.Util.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Context = Android.Content.Context;
using Environment = Android.OS.Environment;namespace UpdateApp
{public class AutoUpdater{private Android.App.AlertDialog confirmDialog = null; //确认是否下载的对话框private Android.App.AlertDialog loadingDialog = null; //正在下载的对话框public MainActivity mainActivity;private UpdateHandler updateHandler;// 保存APK的文件名private static string saveFileName = "my.apk";private static File apkFile;// 进度条与通知UI刷新的handler和msg常量public ProgressBar mProgress;public TextView txtStatus;public int progress;// 当前进度public AutoUpdater(MainActivity activity) {mainActivity = activity;updateHandler = new UpdateHandler(this);apkFile =new File(mainActivity.GetExternalFilesDir(Environment.DirectoryDownloads), saveFileName);}//主方法public void checkUpdate(){//新开启一个线程,进行下载及逻辑判断Task.Run(() => {//获取本地的版本名称(一般而言就是1.0、1.1、1.2的纯数字)string localVersionName = VersionTracking.CurrentVersion;//获取服务器的版本string remoteServerVersion = "2.2"; //远程获取服务器上最新版本,这儿省事儿了,直接默认取了一个较大的值if (Convert.ToDouble(localVersionName)< Convert.ToDouble(remoteServerVersion)){//启动升级的界面updateHandler.SendEmptyMessage((int)UpdateStatusEnum.BeginLoad);}});}//弹框进行下载public void ShowUpdateDialog(){Android.App.AlertDialog.Builder builder = null;builder = new Android.App.AlertDialog.Builder(mainActivity);confirmDialog = builder.SetTitle("软件版本更新").SetMessage("有最新的软件包,请下载并安装!").SetPositiveButton("立即下载", (s, e) => { //确定按钮及内部方法ShowDownloadDialog();confirmDialog.Dismiss();}).SetNegativeButton("以后再说", (s, e) => { //界面上的关闭按钮及方法confirmDialog.Dismiss();}).Create();confirmDialog.Show();}//弹出确认下载的进度条的内容private void ShowDownloadDialog(){CancellationTokenSource cts = new CancellationTokenSource();CancellationToken cancellationToken = cts.Token;Android.App.AlertDialog.Builder builder = null;builder = new Android.App.AlertDialog.Builder(mainActivity);View view = mainActivity.LayoutInflater.Inflate(Resource.Layout.progress, null, false);mProgress = view.FindViewById<ProgressBar>(Resource.Id.progress);txtStatus = view.FindViewById<TextView>(Resource.Id.txtStatus);loadingDialog = builder.SetView(view).SetTitle("正在更新").SetNegativeButton("取消下载", (s, e) => { //界面上的关闭按钮及方法cts.Cancel();}).Create();loadingDialog.Show();DownloadApk(cancellationToken);}//下载APPprivate void DownloadApk(CancellationToken cancellationToken){Task.Run(() => {try { URL url = new URL(@"http://xxx/xxx/xxx/com.updateapp.apk");//apk的网络地址URLConnection conn = url.OpenConnection();conn.Connect();int length = conn.ContentLength;System.IO.Stream ins = conn.InputStream;FileOutputStream fos = new FileOutputStream(apkFile);int count = 0;byte[] buf = new byte[1024];while (!cancellationToken.IsCancellationRequested){int numread = ins.Read(buf);count += numread;progress = (int)(((float)count / length) * 100);//下载进度Message message = new Message();message.What = (int)UpdateStatusEnum.Loading;Bundle extras = new Bundle();extras.PutInt("progress", progress);message.Data = extras;updateHandler.SendMessage(message);if (numread <= 0){Message msg = new Message();//下载完成msg.What = (int)UpdateStatusEnum.Finish; extras.PutInt("progress", 100);msg.Data = extras;updateHandler.SendMessage(msg);//关闭下载框if(loadingDialog!=null) loadingDialog.Dismiss(); break;}fos.Write(buf, 0, numread);}fos.Close();ins.Close();}catch (System.OperationCanceledException el){Log.Info("info", "用户取消了操作!"+el.Message);}catch (AggregateException e){foreach (Exception ex in e.InnerExceptions){Log.Info("info", "发生异常!" + ex.Message);}}}, cancellationToken);}public void installAPK(){try{if(!apkFile.Exists()){Toast.MakeText(mainActivity, "下载的文件不存在!", ToastLength.Short).Show();return;}//这儿是整个的核心Intent intent = new Intent();intent.SetAction(Intent.ActionView);intent.AddFlags(ActivityFlags.NewTask);intent.AddFlags(ActivityFlags.GrantReadUriPermission);intent.AddFlags(ActivityFlags.GrantWriteUriPermission);if (Build.VERSION.SdkInt >=Android.OS.BuildVersionCodes.N){string packageName = mainActivity.ApplicationContext.PackageName;string authority = new StringBuilder(packageName).Append(".fileprovider").ToString();Android.Net.Uri apkUri = FileProvider.GetUriForFile(mainActivity, authority, apkFile);intent.SetDataAndType(apkUri, "application/vnd.android.package-archive");}else{intent.SetDataAndType(Android.Net.Uri.FromFile(apkFile), "application/vnd.android.package-archive");}mainActivity.StartActivity(intent);}catch (Exception ex){Toast.MakeText(mainActivity, "安装installAPK发生异常"+ex.Message, ToastLength.Short).Show();}}}//状态枚举 public enum UpdateStatusEnum:int{ BeginLoad=1,Loading=2,Finish=3}//Handler事件public class UpdateHandler : Android.OS.Handler{private WeakReference<AutoUpdater> weakReference;[Obsolete]public UpdateHandler(AutoUpdater autoUpdater){weakReference = new WeakReference<AutoUpdater>(autoUpdater);}public override void HandleMessage(Message msg){AutoUpdater targetActivity;bool isGetSuccess = weakReference.TryGetTarget(out targetActivity);if (isGetSuccess){switch (msg.What){case (int)UpdateStatusEnum.BeginLoad:targetActivity.ShowUpdateDialog();break;case (int)UpdateStatusEnum.Loading://获取状态数据,并进行展示int progress = msg.Data.GetInt("progress");targetActivity.txtStatus.SetText(progress + "%",TextView.BufferType.Normal);targetActivity.mProgress.SetProgress(progress, true);break;case (int)UpdateStatusEnum.Finish:Toast.MakeText(targetActivity.mainActivity, "下载完毕", ToastLength.Long).Show();targetActivity.installAPK();break;default:break;}}base.HandleMessage(msg);}}}
上面是这个的核心
3、代码下载
代码下载
4、知识点
1、Handler的用法,C#与Java还是不同的,这里涉及到的知识点是匿名类和委托。C#的匿名类是一个field的集合,不能包含方法
2、Android中更新应用的逻辑
每个 Android 应用均有一个唯一的应用 ID,像 Java 或 Kotlin 软件包名称一样,例如 com.example.myapp。此 ID 可以作为每个应用在设备上的唯一标识。Android 设备一次只能安装一个具有指定应用 ID 的应用。
为了让 Android 平台接受更新,更新必须满足以下条件:
应用更新的应用 ID 必须与已安装应用的应用 ID 相同。
应用更新的签名证书必须与已安装应用的签名证书相同,或者必须包含有效的 proof-of-rotation。
应用更新的版本代码必须高于或等于已安装应用的版本代码。
在某些情况下,用户可能需要接受更新。
请注意,如果多个更新具有相同的签名证书并且具有相同或更高的版本代码,Android 内部并没有防范措施能够阻止不同的安装程序更新应用。
如要安装不符合上述条件的应用,用户必须先卸载当前已安装的版本,而卸载操作会清除设备上的所有应用数据。
5、参考文献
主要参考了前两个
1、Android App自动安装
2、Android APP 自动更新实现(适用Android9.0)
3、【Android】APP检测版本升级更新、apk安装
4、Andrioid FileProvider在Xamarin.Forms中的使用
5、Xamarin.Android 中 Handler 的使用
相关文章:

Xamarin.Android实现App内版本更新
目录 1、具体的效果2、代码实现2.1 基本原理2.2 开发环境2.3 具体代码2.3.1 基本设置2.3.2 系统的权限授予2.3.3 进度条的layout文件2.3.4 核心的升级文件 3、代码下载4、知识点5、参考文献 1、具体的效果 有事需要在程序内集成自动更新的功能,网上找了下ÿ…...

运维工程师面经
文章目录 前言RedisMongoDBPython中的GIL(全局解释器锁)Python算法总结 前言 本博客仅做学习笔记,如有侵权,联系后即刻更改 科普: Redis 参考网址 NoSQL技术 基于内存的数据库,并且提供一定的持久化功能…...

stm32之智能垃圾桶实战
之前用过51做过一个垃圾桶的小项目,这里用32重新搞了一下。视频的效果和之前一样,可参考这个垃圾桶效果 。 一、项目描述(同51) 项目主要是模拟不用手动打开垃圾桶盖,而进行自动操作。自动打开的条件如下:…...

【C++面向对象侯捷下】2.转换函数 | 3.non-explicit-one-argument ctor
文章目录 operator double() const {} 歧义了 标准库的转换函数...

UOS Deepin Ubuntu Linux 开启 ssh 远程登录
UOS Deepin Ubuntu Linux 开启 ssh 远程登录 打开控制台 安装 openssh-server sudo apt -y install openssh-server修改 /etc/ssh/ssh_config 文件 sudo vim /etc/ssh/ssh_config找到 # Port 22 去掉 # 注释后 保存 重启 ssh 服务 sudo systemctl restart ssh设置 ssh 服务 开机…...

Postman应用——接口请求和响应(Get和Post请求)
文章目录 新增Request请求Get请求Post请求 Request请求响应Postman响应界面说明请求响应另存为示例(模板)Postman显示的响应数据清空请求响应数据保存到本地文件 这里只讲用的比较多的Get和Post请求方式,也可以遵循restful api接口规范&#…...

Linux查看哪些进程占用的系统 buffer/cache 较高 (hcache,lsof)命令
1、什么是buffer/cache ? buffer/cache 其实是作为服务器系统的文件数据缓存使用的,尤其是针对进程对文件存在 read/write 操作的时候,所以当你的服务进程在对文件进行读写的时候,Linux内核为了提高服务的读写速度,则将…...

(Vue2)自定义创建项目、ESLint、Vuex
自定义创建项目:基于VueCli自定义创建项目架子 安装脚手架->创建项目->选择自定义->Babel/Router/CSS/Linter 路由配置项很多,希望创建项目时就把架子搭好 hash模式和history模式 页面跳转和加载模式 Vue为单页面,只有一个HTML…...

LLaMa
文章目录 Problems403 代码文件LLaMA: Open and Efficient Foundation Language Models方法预训练数据结构优化器一些加速的方法 结果Common Sense ReasoningClosed-book Question AnsweringReading ComprehensionMassive Multitask Language Understanding Instruction Finetu…...

API(九)基于协程的并发编程SDK
一 基于协程的并发编程SDK 场景: 收到一个请求会并发发起多个请求,使用openresty提供的协程说明: 这个是高级课程,如果不理解可以先跳过遗留: APSIX和Kong深入理解openresty 标准lua的协程 ① 早期提供的轻量级协程SDK ngx.thread ngx…...

JavaWeb 学习笔记 7:Filter
JavaWeb 学习笔记 7:Filter 1.快速开始 使用过滤器的方式与 Servlet 类似,要实现一个Filter接口: WebFilter("/*") public class FirstFilter implements Filter {public void init(FilterConfig filterConfig) throws ServletE…...

【AI视野·今日Robot 机器人论文速览 第三十五期】Mon, 18 Sep 2023
AI视野今日CS.Robotics 机器人学论文速览 Mon, 18 Sep 2023 Totally 44 papers 👉上期速览✈更多精彩请移步主页 Interesting: 📚GelSplitter, 基于近红外与可见光融合实现高精度surfaceNormal重建的触觉传感器。(from 华中科技大学) 基于分光镜的紧凑型…...

Elasticsearch 在bool查询中使用分词器
1. 创建索引 test setting和mappings 设置了自定义分词映射规则。 PUT /test {"settings": {"analysis": {"filter": {"my_synonym": {"type": "synonym","updateable": true,"synonyms_path&qu…...

在Python中创建相关系数矩阵的6种方法
相关系数矩阵(Correlation matrix)是数据分析的基本工具。它们让我们了解不同的变量是如何相互关联的。在Python中,有很多个方法可以计算相关系数矩阵,今天我们来对这些方法进行一个总结 Pandas Pandas的DataFrame对象可以使用c…...

物联网、工业大数据平台 TDengine 与苍穹地理信息平台完成兼容互认证
当前,在政府、军事、城市规划、自然资源管理等领域,企业对地理信息的需求迅速增加,人们需要更有效地管理和分析地理数据,以进行决策和规划。在此背景下,“GIS 基础平台”应运而生,它通常指的是一个地理信息…...

this.$nextTick()的使用场景
事件循环机制: 同步代码执行->查找异步队列,推入执行栈,执行Vue.nextTick[事件循环1]->查找异步队列,推入执行栈,执行Vue.nextTick[事件循环2]->查找异步队列,推入执行栈,执行Vue.nex…...

idea(第一次)启动项目,端口变成了8080
先上配置 server:port: 9569 spring:profiles:active: dev 该排查的问题也都没问题,重启idea也试过了,还是8080 解决办法:点击右侧的maven ,左上角的重新导入 reimport all maven projects 我又没有改动pom文件,居然还要点这…...

brpc 学习(一)M1 MacOS构建方法
tags: brpc categories: brpc 写在前面 实习阶段初次接触到 RPC 这样一种协议, 以及 brpc 这样一个很棒的框架, 但是当时没时间认真深入学习, 就是围绕使用 demo 开发, 还是有点不知其所以然的, 最近抽空来学习一下 brpc, 首要的一点就是在开发机上构建项目, 并且能够跑起来,…...

Python 与 Qt c++ 程序共享内存,传递图片
python 代码 这里Python 使用 shared_memory QT 使用 QSharedMemory 简单协议: 前面4个字节是 图片with,height,0,0 后面是图片数据 import sys import struct def is_little_endian():x0x12345678y struct.pack(I,x)return y[0]0x78print(f"is_little_end…...

【2023年中国研究生数学建模竞赛华为杯】E题 出血性脑卒中临床智能诊疗建模 问题分析、数学模型及代码实现
【2023年中国研究生数学建模竞赛华为杯】E题 出血性脑卒中临床智能诊疗建模 1 题目 1.1 背景介绍 出血性脑卒中指非外伤性脑实质内血管破裂引起的脑出血,占全部脑卒中发病率的10-15%。其病因复杂,通常因脑动脉瘤破裂、脑动脉异常等因素,导致…...

2024字节跳动校招面试真题汇总及其解答(五)
17.TCP的拥塞控制 TCP 的拥塞控制是指在 TCP 连接中,发送端和接收端通过协作来控制网络中数据包的流量,避免网络拥塞。TCP 的拥塞控制是 TCP 协议的重要组成部分,它可以确保 TCP 连接的稳定性和可靠性。 TCP 的拥塞控制主要有以下几个目的: 防止网络拥塞:当网络中的数据…...

如何撤销某个已经git add的文件以及如何撤销所有git add提交的文件?
如果你想撤销已经添加(git add)到暂存区的单个文件,可以使用 git reset 命令。以下是具体的命令格式: git reset <file>在这里,<file> 是你想要从暂存区中移除的文件名。比如,如果你想要撤销已…...

JVM高级性能调试
标准的JVM是配置为了高吞吐量,吞吐量是为了科学计算和后台运行使用,而互联网商业应用,更多是为追求更短的响应时间,更低的延迟Latency(说白了就是更快速度),当用户打开网页没有快速响应…...

APK的反编译,签名,对齐
APK的反编译,签名,对齐 – WhiteNights Site 2023年9月22日 标签:Android, 应用开发 记录下相关的命令行参数。 APK的打包与解包 java -jar apktool.jar 首先,需要一个jar包,以我在用的为例:apktool_2.8.…...

Django(20):信号机制
目录 信号的工作机制信号的应用场景两个简单例子Django常用内置信号如何放置信号监听函数代码自定义信号第一步:自定义信号第二步:触发信号第三步:将监听函数与信号相关联 信号的工作机制 Django 框架包含了一个信号机制,它允许若…...

31.链表练习题(2)(王道2023数据结构2.3.7节16-25题)
【前面使用的所有链表的定义在第29节】 试题16:两个整数序列A,B存在两个单链表中,设计算法判断B是否是A的连续子序列。 bool Pattern(LinkList L1,LinkList L2){ //此函数实现比较L1的子串中是否有L2LNode *p, *q; //工作在L1,p记录L1子串…...

排序算法之归并排序
一、归并排序的形象理解 原题链接 示例代码 void merge_sort(int q[], int l, int r) {if (l > r) return;int mid l r >> 1;merge_sort(q, l, mid), merge_sort(q, mid 1, r);int k 0, i l, j mid 1;while (i < mid && j < r) //第一处if (q[i]…...

macOS 下 Termius 中文显示为乱码
👨🏻💻 热爱摄影的程序员 👨🏻🎨 喜欢编码的设计师 🧕🏻 擅长设计的剪辑师 🧑🏻🏫 一位高冷无情的编码爱好者 大家好,我是 DevO…...

Apifox接口测试工具详细解析
最近发现一款接口测试工具--apifox,我我们很难将它描述为一款接口管理工具 或 接口自测试工具。 官方给了一个简单的公式,更能说明apifox可以做什么。 Apifox Postman Swagger Mock JMeter Apifox的特点: 接口文档定义: Api…...

Python 实现 PDF 文件转换为图片 / PaddleOCR
文章用于学习记录 文章目录 前言一、PDF 文件转换为图片二、OCR 图片文字识别提取三、服务器端下载运行 PaddleOCR四、下载权重文件总结 前言 文字识别(Optical Character Recognition,简称OCR)是指将图片、扫描件或PDF、OFD文档中的打印字符…...