MoreRSS

site iconZoDream修改

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

Inoreader Feedly Follow Feedbin Local Reader

ZoDream的 RSS 预览

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" [field]="form.a">`,
    styleUrls: ['./example.component.scss']
})
export class ExampleComponent {
    public readonly form = form(sinal({
        a: 1
    }), schemaPath => {
        required(schemaPath.a);
    })

}

使用 [field] 精选表单绑定,不能自定义 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 则可以在所有子线程中操作

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_PORT为 3000 即可

ini文件

INI配置

参考

【基于docker搭建gogs】