博客 (39)

今天发现在 .NET Framework 和 .NET 9 中使用 System.Uri.EscapeDataString() 方法对字符串进行编码,会产生不同的结果。

譬如“(”符号,前者视其为非保留字符,不进行转义,后者视为保留字符,转义为“%28”。

原因是 .NET Framework 4.8 主要遵循 RFC 2396,而 .NET 9 遵循 RFC 3986。


在跨平台签名验证场景中,对 URL 编码的一致性要求极高,任何细微差别都会导致签名校验失败。

以下是以 RFC 3986 标准为核心、优先使用各平台内置的高一致性方案。


对于 .NET 9,直接使用 Uri.EscapeDataString()。

string encodedData = System.Uri.EscapeDataString(dataToEncode);


对于 .NET Framework,以下是一个遵循 RFC 3986 严格标准的自定义编码方法示例。

static string Rfc3986EscapeDataString(string input)
{
    // 定义 RFC 3986 中明确的未保留字符集(不编码)
    var unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

    var result = new StringBuilder();
    byte[] data = Encoding.UTF8.GetBytes(input); // 统一转换为 UTF-8 字节

    foreach (byte b in data)
    {
        char currentChar = (char)b;
        // 如果是未保留字符,直接输出
        if (unreservedChars.IndexOf(currentChar) != -1)
        {
            result.Append(currentChar);
        }
        else
        {
            // 否则,进行百分号编码(%XX,大写)
            result.Append('%').Append(b.ToString("X2"));
        }
    }
    return result.ToString();
}


对于 PHP,直接使用内置的 rawurlencode() 函数。这个函数的设计初衷就是严格遵循 RFC 3986 标准。

$encoded_data = rawurlencode($data_to_encode);


对于 JavaScript,encodeURIComponent 函数严格遵循 RFC 3986 标准。

let encodedData = encodeURIComponent(dataToEncode);


重要提示:无论使用哪种语言,务必在编码前明确指定字符串使用 UTF-8 编码。编码不一致是导致乱码和签名失败最常见的原因之一 。

xoyozo 11 天前
71

实例化方法一:DbSearcher(String dbFile, QueryType queryType, String key)

实例化方法二:DbSearcher(InputStream is, QueryType queryType, String key)


  • 方法一通过传入文件路径来实例化,方法二通过传入文件流来实例化;

  • 当 queryType 为 MEMORY 时,方法一和方法二只有在实例化时有性能差别,差别在于将指定路径的文件读到内存中保存;在执行 Search 方法时无性能差别,因为都是在内存中执行查找;

  • queryType 为 MEMORYBTREE 的差别在于前者是在内存中查找,速度快但占内存,后者是在文件中查找,速度慢但省内存


错误:Padding is invalid and cannot be removed.

原因:可能是数据库文件与密钥不一致。

xoyozo 1 年前
1,973

SignalR 是一个开源的实时通信库,用于构建实时 Web 应用程序。它提供了一个简单的 API,可以在客户端和服务器之间建立持久连接,以便实时地推送数据。

与传统的 WebSocket 相比,SignalR 提供了更高级的功能和更简单的开发体验。下面是一些主要区别:

  • 支持多种传输方式:SignalR 可以使用多种传输方式,包括 WebSocket、Server-Sent Events(SSE)、长轮询和 Forever Frame。这使得 SignalR 在不同的环境中都能提供实时通信的能力,即使某些浏览器不支持 WebSocket,也可以使用其他传输方式。

  • 自动处理连接管理:SignalR 管理连接的生命周期,包括连接的建立、断开和重新连接。它会自动处理连接的失败和重新连接的逻辑,简化了开发人员的工作。

  • 服务器端推送:SignalR 允许服务器端主动推送消息给客户端,而不需要客户端发起请求。这使得实时通信变得更加高效和实时,适用于聊天应用、实时监控等场景。

  • 跨平台支持:SignalR 可以在多个平台上使用,包括 .NET、Java、JavaScript 等。这使得开发人员可以使用自己熟悉的语言和框架来构建实时应用程序。


微软官方提供了针对 ASP.NET Core Web 应用(Razor 页面)的详细教程,这里给出 MVC 版本入门教程。


最终将创建一个正常运行的聊天应用:

signalr-get-started-finished.png


创建 Web 应用项目

image.png

image.png


添加 SignalR 客户端库

在“解决方案资源管理器”>中,右键单击项目,然后选择“添加”“客户端库”。

在“添加客户端库”对话框中:

  • “提供程序”选择“unpkg”

  • “库”,请输入 @microsoft/signalr@latest。

  • 选择“选择特定文件”,展开“dist/browser”文件夹,然后选择 signalr.js 和 signalr.min.js。

  • 点击“安装” 。

image.png


创建 SignalR Hubs 类

using Microsoft.AspNetCore.SignalR;

/// <summary>
/// Hub 类管理连接、组和消息
/// </summary>
public class ChatHub : Hub
{
        /// <summary>
        /// 可通过已连接客户端调用 SendMessage,以向所有客户端发送消息
        /// </summary>
    public async Task SendMessage(string user, string message)
    {
        // Clients.All 向所有的客户端发送消息
        // ReceiveMessage 是客户端监听的方法
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

其父类 Hub 可管理连接、组和消息。这里演示的是向所有客户端发送消息。


配置 SignalR

打开 Program.cs,添加注入:

builder.Services.AddSignalR();

添加路由:

app.MapHub<ChatHub>("/chatHub");


添加 SignalR 客户端代码

视图页面:

<div class="container">
    <div class="row p-1">
        <div class="col-1">用户</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-1">消息</div>
        <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-6 text-end">
            <input type="button" id="sendButton" value="发送消息" />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <hr />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
</div>
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/chat.js"></script>

chat.js 文件:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

// 在连接建立之前禁用发送按钮
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // 修改此处时应注意脚本注入问题
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});


完成。在线示例:https://xoyozo.net/Demo/SignalRDemo






xoyozo 2 年前
2,754

推荐使用 SignalR 来平替 WebSocket,参考教程


【服务端(.NET)】

一、创建 WebSocketHandler 类,用于处理客户端发送过来的消息,并分发到其它客户端

public class WebSocketHandler
{
    private static List<WebSocket> connectedClients = [];

    public static async Task Handle(WebSocket webSocket)
    {
        connectedClients.Add(webSocket);

        byte[] buffer = new byte[1024];
        WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        while (!result.CloseStatus.HasValue)
        {
            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            Console.WriteLine($"Received message: {message}");

            // 处理接收到的消息,例如广播给其他客户端
            await BroadcastMessage(message, webSocket);

            result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        }

        connectedClients.Remove(webSocket);
        await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
    }

    private static async Task BroadcastMessage(string message, WebSocket sender)
    {
        foreach (var client in connectedClients)
        {
            if (client != sender && client.State == WebSocketState.Open)
            {
                await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }
    }
}

二、在 Program.cs 或 Startup.cs 中插入:

// 添加 WebSocket 路由
app.UseWebSockets();
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/chat")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await WebSocketHandler.Handle(webSocket);
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    else
    {
        await next();
    }
});

这里的路由路径可以更改,此处将会创建一个 WebSocket 连接,并将其传递给 WebSocketHandler.Handle 方法进行处理。

也可以用控制器来接收 WebSocket 消息。


【客户端(JS)】

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>WebSocket 示例(JS + ASP.NET)</title>
    <style>
        #list { width: 100%; max-width: 500px; }
        .tip { color: red; }
    </style>
</head>
<body>
    <h2>WebSocket 示例(JS + ASP.NET)</h2>
    <textarea id="list" readonly rows="20"></textarea>
    <div>
        <input type="text" id="msg" />
        <button onclick="fn_send()">发送</button>
        <button onclick="fn_disconnect()">断开</button>
    </div>
    <p id="tip_required">请输入要发送的内容</p>
    <p id="tip_closed">WebSocket 连接未建立</p>

    <script>
        var wsUrl = "wss://" + window.location.hostname + ":" + window.location.port + "/chat";
        var webSocket;
        var domList = document.getElementById('list');

        // 初始化 WebSocket 并连接
        function fn_ConnectWebSocket() {
            webSocket = new WebSocket(wsUrl);

            // 向服务端发送连接请求
            webSocket.onopen = function (event) {
                var content = domList.value;
                content += "[ WebSocket 连接已建立 ]" + '\r\n';
                domList.innerHTML = content;

                document.getElementById('tip_closed').style.display = 'none';

                webSocket.send(JSON.stringify({
                    msg: 'Hello'
                }));
            };

            // 接收服务端发送的消息
            webSocket.onmessage = function (event) {
                if (event.data) {
                    var content = domList.value;
                    content += event.data + '\r\n';
                    domList.innerHTML = content;
                    domList.scrollTop = domList.scrollHeight;
                }
            };

            // 各种情况导致的连接关闭或失败
            webSocket.onclose = function (event) {
                var content = domList.value;
                content += "[ WebSocket 连接已关闭,3 秒后自动重连 ]" + '\r\n';
                domList.innerHTML = content;

                document.getElementById('tip_closed').style.display = 'block';

                setTimeout(function () {
                    var content = domList.value;
                    content += "[ 正在重连... ]" + '\r\n';
                    domList.innerHTML = content;
                    fn_ConnectWebSocket();
                }, 3000); // 3 秒后重连
            };
        }

        // 检查连接状态
        function fn_IsWebSocketConnected() {
            if (webSocket.readyState === WebSocket.OPEN) {
                document.getElementById('tip_closed').style.display = 'none';
                return true;
            } else {
                document.getElementById('tip_closed').style.display = 'block';
                return false;
            }
        }

        // 发送内容
        function fn_send() {
            if (fn_IsWebSocketConnected()) {
                var message = document.getElementById('msg').value;
                document.getElementById('tip_required').style.display = message ? 'none' : 'block';
                if (message) {
                    webSocket.send(JSON.stringify({
                        msg: message
                    }));
                }
            }
        }

        // 断开连接
        function fn_disconnect() {
            if (fn_IsWebSocketConnected()) {
                // 部分浏览器调用 close() 方法关闭 WebSocket 时不支持传参
                // webSocket.close(001, "Reason");
                webSocket.close();
            }
        }

        // 执行连接
        fn_ConnectWebSocket();
    </script>
</body>
</html>


【在线示例】

https://xoyozo.net/Demo/JsNetWebSocket


【其它】

  • 服务端以 IIS 作为 Web 服务器,那么需要安装 WebSocket 协议

  • 一个连接对应一个客户端(即 JS 中的 WebSocket 对象),注意与会话 Session 的区别

  • 在实际项目中使用应考虑兼容性问题

  • 程序设计应避免 XSS、CSRF 等安全隐患

  • 参考文档:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets

xoyozo 2 年前
1,736

在 Vue 中,当你将一个布尔对象双向绑定到 radio 输入上时,确保 radio 的 value 属性仍然保持布尔类型是很重要的。如果 value 属性是字符串,那么 Vue 会将其解释为字符串而不是布尔值。以下是一个示例,演示如何正确地将布尔对象双向绑定到 radio 按钮,并确保 value 属性保持布尔类型:

<template>
  <div>
    <input type="radio" v-model="myBoolean" :value="true" /> True
    <input type="radio" v-model="myBoolean" :value="false" /> False
    <p>myBoolean: {{ myBoolean }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      myBoolean: false, // 布尔对象
    };
  },
};
</script>

在这个示例中,我们使用了 v-model 指令将 myBoolean 属性与两个 radio 按钮进行了双向绑定。通过将 value 属性设置为 true 和 false,我们确保了 myBoolean 属性保持布尔类型。

这样,当用户选择其中一个 radio 按钮时,myBoolean 属性将保持布尔值而不是字符串。确保 value 属性的类型与要绑定的数据类型匹配,以确保绑定的数据类型保持一致。

同样的问题出现在 select 上也是一样:

<select v-model="cat.colorId" required>
    <option :value="null">不选择</option>
    <option v-for="color in colors" :value="color.id">{{color.name}}</option>
</select>


xoyozo 2 年前
2,099

不知道从哪个版本的 Chrome 或 Edge 开始,我们无法通过 ctrl+v 快捷键将时间格式的字符串粘贴到 type 为 date 的 input 框中,我们想办法用 JS 来实现。


方式一、监听 paste 事件:

const input = document.querySelector('input[type="date"]');
input.addEventListener('paste', (event) => {
    input.value = event.clipboardData.getData('text');
});

这段代码实现了从页面获取这个 input 元素,监听它的 paste 事件,然后将粘贴板的文本内容赋值给 input。

经测试,当焦点在“年”的位置时可以粘贴成功,但焦点在“月”或“日”上不会触发 paste 事件。


方式二、监听 keydown 事件:

const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
    if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
        event.preventDefault();
        var clipboardData = (event.clipboardData || event.originalEvent.clipboardData);
        input.value = clipboardData.getData('text');
    }
});

测试发现报错误:

Uncaught TypeError: Cannot read properties of undefined (reading 'getData')

Uncaught TypeError: Cannot read properties of undefined (reading 'clipboardData')

看来 event 中没有 clipboardData 对象,改为从 window.navigator 获取:

const input = document.querySelector('input[type="date"]');
input.addEventListener('keydown', (event) => {
    if ((navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && event.key === 'v') {
        event.preventDefault();
        window.navigator.clipboard.readText().then(text => {
            input.value = text;
        });
    }
});

缺点是需要用户授权:

image.png

仅第一次需要授权,如果用户拒绝,那么以后就默认拒绝了。


以上两种方式各有优缺点,选择一种适合你的方案就行。接下来继续完善。


兼容更多时间格式,并调整时区

<input type="date" /> 默认的日期格式是 yyyy-MM-dd,如果要兼容 yyyy-M-d 等格式,那么:

const parsedDate = new Date(text);
if (!isNaN(parsedDate.getTime())) {
    input.value = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
}

以 text 为“2023-4-20”举例,先转为 Date,如果成功,再转为英国时间格式“20-04-2023”,以“/”分隔,逆序,再以“-”连接,就变成了“2023-04-20”。

当然如果希望支持中文的年月日,可以先用正则表达式替换一下:

text = text.replace(/\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*/, "$1-$2-$3");


处理页面上的所有 <input type="date" />

const inputs = document.querySelectorAll('input[type="date"]');
inputs.forEach((input) => {
    input.addEventListener(...);
});


封装为独立域

避免全局变量污染,使用 IIFE 函数表达式:

(function() {
  // 将代码放在这里
})();

或者封装为函数,在 jQuery 的 ready 中,或 Vue 的 mounted 中调用。


在 Vue 中使用

如果将粘贴板的值直接赋值到 input.value,在 Vue 中是不能同步更新 v-model 绑定的变量的,所以需要直接赋值给变量:

<div id="app">
    <input type="date" v-model="a" data-model="a" v-on:paste="fn_pasteToDateInput" />
    {{a}}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
    const app = Vue.createApp({
        data: function () {
            return {
                a: null,
            }
        },
        methods: {
            fn_pasteToDateInput: function (event) {
                const text = event.clipboardData.getData('text');
                const parsedDate = new Date(text);
                if (!isNaN(parsedDate.getTime())) {
                    const att = event.target.getAttribute('data-model');
                    this[att] = parsedDate.toLocaleDateString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit' }).split('/').reverse().join('-');
                }
            },
        }
    });
    const vm = app.mount('#app');
</script>

示例中 <input /> 添加了 data- 属性,值同 v-model,并使用 getAttribute() 获取,利用 this 对象的属性名赋值。 

如果你的 a 中还有嵌套对象 b,那么 data- 属性填写 a.b,方法中以“.”分割逐级查找对象并赋值

let atts = att.split('.');
let target = this;
for (let i = 0; i < atts.length - 1; i++) {
    target = target[atts[i]];
}
this.$set(target, atts[atts.length - 1], text); // vue2
target[atts[atts.length - 1]] = text; // vue3


xoyozo 3 年前
2,040

有对象模型:

{ "list": { "1": "a", "2": "b" } }

那么绑定循环:

<option v-for="(value, key) in list" :key="key" :value="key">{{value}}</option>

效果:

<option value="1">a</option>
<option value="2">b</option>

若循环内部需要双向绑定,则:

<input type="text" v-model="list[key]" />

又例:

<div v-for="(value, key) in list" :key="key">{{key}}</div>

效果:

<div>1</div>
<div>2</div>


xoyozo 3 年前
2,228

LigerShark.WebOptimizer.CoreBuildBundlerMinifier
特点

在运行时捆绑和缩小 CSS 和 JavaScript 文件

具有完整的服务器端和客户端缓存,以确保高性能

可禁用缩小

支持使用通配模式

标记帮助程序与缓存破坏

支持内联

支持 SCSS

将 CSS、JavaScript 或 HTML 文件捆绑到单个输出文件中

保存源文件会自动触发重新捆绑

支持通配模式

支持 CI 方案的 MSBuild 支持

缩小单个或捆绑的 CSS、JavaScript 和 HTML 文件

每种语言的缩小选项是可定制的

打开生成的文件时显示水印

任务运行程序资源管理器集成

命令行支持

快捷更新解决方案中所有捆绑包

禁止生成输出文件

转换为 Gulp

注入(Program.cs)

services.AddWebOptimizer();

app.UseWebOptimizer();


指定捆绑的项目在运行时自动缩小 css 和 js,也可以在 AddWebOptimizer 中自定义。默认情况下,生成的文件不会保存到磁盘,而是保存在缓存中。手动编辑 bundleconfig.json 文件来指定需要合并和缩小的文件
功能配置
{
  "webOptimizer": {
    // 确定是否应设置 HTTP 标头以及是否应支持条件 GET (304) 请求。在开发模式下禁用此功能可能会有所帮助。cache-control
    "enableCaching": true,
    // 确定是否用于缓存。在开发模式下禁用可能会有所帮助。IMemoryCache
    "enableMemoryCache": true,
    // 确定管道资产是否缓存到磁盘。这可以通过从磁盘加载 pipline 资产而不是重新执行管道来加快应用程序重新启动的速度。在开发模式下禁用可能会有所帮助。
    "enableDiskCache": true,
    // 设置资产将存储的目录 ifistrue。必须为读/写。enableDiskCache
    "cacheDirectory": "/var/temp/weboptimizercache",
    // 确定元素是否应指向捆绑路径或应为每个源文件创建引用。这在开发模式下禁用很有帮助。
    "enableTagHelperBundling": true,
    // 是一个绝对 URL,如果存在,它会自动为页面上的任何脚本、样式表或媒体文件添加前缀。注册标记帮助程序后,标记帮助程序会自动添加前缀。在此处查看如何注册标记帮助程序。
    "cdnUrl": "https://my-cdn.com/",
    // 确定捆绑包的源文件中没有内容时的行为,默认情况下,请求捆绑包时将引发 404 异常,设置为 true 以获取包含空内容的捆绑包。
    "allowEmptyBundle": false
  }
}


在 bundleconfig.json 捆绑时设置

[
  {
    "outputFileName": "output/bundle.css",
    "inputFiles": [
      "css/lib/**/*.css", // globbing patterns are supported
      "css/input/site.css"
    ],
    "minify": {
        "enabled": true,
        "commentMode": "all"
    }
  },
  {
    "outputFileName": "output/all.js",
    "inputFiles": [
      "js/*.js",
      "!js/ignore.js" // start with a ! to exclude files
    ]
  },
  {
    "outputFileName": "output/app.js",
    "inputFiles": [
      "input/main.js",
      "input/core/*.js" // all .js files in input/core/
    ]
  }
]


热重载(在开发模式中保存文件自动更新浏览器效果)
VS 安装扩展,方法:菜单 - 扩展 - 管理扩展 - 联机 搜索“Bundler & Minifier
其它资料
ASP.NET 官方文档


xoyozo 3 年前
3,736
[
  {
    "outputFileName": "wwwroot/js/Admin/Blog/List.js",
    "inputFiles": [
      "Areas/Admin/Views/Blog/List.cshtml.js"
    ]
  }
]

JS 源文件在视图目录,独立于 .cshtml 文件,又折叠于 .cshtml 文件。

输出目录在 wwwroot 中,若文件名不以“.min.js”结尾,那么会自动生成“.js”和“.min.js”两个文件,分别在开发模式和生产模式的视图中引用。

缺点:只更改 JS 内容,执行“生成解决方案”项目不会重新编译更新 wwwroot 中的 .js 和 .min.js 文件,这时使用“重新生成解决方案”可以解决这个问题。

xoyozo 4 年前
4,242

iPhone 或安卓上传的照片,可能因横向或纵向拍摄,导致上传后照片显示成 90 度、180 度、270 度角旋转,使用 C# 处理该照片时同样会遇到相同的问题。

解决的方案:

客户端使用 base64 显示图片,可按正常方向显示,且不通过服务端即时显示。关键代码:

$('#file_input').change(function (e) {
    if (e.target.files.length == 0) { return; }
    // file 转换为 base64
    var reader = new FileReader();
    reader.readAsDataURL(e.target.files[0]);
    reader.onload = function (event) {
        $('#myImg').attr('src', event.target.result).css({ width: 'auto', height: 'auto' });
    }
});
$('#myImg').on('load', function () {
    console.log('图片加载完成')
});

但 base64 字符串传递到服务端受请求大小限制(一般比直接 POST 文件要小)。所以显示图片的同时应即时使用 POST 方式上传图片(如 jquery.fileupload.js)。

当然如果是在微信中打开网页,使用 JS-SDK 的 wx.chooseImage() 返回的 localId 也可以在 <img /> 中正常显示,然后使用 wx.uploadImage() 上传到微信服务器并获取 serverId,在通过获取临时素材接口取回图片文件。

服务端如果需要处理该图片(缩放、裁切、旋转等),需要先读取照片的拍摄角度,旋正后,再进行处理,相关 C# 代码:

/// <summary>
/// 调整照片的原始拍摄旋转角度
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
private Image ImageRotateFlip(Image img)
{
    foreach (var prop in img.PropertyItems)
    {
        if (prop.Id == 0x112)
        {
            switch (prop.Value[0])
            {
                case 6: img.RotateFlip(RotateFlipType.Rotate90FlipNone); break;
                case 3: img.RotateFlip(RotateFlipType.Rotate180FlipNone); break;
                case 8: img.RotateFlip(RotateFlipType.Rotate270FlipNone); break;
            }
            prop.Value[0] = 1;
        }
    }
    return img;
}
xoyozo 4 年前
4,007