图片验证码

页面显示图片的两种方式

下面的2个img标签都可以返回图片。

<img src="/static/imgs/test.gif" />
<img src="/getimg/" />

第一个直接就能获取静态文件,第二个现在并不能返回图片,还有配合个处理函数:

# urls.py 文件,写一个url的对应关系
    path('getImg/'.lower(), views.get_img),

# views.py 文件,对应的处理函数
def get_img(request):
    data = open('static/imgs/test.gif', 'rb').read()
    return HttpResponse(data)

上面的处理函数,这里看到了HttpResponse的另外一个用法,之前都是返回的字符串。这里返回的是二进制。
第一个方法必须要有一个文件路径,img标签直接去获取这个文件。而第二个方法只需要返回文件的内容就可以了,甚至可以没有这个文件。

生成验证码图片

这里需要依赖2个东西:Pillow模块,字体文件(.ttf文件)
安装Pillow模块:

pip install Pillow

字体文件:windows系统的 C:\Windows\Fonts 目录下有很多字体文件,可以复制一个合适的拿来用。
整个功能就是一个组件,这里在项目目录下创建一个utils文件,统一来存放用的的组件或者工具。把下面的生成验证码的函数以及用到的字体文件都放到utiles文件夹下。
下面是现成的可以生成验证码图片的函数:

import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter

_letter_cases = "abcdefghjkmnpqrstuvwxy"  # 小写字母,去除可能干扰的i,l,o,z
_upper_cases = _letter_cases.upper()  # 大写字母
_numbers = ''.join(map(str, range(3, 10)))  # 数字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))

def create_validate_code(size=(120, 30),
                         chars=init_chars,
                         img_type="GIF",
                         mode="RGB",
                         bg_color=(255, 255, 255),
                         fg_color=(0, 0, 255),
                         font_size=18,
                         font_type="utils/Monaco.ttf",
                         length=4,
                         draw_lines=True,
                         n_line=(1, 2),
                         draw_points=True,
                         point_chance=2):
    """
    @todo: 生成验证码图片
    @param size: 图片的大小,格式(宽,高),默认为(120, 30)
    @param chars: 允许的字符集合,格式字符串
    @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG
    @param mode: 图片模式,默认为RGB
    @param bg_color: 背景颜色,默认为白色
    @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF
    @param font_size: 验证码字体大小
    @param font_type: 验证码字体,默认为 ae_AlArabiya.ttf
    @param length: 验证码字符个数
    @param draw_lines: 是否划干扰线
    @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效
    @param draw_points: 是否画干扰点
    @param point_chance: 干扰点出现的概率,大小范围[0, 100]
    @return: [0]: PIL Image实例
    @return: [1]: 验证码图片中的字符串
    """

    width, height = size  # 宽高
    # 创建图形
    img = Image.new(mode, size, bg_color)
    draw = ImageDraw.Draw(img)  # 创建画笔

    def get_chars():
        """生成给定长度的字符串,返回列表格式"""
        return random.sample(chars, length)

    def create_lines():
        """绘制干扰线"""
        line_num = random.randint(*n_line)  # 干扰线条数

        for i in range(line_num):
            # 起始点
            begin = (random.randint(0, size[0]), random.randint(0, size[1]))
            # 结束点
            end = (random.randint(0, size[0]), random.randint(0, size[1]))
            draw.line([begin, end], fill=(0, 0, 0))

    def create_points():
        """绘制干扰点"""
        chance = min(100, max(0, int(point_chance)))  # 大小限制在[0, 100]

        for w in range(width):
            for h in range(height):
                tmp = random.randint(0, 100)
                if tmp > 100 - chance:
                    draw.point((w, h), fill=(0, 0, 0))

    def create_strs():
        """绘制验证码字符"""
        c_chars = get_chars()
        strs = ' %s ' % ' '.join(c_chars)  # 每个字符前后以空格隔开

        font = ImageFont.truetype(font_type, font_size)
        font_width, font_height = font.getsize(strs)

        draw.text(((width - font_width) / 3, (height - font_height) / 3),
                  strs, font=font, fill=fg_color)

        return ''.join(c_chars)

    if draw_lines:
        create_lines()
    if draw_points:
        create_points()
    strs = create_strs()

    # 图形扭曲参数
    params = [1 - float(random.randint(1, 2)) / 100,
              0,
              0,
              0,
              1 - float(random.randint(1, 10)) / 100,
              float(random.randint(1, 2)) / 500,
              0.001,
              float(random.randint(1, 2)) / 500
              ]
    img = img.transform(size, Image.PERSPECTIVE, params)  # 创建扭曲

    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)  # 滤镜,边界加强(阈值更大)

    return img, strs

代码说明
准备好字体文件,这里也放到utils文件夹下,并在设置里引入字体文件 font_type="utils/Monaco.ttf",
整个模块只有一个函数create_validate_code()。另外内部还有几个函数,但是都是在create_validate_code函数内部使用的。使用的时候只需要调用create_validate_code()方法,然后可以获取到2个返回值,一次是图片的对象和对应的验证码的字符串。

返回验证码图片

重写get_img处理函数,调用上面的模块,返回图片对象的二进制数据给前端的img标签:

# views.py 文件

from io import BytesIO  # 字节的IO操作
from utils.check_code import create_validate_code
def get_img(request):
    img, code = create_validate_code()
    print(img, code)  # img是图片对象,code是对应的字符串
    # file = open('img.png', 'wb')  # 打开文件
    # img.save(file, 'PNG')  # 传入文件句柄,保存到文件,后面的参数是图片的格式
    stream = BytesIO()  # 相当于打开了一个文件
    img.save(stream, 'PNG')  # 这里就不需要生成文件了,而是放在内存里
    # request.session['checkCode'] = code # 把验证码保存到session中
    return HttpResponse(stream.getvalue())

这里还用到了BytesIO,因为要在内存中直接做读写操作。数据读写不一定是文件,也可以在内存中读写。StringIO顾名思义就是在内存中读写str。StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。StringIO和BytesIO是在内存中操作str和bytes的方法,使得和读写文件具有一致的接口。

点击图片刷新

目前需要刷新整个页面才能刷新图片,实际应用中需要点击图片,只刷新验证码图片,页面的其他内容不变化。
这里需要给img标签绑定一个onclick事件,通过这个事件给img标签的src赋值。但是这里会有一个问题,浏览器的机制是只有src的值发生了变化才会发起一个新的请求,这就要求src的值需要和之前的值不同,并且还不能改变请求的地址。下面是实现点击刷新的方法:

<img src="/getimg/" />
<script>
    document.getElementsByTagName('img')[0].onclick = function () {
        // this.src = '/getimg/?';  // 指点点击的第一次会变
        this.src = this.src + '?';
    };
</script>

这里用了一个老师号称机智的办法。get请求中的url会用到问号(?),用来分隔请求地址和参数的。这里每一次点击都会在后面添加一个问号,这样src的值变化了,但是请求的地址不变

富文本编辑框

常见的这类插件有:CKEditor、UEEditor、TinyEditor、KindEditor(下面要讲的)。
讲师的博客地址:http://www.cnblogs.com/wupeiqi/articles/6307554.html
KindEditor官网(中文):http://kindeditor.net/demo.php

安装

首先去官网下载,然后把文件夹复制到static静态文件目录下。您可以根据需求删除以下目录后上传到服务器:

  • asp - ASP程序
  • asp.net - ASP.NET程序
  • php - PHP程序
  • jsp - JSP程序
  • examples - 演示文件

上面这些都是各种语言的示例,所以可以不要。

基本用法

首先在再页面上放一个textarea标签。这个博客的编辑器里绝对不能写textarea标签,所以代码里就故意写错了:

<div>
    <多行文本 id="editor_id" name="content" style="width:700px;height:300px;"></多行文本>
</div>
<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script src="/static/kindeditor/kindeditor-all-min.js"></script>
<script>
    $(function () {
        KindEditor.create('#editor_id', {})
    })
</script>

在有些浏览器上不设宽度和高度可能显示有问题,所以最好设一下宽度和高度。宽度和高度可用inline样式设置,也可用编辑器初始化参数设置。这里先用了前者。
KindEditor.create()方法就是进行编辑器初始化的。第一个参数是一个CSS选择器。第二个参数可以不写,这里写了个空字典和不写一样。是对编辑器进行配置的。
下面是简单的对高度和宽度进行了设置:

<script>
    $(function () {
        KindEditor.create('#editor_id', {
            width: '100%',  // 宽度,可以是百分比也可以是像素
            height: '600px',  // 高度,只能是像素
            minWidth: 400,  // 最小宽度
            minHeight: 200  // 最小高度
        })
    })
</script>

更多的参数设置还是看中文的官方文档吧:http://kindeditor.net/docs/option.html

上传文件

编辑器初始化的部分可以这么写,这里用了items参数把别的按钮都去掉了。主要是设置2个参数,一个是上传文件的url。另一个可以在上传的时候添加额外的参数,这里要把csrf_token加上:

<script>
    $(function () {
        KindEditor.create('#editor_id', {
            items: ['image', 'insertfile'],  // 让工具栏只显示上传图片和上传文件2个按钮
            uploadJson: '/upload/',  // 指定上传提交的url
            // allowImageUpload: false,  // 这个参数设为false,就没有本地上传的分页了。即不允许本地上传
            extraFileUploadParams: {'csrfmiddlewaretoken': "{{ csrf_token }}"}  // 上传时,添加别的参数一并上传。
        })
    })
</script>

服务端收到请求后,要按下面的JSON格式给客户端返回数据。官方文档:http://kindeditor.net/docs/upload.html

# 成功时
{
    "error" : 0,
    "url" : "http://www.example.com/path/to/file.ext"
}
# 失败时
{
    "error" : 1,
    "message" : "错误信息"
}

这里可以进控制台查看前端生成的html,使用的是iframe和form,标签内部还有很多别的标签就不贴了:

<iframe name="kindeditor_upload_iframe_1526735278206" style="display:none;"></iframe>
<form class="ke-upload-area ke-form" action="/upload/?dir=image" enctype="multipart/form-data" method="post" target="kindeditor_upload_iframe_1526735278206"></form>

服务器端要写好对应的处理函数:

# urls.py 文件,url的对应关系
    path('upload/', views.upload),

# views.py文件,对应的处理函数
import json
def upload(request):
    print(request.GET.get('dir'))  # 打印收到的文件的上传方式。image 或 file,另外还有flash等等
    print(request.FILES)  # 打印收到的文件对象
    ret = {'error': 0,  # 0是成功,1是有错误
           'url': '/static/imgs/test.gif',  # 返回图片保存的url,用来给前端生成预览的
           }
    return HttpResponse(json.dumps(ret))

这里先不保存,只是把接收到的内容打印出来,返回一个表示上传成功的信息。信息里包含一个url,这个url是服务器端保存图片的路径,是用来给客户端浏览器生成预览的。这里暂时没保存图片,先把服务器上已经有的一张图片的url返回,让客户端可以生成一个预览。

文件空间管理

这个编辑器还提供文件管理的功能,官方的叫法是浏览远程服务器(FileManager)。需要设置下面的2个参数:

  • allowFileManager :true时显示浏览远程服务器按钮。默认值: false
  • fileManagerJson :指定浏览远程图片的服务器端程序。和上传文件的url的参数一样,设置好url,然后去后端写好处理函数

后端的处理函数就不展开了,不过讲师的博客里有,这里就只留个钩子吧。

MarkDown编辑框

我这里试了下这个Editor.md :https://pandao.github.io/editor.md/index.html

安装

首先去官网下载,然后把文件夹复制到static静态文件目录下。
在页面里写上下面的代码就可以使用默认的设置了:

<body>
<link rel="stylesheet" href="/static/editor.md-master/css/editormd.min.css" />
<div style="height:600px;">
    <div id="editormd"></div>
</div>
<script src="/static/editor.md-master/examples/js/jquery.min.js"></script>
<script src="/static/editor.md-master/editormd.min.js"></script>
<script type="text/javascript">
    $(function() {
        var editor = editormd("editormd", {
            path : "/static/editor.md-master/lib/" // Autoload modules mode, codemirror, marked... dependents libs path
        });

        /*
        // or
        var editor = editormd({
            id   : "editormd",
            path : "/static/editor.md-master/lib/"
        });
        */
    });
</script>
</body>

上面在外层的div是自己加上的,主要是设置一个高度,因为默认的初始化的时候高度和宽度都是100%。所以设置一下高度的话也可以去掉自己的div。

设置

基本的初始化设置有下面这些,从源码里找到的:

        mode                 : "gfm",          //gfm or markdown
        name                 : "",             // Form element name
        value                : "",             // value for CodeMirror, if mode not gfm/markdown
        theme                : "",             // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty
        editorTheme          : "default",      // Editor area, this is CodeMirror theme at v1.5.0
        previewTheme         : "",             // Preview area theme, default empty
        markdown             : "",             // Markdown source code
        appendMarkdown       : "",             // if in init textarea value not empty, append markdown to textarea
        width                : "100%",
        height               : "100%",
        path                 : "./lib/",       // Dependents module file directory
        pluginPath           : "",             // If this empty, default use settings.path + "../plugins/"
        delay                : 300,            // Delay parse markdown to html, Uint : ms
        autoLoadModules      : true,           // Automatic load dependent module files
        watch                : true,
        placeholder          : "Enjoy Markdown! coding now...",
        gotoLine             : true,
        codeFold             : false,
        autoHeight           : false,
        autoFocus            : true,
        autoCloseTags        : true,
        searchReplace        : true,
        syncScrolling        : true,           // true | false | "single", default true
        readOnly             : false,
        tabSize              : 4,
        indentUnit           : 4,
        lineNumbers          : true,
        lineWrapping         : true,
        autoCloseBrackets    : true,
        showTrailingSpace    : true,
        matchBrackets        : true,
        indentWithTabs       : true,
        styleSelectedText    : true,
        matchWordHighlight   : true,           // options: true, false, "onselected"
        styleActiveLine      : true,           // Highlight the current line
        dialogLockScreen     : true,
        dialogShowMask       : true,
        dialogDraggable      : true,
        dialogMaskBgColor    : "#fff",
        dialogMaskOpacity    : 0.1,
        fontSize             : "13px",
        saveHTMLToTextarea   : false,
        disabledKeyMaps      : [],

这里有一些详细的说明:https://pandao.github.io/editor.md/examples/index.html
下载下来的examples文件夹里也有一份一样的。

自定义工具栏

官方文档里有的内容就不反复说明了,初始化的时候给toolbarIcons这个参数赋值,可以是列表。也可以是字符串(只能是full, simple, mini),不过也可以自定义一个名字,然后用字符串引入:

<script>
    $(function () {
        editormd.toolbarModes.steed = ["undo", "redo", "|", "bold", "del", "italic", "quote"];
        var editor = editormd("editormd", {
            path : "/static/editor.md-master/lib/",
            toolbarIcons: 'steed',
        });
    })
</script>

还可以自己创建按钮,偷懒不搞图标的话,也可以直接使用文字,还能给自定义的按钮添加事件:

    $(function () {
        editormd.toolbarModes.steed = ["undo", "redo", "|", "bold", "del", "italic", "quote",
        "||", "top", "submit", "help", "info"];
        var editor = editormd("editormd", {
            path : "/static/editor.md-master/lib/",
            toolbarIcons: 'steed',
            toolbarIconTexts: {top: "回到顶部", submit: '提交'},
            toolbarHandlers: {
                top: function () {
                    alert('回到顶部');
                }
            },
        });
    })

自己以的方法了提供了一下4个对象可以使用,具体还是看官方的例子把:

  • @param {Object}, cm: CodeMirror对象
  • @param {Object}, icon: 图标按钮jQuery元素对象
  • @param {Object}, cursor: CodeMirror的光标对象,可获取光标所在行和位置
  • @param {String}, selection: 编辑器选中的文本

获取文本

这里主要讲下面3个方法

  • .getMarkdown(): 获取MarkDown文本
  • .getHTML(): 获取HTML文本,需要开启 saveHTMLToTextarea: true,
  • .setMarkdown(md):填入MarkDown文本

下面的例子自定义了2个按钮,分别能获取到文本的MarkDown格式以及HTML格式的内容:

{% load static %}
<script>
    $(function () {
        editormd.toolbarModes.steed = ["undo", "redo", "|", "bold", "del", "italic", "quote",
        "||", "get_markdown", "get_html", "top", "help", "info"];
        var editor = editormd("editormd", {
            path : "/static/editor.md-master/lib/",
            toolbarIcons: 'steed',
            saveHTMLToTextarea: true,
            toolbarIconTexts: {get_markdown: "Get MarkDown", get_html: 'Get HTML',  top: "TOP"},
            toolbarHandlers: {
                get_markdown: function() {
                    alert(editor.getMarkdown());
                },
                get_html: function() {
                    alert(editor.getHTML());
        }
            },
        });
        $.get("{% static "editor.md-master/examples/test.md" %}", function(md){
            editor.setMarkdown(md);
        })
    })
</script>

另外上面的例子的最后一个 $.get() 方法是获取到文本后,在回调函数里调用editor.setMarkdown(md);填写到文本框内。
获取MarkDown的方法获取到文本后,就可以实现上传到服务器的功能了。
获取HTML的方法可以获取到HTML的文本,不过貌似没什么用,要把MarkDown文本直接展示在HTML上有另外的转化方法。
自动填入文本框,则可以实现文档的修改的功能。

显示MarkDown

之前将编辑器的内容上传并保存到数据库,如果上传的内容是 .getMarkdown() 获取的Markdown文本。那么现在要做一个展示的页面,将数据库中的内容显示到页面上,并且展示的应该是html格式:

{% load static %}
<div class="container">
    <div id="doc-content"><textarea style="display: none">{{ article_obj.articledetail.content }}</textarea></div>
</div>
<script src="{% static "editor.md-master/editormd.js" %}"></script>
<script src="{% static "editor.md-master/lib/marked.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/prettify.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/raphael.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/underscore.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/sequence-diagram.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/flowchart.min.js" %}"></script>
<script src="{% static "editor.md-master/lib/jquery.flowchart.min.js" %}"></script>
<script>
    $(function () {
        editormd.markdownToHTML('doc-content')
    })
</script>

上面不知道为什么要引入那么多js,不过一个都不用试下来是无法显示的。官网的例子里也引用那么多js,这里就全部引用了。
另外,这里的markdownToHTML没有加参数,使用的都是默认设置。如果需要调整,那么第二个参数传入一个字典进行设置:

<script type="text/javascript">
    $(function() {
        editormd.markdownToHTML("doc-content", {
            htmlDecode : "style,script,iframe",
            emoji : true,
            taskList : true,
            tex : true, // 默认不解析
            flowChart : true, // 默认不解析
            sequenceDiagram : true, // 默认不解析
            codeFold : true
        });
    });
</script>