admin 发布的文章

在.Net Core下,没有可以支持跨平台的Drawing类库,官网提供的Common.Drawing只能在Windows下使用,那么在.Net Core下该如何处理图片呢?其实有很多第三方提供了解决方案,而我比较喜欢用的是Mono团队提供的SkiaSharp,原因是稳定而且支持的也很好,性能上也还好。

一、SkiaSharp是什么?

1.Skia介绍

Skia是Google旗下的2D图形处理库,下面是援引百科中的词条:

skia是个2D向量图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现。不仅用于Google Chrome浏览器,新兴的Android开放手机平台也采用skia作为绘图处理,搭配OpenGL/ES与特定的硬件特征,强化显示的效果。

Skia官网中是这样介绍的:

Skia is an open source 2D graphics library which provides common APIs that work across a variety of hardware and software platforms. It serves as the graphics engine for Google Chrome and Chrome OS, Android, Mozilla Firefox and Firefox OS, and many other products.

2.SkiaSharp介绍

SkiaSharp故名思义,就是在.net下使用Skia API的库,是SkiaSharp是由mono团队开发并进行持续维护,至今已经多年了。目前的最新版本是1.60.3,当前支持.net下的:

  • .NET Standard 1.3
  • .NET Core
  • Tizen
  • Xamarin.Android
  • Xamarin.iOS
  • Xamarin.tvOS
  • Xamarin.watchOS
  • Xamarin.Mac
  • Windows Classic Desktop (Windows.Forms / WPF)
  • Windows UWP (Desktop / Mobile / Xbox / HoloLens)

SkiaSharp项目:https://github.com/mono/SkiaSharp

二、SkiaSharp的安装

可以通过nuget命令进行安装:

    nuget install skiasharp

或者在要使用的项目下,打开nuget管理器,搜索skiasharp进行安装。

三、SkiaSharp的使用

1.生成缩略图

这里假设已经安装好SkiaSharp 1.60.3版本。
我们先把要缩略的原图加载到内存中:

using (var input = File.OpenRead($"{PlatformServices.Default.Application.ApplicationBasePath}wwwroot/{pic}"))

这里的变量pic是图片的相对路径。
之后实例化一个SKManagedStream

using (var inputStream = new SKManagedStream(input))

最后,把inputStream加载到SKBitmap画布中

    using (var original = SKBitmap.Decode(inputStream))

之后重新设置图片的尺寸,也就是完成缩略处理:

            using (var resized = original
               .Resize(new SKImageInfo(width, height), SKBitmapResizeMethod.Lanczos3))
            {
                if (resized == null) return "";
                using (var image = SKImage.FromBitmap(resized))
                {
                    using (var output =
                           File.OpenWrite($"{PlatformServices.Default.Application.ApplicationBasePath}wwwroot/{thumb_name}"))
                    {
                        image.Encode(SKEncodedImageFormat.Png,quality)
                            .SaveTo(output);
                    }
                }
            }

其中,变量widthheight分别为缩略图的宽度和高度,thumb_name为缩略图要保存的文件名,quality是质量,一般设置为75,或者是其他的自己觉得合适的值。
完整的例子:

    using System.IO;
    using Microsoft.Extensions.PlatformAbstractions;
    using SkiaSharp;
    
            public static string MakeThumb(string pic,string thumb_dir, int width, int height)
            {
                const int quality = 75; //质量为75%
                using (var input = File.OpenRead($"{PlatformServices.Default.Application.ApplicationBasePath}wwwroot/{pic}"))
                using (var inputStream = new SKManagedStream(input))
                using (var original = SKBitmap.Decode(inputStream))
                {
                    string[] arr_pic = pic.Split('/');
                    string filename = arr_pic[arr_pic.Length - 1];  //完整文件名
                    string[] arr_filename = filename.Split('.');
                    string ext = "";
                    if (arr_filename.Length >= 2)
                    {
                        ext = arr_filename[arr_filename.Length - 1];    //最后一个为扩展名
                    }
                    string thumb_name = $"{filename.Remove(filename.Length - ext.Length - 1)}-{width}x{height}.png";  //文件名,缩略图保存为png
                    string save_dir = $"/attach/{thumb_dir}/thumb/{DateTime.Now.ToString("yyyy-MM-dd")}";
                    string savepath = $"{PlatformServices.Default.Application.ApplicationBasePath}wwwroot{save_dir}";
                    if (!Directory.Exists(savepath))
                    {
                        Directory.CreateDirectory(savepath);
                    }
                    string thumb_file = $"{save_dir}/{thumb_name}";
                    using (var resized = original
                       .Resize(new SKImageInfo(width, height), SKBitmapResizeMethod.Lanczos3))
                    {
                        if (resized == null) return "";
                        using (var image = SKImage.FromBitmap(resized))
                        {
                            using (var output =
                                   File.OpenWrite($"{savepath}/{thumb_name}"))
                            {
                                image.Encode(SKEncodedImageFormat.Png,quality)
                                    .SaveTo(output);
                            }
                        }
                    }
                    return thumb_file;
                }
            }

2.把指定的字体打印到图片上

其实图片的文字水印、图片验证码都可以从这个例子上扩充出来。
首先还是要安装SkiaSharp,之后,实例化SKImageInfo

    var info = new SKImageInfo(width, height);

创建一个新的SKSurface

    using (var surface = SKSurface.Create(info))

设置画布背景透明:

    var canvas = surface.Canvas;
    canvas.Clear(SKColors.White);

设置SKPaint的参数

           var paint = new SKPaint
            {
                Color = SKColors.Black,//颜色
                IsAntialias = true,//抗锯齿
                Style = SKPaintStyle.Fill,
                TextAlign = SKTextAlign.Center,//居中
                TextSize = 40F,//字号
                Typeface= SkiaSharp.SKTypeface.FromFile(fontpath, 0)//加载字体
            };

这里除了指定字体的路径之外,还可以使用SkiaSharp.SKTypeface.FromFamilyName("微软雅黑",SKTypefaceStyle.Bold)来通过字体名来设置要使用的字体;参数fontpath是字体的物理路径。

参数设置好之后,进行绘图:

    var coord = new SKPoint(info.Width / 2, (info.Height + paint.TextSize / 2);
    canvas.DrawText(text, coord, paint);

最后,生成图片:

    using (var image = surface.Snapshot())
    using (var data = image.Encode(SKEncodedImageFormat.Png, 100))

一个简单的例子:

    using SkiaSharp;
    using System.Linq;
            public static byte[] CreateImage(string fontpath, string text,float font_size=100)
            {
                var info = new SKImageInfo(1100, 480);
                using (var surface = SKSurface.Create(info))
                {
                    var canvas = surface.Canvas;
                    canvas.Clear(SKColors.White);
                    
                    var paint = new SKPaint
                    {
                        Color = SKColors.Black,
                        IsAntialias = true,
                        Style = SKPaintStyle.Fill,
                        TextAlign = SKTextAlign.Center,
                        TextSize = font_size,
                        Typeface= SkiaSharp.SKTypeface.FromFile(fontpath, 0)
                    };
                    var coord = new SKPoint(info.Width / 2, (info.Height + paint.TextSize) / 2);
                    canvas.DrawText(text, coord, paint);                
                    using (var image = surface.Snapshot())
                    using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
                    {
                        return data.ToArray();
                    }
                }
            }

这个是指定的文字内容使用指定的字体直接显示到空白图片上,但是不支持文字换行。我们下面的例子是对上面的进行改进,支持文字换行:

    public static byte[] CreateImage(string fontpath, string text,float font_size=100)
    {
        //支持文字多行
        List<string> list = text.Split('\n').ToList();
        list.RemoveAll(x => { return string.IsNullOrEmpty(x.Trim()); });    //删除空行
        list.Reverse(); //顺序反转
        float line_height = 1.5F;   //行距
        float height = 480;
        if (list.Count * line_height*font_size >= height)
        {
            height = list.Count * line_height * font_size;
        }
        var info = new SKImageInfo(1100, (int)height);
        using (var surface = SKSurface.Create(info))
        {
            var canvas = surface.Canvas;
            canvas.Clear(SKColors.White);
            
            var paint = new SKPaint
            {
                Color = SKColors.Black,
                IsAntialias = true,
                Style = SKPaintStyle.Fill,
                TextAlign = SKTextAlign.Center,
                TextSize = font_size,
                Typeface= SkiaSharp.SKTypeface.FromFile(fontpath, 0)
            };

            int i = 0;
            list.ForEach(x =>
            {
                var coord = new SKPoint(info.Width / 2, (info.Height + paint.TextSize * (list.Count - i) - paint.TextSize * i * 1.5F) / 2);
                canvas.DrawText(x.Trim(), coord, paint);
                i++;
                
            });

            using (var image = surface.Snapshot())
            using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
            {
                return data.ToArray();
            }
        }
    }

测试地址:http://tool.dwz.nz/fonts
源码:https://github.com/hongbai/FontsPrint

四、注意事项

如果要在Linux上使用,还需要同时上传libSkiaSharp.so文件,放到与SkiaSharp.dll同一文件夹下。libSkiaSharp.so文件可以在SkiaSharp的github上下载最新的发行版本,下载地址:https://github.com/mono/SkiaSharp/releases

五、总结

通过以上两个例子,我们可以发现,SkiaSharp的使用方法非常简单方便,而且各方面支持的都很不错,支持跨平台。功能上我暂时只在以上两个例子中使用,如果以后在其他方面用到的话,我会继续更新。代码写的丑,多包涵。

以上。

参考:
Skia Graphics Library
Skia 百度百科
SkiaSharp Github项目
[gitwidget type='github' url='mono/SkiaSharp']

简要

iTextSharp的功能很强大,之前我也写过使用pdf模板来生成PDF的文章(《使用iText5来处理PDF》),当然这里我还是使用iTextSharp5这个版本,原因是这个版本还是开源免费的。

前面不多说,还是通过nuget来安装iTextSharp5,这里还需要安装itextsharp.xmlworker,之后开始做html模板,及对应的css。这里需要注意的是,div等可以有宽度的标签,css中不能有width/height这种标签,但是可以用max-width,另外如果有图片的话,需要转成base64代码,文件的编码格式需要使用纯净的utf-8编码保存,不能使用其他编码及带BOM的utf-8,css需要是单独的一个文件,或者是在html标签中使用内联的方式。我这里使用的是独立的css文件,另外我的例子里是保存pdf到服务器上,如果生成之后需要直接让用户下载,那么只需要小改动就可以,我这里就不做改动了。

代码

using iTextSharp.text;
using System.IO;
using System.Text;
using iTextSharp.text.pdf;
using iTextSharp.tool.xml;
using iTextSharp.tool.xml.html;
using iTextSharp.tool.xml.pipeline.html;
using iTextSharp.tool.xml.pipeline.css;
using iTextSharp.tool.xml.parser;
using iTextSharp.tool.xml.pipeline.end;

        public static void Create()

        {

            string css = System.Web.HttpContext.Current.Request.MapPath("/html/stype.css"); //css物理路径
            string html = FileObj.ReadFile(System.Web.HttpContext.Current.Request.MapPath("/html/template.html"), Encoding.UTF8); //读取html模板内容
            string pdf = System.Web.HttpContext.Current.Request.MapPath("/pdf/file.pdf"); //设置生成pdf的保存路径

            using (var ms = new FileStream(pdf,FileMode.Create))
            {
                using (var document = new Document(iTextSharp.text.PageSize.A4)) //设置页面大小
                {
                    using (PdfWriter writer = PdfWriter.GetInstance(document, ms))
                    {
                        document.Open();
                        using (var strReader = new StringReader(html))
                        {
                            XMLWorkerFontProvider fontProvider = new XMLWorkerFontProvider(XMLWorkerFontProvider.DONTLOOKFORFONTS);
//    注册指定的字体,第二个参数是给字体命名,在css中需要指定字体                        //fontProvider.Register(System.Web.HttpContext.Current.Request.MapPath("/fonts/simhei.ttf"), "simhei");
                            fontProvider.RegisterDirectory(@"C:\Windows\Fonts"); //直接注册全部的系统字体
                            CssAppliers cssAppliers = new CssAppliersImpl(fontProvider);

                            //设置factories
                            HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers);
                            htmlContext.SetTagFactory(Tags.GetHtmlTagProcessorFactory());
                            //设置css
                            ICSSResolver cssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(false);
                            cssResolver.AddCssFile(css,true);

                            //生成导出
                            IPipeline pipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlContext, new PdfWriterPipeline(document, writer)));
                            var worker = new XMLWorker(pipeline, true);                            
                            var xmlParse = new XMLParser(true, worker);
                            xmlParse.Parse(strReader);
                            xmlParse.Flush();
                        }
                        document.Close();
                    }
                }
            }

        }

其他说明

1.中文

如果要支持中文,在css中一定要设置支持中文的字体,比如内容的最外面是body标签,可以直接设置
body{font-family:'simhei';}
类似这样的,或者是对应的标签指定要使用的字体。

2.分页

如果对自动分页的位置不满意,可以通过css来控制分页。
创建分页:style="page-break-after: always;"
避免分页:style="page-break-inside: avoid;"