使用layui 表格 下拉框 的实现_layui 下拉框-CSDN博客

mikel阅读(372)

1: 想要实现的 效果 :

2:使用 templet 模版 (代码如下 前提 :接口正常 能获取到数据并展示)

table.render({
elem: ‘#munu-table’
, url: ‘xxx.com/xxxx’
,id:”table”

,page:{
limit: 15
, limits: [15, 20, 30, 40, 50, 100, 150, 200, 250, 300, 400, 500, 800, 1000]
// ,curr:localStorage.getItem(‘spgl_curr’) ? localStorage.getItem(‘spgl_curr’) : 1
}
,totalRow: true //开启合计行
, cols: [[
{type: ‘checkbox’, width: 40, sort: true},
{field: ‘id’, width: 140, title: ‘id’,hide:true},
{field: ‘image’, width: 70, title: ‘图片’,templet:function (d) {
return ‘<img class=”productimages” width=”16px” src=’+d.image+’>’;
}},
{field: ‘list_number’, width: 100, title: ‘清单编号’},
{field: ‘storage_id’, width: 100, title: ‘入库编号’},
{field: ‘batch_number’, width: 80, title: ‘批次号’},
{field: ‘sku’, width: 80, title: ‘sku’},
{field: ‘storage_quantity’, width: 90, title: ‘入库数量’},
{field: ‘sample_quantity’, width: 90, title: ‘留样数量’},
{field: ‘actual_packing_quantity’, width: 115, title: ‘实际装箱数量’},
{field: ‘purchaser’, width: 80, title: ‘采购人’},

{field: ‘outbound_date’, width: 100, title: ‘出库日期’},

{field: ‘pending_days’, width: 100, title: ‘待处理天数’},
{field: ‘processing_status’, width: 100, title: ‘处理进度’},
{field: ‘processor’, width: 80, title: ‘处理人’},
{field: ‘created_time’, width: 100, title: ‘创建时间’},
{field: ‘modified_time’, width: 100, title: ‘修改时间’},

{
field: ‘processing_method’,
title: ‘处理方式’,
align: ‘center’,
width: 200,
templet: function (d) {
var selectHtml = ‘<select name=”paid” class=”sel_xlk” lay-filter=”stateSelect” lay-verify=”required” data-state=”‘ + d.paid + ‘” data-value=”‘ + d.id + ‘” >’ +
‘ <option value=”签补协议”>签补协议</option>’ +
‘ <option value=”等待供应商补发数量”>等待供应商补发数量</option>’ +
‘ <option value=”报损”>报损</option>’ +
‘ <option value=”解除协议”>解除协议</option>’ +
‘ </select>’;

return d.processing_method + selectHtml;
}
},

]]
, done: function(res, curr, count) {
console.log(res); // 在控制台打印获取到的数据

$(“.layui-table-body”).css(‘overflow’,’visible’);
$(“.layui-table-box”).css(‘overflow’,’visible’);
$(“.layui-table-view”).css(‘overflow’,’visible’);

var tableElem = this.elem.next(‘.layui-table-view’);
count || tableElem.find(‘.layui-table-header’).css(‘overflow’, ‘auto’);
layui.each(tableElem.find(‘select[name=”paid”]’), function (index, item) {
var elem = $(item);
elem.val(elem.data(‘state’)).parents(‘div.layui-table-cell’).css(‘overflow’, ‘visible’);
});
form.render();//刷新表单

}

});
注意:templet 的用法 在手册中 只是简单的举了几个例子 。

文档:参考链接

3:注意done 里面的回调 :

done: function(res, curr, count) {
console.log(res); // 在控制台打印获取到的数据

$(“.layui-table-body”).css(‘overflow’,’visible’);
$(“.layui-table-box”).css(‘overflow’,’visible’);
$(“.layui-table-view”).css(‘overflow’,’visible’);

var tableElem = this.elem.next(‘.layui-table-view’);
count || tableElem.find(‘.layui-table-header’).css(‘overflow’, ‘auto’);
layui.each(tableElem.find(‘select[name=”paid”]’), function (index, item) {
var elem = $(item);
elem.val(elem.data(‘state’)).parents(‘div.layui-table-cell’).css(‘overflow’, ‘visible’);
});
form.render();//刷新表单

}
layui.each(tableElem.find(‘select[name=”paid”]’) pid 是 field: ‘pid’, 的值。

上面的代码 加上后才能正常的展示 下拉 数据 如果没有 这段代码 点击 下拉图标 不会起作用。

4:修改数据:

form.on(‘select(stateSelect)’, function (data) {//修改类型
let id = data.elem.dataset.value; //当前数据的id
let processing_method = data.elem.value; //当前字段变化的值
// 传值:表单变化后的值传递到后台数据库进行实时修改,例如,根据id修改这条数据的状态。

console.log(id);
console.log(processing_method);

$.ajax({
type: ‘post’,
url:’xxx.com/xxx’, // ajax请求路径
data: {
id: id,
processing_method: processing_method
},
success: function(data){

var data = JSON.parse(data)
layer.msg(data.msg);

// layer.msg(‘修改成功’);

// console.log(data);
//执行重载
//table.reload(‘bizInvoiceTable’);
//window.location.href = Feng.ctxPath + ‘/bizInvoice’
}
});
});

总结: 我遇到最多的问题就是 选择 下拉图标的时候 不能展示下拉的数据 。 所以一定要注意 红色字段。

未解决的问题 : 下拉框会被覆盖 f12 能高亮度找到 但是被表格下面的滑块 覆盖了 。

有滑块的时候 会出现 每列的线格子 会与 tittle 对不齐
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_51110402/article/details/132511932

layui实现table添加行功能 table里有select可选择可编辑 并且与form表单一起提交数据保存_layuitable 可编辑,有下拉框和输入框-CSDN博客

mikel阅读(348)

来源: layui实现table添加行功能 table里有select可选择可编辑 并且与form表单一起提交数据保存_layuitable 可编辑,有下拉框和输入框-CSDN博客

这个例子中的下拉框是 可选择 并且 可输入的 还有一个下拉框的点击事件 选择一个值的时候 带出后面几列的值
<%@ page language=”java” import=”java.util.*” pageEncoding=”UTF-8″%>
<%@ include file=”/WEB-INF/page/public/tag.jsp”%>
<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN”>
<html>
<head>
<meta charset=”utf-8″>
<title><jsp:include page=”/WEB-INF/page/public/top_title.jsp”/></title>
<meta name=”renderer” content=”webkit”>
<meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″>
<meta name=”viewport” content=”width=device-width, initial-scale=1, maximum-scale=1″>
<meta name=”apple-mobile-web-app-status-bar-style” content=”black”>
<meta name=”apple-mobile-web-app-capable” content=”yes”>
<meta name=”format-detection” content=”telephone=no”>
<link rel=”stylesheet” href=”${contextPath}/statics/css/layui.css” media=”all”/>
<style>
        /* 防止下拉框的下拉列表被隐藏—必须设置— 此样式和表格的样式有冲突 如果表格列数太多 会出现错乱的情况 目前我的解决方法是忽略下拉框的美化渲染 <select lay-ignore> */
        .layui-table-cell {
            overflow: visible;
        }
        .layui-table-box {
            overflow: visible;
        }
        .layui-table-body {
            overflow: visible;
        }
        /* 设置下拉框的高度与表格单元相同 */
        td .layui-form-select{
margin-top: -10px;
margin-left: -15px;
margin-right: -15px;
}
    </style>
</head>
<body class=”childrenBody”>
<div  style=”padding:15px;”>
<blockquote class=”layui-elem-quote”>
<span class=”layui-breadcrumb” style=”visibility: visible;”>
<a href=”main.html”>首页</a>
<span lay-separator=””>/</span>
<a href=”${contextPath}/storage/toList”>物料入库</a>
<span lay-separator=””>/</span>
<a><cite>物料入库新增</cite></a> </span>
</blockquote>
<form class=”layui-form” id=”fromId” action=”#”>
  <fieldset class=”layui-elem-field”>
<div style=”padding-top:25px;” class=”layui-field-box”>
  <div class=”layui-form-item”>
<label class=”layui-form-label”>入库单编号</label>
<div class=”layui-input-inline” style=”width:13%”>
  <input type=”text” name=”storagecode” placeholder=”请输入” class=”layui-input” lay-verify=”required”>
</div>
<label class=”layui-form-label”>入库人</label>
<div class=”layui-input-inline” style=”width:13%”>
<input type=”text” name=”storageuser” placeholder=”请输入” class=”layui-input”>
</div>
<label class=”layui-form-label”>仓库</label>
<div class=”layui-input-inline” style=”width:13%”>
<input type=”text” name=”warehouseid” placeholder=”请输入” class=”layui-input”>
</div>
<label class=”layui-form-label”>金额</label>
<div class=”layui-input-inline” style=”width:13%”>
<input type=”text” name=”money” placeholder=”请输入” class=”layui-input”>
</div>
  </div>
  <div class=”layui-form-item”>
    <label class=”layui-form-label”>入库日期</label>
    <div class=”layui-input-inline” style=”width:13%”>
    <input type=”text” name=”storagetime” placeholder=”请选择” class=”layui-input” id=”date”>
    </div>
    <label class=”layui-form-label”>制单人</label>
<div class=”layui-input-inline” style=”width:13%”>
<input type=”text” name=”documentmaker” placeholder=”请输入” class=”layui-input”>
</div>
<label class=”layui-form-label”>供应商</label>
<div class=”layui-input-inline” style=”width:13%”>
<input type=”text” name=”supplier” placeholder=”请输入” class=”layui-input”>
</div>
</div>
</div>
  </fieldset>
  <script type=”text/html” id=”selectTool”>
 <select name=”selectDemo” lay-filter=”selectDemo” lay-search>
<option value=””>请选择或输入</option>
        {{# layui.each(${selectByExample}, function(index, item){ }}
        <option>{{ item.materialcode }}</option>
        {{# }); }}
    </select>
</script>
  <script type=”text/html” id=”toolbarDemo”>
  <div align=”right” class=”layui-btn-container”>
  <button id=”addTable” class=”layui-btn layui-btn-sm layui-btn-normal” lay-event=”add”>添加行</button>
  </div>
</script>
  <script type=”text/html” id=”bar”>
<a class=”layui-btn layui-btn-danger layui-btn-xs” lay-event=”del”>删除</a>
    </script>
<table id=”demo” lay-filter=”tableFilter”></table>
<div class=”layui-form-item”  style=”margin-top: 30px;text-align: center;”>
    <button class=”layui-btn” lay-submit=”” lay-filter=”*”>保存</button>
    <a href=”${contextPath}/storage/toList” class=”layui-btn layui-btn-primary”>返回</a>
</div>
</form>
</div>
<script type=”text/JavaScript” src=”${contextPath}/statics/layui.js”></script>
<script>
layui.use([‘laydate’,’table’,’form’,’JQuery’], function(){
  var table = layui.table,
      form = layui.form,
    laydate = layui.laydate,
    $ = layui.JQuery;
  //时间控件
  laydate.render({
  elem: ‘#date’ //指定元素
  });
  //下拉框监听事件
  form.on(‘select(selectDemo)’, function(data){
//这里是当选择一个下拉选项的时候 把选择的值赋值给表格的当前行的缓存数据 否则提交到后台的时候下拉框的值是空的
var elem = data.othis.parents(‘tr’);
  var dataindex = elem.attr(“data-index”);
  $.each(tabledata,function(index,value){
        if(value.LAY_TABLE_INDEX==dataindex){
        value.materialcode = data.value;
        }
      });
      //这个是根据下拉框选的值 查询后台 带出后面几列的数据并赋值给页面 没有需要的同学忽略掉即可
     if(data.value){ $.ajax({
url:”${contextPath}/storage/toSelect”,
async:true,
type:”post”,
data:{“materialcode”:data.value},
success:function(data){
if(typeof(data) == ‘string’){
data = JSON.parse(data)
}
//给页面赋值
elem.find(“td[data-field=’materialname’]”).children().html(data.data.materialname);
elem.find(“td[data-field=’specifications’]”).children().html(data.data.specifications);
elem.find(“td[data-field=’warehouseid’]”).children().html(data.data.warehouseid);
elem.find(“td[data-field=’warningnumber’]”).children().html(data.data.warningnumber);
elem.find(“td[data-field=’topwarning’]”).children().html(data.data.topwarning);
elem.find(“td[data-field=’unitprice’]”).children().html(data.data.unitprice);
//给表格缓存赋值
$.each(tabledata,function(index,value){
      if(value.LAY_TABLE_INDEX==dataindex){
      value.materialname = data.data.materialname;
      value.specifications = data.data.specifications;
      value.warehouseid = data.data.warehouseid
      value.warningnumber = data.data.warningnumber;
      value.topwarning = data.data.topwarning
      value.unitprice = data.data.unitprice;
      }
     });
}
    });
     }
});
//第一个实例 加载表格
var tableIns = table.render({
 elem: ‘#demo’
 ,toolbar: ‘#toolbarDemo’
 ,defaultToolbar:[]
 ,limit:100
 ,cols: [[ //表头
    {field: ‘materialcode’, title: ‘物料编号’,templet: ‘#selectTool’}
   ,{field: ‘materialname’, title: ‘物料名称’,edit: ‘text’}
   ,{field: ‘number’, title: ‘数量’,edit: ‘text’}
   ,{field: ‘specifications’, title: ‘规格’,edit: ‘text’}
   ,{field: ‘warehouseid’, title: ‘仓库’,edit: ‘text’}
   ,{field: ‘warningnumber’, title: ‘最低库存’,edit: ‘text’}
   ,{field: ‘topwarning’, title: ‘最高库存’,edit: ‘text’}
   ,{field: ‘unitprice’, title: ‘单价’,edit: ‘text’}
   ,{field: ‘subtotal’, title: ‘小计’}
   ,{title: ‘操作’,align:’center’, toolbar: ‘#bar’}
 ]]
,data:[{  “materialcode”: “”
      ,”materialname”: “”
      ,”number”: “”
      ,”specifications”: “”
      ,”warehouseid”: “”
      ,”warningnumber”: “”
      ,”topwarning”: “”
      ,”unitprice”: “”
      ,”subtotal”: “”
    }]
,done: function(res, curr, count){
    //如果是异步请求数据方式,res即为你接口返回的信息。
    //如果是直接赋值的方式,res即为:{data: [], count: 99} data为当前页数据、count为数据总长度
    tabledata = res.data;
    //去掉下拉框的失焦事件 否则在下拉框里输入值 失焦后变回下拉选项里的值了 没有需要的同学忽略掉即可
    $(‘.layui-form-select’).find(‘input’).unbind(“blur”);
    //这里是表格重载的时候 回显下拉框的数据
    $(‘tr’).each(function(e){
    var $cr=$(this);
    var dataindex = $cr.attr(“data-index”);
        $.each(tabledata,function(index,value){
        if(value.LAY_TABLE_INDEX==dataindex){
        $cr.find(‘input’).val(value.materialcode);
        }
       });
      });
        //这里是下拉框输入值改变时触发 给表格缓存赋值 没有需要的同学忽略掉即可
        $(‘.layui-form-select’).find(‘input’).on(“change”,function(e){
        var $cr=$(e.target);
        console.log($cr);
        var dataindex = $cr.parents(‘tr’).attr(“data-index”);
        var selectdata = $cr.val();
            $.each(tabledata,function(index,value){
            if(value.LAY_TABLE_INDEX==dataindex){
            value.materialcode = selectdata;
            }
           });
        });
        //这里是数量的输入事件 计算小计用的 小计 = 数量 X 单价
        var numberelem = $(‘.layui-table-main’).find(“td[data-field=’number’]”);
        var unitpriceelem = $(‘.layui-table-main’).find(“td[data-field=’unitprice’]”);
        numberelem.on(“input”,function(e){
        var $cr=$(e.target);
        var dataindex = $cr.parents(‘tr’).attr(“data-index”);
        var unitprice = $cr.parents(‘tr’).find(“td[data-field=’unitprice’]”).children().html();
        var sub = unitprice*e.target.value;
        $cr.parents(‘tr’).find(“td[data-field=’subtotal’]”).children().html(sub);
        $.each(tabledata,function(index,value){
            if(value.LAY_TABLE_INDEX==dataindex){
            value.subtotal = sub;
            }
           });
        });
        //这里是单价的输入事件 计算小计用的 小计 = 数量 X 单价
        unitpriceelem.on(“input”,function(e){
        var $cr=$(e.target);
        var dataindex = $cr.parents(‘tr’).attr(“data-index”);
        var number = $cr.parents(‘tr’).find(“td[data-field=’number’]”).children().html();
        var sub = number*e.target.value;
        $cr.parents(‘tr’).find(“td[data-field=’subtotal’]”).children().html(sub);
        $.each(tabledata,function(index,value){
            if(value.LAY_TABLE_INDEX==dataindex){
            value.subtotal = sub;
            }
           });
        });
  }
});
var tabledata;
//监听工具条删除按钮
   table.on(‘tool(tableFilter)’, function(obj){
    if(obj.event === ‘del’){
    obj.del();
      };
     }
  );
 //头工具栏添加按钮事件
  table.on(‘toolbar(tableFilter)’, function(obj){
if(obj.event === ‘add’){
tabledata.push({
  “materialcode”: “”
        ,”materialname”: “”
        ,”number”: “”
        ,”specifications”: “”
        ,”warehouseid”: “”
        ,”warningnumber”: “”
    ,”topwarning”: “”
        ,”unitprice”: “”
        ,”subtotal”: “”
          })
      table.reload(‘demo’, {
    data: tabledata
  });
    };
  });
 //提交数据到后台保存
  form.on(‘submit(*)’, function(data){
// console.log(data.elem) //被执行事件的元素DOM对象,一般为button对象
// console.log(data.form) //被执行提交的form对象,一般在存在form标签时才会返回
//  console.log(data.field) //当前容器的全部表单字段,名值对形式:{name: value}
//  console.log(tabledata) //当前容器的全部表单字段,名值对形式:{name: value}
  $.ajax({
url:”${contextPath}/storage/toSave”,
async:true,
type:”post”,
data:$(data.form).serialize()+’&tabledata=’+JSON.stringify(tabledata),
success:function(data){
if(typeof(data) == ‘string’){
data = JSON.parse(data)
}
if(data.code == 0){
layer.msg(data.msg);
window.location.href=”${contextPath}/storage/toList”;
}else{
layer.msg(data.msg);
}
}
      });
  return false; //阻止表单跳转。如果需要表单跳转,去掉这段即可。
});
});
</script>
</body>
</html>
————————————————
                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_26814945/article/details/83275765

.NET 权限工作流框架 TOP 榜 - 小码编匠 - 博客园

mikel阅读(405)

来源: .NET 权限工作流框架 TOP 榜 – 小码编匠 – 博客园

前言

.NET权限管理及快速开发框架、最好用的权限工作流系统。

基于经典领域驱动设计的权限管理及快速开发框架,源于Martin Fowler企业级应用开发思想及最新技术组合(SQLSugar、EF、Quartz、AutoFac、WebAPI、Swagger、Mock、NUnit、Vue2/3、Element-ui/plus、IdentityServer等)。已成功在docker/jenkins中实施。

核心模块包括:组织机构、角色用户、权限授权、表单设计、工作流等。

它的架构精良易于扩展,是中小企业的首选。

版本说明

1、主分支main运行环境默认为.NET SDK 6.0,支持.NET未来版本,需要.NET SDK 4.0/4.5开发环境的同学请查看本项目4.0分支,已停止维护。

2、目前OpenAuth.Net以全部开源的方式向大众开放,对于有经验的开发者,官方文档足以满足日常开发。为了能让项目走的更远,特推出基于vue2 + element-ui /vue3 + element-plus的单页面应用程序,即企业版/高级版OpenAuth.Pro

开源地址:http://demo.openauth.net.cn:1802

3、该版本是一套后端基于OpenAuth.WebAPI接口,前端基于vue-element-admin,采用VUE全家桶(VUE+VUEX+VUE-ROUTER)单页面SPA开发的管理后台。

预览地址:http://demo.openauth.net.cn:1803

另外 企业版包含一套基于有赞Vant+Vue3的移动端界面。

预览地址:http://demo.openauth.net.cn:1804

核心看点

  • 同时支持EntityFramework、SQLSugar两款最流行的ORM框架
  • 符合国情的RBAC权限体系。超强的自定义权限控制功能,可灵活配置用户、角色可访问的数据权限。
  • 完整的字段权限控制,可以控制字段可见及API是否返回字段值
  • 可拖拽的表单设计。详情:可拖拽表单
  • 可视化流程设计
  • 全网最好用的打印解决方案。详情:智能打印
  • 基于Quartz.Net的定时任务控制,可随时启/停,可视化配置Cron表达式功能
  • 基于CodeSmith的代码生成功能,可快速生成带有头/明细结构的页面
  • 支持SQLServer、mySQL、Oracle、PostgreSql数据库,理论上支持所有数据库
  • 集成IdentityServer4,实现基于OAuth2的登录体系
  • 建立三方对接规范,已有系统可以无缝对接流程引擎
  • 前端采用 vue + layui + element-ui + ztree + gooflow + leipiformdesign
  • 后端采用 .NET Core +EF core+ autofac + quartz +IdentityServer4 + nunit + swagger
  • 设计工具 PowerDesigner + Enterprise Architect

项目截图

流程中心

表单设计

数据权限

仓储中心

项目经验

教科书级的分层思想,哪怕苛刻的你阅读的是大神级精典大作(如:《企业应用架构模式》《重构与模式》《ASP.NET设计模式》等),你也可以参考本项目。不信?有图为证,Resharper自动生成的项目引用关系,毫无PS痕迹!

官方地址

  • 网站:http://www.openauth.net.cn
  • 文档:http://doc.openauth.net.cn
  • 项目:https://gitee.com/dotnetchina/OpenAuth.Net

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

.NET 窗口/屏幕截图 - 唐宋元明清2188 - 博客园

mikel阅读(384)

来源: .NET 窗口/屏幕截图 – 唐宋元明清2188 – 博客园

图像采集源除了显示控件(上一篇《.NET 控件转图片》有介绍从界面控件转图片),更多的是窗口以及屏幕。

窗口截图最常用的方法是GDI,直接上Demo吧:

复制代码
 1         private void GdiCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var bitmap = CaptureScreen();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(bitmap);
 5         }
 6         /// <summary>
 7         /// 截图屏幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //获取窗口位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureByGdi(desktopWindow, 0d, 0d, lpRect.Width, lpRect.Height);
16         }
17         private BitmapSource ConvertBitmapToBitmapSource(Bitmap bitmap)
18         {
19             using MemoryStream memoryStream = new MemoryStream();
20             // 将 System.Drawing.Bitmap 保存到内存流中
21             bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
22             // 重置内存流的指针到开头
23             memoryStream.Seek(0, SeekOrigin.Begin);
24 
25             // 创建 BitmapImage 对象并从内存流中加载图像
26             BitmapImage bitmapImage = new BitmapImage();
27             bitmapImage.BeginInit();
28             bitmapImage.StreamSource = memoryStream;
29             bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
30             bitmapImage.EndInit();
31             // 确保内存流不会被回收
32             bitmapImage.Freeze();
33             return bitmapImage;
34         }
35         /// <summary>
36         /// 截图窗口/屏幕
37         /// </summary>
38         /// <param name="windowIntPtr">窗口句柄(窗口或者桌面)</param>
39         /// <param name="left">水平坐标</param>
40         /// <param name="top">竖直坐标</param>
41         /// <param name="width">宽度</param>
42         /// <param name="height">高度</param>
43         /// <returns></returns>
44         private static Bitmap CaptureByGdi(IntPtr windowIntPtr, double left, double top, double width, double height)
45         {
46             IntPtr windowDc = GetWindowDC(windowIntPtr);
47             IntPtr compatibleDc = CreateCompatibleDC(windowDc);
48             IntPtr compatibleBitmap = CreateCompatibleBitmap(windowDc, (int)width, (int)height);
49             IntPtr bitmapObj = SelectObject(compatibleDc, compatibleBitmap);
50             BitBlt(compatibleDc, 0, 0, (int)width, (int)height, windowDc, (int)left, (int)top, CopyPixelOperation.SourceCopy);
51             Bitmap bitmap = System.Drawing.Image.FromHbitmap(compatibleBitmap);
52             //释放
53             SelectObject(compatibleDc, bitmapObj);
54             DeleteObject(compatibleBitmap);
55             DeleteDC(compatibleDc);
56             ReleaseDC(windowIntPtr, windowDc);
57             return bitmap;
58         }
复制代码

根据user32.dll下拿到的桌面信息-句柄获取桌面窗口的设备上下文,再以设备上下文分别创建内存设备上下文、设备位图句柄

复制代码
 1 BOOL BitBlt(
 2     HDC   hdcDest,  // 目标设备上下文
 3     int   nXDest,   // 目标起始x坐标
 4     int   nYDest,   // 目标起始y坐标
 5     int   nWidth,   // 宽度(像素)
 6     int   nHeight,  // 高度(像素)
 7     HDC   hdcSrc,   // 源设备上下文
 8     int   nXSrc,    // 源起始x坐标
 9     int   nYSrc,    // 源起始y坐标
10     DWORD dwRop    // 操作码(如CopyPixelOperation.SourceCopy)
11 );
复制代码

图像位块传输BitBlt是最关键的函数,Gdi提供用于在设备上下文之间进行位图块的传输,从原设备上下文复现位图到创建的设备上下文

另外,与Bitblt差不多的还有StretchBlt,StretchBlt也是复制图像,但可以同时对图像进行拉伸或者缩小,需要缩略图可以用这个方法

然后以设备位图句柄输出一个位图System.Drawing.Bitmap,使用到的User32、Gdi32函数:

 View Code

还有一种比较简单的方法Graphics.CopyFromScreen,看看调用DEMO:

复制代码
 1         private void GraphicsCaptureButton_OnClick(object sender, RoutedEventArgs e)
 2         {
 3             var image = CaptureScreen1();
 4             CaptureImage.Source = ConvertBitmapToBitmapSource(image);
 5         }
 6         /// <summary>
 7         /// 截图屏幕
 8         /// </summary>
 9         /// <returns></returns>
10         public static Bitmap CaptureScreen1()
11         {
12             IntPtr desktopWindow = GetDesktopWindow();
13             //获取窗口位置大小
14             GetWindowRect(desktopWindow, out var lpRect);
15             return CaptureScreenByGraphics(0, 0, lpRect.Width, lpRect.Height);
16         }
17         /// <summary>
18         /// 截图屏幕
19         /// </summary>
20         /// <param name="x">x坐标</param>
21         /// <param name="y">y坐标</param>
22         /// <param name="width">截取的宽度</param>
23         /// <param name="height">截取的高度</param>
24         /// <returns></returns>
25         public static Bitmap CaptureScreenByGraphics(int x, int y, int width, int height)
26         {
27             var bitmap = new Bitmap(width, height);
28             using var graphics = Graphics.FromImage(bitmap);
29             graphics.CopyFromScreen(x, y, 0, 0, new System.Drawing.Size(width, height), CopyPixelOperation.SourceCopy);
30             return bitmap;
31         }
复制代码

Graphics.CopyFromScreen调用简单了很多,与GDI有什么区别?

Graphics.CopyFromScreen内部也是通过GDI.BitBlt来完成屏幕捕获的,封装了下提供更高级别、易胜的API。

测试了下,第一种方法Gdi32性能比Graphics.CopyFromScreen性能略微好一点,冷启动时更明显点,试了2次耗时大概少个10多ms。

所以对于一般应用场景,使用 Graphics.CopyFromScreen 就足够了,但如果你需要更高的控制权和性能优化,建议使用 Gdi32.BitBlt

kybs00/CaptureImageDemo (github.com)

C# 网络编程:.NET 开发者的核心技能 - 小码编匠 - 博客园

mikel阅读(317)

来源: C# 网络编程:.NET 开发者的核心技能 – 小码编匠 – 博客园

前言

数字化时代,网络编程已成为软件开发中不可或缺的一环,尤其对于 .NET 开发者而言,掌握 C# 中的网络编程技巧是迈向更高层次的必经之路。无论是构建高性能的 Web 应用,还是实现复杂的分布式系统,网络编程都是支撑这一切的基石。

本篇主要为 .NET 开发者提供一份全面而精炼的 C# 网络编程入门,从基础知识到高级话题,逐一剖析,帮助你建立起扎实的网络编程功底,让你在网络世界的编码之旅中游刃有余。

一、HTTP 请求

HTTP(Hypertext Transfer Protocol)是互联网上应用最为广泛的一种网络协议,主要用于从万维网服务器传输超文本到本地浏览器的传输协议。

在C#中,处理HTTP请求有多种方式,从传统的System.Net命名空间到现代的HttpClient类,每种方法都有其适用场景。

1、使用 HttpClient 发送HTTP请求

HttpClient是C#中推荐用于发送HTTP请求的类,它提供了异步的API,可以更好地处理长时间运行的操作,避免阻塞UI线程。

以下是一个简单的GET请求示例:

复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class HttpClientExample
{
    public static async Task Main()
    {
        using var client = new HttpClient();
        var response = await client.GetAsync("https://api.example.com/data");
        
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

 2、使用 WebClient 发送HTTP请求

尽管WebClient类仍然存在于.NET Framework中,但在.NET Core和后续版本中,它已被标记为过时,推荐使用HttpClient

不过,对于简单的同步请求,WebClient仍然可以使用:

复制代码
using System;
using System.IO;
using System.Net;

class WebClientExample
{
    public static void Main()
    {
        using (var client = new WebClient())
        {
            try
            {
                string result = client.DownloadString("https://api.example.com/info");
                Console.WriteLine(result);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}
复制代码

3、使用 HttpRequestMessage 和 HttpMessageHandler

对于更复杂的HTTP请求,如需要自定义请求头或处理认证,可以使用HttpRequestMessageHttpMessageHandler

这种方式提供了更多的灵活性和控制:

复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

class HttpRequestMessageExample
{
    public static async Task Main()
    {
        using var client = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/info");
        request.Headers.Add("Authorization", "Bearer your-access-token");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

4、注意事项

  • 安全性和性能: 使用HttpClient时,确保在一个应用程序的生命周期内重用同一个实例,而不是每次请求都创建新的实例。
  • 错误处理: 总是对HTTP请求的结果进行检查,处理可能发生的异常和非成功的HTTP状态码。
  • 超时和取消: 使用HttpClient时,可以通过CancellationToken来控制请求的超时和取消。

通过掌握这些知识点,能够在C#中有效地处理各种HTTP请求,从简单的GET请求到复杂的POST请求,包括身份验证和错误处理。

二、WebSocket 通信

WebSocket是一种在单个TCP连接上进行全双工通信的协议,它提供了比传统HTTP请求/响应模型更低的延迟和更高的效率,非常适合实时数据流、聊天应用、在线游戏等场景。在C#中,无论是服务器端还是客户端,都可以使用WebSocket进行通信。

1、客户端使用 WebSocket

在C#中,你可以使用System.Net.WebSockets命名空间下的ClientWebSocket类来创建WebSocket客户端。下面是一个简单的示例,展示了如何连接到WebSocket服务器并发送和接收消息:

复制代码
using System;
using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// WebSocket客户端类,用于与WebSocket服务器建立连接和通信。
/// </summary>
public class WebSocketClient
{
    /// <summary>
    /// 客户端WebSocket实例。
    /// </summary>
    private readonly ClientWebSocket _webSocket = new ClientWebSocket();
    
    /// <summary>
    /// 用于取消操作的CancellationTokenSource。
    /// </summary>
    private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

    /// <summary>
    /// 连接到指定的WebSocket服务器。
    /// </summary>
    /// <param name="uri">WebSocket服务器的URI。</param>
    public async Task Connect(string uri)
    {
        // 使用提供的URI连接到WebSocket服务器
        await _webSocket.ConnectAsync(new Uri(uri), _cancellationTokenSource.Token);
    }

    /// <summary>
    /// 向WebSocket服务器发送消息。
    /// </summary>
    /// <param name="message">要发送的消息字符串。</param>
    public async Task SendMessage(string message)
    {
        // 将消息转换为UTF8编码的字节
        byte[] buffer = Encoding.UTF8.GetBytes(message);
        
        // 创建ArraySegment,封装要发送的字节缓冲区
        ArraySegment<byte> segment = new ArraySegment<byte>(buffer);
        
        // 发送消息到WebSocket服务器
        await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
    }

    /// <summary>
    /// 接收WebSocket服务器发送的消息。
    /// </summary>
    /// <param name="onMessageReceived">接收到消息时调用的回调函数。</param>
    public async Task ReceiveMessage(Action<string> onMessageReceived)
    {
        // 当WebSocket连接处于打开状态时,持续接收消息
        while (_webSocket.State == WebSocketState.Open)
        {
            var buffer = new byte[1024];
            
            // 接收来自WebSocket服务器的数据
            var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationTokenSource.Token);
            
            // 如果接收到的类型为关闭,则关闭连接
            if (result.MessageType == WebSocketMessageType.Close)
            {
                await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                break;
            }
            
            // 将接收到的字节转换为字符串,并通过回调函数处理
            var receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
            onMessageReceived(receivedMessage);
        }
    }

    /// <summary>
    /// 断开与WebSocket服务器的连接。
    /// </summary>
    public async Task Disconnect()
    {
        // 取消接收和发送操作
        _cancellationTokenSource.Cancel();
        
        // 关闭WebSocket连接
        await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
    }
}
复制代码

2、服务器端使用 WebSocket

在服务器端,可以使用ASP.NET Core中的Microsoft.AspNetCore.WebSockets来支持WebSocket。

下面是一个简单的WebSocket服务端点配置示例:

复制代码
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

public class Startup
{
    /// <summary>
    /// 配置服务容器。
    /// </summary>
    /// <param name="services">服务集合。</param>
    public void ConfigureServices(IServiceCollection services)
    {
        // 添加控制器服务
        services.AddControllers();
    }

    /// <summary>
    /// 配置应用管道。
    /// </summary>
    /// <param name="app">应用构建器。</param>
    /// <param name="env">主机环境。</param>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 在开发环境中启用异常页面
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // 启用路由
        app.UseRouting();

        // 启用WebSocket中间件
        app.UseWebSockets();

        // 配置端点处理器
        app.UseEndpoints(endpoints =>
        {
            // 映射默认的GET请求处理器
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });

            // 映射WebSocket请求处理器
            endpoints.Map("/ws", async context =>
            {
                // 检查当前请求是否为WebSocket请求
                if (context.WebSockets.IsWebSocketRequest)
                {
                    // 接受WebSocket连接
                    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();

                    // 持续监听WebSocket消息
                    while (true)
                    {
                        // 准备接收缓冲区
                        var buffer = new byte[1024 * 4];
                        
                        // 接收WebSocket消息
                        var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

                        // 如果收到的类型为关闭消息,则关闭连接
                        if (result.MessageType == WebSocketMessageType.Close)
                        {
                            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                            break;
                        }

                        // 解码接收到的消息
                        var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        Console.WriteLine($"Received: {message}");

                        // 回复消息给客户端
                        await webSocket.SendAsync(
                            new ArraySegment<byte>(Encoding.UTF8.GetBytes($"Echo: {message}")),
                            result.MessageType,
                            result.EndOfMessage,
                            CancellationToken.None);
                    }
                }
                else
                {
                    // 如果不是WebSocket请求,则返回400错误
                    context.Response.StatusCode = 400;
                }
            });
        });
    }
}
复制代码

在上面的服务器端代码中,首先启用了WebSocket中间件,然后映射了一个/ws端点来处理WebSocket连接。

当收到连接请求时,我们接受连接并进入循环,监听客户端发送的消息,然后简单地回传一个回显消息。

3、说明

WebSocket为C#开发者提供了强大的实时通信能力,无论是构建复杂的实时数据流应用还是简单的聊天室,WebSocket都是一个值得考虑的选择。通过掌握客户端和服务器端的实现细节,可以充分利用WebSocket的优势,创建高性能和低延迟的实时应用。

三、 Socket 编程

Socket编程是计算机网络通信中的基础概念,它提供了在不同计算机之间发送和接收数据的能力。

在C#中,Socket编程主要通过System.Net.Sockets命名空间下的Socket类来实现。Socket可以用于创建TCP/IP和UDP两种主要类型的网络连接,分别对应于流式套接字(Stream Sockets)和数据报套接字(Datagram Sockets)。

1、Socket 基础

Socket地址族:指定网络协议的类型,如AddressFamily.InterNetwork用于IPv4。

Socket类型:SocketType.Stream用于TCP,SocketType.Dgram用于UDP。

Socket协议:ProtocolType.TcpProtocolType.Udp,分别用于TCP和UDP。

2、TCP Socket 客户端

TCP Socket客户端通常用于建立持久的连接,并通过流的方式发送和接收数据。

以下是一个简单的TCP客户端示例:

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class TcpClientExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // 连接到服务器
                IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, 11000);
                socket.Connect(remoteEP);

                // 发送数据
                string message = "Hello Server!";
                byte[] data = Encoding.ASCII.GetBytes(message);
                socket.Send(data);

                // 接收服务器响应
                data = new byte[1024];
                int bytes = socket.Receive(data);
                Console.WriteLine("Received: {0}", Encoding.ASCII.GetString(data, 0, bytes));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

3、TCP Socket 服务器

TCP Socket服务器负责监听客户端的连接请求,并处理来自客户端的数据。

以下是一个简单的TCP服务器示例:

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class TcpServerExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // 绑定到本地端口
                IPAddress ipAddress = IPAddress.Any;
                IPEndPoint localEP = new IPEndPoint(ipAddress, 11000);
                listener.Bind(localEP);

                // 监听连接
                listener.Listen(10);

                // 接受客户端连接
                Console.WriteLine("Waiting for a connection...");
                Socket handler = listener.Accept();

                // 接收数据
                byte[] data = new byte[1024];
                int bytes = handler.Receive(data);
                Console.WriteLine("Text received: {0}", Encoding.ASCII.GetString(data, 0, bytes));

                // 发送响应
                string response = "Hello Client!";
                byte[] responseData = Encoding.ASCII.GetBytes(response);
                handler.Send(responseData);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

4、UDP Socket

UDP Socket用于无连接的、不可靠的网络通信,通常用于实时数据传输,如视频流或游戏。

以下是一个简单的UDP客户端和服务器示例:

UDP客户端

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class UdpClientExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (UdpClient client = new UdpClient())
            {
                // 发送数据
                string message = "Hello UDP Server!";
                byte[] data = Encoding.ASCII.GetBytes(message);
                IPEndPoint server = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000);
                client.Send(data, data.Length, server);

                // 接收服务器响应
                data = client.Receive(ref server);
                Console.WriteLine("Received: {0}", Encoding.ASCII.GetString(data));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

UDP服务器

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

public class UdpServerExample
{
    public static void Main()
    {
        try
        {
            // 创建一个新的Socket实例
            using (UdpClient listener = new UdpClient(11000))
            {
                // 接收数据
                IPEndPoint client = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = listener.Receive(ref client);
                Console.WriteLine("Text received: {0}", Encoding.ASCII.GetString(data));

                // 发送响应
                string response = "Hello UDP Client!";
                byte[] responseData = Encoding.ASCII.GetBytes(response);
                listener.Send(responseData, responseData.Length, client);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e.ToString());
        }
    }
}
复制代码

以上示例展示了如何使用C#中的Socket类来实现TCP和UDP的客户端与服务器通信。

在实际应用中,可能还需要处理并发连接、错误处理和资源管理等问题。

此外,对于TCP通信,考虑到性能和资源使用,通常建议使用异步编程模型。

四、C# 网络安全

C# 中进行网络编程时,网络安全是一个至关重要的方面,涉及数据传输的保密性、完整性和可用性。以下是一些关键的网络安全知识点,它们对于构建安全的网络应用程序至关重要:

1、SSL/TLS 加密

在C#中使用HttpClient时,可以通过HttpClientHandler类来配置SSL/TLS相关的选项,确保HTTPS请求的安全性。

下面是一个示例,演示了如何使用HttpClientHandler来配置SSL/TLS设置:

复制代码
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 创建 HttpClientHandler 实例
        var handler = new HttpClientHandler();

        // 配置 SSL/TLS 设置
        // 设置检查服务器证书的委托
        handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        // 设置是否自动重定向
        handler.AllowAutoRedirect = true;

        // 设置代理
        // handler.UseProxy = true;
        // handler.Proxy = new WebProxy("http://proxy.example.com:8080");

        // 创建 HttpClient 实例
        using var httpClient = new HttpClient(handler);

        // 设置请求头部
        httpClient.DefaultRequestHeaders.Accept.Clear();
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        // 发送 HTTPS 请求
        var response = await httpClient.GetAsync("https://api.example.com/data");

        // 检查响应状态
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Failed to retrieve data: {response.StatusCode}");
        }
    }
}
复制代码

解释

  • ServerCertificateCustomValidationCallback:此属性允许你指定一个委托,用来验证服务器的SSL证书。在这个示例中,我们使用了HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,它会接受任何证书,这在测试环境中可能有用,但强烈建议在生产环境中使用更严格的证书验证逻辑。
  • AllowAutoRedirect:此属性控制是否允许HttpClient自动处理重定向。默认情况下,它是开启的。
  • UseProxy 和 Proxy:如果需要通过代理服务器发送请求,可以配置这两个属性。
  • DefaultRequestHeaders:用于设置请求的默认头部,如Accept,以指定期望的响应格式。

注意事项:

  • 实际应用中,不建议使用DangerousAcceptAnyServerCertificateValidator,因为它绕过了正常的证书验证,可能使应用程序暴露于中间人攻击。在生产环境中,应该实现自己的证书验证逻辑,确保只接受有效和可信的证书。
  • 此外,如果应用程序需要处理特定的SSL/TLS协议版本或加密套件,也可以通过SslProtocols属性进一步定制HttpClientHandler的SSL/TLS设置。
  • 例如,可以将其设置为SslProtocols.Tls12SslProtocols.Tls13,以限制使用的协议版本。

2、密码安全存储

在C#中安全地存储密码是一个至关重要的实践,尤其是当涉及到用户账户和敏感信息时。为了保护密码不被泄露或破解,应避免以明文形式存储密码,而是采用加密或哈希的方式。

以下是一些推荐的实践:

  • 使用哈希函数

使用安全的哈希函数,如SHA-256或SHA-512,可以将密码转换为一个固定长度的摘要。但是,简单的哈希容易受到彩虹表攻击,因此需要加入盐值(salt)。

示例代码:

复制代码
using System;
using System.Security.Cryptography;
using System.Text;

public static class PasswordHasher
{
    public static string HashPassword(string password, byte[] salt)
    {
        using (var sha256 = SHA256.Create())
        {
            var passwordSalted = Encoding.UTF8.GetBytes(password + Encoding.UTF8.GetString(salt));
            var hash = sha256.ComputeHash(passwordSalted);
            return Convert.ToBase64String(hash);
        }
    }

    public static byte[] GenerateSalt()
    {
        using (var rng = new RNGCryptoServiceProvider())
        {
            var salt = new byte[32];
            rng.GetBytes(salt);
            return salt;
        }
    }
}

// 使用示例
byte[] salt = PasswordHasher.GenerateSalt();
string hashedPassword = PasswordHasher.HashPassword("password123", salt);
复制代码
  • 使用加盐哈希

在哈希密码之前,先将随机生成的盐值与密码结合。这可以防止彩虹表攻击和暴力破解。

  • 使用慢速哈希函数

使用像PBKDF2、bcrypt、scrypt或Argon2这样的慢速哈希函数,可以显著增加破解难度,因为它们设计时考虑了防止暴力破解。

示例代码:

复制代码
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public static class PasswordHasher
{
    public static string HashPasswordUsingBcrypt(string password)
    {
        using (var bcrypt = new Rfc2898DeriveBytes(password, 16, 10000)) // 16 bytes of salt, 10000 iterations
        {
            return Convert.ToBase64String(bcrypt.GetBytes(24)); // 24 bytes of hash
        }
    }
}

// 使用示例
string hashedPassword = PasswordHasher.HashPasswordUsingBcrypt("password123");
复制代码
  • 存储哈希和盐值

在数据库中,除了存储哈希后的密码,还应存储用于该密码的盐值,以便在验证时使用相同的盐值重新计算哈希。

  • 验证密码

在用户登录时,从数据库中检索哈希和盐值,使用相同的哈希函数和盐值对输入的密码进行哈希,然后与存储的哈希值进行比较。

示例代码:

public static bool VerifyPassword(string inputPassword, string storedHash, byte[] storedSalt)
{
    string hashOfInput = PasswordHasher.HashPassword(inputPassword, storedSalt);
    return hashOfInput == storedHash;
}
  • 不要存储密码重置问题的答案

密码重置问题的答案应该像密码一样被安全地处理,避免以明文形式存储。

ASP.NET Core提供了内置的密码哈希和验证方法,使用这些框架通常比手动实现更安全。总之,安全地存储密码涉及到使用强哈希算法、加盐、适当的迭代次数和存储机制。同时,保持对最新安全实践的关注,并定期更新代码以应对新的威胁。

3、防止SQL注入

使用参数化查询或ORM工具等,防止SQL注入攻击。

string query = "SELECT * FROM SystemUser WHERE Username = @username";
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@username", inputUsername);

4、防止跨站脚本攻击(XSS)

对用户输入进行合适的编码和验证,防止恶意脚本注入。

string userContent = "<script>alert('XSS');</script>";
string encodedContent = HttpUtility.HtmlEncode(userContent);

5、防止跨站请求伪造(CSRF)

ASP.NET MVC可以使用Anti-Forgery Token等机制来防止CSRF攻击。

@Html.AntiForgeryToken()

6、身份验证和授权

使用更高级的身份验证机制,如JWT(JSON Web Token),并在应用中实施合适的授权策略。

[Authorize]
public ActionResult SecureAction()
{
    // 安全操作
}

7、判断文件安全

在C#中,判断一个文件是否”安全”可以从多个角度考量,这通常涉及到文件的来源、内容、权限以及是否包含潜在的恶意代码等。

下面我会介绍几种可能的方法来检查文件的安全性:

  • 检查文件的来源

确保文件是从可信的源下载或获取的。在Web应用程序中,可以使用Content-Disposition响应头来检查文件是否作为附件提供,以及文件名是否符合预期。

  • 验证文件的类型和扩展名

通过检查文件的扩展名或MIME类型来确定文件类型是否符合预期,例如,如果期望图片文件,那么只接受.jpg.png等扩展名。

复制代码
private bool IsFileSafeByExtension(string filePath)
{
    string[] allowedExtensions = { ".jpg", ".png", ".gif" };
    string extension = Path.GetExtension(filePath).ToLower();
    return allowedExtensions.Contains(extension);
}
复制代码
  • 检查文件的内容

使用文件签名或魔法数字来验证文件的实际类型与声明的类型是否一致,防止扩展名欺骗。

复制代码
private bool IsFileSafeByContent(string filePath)
{
    byte[] magicNumbers = File.ReadAllBytes(filePath);
    if (magicNumbers.Length >= 2 && magicNumbers[0] == 0xFF && magicNumbers[1] == 0xD8) // JPEG
    {
        return true;
    }
    // Add checks for other formats...
    return false;
}
复制代码
  • 扫描病毒和恶意软件

使用反病毒软件或在线API来检查文件是否含有病毒或恶意软件,VirusTotal 提供了API来检查文件是否含有病毒,https://www.virustotal.com/ 具体示例如下

复制代码
using System;  
using System.Net.Http;  
using System.Threading.Tasks;  
using Newtonsoft.Json; // 需要安装Newtonsoft.Json NuGet包  
  
class Program  
{  
    static async Task Main(string[] args)  
    {  
        string apiKey = "API密钥";  
        string fileUrl = "文件ID";  
  
        string url = $"https://www.virustotal.com/vtapi/v3/files/{fileUrl}/report";  
        HttpClient client = new HttpClient();  
        client.DefaultRequestHeaders.Add("x-apikey", apiKey);  
  
        HttpResponseMessage response = await client.GetAsync(url);  
  
        if (response.IsSuccessStatusCode)  
        {  
            string responseBody = await response.Content.ReadAsStringAsync();  
            dynamic report = JsonConvert.DeserializeObject(responseBody);  
  
            if (report.positives > 0)  
            {  
                Console.WriteLine("文件含有病毒或恶意软件。");  
            }  
            else  
            {  
                Console.WriteLine("文件安全。");  
            }  
        }  
        else  
        {  
            Console.WriteLine("API请求失败。");  
        }  
    }  
}
复制代码
  • 检查文件权限

确保文件具有正确的权限,以防止未经授权的访问。

复制代码
private bool IsFileSafeByPermissions(string filePath)
{
    var fileInfo = new FileInfo(filePath);
    var security = fileInfo.GetAccessControl();
    // Check permissions here...
    return true; // Placeholder logic
}
复制代码
  • 文件大小检查

限制文件的大小,避免消耗过多的磁盘空间或内存。

private bool IsFileSafeBySize(string filePath, long maxSizeInBytes)
{
    var fileInfo = new FileInfo(filePath);
    return fileInfo.Length <= maxSizeInBytes;
}
  • 内容安全策略(CSP)

在Web应用中,使用CSP来限制加载的资源类型和来源,防止XSS等攻击。

  • 综合检查函数示例
复制代码
private bool IsFileSafe(string filePath)
{
    return IsFileSafeByExtension(filePath) &&
           IsFileSafeByContent(filePath) &&
           IsFileSafeFromVirus(filePath) &&
           IsFileSafeByPermissions(filePath) &&
           IsFileSafeBySize(filePath, 1024 * 1024); // Limit to 1MB
}
复制代码

请注意,上述代码片段仅作为示例,实际应用中可能需要调整和补充具体的实现细节,例如引入实际的病毒扫描库或API,以及更复杂的权限和内容检查逻辑。

安全检查是多层面的,需要结合具体的应用场景和需求进行综合考量。

8、安全的Cookie处理

Cookies是Web开发中用于存储用户信息的一种常用机制,它们可以在客户端浏览器中保存小量的数据,以便服务器可以跟踪用户的偏好设置、登录状态等信息。然而,如果Cookie处理不当,可能会引发严重的安全问题,如数据泄露、会话劫持(Session Hijacking)和跨站脚本攻击(XSS)。因此,确保Cookie的安全处理至关重要。

以下是处理Cookie时应当遵循的一些最佳实践:

  • 使用HTTPS:传输Cookie时,务必使用HTTPS加密连接。HTTPS可以防止中间人攻击(Man-in-the-Middle Attack),保护Cookie数据免受窃听。
  • 设置HttpOnly标志:将Cookie标记为HttpOnly可以阻止JavaScript脚本访问Cookie,从而降低跨站脚本攻击(XSS)的风险。
  • 设置Secure标志:当Cookie被标记为Secure时,它们只会在HTTPS连接下发送,确保数据在传输过程中的安全性。
  • 限制Cookie的有效路径和域:通过设置Cookie的Path和Domain属性,可以控制哪些页面可以访问特定的Cookie,减少攻击面。
  • 使用SameSite属性:SameSite属性可以控制Cookie是否随跨站点请求发送,减少跨站请求伪造(CSRF)攻击的可能性。可以选择Strict、Lax或None三种模式之一。
  • 设置合理的过期时间:为Cookie设定一个适当的过期时间,可以避免永久性Cookie带来的安全风险,同时也便于清理不再需要的用户信息。
  • 定期审查和更新Cookie策略:定期检查Cookie的使用情况,确保所有Cookie设置符合最新的安全标准和隐私法规。

通过遵循这些最佳实践,可以大大增强应用程序的安全性,保护用户数据免受恶意攻击。在Web开发中,安全的Cookie处理不仅是技术要求,也是对用户隐私和数据安全的责任体现。

复制代码
using System;
using System.Web;

public class CookieHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // 创建一个新的Cookie对象
        HttpCookie cookie = new HttpCookie("UserSession");

        // 设置Cookie值
        cookie.Value = "123456"; // 假设这是用户的唯一标识符

        // 设置Cookie的过期时间
        cookie.Expires = DateTime.Now.AddDays(1); // 设置Cookie在一天后过期

        // 设置HttpOnly属性以增加安全性
        cookie.HttpOnly = true;

        // 如果你的网站支持HTTPS,设置Secure属性
        if (context.Request.IsSecureConnection)
            cookie.Secure = true;

        // 添加Cookie到响应中
        context.Response.AppendCookie(cookie);
    }

    public bool IsReusable
    {
        get { return false; }
    }
}
复制代码

在.NET Core或.NET 6+中,使用不同的API来处理Cookie,例如Microsoft.AspNetCore.Http命名空间下的IResponseCookies接口。

五、总结

通过文章的全面介绍 C# 网络编程,相信对这一块内容有了了解和理解。从简单的 HTTP 请求到复杂的套接字通信,从异步编程模型到安全协议的应用,每一步都为我们构建现代网络应用奠定了坚实的基。在实际项目中,根据需求深入学习和实践这些知识点,将有助于提升.NET开发者在网络编程领域的能力。持续学习和实践是成为优秀 .NET 开发者的不二法门。

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

我们的前端开发逆天了!1 小时搞定了新网站,还跟我说 “不要钱” - 程序员鱼皮 - 博客园

mikel阅读(414)

来源: 我们的前端开发逆天了!1 小时搞定了新网站,还跟我说 “不要钱” – 程序员鱼皮 – 博客园

大家好,我是程序员鱼皮。前段时间我们上线了一个新软件 剪切助手 ,并且针对该项目做了一个官网:

很多同学表示官网很好看,还好奇是怎么做的,其实这个网站的背后还有个有趣的小故事。。。

鱼皮:我们要做个官网,能下载应用就行,一周时间怎么样?

我们的前端开发 – 多喝热水同学:一周?太小瞧我了吧,1 小时给你搞定!

鱼皮:唔嘈,你很勇哦?

本来以为他是开玩笑的,没想到,1 小时后,他真的给我看了网站效果,而且比预期的好太多了。我的评价是:逆天

他给我解释道:其实我用了一个新框架,基本不用自己写代码,而且还可以白票平台来免费部署网站~

鱼皮:不错不错,回头给我的读者们也分享一下!

于是,就有了下面这篇文章:

对于前端同学来说,用的最多的 Web 框架无非就是 React/Vue/Angular 这三大件了,那本文将带你了解一个新的 Web 框架 Astro,并手把手带你使用 Astro 搭建一个属于自己的站点,用过的都说真香!

关于技术栈的选择

假如现在有这样一个需求,公司需要你去做一个官网落地页,没什么别的要求,界面美观且能介绍公司就行,你会怎么选技术栈?

如果是以前,我可能会挑一个自己熟悉的语言去快速开发,但是现在你问我选什么技术栈,我可能会选择 Astro,为什么?且听我娓娓道来~

首先如果是自己花时间去开发的话,我们需要搭建网站的整体布局,如导航栏、logo、页脚等等,还需要考虑移动端的适配、网站 SEO 优化等等…

我不知道你们会不会觉得有点烦?反正我是有点烦了,且自己做出来的可能还没那么好看…

image

如果布局、适配、SEO 这些都配好了,只需要改改文字的话那该多好!

image

没错,依靠 Astro 强大的主题生态就可以帮助我们快速完成这些事情!像我们公司的产品 剪切助手(https://jianqiezhushou.com) 的官网就是用 Astro 搭建的,如下:

image

image

还是很好看的有木有,且移动端的响应式适配、SEO 通通都搞定,一举多得!

接下来我们就简单了解一下 Astro 这个框架,然后使用 Astro 快速生成一个自己的站点!

Astro 框架介绍

关于 Astro 的介绍,官方文档(https://docs.astro.build/zh-cn/concepts/why-astro)给出了很明确的定位:“最适合构建像博客、营销网站、电子商务网站这样的以内容驱动的网站的 Web 框架”

image

它默认就支持服务端渲染,且支持 React、Preact、Svelte、Vue、Solid、Lit、HTMX、Web 组件,这意味着你可以用任意框架的写法来编写 Astro,这一特性在 Astro 中被称为 “群岛”。

快速拥有一个 Astro 应用

这里我们不会从零到一的去介绍 Astro 的写法,感兴趣的同学可以简单从官网过一遍入门指南(https://docs.astro.build/zh-cn/getting-started),我们要做的就是依靠 Astro 强大的主题模板,实现只需要改改文字、写一写 Markdown 就能轻松搭建一个漂亮的博客网站!

1)选择主题模板

进入 Astro 官方模板网站(https://astro.build/themes),挑选一个自己心仪的模板,如下:

image

我选择的模板是 https://astro.build/themes/details/astro-boilerplate/ ,我们进入到这个模板的详情页,可以看到有两个按钮,如下:

image

第一个是源码,第二个是在线效果的演示。

我们点击 Get Started 获取项目的源代码。

2)查看模板的 README 文档

通过 README 文档我们可以了解到如下信息

  1. 怎么去启动这个项目?
  2. 怎么构建发布?

如下图:

image

那么接下来我们就按照 README 中所描述的步骤来操作,第一步我们先把项目拉取到本地,执行如下命令:

git clone --depth=1 https://github.com/ixartz/Astro-boilerplate

image

在编辑器中打开这个项目,并安装项目依赖,如下:

image

安装依赖完成后启动项目,项目启动后我们访问 http://localhost:4321 ,如下图:

image

ok,现在我们就得到了一个最原始的模板,和之前的预览效果是一致的,如下:

image

3)更换项目中的个人信息

现在我们要做的就是把里面的文字换成自己的信息,没有的信息我们可以删掉,这里可以通过查看 index.astro 文件来了解整个网站的大致布局,从而找到我们要调整的组件,如下:

image

如果你不知道怎么调整也可以用另一种更简单的方法,直接搜索内容关键词,来找到我们要修改的内容,如下:

image

下面是我调整后的效果,如下:

image

看起来也不赖,主打一个简约风格。

4)如何发文

这是一个博客站,所以还需要找到发文入口,我们找到 posts 文件夹,在此文件夹下编写 markdown 文件即可,配置按已有的格式修改,如下:

image

ok,接下来我们就尝试一下发一篇文章,在 posts 文件夹下新建一个 md 格式的文件,往里面写入一些内容,如下:

image

可以看到,我们编写的 markdown 已经被解析为了文章~

至此网站的搭建已经结束了,剩下的就是自己在上面添加内容。

现在网站我们有了,还需要让别人能够访问你的网站,一般到这一步我们需要服务器,域名等等,如果没有的话怎么办?

白票!!将白票贯彻到底!!!

image

我们可以白票的第三方服务有:

1)GitHub Pages

2)Netlify

3)Cloudflare

4)Vercel

等等…

这里我们就以 Netlify 为例,其他的大家感兴趣可以自行去了解。

部署

1)创建仓库

首先我们需要一个能够存放代码的地方,我们去 GitHub 创建一个代码仓库,并上传代码,如下:

image

2)将仓库关联到 Netlify

进入到 Netlify登录页(https://app.netlify.com/login),这里因为我们的代码放在了 GitHub,所以我们选择使用 GitHub 进行登录,如下:

image

选择导入已有的项目,如下:

image

从 GitHub 导入,如下:

image

找到我们博客所在的代码仓库,如下:

image

点击仓库我们会进入到部署配置页,如下:

image

一些关键的配置说明都列出来了,按照要求配置即可,没有特别说明的目前不需要关注,点击部署后等待几分钟即可完成部署,如下:

image

现在我们访问 https://codereshui.netlify.app 就能看到部署的网站了,且后续有新的内容变更的时候(比如发文),网站会自动构建并发布!

妥妥的一条龙服务!!!好了,这篇文章就肝到这里,大家也可以把自己的网站搞起来了~

mysql 使用show tables表存在,但是select时却提示表不存在!_show tables 能查到表 select 查不到表-CSDN博客

mikel阅读(645)

来源: mysql 使用show tables表存在,但是select时却提示表不存在!_show tables 能查到表 select 查不到表-CSDN博客

这个问题困扰了很久,情况是某张表被损坏。

直接使用select查询时报错:

ERROR 1146 (42S02): Last_Error: Error ‘Table ‘xxxxxx’ doesn’t exist’ Error ” ERROR 1146 (42S02): Table
咦!怎么表无缘无故就不见了???

使用 show tables; 命令却发现表是存在的,瞬间懵逼了。无论发生什么情况,肯定是有原因的,哈哈哈。

最终解决方案是,不需要停mySQL服务,直接通过mySQL的my.conf查看到datadir目录,进入目录后,找到指定出问题的表关联文件:

1、tableName.frm
2、tableName.ibd
直接删除,至此才把出问题的表完全删除掉。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/yangjiechao945/article/details/96458466

报错 General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83‘ for column ‘per_name‘ at row 1-CSDN博客

mikel阅读(244)

解决办法:将数据库此字段设置为 utf8mb4_general_ci 即可。

来源: 报错 General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83‘ for column ‘per_name‘ at row 1-CSDN博客

插入数据报错:
SQLSTATE[HY000]: General error: 1366 Incorrect string value: ‘\xF0\x9F\x8D\x83’ for column ‘per_name’ at row 1。

产生错误原因是,入库字段设置的字节无法满足要求。一般文字字节在1-3之间,但是有些生僻字,例如产生此报错的文字是四个字节就无法入库而报错。

解决办法:将数据库此字段设置为 utf8mb4_general_ci 即可。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/lwaimj/article/details/110536257

C# 开发技巧 轻松监控方法执行耗时 - 小码编匠 - 博客园

mikel阅读(359)

来源: C# 开发技巧 轻松监控方法执行耗时 – 小码编匠 – 博客园

MethodTimer.Fody 是一个功能强大的库,可以用于测量 .NET 应用程序中的方法的执行时间。允许你在不修改代码的情况下,自动地测量和记录方法的执行时间。

这个工具是基于.NET的 weaving 技术,通过修改IL(Intermediate Language,中间语言)代码来插入计时逻辑,从而在方法调用前后记录时间戳,进而计算出方法的执行时间。

它使用 Fody 插件框架可以无缝集成到项目中,所以向代码中添加性能测量功能变得非常容易。

使用方法

1、安装NuGet包

在Visual Studio中,打开NuGet包管理器,搜索并安装MethodTimer.Fody或者使用命令方式

PM> Install-Package Fody
PM> Install-Package MethodTimer.Fody

具体操作如下图所示:

2、使用 Time 特性

复制代码
using MethodTimer;

namespace DemoConsole
{
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            Console.WriteLine("测试方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法
        /// </summary>
        [Time]
        public void DoSomething()
        {
            Console.WriteLine("测试方法执行时间!!!");
        }
    }
}
复制代码

Fody是一个.NET的weaving框架,需要确保项目已经启用了Fody,并且在项目属性的”Fody”标签页中添加了MethodTimer模块。

3、执行效果

启动运行程序,可以在输出窗口查看方法的执行耗时,具体如下图所示:

4、其他说明

Time 特性不仅可以加在方法上还可以直接添加到 Class 上,具体如下代码所示:

复制代码
using MethodTimer;

namespace ConsoleApp3
{
    [Time]
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            new Program().ToDoSomething();

            Console.WriteLine("方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法1
        /// </summary>
      
        public void DoSomething()
        {
            Console.WriteLine("001——测试执行时间方法!!!");
           
        }
        /// <summary>
        /// 示例方法2
        /// </summary>

        public void ToDoSomething()
        {
            Console.WriteLine("002——测试执行时间方法!!!");

        }
    }
}
复制代码

运行程序后,可以输出类中每个方法的执行时间。

实际上,在代码中添加了 Time 特性以后,Fody 会自动生成下面的代码

复制代码
 public class MyClass
 {
        [Time]
        public void DoSomething()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            // 原始方法体
            System.Threading.Thread.Sleep(1000); // 模拟工作

            stopwatch.Stop();

            // 输出或记录执行时间
            Console.WriteLine($"执行时间:{stopwatch.Elapsed.TotalMilliseconds} ms");
        }
 }
复制代码

5、拦截记录

如果想手动处理日志记录,可以定义一个静态类来拦截日志记录,方法的示例,具体如下代码所示

复制代码
public static class MethodTimeLogger
{
    public static void Log(MethodBase methodBase, TimeSpan elapsed, string message)
    {
        //Do some logging here
    }
}
复制代码

生成后的代码

复制代码
public class MyClass
{
    public void MyMethod()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            Console.WriteLine("Hello");
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(MyClass.MyMethod), stopwatch.Elapsed);
        }
    }
}
复制代码

MethodTimer.Fody是一个非常有用的工具,尤其在性能调优阶段,可以帮助你快速识别出哪些方法是性能瓶颈,从而针对性地进行优化。

主要特点

1、非侵入式

MethodTimer.Fody不需要在源代码中添加额外的计时代码,只需要在项目中添加相应的NuGet包,并在项目属性中做一些配置,就可以自动地为方法添加计时功能。

2、灵活的配置

你可以选择性地对某些方法进行计时,或者排除不想被计时的方法。这通常通过方法的特性或者类的命名空间来进行配置。

3、输出结果多样化

MethodTimer.Fody可以将计时结果输出到不同的地方,如控制台、日志文件或者通过事件追踪(ETW)等方式,这取决于你的配置。

4、性能影响小

尽管MethodTimer.Fody在方法中插入了计时逻辑,但它被设计得尽可能地对性能影响最小,通过精心优化的IL代码插入策略来实现这一点。

总结

MethodTimer.Fody 是一个强大的工具,提供了简便的方式来监控 C# 方法的执行时间,特别适用于需要快速诊断性能问题的场合。

通过其灵活的配置和非侵入性的特性,它可以无缝地融入现有的开发流程中,帮助我们团队提高应用的性能和响应速度。

这个工具特别适合在开发和测试阶段快速识别性能瓶颈,而无需在代码中显式地添加计时代码,可以保持源代码的整齐性和可维护性。

开源地址

https://github.com/Fody/MethodTimer

 

如果觉得这篇文章对你有用,欢迎加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行交流心得,共同成长。

 

【弯曲矫正】智能文字识别技术-弯曲矫正概述 - 合合技术团队 - 博客园

mikel阅读(488)

来源: 【弯曲矫正】智能文字识别技术-弯曲矫正概述 – 合合技术团队 – 博客园

一、背景

电子文档由于更容易存档、编辑、签名和共享,越来越多的文档需电子化,随着高质量摄像头在手机等移动设备上的普及,利用移动设备对文档进行数字化采集已经非常普遍。通过图像校正与图像质量提升,移动设备采集的文档图像质量甚至可以与专用的文档扫描仪相当。然而,文档总是由于纸张几何形状和捕获条件不受控制而形变。这阻碍了形变图像的信息提取,降低可读性,对数据增强和下游任务如OCR识别、版面分析与还原等任务增加难度。

二、方法概述

为解决文档弯曲矫正问题,学术界已有多种方案。

一类是利用多目相机,结构光或者激光雷达等设备对文档进行扫描,获得文档表面的3D结构信息,进而对文档校正展平。这类方法一般可以得到比较好的校正效果,但依赖专用设备的特点限制了其使用场景。

还有一类是利用显式的几何模型以适应形变文档曲面,这类方法完全依靠图像信息以及文档形变的先验知识对图像进行校正。这类方法一般需要进行文字行或者表格线的检测,并假设曲面符合特定的几何约束,如曲面是柱面。这类方法可以在普通的移动设备上实现,但是其校正效果受文字行检测准确度的限制,对文档版式比较敏感,无法处理存在大量图表的文档,且误检的文字行有可能会对校正造成严重干扰。

还有一类基于优化的方法,利用损失函数缓慢迭代优化以获得形变矫正结果,但时间较长不适合实时应用。

最近,数据驱动的方法已经流行起来。 这些方法训练一个形变矫正神经网络,学习形变场,从而得到类似扫描的结果。 这样的网络可以实现实时矫正。Das等人使用 CNN 检测文档的折痕并进行分割文件分成多个块进行矫正。 Xing等人 应用CNN估计文档变形和相机姿态以进行校正。 Ramanna等人通过利用 pix2pixhd 网络去除文档的卷曲和几何失真。 然而,这些方法仅适用于简单变形和单调背景。

Ma等人提出了一个堆叠的 U-Net,它经过训练端到端预测翘曲的前向映射。 由于生成的数据集与真实世界的图像有很大不同,[15] 对其进行了训练在真实世界的图像上测试时,数据集的泛化能力较差。Das等人认为当合成训练数据集仅使用 2D 变形进行训练时,弯曲矫正模型并不总是表现良好,因此他们创建了一个 Doc3D 数据集,该数据集具有多种类型的像素级文档图像偏移场,同时使用真实世界文档和渲染软件。

同时,提出了一种去扭曲网络和细化网络来校正文档图像的几何和阴影。李等人 在 3D 空间中生成训练数据集,并使用渲染引擎获得更精细、更逼真的失真文档图像细节。他们提出了基于图像块(patch)的学习方法,并通过在梯度域中的处理将patch结果拼接到校正后的文档中,以及用于去除阴影的光照校正网络。与之前的方法相比,这些文献更关心生成的训练数据集和真实世界测试数据集之间的差异,并专注于生成更真实的训练数据集以提高真实世界图像的泛化能力。尽管这些结果令人惊叹,但深度神经网络的学习和表达能力并未得到充分探索。

三、合合方案

我们将弯曲矫正问题定义如下:

其中u 是形变场S(Source)是弯曲图T(Target)是平整图。一个理想的空间变换(spatial transformation)需要有两个衡量标准,及相似度和正则项,一方面我们期望弯曲样本变换后与目标(平整样本)越相似越好,图像相似性有很多种标准,常见的有相关系数(Correlation Coefficient, CC)、归一化的相关系数(NCC)、互信息(Mutual Information, MI)均方误差(MSE)等。

另一方面,我们也希望这个变换是空间上平滑且连续的,这样能保证变换遵循物理模型,存在连续可逆的变换,使得我们的变换在数据合成等方面有更广泛的应用。

和相似度损失函数类似,正则项在网络里也有多种实现方式,一种是通过对位移场直接进行空间梯度惩罚,一种则是通过对速度场进行约束后再通过积分层得到最终形变场,还有一种则是在训练过程中通过循环损失函数来实现。

形变矫正网络可以是encoder-decoder类似结构,由于惩罚项如果直接施加在位移场上,大位移场景模型的矫正能力就会降低,有方案通过多次迭代矫正过程位移场来实现大形变。

我们则参考配准中的流模型(fluid model),用速度场来建模形变场,并通过积分层来实现最终的形变场。事实上,位移场也可以被视作是轨迹固定的流场(直线)。 对于不同的正则项, 在大部分情况下,直线轨迹并不是最优解。直线轨迹得到的正则项的值很多情况下会更大点。 作为对比,引入速度场在这种情形下实现了更多的自由度。

如果你对这一块感到困惑,可以想象连接世界地图上两个地方的最短路径, 大部分情况下都不是直线 [Ref]。速度场求解可转换为如下问题,其中L是对速度场施加的正则项。

 

空间变换网络一开始提出时只是简单用作仿射变换等,后来采用了采样网格的方式使得它功能更加强大。对于大小为[W, H]的二维图像来说,其位移场大小为[W, H, 2]。位移场表示每个像素在各个方向(x,y轴)的位移。空间变换网络会根据位移场生成一个归一化后的采样网格,然后用该网络对图像进行采样,就得到了矫正后的图像。

 


 

  1. Shaodi You, et al. 2017. Multiview Rectification of Folded Documents. IEEE Transactions on Pattern Analysis and Machine Intelligence.
  2. Taeho Kil, et al. 2017. Robust Document Image Dewarping Method Using Text-Lines and Line Segments. In Proceedings of the International Conference on Document Analysis and Recognition. IEEE, 865;870.
  3. Beom Su Kim, et al. 2015. Document Dewarping via Text-Line Based Optimization. Pattern Recognition 48, 11 (2015), 3600–3614.
  4. Sagnik Das, et al. 2019. DewarpNet:Single-image Document Unwarping with Stacked 3D and 2D Regression Networks. In Proceedings of the International Conference on Computer Vision.
  5. Hao Feng, et al. 2021. DocTr:Document Image Transformer for Geometric Unwarping and Illumination Correction. In Proceedings of the ACM International Conference on Multimedia.
  6. Guo-Wang Xie, Fei Yin, Xu-Yao Zhang, and Cheng-Lin Liu. 2020. Dewarping Document Image by Displacement Flow Estimation with Fully Convolutional Network. In Document Analysis Systems. Springer, 131–144.
  7. Gaofeng Meng, et al. 2015. Extraction of Virtual Baselines from Distorted Document Images Using Curvilinear Projection. In Proceedings of the International Conference on Computer Vision.
  8. Vincent Arsigny, et al. 2005. A log-Euclidean framework for statistics on diffeomorphisms. In International Conference on Medical Image Computing and Computer-Assisted Intervention, pages 924–931. Springer.
  9. John Ashburner. 2007. A fast diffeomorphic image registration algorithm. Neuroimage, 38(1):95–113.
  10. Beg, M.F., et al. 2005. Computing large deformation metric mappings via geodesic flows of diffeomorphisms. Journal of Computer Vision, 139–157.
  11. Brian Avants et al. 2004. Geodesic estimation for large deformation anatomical shape averaging and interpolation. Neuroimage, 23:S139–S150.
  12. Adrian V Dalca, et al. 2019. Unsupervised learning of probabilistic diffeomorphic registration for images and surfaces. Medical image analysis, 57:226–236.
  13. Zhengyang Shen, et al. 2019. Networks for joint affine and non-parametric image registration. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pages 4224–4233.
  14. fluid(流) 方法图像配准简介 – 知乎
  15. GitHub – uncbiag/registration: Image Registration