当前位置: 首页 > news >正文

OpenSSL实现AES-CBC加解密,可一次性加解密任意长度的明文字符串或字节流(QT C++环境)

本篇博文讲述如何在Qt C++的环境中使用OpenSSL实现AES-CBC-Pkcs7加/解密,可以一次性加解密一个任意长度的明文字符串或者字节流,但不适合分段读取加解密的(例如,一个4GB的大型文件需要加解密,要分段读取,每次读取10MB,就加解密10MB,这种涉及全文件填充,而不是每个10MB片段填充具有较复杂的上下文处理,本文不探讨这种)

Qt中的QByteArray比较好用,所以我本篇文章不使用标准C++的unsigned char数组,而是用QByteArray代替,所以要依赖Qt的环境,如果你不用Qt就想办法把QByteArray改回unsigned char数组。

一、简介

AES:略(自行百度)

CBC:略(自行百度)

PKCS7:填充方式,AES支持多种填充方式:如NoPadding、PKCS5Padding、ISO10126Padding、PKCS7Padding、ZeroPadding。PKCS7兼容PKCS5,目前PKCS7比较通用。

二、实现方式

1.使用以下3个接口实现AES-CBC加解密(注意:PKCS7的填充需要自己另外实现)这3个接口在Openssl的v3.0版本以后被废弃:

int AES_set_encrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,size_t length, const AES_KEY *key, unsigned char *ivec, const int enc);

2.使用Openssl的EVP接口实现AES-CBC加解密(推荐,v3.0以前以后得版本都可以兼容,但执行效率比上面3个接口稍差):

EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *c);
EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *c);int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *c, int pad);int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl, const unsigned char *key, const unsigned char *iv);
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl, const unsigned char *key, const unsigned char *iv);
int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);

三、实现方式:非EVP接口

描述:这种方式使用Openssl v3.0以前的旧接口实现,Pkcs7填充需要自己实现。

1.先写一个处理Pkcs7填充/去填充的工具类;

#ifndef PADDING_H
#define PADDING_H#include <QByteArray>/*** @brief 数据填充类(对齐类)* 算法数据填充模式,提供对数据进行PKCS7填充和去除填充的相关函数。*/class Padding
{
public:Padding();/*** @brief GntPadding::getPKCS7PaddedLength* 根据原始数据长度,计算进行PKCS7填充后的数据长度* @param dataLen 原始数据长度* @param alignSize 对齐字节数* @return 返回填充后的数据长度*/static int getPKCS7PaddedLength(int dataLen, int alignSize);/*** @brief Padding::PKCS7Padding* 采用PKCS7Padding方式,将in数据进行alignSize字节对齐填充。* 此函数用于加密前,对明文进行填充。* @param in 数据* @param alignSize 对齐字节数* @return 返回填充后的数据*/static QByteArray PKCS7Padding(const QByteArray &in, int alignSize);/*** @brief Padding::PKCS7UnPaddinged* 采用PKCS7Padding方式,将in数据去除填充。* 此函数用于解密后,对解密结果进一步去除填充,以得到原始数据,直接在原字节数组中剔除* (由于减少了整个字节数组的拷贝,所以比Padding::PKCS7UnPadding效率高一些)* @param in 数据字节数组* @return 无返回*/static void PKCS7UnPadding(QByteArray &in);
};#endif // PADDING_H
#include "padding.h"Padding::Padding()
{}int Padding::getPKCS7PaddedLength(int dataLen, int alignSize)
{// 计算填充的字节数(按alignSize字节对齐进行填充)int remainder = dataLen % alignSize;int paddingSize = (remainder == 0) ? alignSize : (alignSize - remainder);return (dataLen + paddingSize);
}QByteArray Padding::PKCS7Padding(const QByteArray &in, int alignSize)
{// 计算需要填充字节数(按alignSize字节对齐进行填充)int remainder = in.size() % alignSize;int paddingSize = (remainder == 0) ? alignSize : (alignSize - remainder);char paddingChar = static_cast<char>(paddingSize);// 进行填充QByteArray temp(in);temp.append(paddingSize, paddingChar);return temp;
}void Padding::PKCS7UnPadding(QByteArray &in)
{char paddingSize = in.at(in.size() - 1);in.chop(static_cast<int>(paddingSize));
}

2.封装aes-cbc的加解密接口

/**
* @brief AES::cbc_encrypt
* CBC模式加解密,填充模式采用PKCS7,
* 对任意长度明文进行一次加解密,根据机器内存情况,尽量不要太长。
* @param in 输入数据
* @param out 输出结果
* @param key 密钥,长度必须是16/24/32字节,否则加密失败
* @param ivec 初始向量,长度必须是16字节
* @param enc true-加密,false-解密
* @return 执行结果
*/
bool AES::cbc_encrypt(const QByteArray &in, QByteArray &out, const QByteArray &key, const QByteArray &ivec, bool enc)
{// 检查密钥合法性(只能是16、24、32字节)if(key.size() != 16 && key.size() != 24 && key.size() != 32){qInfo() << __FUNCTION__ << "aes cbc key size error!";return false;}if(ivec.size() != 16) // 初始向量为16字节{qInfo() << __FUNCTION__ << "aes cbc ivc size error!";return false;}if (enc){// 生成加密keyAES_KEY aes_key;if (AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(key.data()), key.size() * 8, &aes_key) != 0){return false;}// 进行PKCS7填充QByteArray inTemp = Padding::PKCS7Padding(in, AES_BLOCK_SIZE);// 执行CBC模式加密QByteArray ivecTemp = ivec; // ivec会被修改,故需要临时变量来暂存out.resize(inTemp.size()); // 调整输出buf大小AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(inTemp.data()),reinterpret_cast<unsigned char*>(out.data()),static_cast<size_t>(inTemp.size()),&aes_key,reinterpret_cast<unsigned char*>(ivecTemp.data()),AES_ENCRYPT);return true;}else{// 生成解密keyAES_KEY aes_key;if (AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(key.data()), key.size() * 8, &aes_key) != 0){return false;}// 执行CBC模式解密QByteArray ivecTemp = ivec; // ivec会被修改,故需要临时变量来暂存out.resize(in.size()); // 调整输出buf大小AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(in.data()),reinterpret_cast<unsigned char*>(out.data()),static_cast<size_t>(in.size()),&aes_key,reinterpret_cast<unsigned char*>(ivecTemp.data()),AES_DECRYPT);// 解除PKCS7填充Padding::PKCS7UnPadding(out);return true;}
}

四、实现方式:EVP接口

描述:这种方式使用Openssl的evp接口实现,内置各种填充不需要自己实现,支持大型文件分段读取分段加解密,使用简易性和灵活性都很高,执行效率和资源消耗比非evp接口差一些。

/**
* @brief AES::evp_cbc_encrypt
* CBC模式加解密,填充模式采用PKCS7,
* 对任意长度明文进行一次加解密,根据机器内存情况,尽量不要太长。
* @param in 输入数据
* @param out 输出结果
* @param key 密钥,长度必须是16/24/32字节,否则加密失败
* @param ivec 初始向量,长度必须是16字节
* @param enc true-加密,false-解密
* @return 执行结果
*/
bool AES::evp_cbc_encrypt(const QByteArray &in, QByteArray &out, const QByteArray &key, const QByteArray &ivec, bool enc)
{const EVP_CIPHER *cipher = nullptr;switch (key.size()){case 16:cipher = EVP_aes_128_cbc();break;case 24:cipher = EVP_aes_192_cbc();break;case 32:cipher = EVP_aes_256_cbc();break;default:{qInfo() << __FUNCTION__ << "aes cbc key size error!";return false;}}if(ivec.size() != 16){qInfo() << __FUNCTION__ << "aes cbc ivc size error!";return false;}// 创建 EVP cipher 上下文EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();// 初始化 cipher 上下文(初始化为默认值)EVP_CIPHER_CTX_init(ctx);// 加解密实际输出长度中间标记int out_update_len = 0;int out_final_len = 0;// openssl接口执行结果返回值int ret = 0;// 加密if(enc){// 设置 cipher 上下文,设置秘钥、初始向量ret = EVP_EncryptInit_ex(ctx, cipher, nullptr, reinterpret_cast<const unsigned char*>(key.data()), reinterpret_cast<const unsigned char*>(ivec.data()));if(ret != 1){qInfo() << __FUNCTION__ << "EVP_EncryptInit_ex() error!";EVP_CIPHER_CTX_free(ctx);return false;}// 加密填充方式 PKCS7填充(会自动兼容PKCS5)ret = EVP_CIPHER_CTX_set_padding(ctx, EVP_PADDING_PKCS7);if(ret != 1){qInfo() << __FUNCTION__ << "EVP_CIPHER_CTX_set_padding() error! set PKCS7 Padding error!";EVP_CIPHER_CTX_free(ctx);return false;}// 密文输出缓冲区设置大小(未加密数据的大小+一个加密填充块大小),不一定会完全使用。out.resize(in.size() + AES_BLOCK_SIZE);// 对数据进行加密out_update_len = out.size();ret = EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(out.data()), &out_update_len, reinterpret_cast<const unsigned char*>(in.data()), in.size());if(ret != 1){qInfo() << __FUNCTION__ << "EVP_EncryptUpdate() error!";EVP_CIPHER_CTX_free(ctx);return false;}// 完成加密过程并获取最终结果out_final_len = 0;ret = EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(out.data()) + out_update_len, &out_final_len);if(ret != 1){qInfo() << __FUNCTION__ << "EVP_EncryptFinal_ex() error!";EVP_CIPHER_CTX_free(ctx);return false;}}else // 解密{// 设置 cipher 上下文,设置秘钥、初始向量ret = EVP_DecryptInit_ex(ctx, cipher, nullptr, reinterpret_cast<const unsigned char*>(key.data()), reinterpret_cast<const unsigned char*>(ivec.data()));if(ret != 1){qInfo() << __FUNCTION__ << "EVP_DecryptInit_ex() error!";EVP_CIPHER_CTX_free(ctx);return false;}// 解密填充方式 PKCS7填充(会自动兼容PKCS5)ret = EVP_CIPHER_CTX_set_padding(ctx, EVP_PADDING_PKCS7);if(ret != 1){qInfo() << __FUNCTION__ << "EVP_CIPHER_CTX_set_padding() error! set PKCS7 Padding error!";EVP_CIPHER_CTX_free(ctx);return false;}// 明文输出缓冲区设置大小(未解密数据的大小),不一定会完全使用。out.resize(in.size());out.fill(0);// 对数据进行解密out_update_len = out.size();ret = EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(out.data()), &out_update_len, reinterpret_cast<const unsigned char*>(in.data()), in.size());if(ret != 1){qInfo() << __FUNCTION__ << "EVP_DecryptUpdate() error!";EVP_CIPHER_CTX_free(ctx);return false;}// 完成解密过程并获取最终结果out_final_len = 0;ret = EVP_DecryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(out.data()) + out_update_len, &out_final_len);if(ret != 1){qInfo() << __FUNCTION__ << "EVP_DecryptFinal_ex() error!";EVP_CIPHER_CTX_free(ctx);return false;}}// 总的实际加解密数据长度为前面更新部分的长度加上最终部分的长度out_update_len += out_final_len;// 去除输出缓冲区末尾多余的长度out.chop(out.size() - out_update_len);// 释放 cipher 上下文EVP_CIPHER_CTX_free(ctx);return true;
}

五、测试结果:

加密:

// 点击了加密按钮,触发加密流程
void AESTestWidget::on_btnEncrypt_clicked()
{//加密明文字符串QByteArray encryptText;QByteArray encryptTextBase64;QByteArray key(ui->leEncryptKey->text().toUtf8());QByteArray ivec(ui->leEncryptIV->text().toUtf8());// 加密(将明文加密为二进制数据)//AES::cbc_encrypt(ui->tePlaintext2Encrypt->toPlainText().toUtf8(), encryptText, key, ivec, true);AES::evp_cbc_encrypt(ui->tePlaintext2Encrypt->toPlainText().toUtf8(), encryptText, key, ivec, true);// 对加密后的二进制数据进行base64编码并显示encryptTextBase64 = encryptText.toBase64();ui->teCiphertextBase64->setText(QString::fromUtf8(encryptTextBase64));// 显示密文二进制数据的十六进制字符串ui->teCiphertextHex->setText(QString::fromUtf8(encryptText.toHex()));}

解密:

// 点击了解密按钮,触发解密流程
void AESTestWidget::on_btnDecrypt_clicked()
{//解密base64编码的密文字符串QByteArray decryptText;QByteArray key(ui->leDecryptKey->text().toUtf8());QByteArray ivec(ui->leDecryptIV->text().toUtf8());// 解密(先将base64编码的密文转为二进制字节数据再解密)//AES::cbc_encrypt(QByteArray::fromBase64(ui->teCiphertext2Decrypt->toPlainText().toUtf8()),decryptText, key, ivec, false);AES::evp_cbc_encrypt(QByteArray::fromBase64(ui->teCiphertext2Decrypt->toPlainText().toUtf8()),decryptText, key, ivec, false);qDebug() << "解密结果:" << decryptText;ui->tePlaintextByDecrypt->setText(QString::fromUtf8(decryptText));
}

相关文章:

OpenSSL实现AES-CBC加解密,可一次性加解密任意长度的明文字符串或字节流(QT C++环境)

本篇博文讲述如何在Qt C的环境中使用OpenSSL实现AES-CBC-Pkcs7加/解密&#xff0c;可以一次性加解密一个任意长度的明文字符串或者字节流&#xff0c;但不适合分段读取加解密的&#xff08;例如&#xff0c;一个4GB的大型文件需要加解密&#xff0c;要分段读取&#xff0c;每次…...

cURL:命令行下的网络工具

序言 在当今互联网时代&#xff0c;我们经常需要与远程服务器通信&#xff0c;获取数据、发送请求或下载文件。在这些情况下&#xff0c;cURL 是一个强大而灵活的工具&#xff0c;它允许我们通过命令行进行各种类型的网络交互。本文将深入探讨 cURL 的基本用法以及一些高级功能…...

Baumer工业相机堡盟工业相机如何通过NEOAPISDK查询和轮询相机设备事件函数(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPISDK查询和轮询相机设备事件函数&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机NEOAPI SDK和相机设备事件的技术背景Baumer工业相机通过NEOAPISDK在相机中查询和轮询相机设备事件函数功能1.引用合适的类文件2.通过NEOAPISDK…...

Day45代码随想录动态规划part07:70. 爬楼梯(进阶版)、322. 零钱兑换、279.完全平方数、139.单词拆分

Day45 动态规划part07 完全背包 70. 爬楼梯&#xff08;进阶版&#xff09; 卡码网链接&#xff1a;57. 爬楼梯&#xff08;第八期模拟笔试&#xff09; (kamacoder.com) 题意&#xff1a;假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬至多m (1 < m < n)个…...

土壤重金属含量分布、Cd镉含量、Cr、Pb、Cu、Zn、As和Hg、土壤采样点、土壤类型分布

土壤是人类赖以生存和发展的重要资源之一,也是陆地生态系统重要的组成部分。近年来, 随着我国城市化进程加快&#xff0c;矿产资源开发、金属加工冶炼、化工生产、污水灌溉以及不合理的化肥农药施用等因素导致重金属在农田土壤中不断富集。重金属作为土壤环境中一种具有潜在危害…...

力扣:100284. 有效单词(Java)

目录 题目描述&#xff1a;输入&#xff1a;输出&#xff1a;代码实现&#xff1a; 题目描述&#xff1a; 有效单词 需要满足以下几个条件&#xff1a; 至少 包含 3 个字符。 由数字 0-9 和英文大小写字母组成。&#xff08;不必包含所有这类字符。&#xff09; 至少 包含一个 …...

如何快速掌握DDT数据驱动测试?

前言 网盗概念相同的测试脚本使用不同的测试数据来执行&#xff0c;测试数据和测试行为完全分离&#xff0c; 这样的测试脚本设计模式称为数据驱动。(网盗结束)当我们测试某个网站的登录功能时&#xff0c;我们往往会使用不同的用户名和密码来验证登录模块对系统的影响&#x…...

OpenCV如何实现背投(58)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV直方图比较(57) 下一篇&#xff1a;OpenCV如何模板匹配(59) 目标 在本教程中&#xff0c;您将学习&#xff1a; 什么是背投以及它为什么有用如何使用 OpenCV 函数 cv::calcBackP…...

5-在Linux上部署各类软件

1. MySQL 数据库安装部署 1.1 MySQL 5.7 版本在 CentOS 系统安装 注意&#xff1a;安装操作需要 root 权限 MySQL 的安装我们可以通过前面学习的 yum 命令进行。 1.1.1 安装 配置 yum 仓库 # 更新密钥 rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022# 安装Mysql…...

【Jenkins】持续集成与交付 (八):Jenkins凭证管理(实现使用 SSH 、HTTP克隆Gitlab代码)

🟣【Jenkins】持续集成与交付 (八):Jenkins凭证管理(实现使用 SSH 、HTTP克隆Gitlab代码) 1、安装Credentials Binding、git插件2、凭证类型及用途3、(用户名和密码类型)凭证的添加和使用3.1 用户密码类型3.2 测试凭证是否可用3.3 开始构建项目3.3 查看结果(进入Jenk…...

开源模型应用落地-CodeQwen模型小试-SQL专家测试(二)

一、前言 代码专家模型是基于人工智能的先进技术&#xff0c;它能够自动分析和理解大量的代码库&#xff0c;并从中学习常见的编码模式和最佳实践。这种模型可以提供准确而高效的代码建议&#xff0c;帮助开发人员在编写代码时避免常见的错误和陷阱。 通过学习代码专家模型&…...

Arch Linux安装macOS

安装需要的包 sudo pacman -S qemu-full libvirt virt-manager p7zip yay -S dmg2img安装步骤 cd ~ git clone --depth 1 --recursive https://github.com/kholia/OSX-KVM.git cd OSX-KVM # 选择iOS版本 ./fetch-macOS.py #将上一步下载的BaseSystem.dmg转换格式 dmg2img -…...

接口自动化框架篇:Pytest + Allure报告企业定制化实现!

接口自动化框架是现代软件开发中的重要组成部分&#xff0c;能够帮助开发团队提高测试效率和质量。本文将介绍如何使用Pytest作为测试框架&#xff0c;并结合Allure报告进行企业定制化实现。 目标规划 在开始编写接口自动化测试框架之前&#xff0c;我们需要先进行目标规划。…...

保持 Hiti 证卡打印机清洁的重要性和推荐的清洁用品

在证卡印刷业务中&#xff0c;保持印刷设备的清洁至关重要。特别是对于 Hiti 证卡打印机来说&#xff0c;它们是生产高质量证卡的关键工具。保持设备清洁不仅可以保证打印质量和效率&#xff0c;还可以延长其使用寿命。本文将探讨保持 Hiti 证卡打印机清洁卡的重要性&#xff0…...

Unity C#的底层原理概述

文章目录 前言IL与IL2CPP总结 前言 看到底层二字&#xff0c;会感到很高深&#xff0c;好似下一秒就要踏入深渊。实际上&#xff0c;对于C#底层的理解非常简单&#xff0c;比冒泡排序这种基础算法还要简单。 底层的两种机制&#xff1a;Mono和IL2CPP。 IL2CPP其中的"2&qu…...

国产数据库的发展势不可挡

前言 新的一天又开始了&#xff0c;光头强强总不紧不慢地来到办公室&#xff0c;准备为今天一天的工作&#xff0c;做一个初上安排。突然&#xff0c;熊二直接进入办公室&#xff0c;说&#xff1a;“强总老大&#xff0c;昨天有一个数据库群炸了锅了&#xff0c;有一位姓虎的…...

权益商城系统源码 现支持多种支付方式

简介&#xff1a; 权益商城系统源码&#xff0c;支持多种支付方式&#xff0c;后台商品管理&#xff0c;订单管理&#xff0c;串货管理&#xff0c;分站管理&#xff0c;会员列表&#xff0c;分销日志&#xff0c;应用配置。 上传到服务器&#xff0c;修改数据库信息&#xff…...

python安装问题及解决办法(pip不是内部或外部命令也不是可运行)

pip是python的包管理工具&#xff0c;使python可在cmd&#xff08;命令行窗口&#xff0c;WinR后输入cmd&#xff09;中执行 针对 “pip不是内部或外部命令也不是可运行” 问题&#xff0c;需要在安装的时候将python添加到环境变量中 上图第三个选项必须勾选才能在cmd中使用pi…...

Json高效处理方法

一、参考我之前的博客,Delphi可以很方便的把类和结构体转换成JSON数据,但是数据量大了,就会非常之慢,1万条数据需要20秒左右。如果引用Serializers单元,那么100万数据只需要4秒左右,每秒处理20万+,速度还是很快的。 二、写一个简单的类  TPeople = class private …...

若依分离版-前端使用echarts组件

1 npm list:显示已安装的模块 该命令用于列出当前项目的所有依赖关系&#xff0c;包括直接依赖和间接依赖。执行 npm list 时&#xff0c;npm 将从当前目录开始&#xff0c;递归地列出所有已安装的模块及其版本信息 npm list 2 npm outdated:用于检查当前项目中的npm包是否有…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

LeetCode - 394. 字符串解码

题目 394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; 思路 使用两个栈&#xff1a;一个存储重复次数&#xff0c;一个存储字符串 遍历输入字符串&#xff1a; 数字处理&#xff1a;遇到数字时&#xff0c;累积计算重复次数左括号处理&#xff1a;保存当前状态&a…...

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

Python Einops库:深度学习中的张量操作革命

Einops&#xff08;爱因斯坦操作库&#xff09;就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库&#xff0c;用类似自然语言的表达式替代了晦涩的API调用&#xff0c;彻底改变了深度学习工程…...

django blank 与 null的区别

1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是&#xff0c;要注意以下几点&#xff1a; Django的表单验证与null无关&#xff1a;null参数控制的是数据库层面字段是否可以为NULL&#xff0c;而blank参数控制的是Django表单验证时字…...

Kafka主题运维全指南:从基础配置到故障处理

#作者&#xff1a;张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1&#xff1a;主题删除失败。常见错误2&#xff1a;__consumer_offsets占用太多的磁盘。 主题日常管理 …...

Unity中的transform.up

2025年6月8日&#xff0c;周日下午 在Unity中&#xff0c;transform.up是Transform组件的一个属性&#xff0c;表示游戏对象在世界空间中的“上”方向&#xff08;Y轴正方向&#xff09;&#xff0c;且会随对象旋转动态变化。以下是关键点解析&#xff1a; 基本定义 transfor…...

AD学习(3)

1 PCB封装元素组成及简单的PCB封装创建 封装的组成部分&#xff1a; &#xff08;1&#xff09;PCB焊盘&#xff1a;表层的铜 &#xff0c;top层的铜 &#xff08;2&#xff09;管脚序号&#xff1a;用来关联原理图中的管脚的序号&#xff0c;原理图的序号需要和PCB封装一一…...