MoreRSS

site iconZoDream修改

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

Inoreader Feedly Follow Feedbin Local Reader

ZoDream的 RSS 预览

文件解析笔记

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 再考虑 leb128uint 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 则可以在所有子线程中操作

nas 使用 Docker 安装 gogs

2024-08-04 18:43:01

nas 使用 Docker 安装 gogs

环境

NAS设备:绿联DXP4800 Plus

NAS系统:UGOS PRO

第一步:安装 Docker

安装 Docker

第二步:安装 Docker 镜像

无法直接 DockerHub 可以使用 阿里云的镜像加速

DockerHub

配置Dockerhub加速

搜索 gogs 下载

gogs

第三步:安装 gogs

配置自动重启 屏幕截图 2024-08-04 110558.png

配置文件保存位置 屏幕截图 2024-08-04 105940.png

配置端口映射 屏幕截图 2024-08-04 105954.png

访问网页进行GOGS配置 屏幕截图 2024-08-04 110407.png

配置数据库图片来源:https://www.cnblogs.com/yuexiaoyun/articles/11946103.html

配置端口图片来源:https://www.cnblogs.com/yuexiaoyun/articles/11946103.html

一些问题

Q: nas重启后网址无法打开

A:可能是gogs安装时,端口配置错误,可以到gogs的文件保存位置下找到 gogs/conf/app.ini 文件修改 [server]HTTP_PORT3000 即可

ini文件

INI配置

参考

基于docker搭建gogs

复制 android 手机中的文件到电脑

2024-08-04 18:40:38

复制 android 手机中的文件到电脑

方法一:直接复制

使用USB连接电脑

在手机上选择“USB用于传输文件”

优点

  1. 不需要其他操作,简单方便

缺点

  1. 文件一多就会复制很慢很慢

方法二:使用第三方app直接传

缺点

  1. 需要安装app
  2. 传输慢
  3. android/data 下的文件可能无法获取到并传输,如果再手机上把 android/data复制到 Download,可能无法复制目录中的文件

方法三:先再手机上使用第三方app压缩再传输

例如使用 ES文件浏览器

优点

  1. 压缩后,传输会快很多

缺点

  1. 有些文件名中有特殊字符会导致压缩失败

方法三:使用adb pull 下载

需要在电脑中安装 adb

使用USB连接电脑

在手机上选择“USB用于传输文件”

再手机 设置 中打开 开发者模式,开启 USB调试功能

电脑上打开终端命令行

# adb pull <手机中的文件路径> <保存在电脑的位置>
adb pull /storage/emulated/0/Android/data/com.xxx files

优点

  1. 就算文件多,传输也很快

缺点

  1. 一些文件名中有特殊字符会导致传输中断,无法跳过,可能是 linux 和 windows 系统的原因导致的,电脑是 linux 系统的话不会出现这个问题

方法四【推荐】:使用adb压缩下载

注意千万不要使用以下命令

这两种命令的原理就是压缩/storage/emulated/0/Android/data/com.xx不保存直接输出终端,再保存终端输出的内容到 aaa.tar 文件,但是会导致文件出现0xFF 0xFE 和 很多 0x0字节,导致解压缩软件无法读取

adb exec-out tar chf - -C /storage/emulated/0/Android/data/com.xx files > aaa.tar

adb shell 'tar -cf - /storage/emulated/0/Android/data/xx 2>/dev/null' > backup.tar.gz

可以使用先压缩到手机,再下载出来

# 压缩 /storage/emulated/0/Android/data/com.xx 到 /storage/emulated/0/Download/files.tar 文件中
adb exec-out tar chf /storage/emulated/0/Download/files.tar /storage/emulated/0/Android/data/com.xx

# 下载
adb pull /storage/emulated/0/Download/files.tar files.tar

tar 命令

-c: 建立压缩档案
-x:解压
-t:查看内容
-r:向压缩归档文件末尾追加文件
-u:更新原压缩包中的文件
-f: 文件
-h: 解除软链接

所以 tar chf <保存的文件> <要压缩的目录> 就是 tar -c <保存的文件> -h -f <要压缩的目录> 的意思

优点

  1. 所有的文件都能传输
  2. 在手机上压缩更快

缺点

  1. 需要手机空间大