MoreRSS

site iconZoDream修改

作者开发了一个名为 ZoDream 的php框架。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

ZoDream的 RSS 预览

OpenCloudOS Server 9 安装 Nginx 1.31、PHP 8.5、MariaDB 13.02

2026-10-25 17:41:52

OpenCloudOS Server 9 安装 Nginx 1.31、PHP 8.5、MariaDB 13.02

更新所有软件

dnf update -y



make -j$(nproc) # 使用所有 CPU 核心进行编译

Nginx

安装基本编译环境

dnf install -y gcc gcc-c++ make pcre pcre-devel zlib zlib-devel openssl openssl-devel wget 
    libzstd-devel  # libzip
    bzip2-devel libffi-devel sqlite-devel readline-devel # python 依赖
    nasm # libjpeg-turbo
    ncurses-devel libaio-devel boost-devel systemd-devel git # MariaDB

编译python

默认已安装python3.11

wget https://www.python.org/ftp/python/3.15.1/Python-3.15.1.tar.xz

tar -xf Python-3.15.1.tar.xz
cd Python-3.15.1


./configure --enable-optimizations --with-ensurepip=install

make

make altinstall

python3.15 --version  # 检查 Python 版本
pip3.15 --version     # 检查 pip 版本(若未安装 pip,执行 python3.12 -m ensurepip)

下载源码包

下载 pcre2

下载 zlib

源码包放到 /usr/local/src 下解压

cd /usr/local/src #进入src文件夹
wget https://nginx.org/download/nginx-1.31.8.tar.gz #下载nginx源码包

解压三个源码压缩包

tar -zxvf pcre2-10.49.tar.gz # 解压缩
tar -zxvf zlib-1.3.2.tar.gz
tar -zxvf nginx-1.31.8.tar.gz

编译Nginx

cd nginx-1.31.8

./configure --sbin-path=/usr/local/nginx/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf --pid-path=/usr/local/nginx/logs/nginx.pid --with-http_ssl_module --with-pcre=../pcre2-10.49 --with-zlib=../zlib-1.3.2

make            #编译
make install        #安装

如果后面遇到某个功能没有,只要修改 configure 中的参数就行了,重复执行 make make install 即可

配置启动Nginx

/usr/local/nginx/sbin/nginx             #启动服务
/usr/local/nginx/sbin/nginx -s reload   #重新加载服务
/usr/local/nginx/sbin/nginx -s stop     #停止服务
ps -ef | grep nginx                         #查看服务进程

更改配置

vi /usr/local/nginx/conf/nginx.conf # 这是站点配置文件

开始设置开启启动项

vi /usr/lib/systemd/system/nginx.service
[Unit]
Description=nginx
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/nginx/sbin/nginx
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecReload=/usr/local/nginx/sbin/nginx -s reload
PrivateTmp=true

[Install]
WantedBy=multi-user.target

启用开机启动服务

systemctl enable nginx.service

最终的启动运行命令

service nginx start
service nginx stop
service nginx restart
service nginx reload

PHP 8

安装基本编译环境

dnf install -y libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel curl curl-devel openssl openssl-devel sqlite-devel gmp-devel oniguruma-devel readline-devel libxslt-devel

安装CMake

wget https://github.com/Kitware/CMake/releases/download/v4.4.4/cmake-4.4.4.tar.gz

tar -zxvf cmake-4.4.4.tar.gz
cd cmake-4.4.4
./bootstrap
gmake
ln -s /usr/local/src/cmake-4.4.4/bin/cmake /usr/bin/cmake
cmake --version

安装libzip 必须先安装CMake

下载 libzip

dnf remove libzip
wget https://libzip.org/download/libzip-1.12.1.tar.gz
tar -zxvf libzip-1.12.1.tar.gz
cd libzip-1.12.1
mkdir build
cd build
cmake ..
make
make install

安装完成后,查看是否存在/usr/local/lib64/pkgconfig目录,如果存在,执行如下命令来设置PKG_CONFIG_PATH:

export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig/"

下载源码包

cd /usr/local/src
wget https://www.php.net/distributions/php-8.5.12.tar.gz  # 下载
tar -zxvf php-8.5.12.tar.gz   # 解压缩

安装 libjpeg

默认已安装2.0

wget https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.2.0/libjpeg-turbo-3.2.0.tar.gz

tar -zxvf libjpeg-turbo-3.2.0.tar.gz
cd libjpeg-turbo-3.2.0
mkdir build
cd build
cmake ..
make
make install # 此时,将libjpeg安装到了默认目录:/usr/local/lib

编译安装

cd php-8.5.12


# dnf -y install libvpx-devel libwebp-devel       ## webp 图片支持需要 --with-webp

./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --enable-mbstring --enable-ftp --enable-gd --enable-gd-jis-conv --enable-mysqlnd --enable-pdo --enable-sockets --enable-fpm --enable-xml --enable-soap --enable-pcntl --enable-cli --enable-bcmath --with-openssl --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --with-zlib --with-iconv --with-curl --with-zip --with-gettext --with-webp=/usr/include/webp --with-jpeg=/usr/include --with-freetype=/usr/include/freetype2


make            #编译
make install        #安装

新增用户

cat /etc/passwd

# useradd -s /sbin/nologin -M www

groupadd www

useradd -g www www -s /sbin/nologin

赋予权限

chown -R www:www /run/php-fpm

修改配置文件

cd /usr/local/php/etc
cp php-fpm.conf.default php-fpm.conf
cd /usr/local/php/etc/php-fpm.d
cp www.conf.default www.conf


find /usr/local/src/php-8.5.12 -name php.ini*
cp /usr/local/src/php-8.5.12/php.ini-production /usr/local/php/etc/php.ini


vim /usr/local/php/etc/php.ini

将 ;cgi.fix_pathinfo=1 改为 cgi.fix_pathinfo=0

将 expose_php = On 改为 expose_php = Off 隐藏版本号

vim /usr/local/php/etc/php-fpm.conf

将 [global] 下的 pid = run/php-fpm.pid 启用, 后面 php-fpm.service 中的 PIDFile 必须设成一样的路径,否则会出现找不到php-fpm.pid的情况

vim /usr/local/php/etc/php-fpm.d/www.conf

将 user = nobody group = nobody 改为 user = www group = www 需要先添加 www 用户和 www 组

初始命令

/usr/local/php/sbin/php-fpm -R   # 这里后面带个 -R  表示用root 用户启动
/usr/bin/pkill -9 php-fpm
pstree -p | grep php

设置开机启动项

vi /usr/lib/systemd/system/php-fpm.service
[Unit]
Description=php-fpm
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/php/var/run/php-fpm.pid
ExecStart=/usr/local/php/sbin/php-fpm
ExecStop=/usr/bin/pkill -9 php-fpm
PrivateTmp=true

[Install]
WantedBy=multi-user.target
systemctl enable php-fpm.service

可以使用systemctl命令管理php-fpm:

systemctl start php-fpm.service  #启动
systemctl stop php-fpm.service   #停止
service php-fpm start
service php-fpm stop
service php-fpm restart
service php-fpm reload

PHP拓展安装

imagick

sudo yum -y install ImageMagick-devel
cd /usr/local/src
tar xvf imagick-3.8.1.tgz

cd imagick-3.8.1

/usr/local/php/bin/phpize
./configure  --with-php-config=/usr/local/php/bin/php-config
make
make install

php.ini

extension=imagick.so

搭配Nginx运行PHP站点

配置 Nginx

vi /usr/local/nginx/conf/nginx.conf

在 http 下添加

client_max_body_size 200M; #配置客户端请求体最大值
client_body_buffer_size 50m; #配置请求体缓存区大小
server_tokens off; # 隐藏响应头中的版本号

gzip on;
gzip_min_length 1k; #不压缩临界值,大于1k的才压缩
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript text/css application/font-woff; # 需要压缩什么文件就加上文件类型,对于图片还是不要使用gzip压缩,直接在本地修改成其他图片格式效果更好
server {
    server_name  zodream.cn;
    root         /data/www/html; # 设置站点根目录

    location / {
        # root   html;
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$query_string; # 美化和伪静态需要设置路由重写
    }

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    ## 配置 xxsqladmin 路径指向phpmyadmin
    location ~ ^/xxsqladmin/.*\.php$ {
        root   /data/shop/phpmyadmin;
        set $real_script_name $fastcgi_script_name;
        if ($fastcgi_script_name ~ "^/xxsqladmin(.+?\.php)(.*)$") {
            set $real_script_name $1;
        }
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$real_script_name;
        include        fastcgi_params;
    }

    ## 配置 xxsqladmin 资源文件路径指向phpmyadmin
    location ~ ^/xxsqladmin.* {
        root   /data/shop/phpmyadmin;
        rewrite ^/xxsqladmin(.*)$ /$1 break;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    location ~ /\.ht {
        deny  all;
    }

}

修改完保存文件并重启Nginx服务器

MariaDB

useradd -r -s /sbin/nologin -d /data/mariadb mysql

mkdir -p /data/mariadb
chown -R mysql:mysql /data/mariadb

安装

cd /usr/local/src

wget https://archive.mariadb.org/mariadb-13.0.2/source/mariadb-13.0.2.tar.gz

tar xf mariadb-13.0.2.tar.gz
cd mariadb-13.0.2



cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/mariadb -DMYSQL_DATADIR=/data/mariadb -DSYSCONFDIR=/etc -DMYSQL_USER=mysql -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DWITHOUT_MROONGA_STORAGE_ENGINE=1 -DWITH_DEBUG=0 -DWITH_SSL=system -DWITH_ZLIB=system -DWITH_PCRE=system -DWITH_BOOST=system -DWITH_SYSTEMD=yes -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci  # 有几个参数可能无效,但加上也没大问题

make -j$(nproc) # 使用所有 CPU 核心进行编译

make install

初始化

cd /usr/local/mariadb

# 初始化数据目录
./scripts/mysql_install_db --datadir=/data/mariadb --user=mysql

# 将安装目录下的 bin 加入环境变量
echo 'export PATH=/usr/local/mariadb/bin:$PATH' > /etc/profile.d/mariadb.sh
source /etc/profile.d/mariadb.sh


cat > /etc/my.cnf <<EOF
[mariadb]
basedir=/usr/local/mariadb
datadir=/data/mariadb
port=3306
socket=/data/mariadb/mariadb.sock
pid-file=/data/mariadb/mariadb.pid
log-error=/data/mariadb/mariadb.log
user=mysql

[client]
socket=/data/mariadb/mariadb.sock

[mariadb]
socket=/data/mariadb/mariadb.sock
EOF

添加开启启动项

vi /etc/systemd/system/mariadb.service
[Unit]
Description=MariaDB Database Server
After=network.target
After=syslog.target

[Service]
Type=simple
User=mysql
Group=mysql

# 你的安装路径下的 mariadbd
ExecStart=/usr/local/mariadb/bin/mariadbd --defaults-file=/etc/my.cnf

# 如果启动失败,尝试延长超时时间
TimeoutSec=300

# 确保进程退出后能被 systemd 正确回收
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

验证安装

systemctl daemon-reload

systemctl enable --now mariadb


systemctl status mariadb # 验证状态

启动和设置开机启动

systemctl start mariadb  # 开启服务

systemctl enable mariadb  # 设置为开机自启动服务

设置修改密码

mariadb-secure-installation 默认执行可能出现找不到 /tmp/mariadb.sock 的情况

所以需要使用

mariadb-secure-installation --socket=/data/mariadb/mariadb.sock

但是执行完使用 mariadb -u root 发现还是不需要密码,密码没修改成功。

这是可以使用sql语句修改

mariadb

ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('你的新密码');
FLUSH PRIVILEGES;

使用 Certbot 获取 letsencrypt 证书

dnf install certbot python3-certbot-nginx

certbot --nginx

如果出现 找不到 nginx

ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx
ln -s /usr/local/nginx/conf/ /etc/nginx

手动更新证书

certbot renew

历史版本

TencentOS Server 3.1 安装 Nginx 1.23、PHP 8.2、MariaDB 10.11

CentOs 7 安装apche php mysql

angular 21 升级使用 signals 方案笔记

2025-12-30 19:09:47

angular 21 升级使用 signals 方案笔记

signals 提供新的数据绑定和变更检测机制。从 angular 17 就开始引入了,

优点:

  1. 更精细的响应检测。

缺点:

  1. 变化太大,

signal

基本的绑定,可读可写。

angular 17 之前:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public text = 'hi';

    public tap() {
        this.text = 'hello';
    }
}

angular 17 之后:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text() }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly text = signal<string>('hi');

    public tap() {
        this.text.set('hello');
        this.text.update(v => {
            return 'hello';
        });
    }
}

注意

  1. 使用 set 或 update 更新值。
  2. 当值是对象时,使用 update 更新某个属性,必须生成新的对象,才能有效检测
    
    public readonly data = signal({
     a: 1,
     b: 2
    });
    // 错误示范
    this.data.update(v => {
     v.a = 3
     return v;
    });

// 正确使用 this.data.update(v => { v.a = 3; return {...v}; // 或者 return {...v, a: 3}; });

3. 当值时数组时,使用 `update` 新增或删除某一项
```ts
public readonly items = signal<number[]>([]);
// 错误示范
this.items.update(v => {
    v.push(1);
    v.pop();
    return v;
});

// 正确使用
this.items.update(v => {
    v.push(1);
    return [...v];
    // 或者
    return [...v, 1];
});

input、output、model

input 相当于 @Input(), 但是可读不可写,增加了自定义转换

output 相当于 @Output(),

model 相对于 @Input() + @Output(),可读可写

angular 17 之前:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    @Input() public text = 'hi';
    @Output() public textChange = new EventEmitter();
}

angular 17 之后:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text() }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly text = input('hi', {tranform: parseInt});
    public readonly textChange = output();

    public readonly text = model('');

}

form

@Component({
    standalone: false,
    selector: 'app-example',
    template: `<input type="number" [formField]="form.a">`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly form = form(sinal({
        a: 1
    }), schemaPath => {
        required(schemaPath.a);
    })

}

使用 [formField] 精选表单绑定,不能自定义 name 属性,

注意

  1. 表单 name 属性自动生成 类似于 [项目名].form1.a,如果介意 name,则推荐使用 原本 FormBuilder 方式
  2. 一些值限制的变化, select 的值为 string,input type=checkbox 为 boolean, type=number 为 number, 其他则必须为 string

effect

检测 值的变化,替代 ngOnChanges

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text() }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly text = model('');

    constructor() {
        let previousText = '';
        effect(() => {
            this.text();
            // 当 text 发生变化时,TODO
            previousText = this.text(); // 使用此方法实现值的前后变化检测
        });
    }
}

computed

关联变化,提供值变化前后对比

angular 17 之前:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text }} {{ twoText }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public text = 'hi';

    public get twoText() {
        return this.text + ', two';
    }

    public tap() {
        this.text = 'hello';
    }
}

angular 17 之后:

@Component({
    standalone: false,
    selector: 'app-example',
    template: `{{ text() }} {{ twoText() }}`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly text = signal<string>('hi');

    public readonly twoText = computed(() => {
        return this.text() + ', two';
    });

    public tap() {
        this.text.set('hello');
        this.text.update(v => {
            return 'hello';
        });
    }
}

与 signals 使用时失效的旧方法

@HostBinding 无法与 signal 等使用

@HostBinding('class.open') // 无效
public readonly toggle = model(false);

可以使用 effect 同步或


@Component({
    standalone: false,
    selector: 'app-example',
    template: ``,
    styleUrls: ['./example.component.scss'],
    host: {
        '[class.open]': 'toggle()'
    }
})
export class ExampleComponent {
    public readonly toggle = model(false);


}

文件解析笔记

2025-09-12 00:02:25

文件解析笔记

工具

ImHex

基础知识

数据类型 子节数 说明
byte 1 占 8 个二进制字符,特殊知识:以 0x80 为分界
sbyte 1 占 8 个二进制字符
short 2
ushort 2
int 4
uint 4
float 4
long 8
ulong 8
double 8
char 1 、 2 在 c 等低级语言中占 1 个子节,在 c# 等高级语言中占 2 子节
string 无固定长度 一种是定义长度,再取长度的字节数转,另一种是以 0x0 为结束符
leb128 无固定长度 每次读一个子节, 子节值大于等于 0x80 表示有需要读下一个子节

端

主要为大端小端,主要是指有多个固定字节数的数据类型的字节写入顺序不同, byte sbyte 只有一个字节,没有大小端之分;string char leb128 都不固定长度,依赖前一个字节指示是否有下一个字节,所以无法分大小端。

大端(BigEndian, 简写BE),高位写在前面,反转过来写的意思,例如 (ushort)1 转成字节数组是 [0x1, 0x0] 写入流时是先写 0x0 在写入 0x1

小端(LittleEndian, 简写LE),低位写在前面,顺着字节数组写的意思,例如 (ushort)1 转成字节数组是 [0x1, 0x0] 写入流时是先写 0x1 在写入 0x0

一般来说:网络字节流的是大端,计算机文件流是小端。

字符串编码

Unicode 固定文件头 0xFF 0xFE,大端则是 0xFE 0xFF UTF8 分有头和无头,有固定头 0xEF 0xBB 0xBF

无头编码,主要根据 char(两个字节) 存储方式判断,一般来说 0x0-0x7F 是指占用一个字节,0x80-0xFF 根据定义可以指示多个字节

UTF8

字节数 字节分布
1 0x00-0x7F
2 0xC2-0xDF 0x80-0xBF
3 0xE0 0xA0-0xBF 0x80-0xBF
3 0xE1-0xEC 0x80-0xBF 0x80-0xBF
3 0xED 0x80-0x9F 0x80-0xBF
3 0xEE-0xEF 0x80-0xBF 0x80-0xBF
4 0xF0 0x90-0xBF 0x80-0xBF 0x80-0xBF
4 0xF1-0xF3 0x80-0xBF 0x80-0xBF 0x80-0xBF
4 0xF4 0x80-0x8F 0x80-0xBF 0x80-0xBF

压缩

一般是固定的头,固定几个字节指示

  1. 无损压缩
算法类型 代表算法 适用场景
字典编码 LZ77, LZ78, LZW 重复数据压缩(文本)
熵编码 Huffman, 算术编码 通过加入长度来表示重复字节或字节数组

图片无损压缩: 对单个像素点(R|G|B|A)进行处理,例如不透明图片就可以只保存(R|G|B)三个颜色值,更进一步有单个色值的,例如只保存(R)通道

  1. 有损压缩

一般只针对特殊领域,例如图像

图象的有损压缩:分成一个个(width*height)小的区域进行近似关联处理,常见的是分成 (4*4) 的小区域,每个区域处理成 8 个字节

加密

加密算法

常见的加密算法:

  1. 对称加密:DES、AES
  2. 非对称加密:RSA、ECC

这些复杂的加密算法只适用于文件少且重要的,速度较慢、内存占用较高

一般对速度和性能有要求的就简单的使用xor(异或处理)

基本步骤

  1. 找文件的文件头,一般都是固定的内容,有多个同类的文件就很容易识别出来
  2. 找文本字符串,可用用做定位用,除非加密的文件,找到一个连续的文本,多切换编码试试,查看文本结尾后一个子节是不是 0x0, 不是的话就肯定在前面标记了长度,
  3. 长度首先考虑 byte 再考虑 leb128 再 uint int long
  4. 如果判断是 leb128, 那就可以肯定 其他长度都用的 leb128, 甚至 uint ulong 等值也转成了 leb128
  5. 找可能的分隔标记符,看见大片有规律的重复出现的子节就可靠是无意义的分隔符
  6. 找 0x0, 0x0 多用于补位,例如数据类型固定占用多个子节,不足就是 0x0,还有就是补数据内容的长度
  7. 多数数据都是重复的数据结构,即属性个数一样,用不同的数据类型去算个数是否一致。

密码本开发笔记之读写与保存

2025-03-06 22:37:16

密码本开发笔记之读写与保存

第一版 读写共用同一个原始文件

每条记录可以看作一个数据块

struct Chunk {
    int id;
    long offset;
    long length;
}

顺序读取每一块并记录每一块的起始位置和数据长度。

但是更新写入就有点复杂了。

  1. 新增,直接在文件流末尾写入。
  2. 删除,找到下一块的位置,然后循环读取后续部分的数据前移一段位置,然后更新文件的长度,最后还要更新每一块新的起始位置。
  3. 更新,需要先算出数据的长度,然后判断要移除部分空间还是扩张部分空间,然后写入数据,最后还要更新每一块的起始位置。

缺点

  1. 频繁读写,动不动就要移动子节。
  2. 实时写入,撤销麻烦。

第二版 读原始文件,写临时文件

先读取数据块,并标记数据块是原始文件。

struct Chunk {
    int id;
    bool isTemporarySource;
    long offset;
    long length;
}

所有未保存的删除写入更新,都是发生在临时文件中。

  1. 新增,在临时文件末尾写入。
  2. 删除,直接删除一个数据块记录进行了,不需要进行文件读写。
  3. 更新,在临时文件末尾写入,并更新数据块记录为临时文件和新的起始位置及长度。
  4. 保存,获取所有数据块记录从0循环写入到原始文件中,最后更新原始文件的长度。

优点

  1. 减少了对原始文件的写入
  2. 不在自动保存,可以撤销。

基于 SkiaSharp 的轮廓获取

2024-11-22 18:33:46

基于 SkiaSharp 的轮廓获取

源代码

示例

事前须知

本文基于透明图片的透明度获取轮廓;如不透明图片获取轮廓需要先把图片转成灰度图片,根据灰度值获取轮廓。

原理

  1. 需要获取一个物体的起始点,通常是从左至右逐行遍历像素点,获取第一个不透明点
  2. 获取相邻的下一个不透明点,通常是以当前点为中心,顺时针遍历八个方向的点,找到的一个个点就是下一个点。
  3. 初始点第一个方向为正上个方,下一个点的第一个方向就应该是相对与上一个点的方向顺时针转2下。
o c o o
o o b o
o a o o
o o o

// 从 a 找到 b,方向为 1 (0 是正上方)
// 那个 b 就在 a 的 1 方向,a 就在 b 的 5方向
// 但 b 的 6 方向已经被 a 找过了, 所以 b 的起始方向就是 7 
// 总结 b 在 a 的 n 方向,则 b 的起始方向为 n + 6 

代码

/// <summary>
/// 边界算法
/// </summary>
/// <returns></returns>
private static SKPath? TraceContour(SKPixmap pixMap, int beginX, int beginY)
{
    var path = new SKPath();
    path.MoveTo(beginX, beginY);
    var directItems = new int[][] {
        [0, -1], [1, -1], 
                 [1, 0],
        [1, 1], [0, 1], [-1, 1],
        [-1, 0], [-1, -1]
    };
    var beginDirect = 0;
    var isBegin = false;
    var curX = beginX;
    var curY = beginY;
    while (!isBegin)
    {
        var i = 0;
        var direct = beginDirect;
        var hasPoint = false;
        while (i ++ <= directItems.Length)
        {
            var x = curX + directItems[direct][0];
            var y = curY + directItems[direct][1];
            // 判断点是否是透明像素点
            if (IsTransparent(pixMap, x, y))
            {
                direct = (direct + 1) % directItems.Length;
                continue;
            }
            hasPoint = true;
            curX = x;
            curY = y;
            if (curX == beginX && curY == beginY)
            {
                isBegin = true;
                path.Close();
            }
            else
            {
                path.LineTo(curX, curY);
            }
            beginDirect = (direct + 6) % directItems.Length;
            break;
        }
        if (!hasPoint)
        {
            // 所有方向都没有找到下一个不透明点,表明这就是一个孤点
            return null;
        }
    }
    return path;
}

升级版:获取外轮廓,允许点的

/// <summary>
/// 物体轮廓获取
/// </summary>
public class ImageContourTrace
{
    public ImageContourTrace()
    {

    }

    public ImageContourTrace(bool isOutline)
    {
        IsOutline = isOutline;
    }
    /// <summary>
    /// 外边框,即靠近物体的透明区域
    /// </summary>
    public bool IsOutline { get; set; }
    /// <summary>
    /// 是否需要获取一个点
    /// </summary>
    public bool IsAllowDot { get; set; }

    /// <summary>
    /// 获取图片上所有物体轮廓
    /// </summary>
    /// <param name="image"></param>
    /// <returns></returns>
    public async Task<SKPath[]> GetContourAsync(SKBitmap image, CancellationToken token = default)
    {
        using var imagePixMap = image.PeekPixels();
        return await GetContourAsync(imagePixMap, token);
    }

    /// <summary>
    /// 获取图片上所有物体轮廓
    /// </summary>
    /// <param name="image"></param>
    /// <returns></returns>
    public async Task<SKPath[]> GetContourAsync(SKImage image, CancellationToken token = default)
    {
        using var imagePixMap = image.PeekPixels();
        return await GetContourAsync(imagePixMap, token);
    }

    /// <summary>
    /// 获取图片上所有物体轮廓
    /// </summary>
    /// <param name="image"></param>
    /// <returns></returns>
    public Task<SKPath[]> GetContourAsync(SKPixmap pixMap, CancellationToken token = default)
    {
        return Task.Factory.StartNew(() => {
            return GetContour(pixMap, token);
        }, token);
    }
    /// <summary>
    /// 获取所有物体的轮廓
    /// </summary>
    /// <param name="pixMap"></param>
    /// <returns></returns>
    public SKPath[] GetContour(SKPixmap pixMap, CancellationToken token = default)
    {
        var items = new List<SKPath>();
        for (var i = 0; i < pixMap.Height; i++)
        {
            for (var j = 0; j < pixMap.Width; j++)
            {
                if (token.IsCancellationRequested)
                {
                    return [..items];
                }
                if (IsTransparent(pixMap, j, i) || Contains(items, j, i))
                {
                    continue;
                }
                var path = GetContour(pixMap, j, i);
                if (path is null)
                {
                    continue;
                }
                items.Add(path);
            }
        }
        return [.. items];
    }

    /// <summary>
    /// 根据坐标获取轮廓边界算法
    /// </summary>
    /// <param name="pixMap"></param>
    /// <param name="beginX"></param>
    /// <param name="beginY"></param>
    /// <returns></returns>
    public SKPath? GetContour(SKPixmap pixMap, int beginX, int beginY)
    {
        var path = new SKPath();
        path.MoveTo(beginX, beginY - (IsOutline ? 1 : 0));
        var directItems = new int[][] {
            [0, -1], [1, -1],
                        [1, 0],
            [1, 1], [0, 1], [-1, 1],
            [-1, 0], [-1, -1]
        };
        var beginDirect = 0;
        var isBegin = false;
        var curX = beginX;
        var curY = beginY;
        while (!isBegin)
        {
            var i = 0;
            var direct = beginDirect;
            var hasPoint = false;
            while (i++ <= directItems.Length)
            {
                var x = curX + directItems[direct][0];
                var y = curY + directItems[direct][1];
                if (IsTransparent(pixMap, x, y))
                {
                    direct = (direct + 1) % directItems.Length;
                    if (IsOutline)
                    {
                        path.LineTo(x, y);
                    }
                    continue;
                }
                hasPoint = true;
                curX = x;
                curY = y;
                if (curX == beginX && curY == beginY)
                {
                    isBegin = true;
                    path.Close();
                }
                else if (!IsOutline)
                {
                    path.LineTo(curX, curY);
                }
                beginDirect = (direct + 6) % directItems.Length;
                break;
            }
            if (!hasPoint)
            {
                if (IsOutline)
                {
                    path.Close();
                    return path;
                }
                // 所有方向都没有不透明点,就是一个孤点
                return IsAllowDot ? path : null;
            }
        }
        return path;
    }

    private static bool Contains(IEnumerable<SKPath> items, int x, int y)
    {
        foreach (var item in items)
        {
            if (item.Contains(x, y))
            {
                return true;
            }
        }
        return false;
    }

    private static bool IsTransparent(SKPixmap pixMap, int x, int y)
    {
        if (x < 0 || y < 0 || x >= pixMap.Width || y >= pixMap.Height)
        {
               return true;
        }
        return pixMap.GetPixelColor(x, y).Alpha == 0;
    }
}

SkiaSharp 把 pixel byte[] 转成 SKBitmap

2024-08-19 18:54:04

SkiaSharp 把 pixel byte[] 转成 SKBitmap

关键方法

/// <summary>
/// 把像素字节数组转图片
/// </summary>
/// <param name="buffer">像素数组,例如: [r, g, b, a, r, g, b, a ...]</param>
/// <param name="width">图片的宽度,例如: 512</param>
/// <param name="height">图片的高度,例如: 1024</param>
/// <param name="format">指定像素数组的组成方式,例如:SKColorType.Rgba8888</param>
/// <returns></returns>
public static SKBitmap Decode(byte[] buffer, int width, int height, SKColorType format)
{
    var data = SKData.CreateCopy(buffer);
    var newInfo = new SKImageInfo(width, height, format);
    var bitmap = new SKBitmap();
    bitmap.InstallPixels(newInfo, data.Data);
    return bitmap;
}

public static SKImage Decode(byte[] buffer, int width, int height, SKColorType format)
{
    var newInfo = new SKImageInfo(width, height, format);
    var data = SKData.CreateCopy(buffer);
    return SKImage.FromPixels(newInfo, data);
}

解密不知的图片格式

SkiaSharp 默认支持 png jpg 等文件格式,但是一些不支持的文件格式怎么显示呢?

例如:pvr 格式就不能直接解码。

  1. 第一步,先解析 pvr 文件,获取 pvr 文件头得到:图片的宽高,编码方式,数据开始的开始的位置
  2. 读取内容数据
  3. 判断 编码方式 是否被 SkiaSharp 支持,所有支持的颜色编码方式在 SKColorType 中
  4. 不支持的编码方式,需要先转成支持的,例如转成 SKColorType.Rgba8888, 即:每个像素占四个字节,分别为 [Red,Green,Blue,Alpha],总字节为 宽*高*4, 以逐行横行存储, (行*宽+列)*4 获取像素点的位置
  5. 调用上述方法 Decode(转换后的数据, 宽, 高, SKColorType.Rgba8888) 即可

可能出现的问题

Q: Attempted to read or write protected memory

A: SkiaSharp 是线程安全的,两个线程不应访问同一个对象。SKBitmap 只能在UI主线程中操作,SKImage 则可以在所有子线程中操作