前端使用Canvas生成海报

最近项目需要涉及到前端海报合成分享功能,前端靠不上只能自己上...
现学现卖,相关链接 :

CSS代码如下

 * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
}

HTML代码如下

<button id="btn" style="height:50px;width:100%;margin:0 auto">点我生成海报</button>
<!--合成后的海报图-->
<img id="out" src="" style="width:100%;border:1px solid black;margin-top:20px;display: none;">

JS代码如下

 CanvasRenderingContext2D.prototype.wrapText = function (text, x, y, maxWidth, lineHeight) {
        if (typeof text != 'string' || typeof x != 'number' || typeof y != 'number') {
            return;
        }
        var context = this;
        var canvas = context.canvas;
        if (typeof maxWidth == 'undefined') {
            maxWidth = (canvas && canvas.width) || 300;
        }
        if (typeof lineHeight == 'undefined') {
            lineHeight = (canvas && parseInt(window.getComputedStyle(canvas).lineHeight)) || parseInt(window.getComputedStyle(document.body).lineHeight);
        }
        // 字符分隔为数组
        var arrText = text.split('');
        var line = '';
        for (var n = 0; n < arrText.length; n++) {
            var testLine = line + arrText[n];
            var metrics = context.measureText(testLine);
            var testWidth = metrics.width;
            if (testWidth > maxWidth && n > 0) {
                context.fillText(line, x, y);
                line = arrText[n];
                y += lineHeight;
            } else {
                line = testLine;
            }
        }
        context.fillText(line, x, y);
    };

    function nickname(x, y) {
        context.fillStyle = 'grey';
        context.font = '32px  SC';
        context.wrapText("iuu 邀请你一起学习", x, y, 700, 60)
    }

    // 课程标题
    function goodsTitle(x, y) {
        context.fillStyle = 'black';
        context.font = '40px  bold';
        context.wrapText("第131讲:如何提升社会科学类核心期刊投稿的命中率", x, y, 700, 60)
    }

    // 描述
    function shopMsg(x, y) {
        context.fillStyle = 'grey';
        context.font = '25px  bold';
        context.wrapText("— 科研写作研究所", x, y, 700, 60)
    }
    // 图片描述
    function imgMsg(x, y) {
        context.fillStyle = 'grey';
        context.font = '24px  SC';
        context.fillText("长按保存图片", 310, 1250)
    }
    // 背景
    function bgImgScaleToFill(img) {
        var scale = Math.max(canvas.width / img.width, canvas.height / img.height);
        var x = (canvas.width / 2) - (img.width / 2) * scale;
        var y = (canvas.height / 2) - (img.height / 2) * scale;
        context.drawImage(img, x, y, img.width * scale, img.height * scale);
    }

    // 商品图
    function goodsImgScaleToFit(img, x, y) {
        var scale = Math.min(canvas.width / img.width, canvas.height / img.height);
        context.drawImage(img, x, y, img.width * scale, img.height * scale);
    }
    // 二维码
    function qrImgScaleToFill(img, x, y) {
        context.drawImage(img, x, y, img.width, img.height);
    }
    // 头像
    function avatarImgScaleToFill(img, x, y, w, h) {
        context.drawImage(img, x, y, w, h);
    }
    var canvas = document.createElement("canvas");
    var context = canvas.getContext("2d");
    window.onload = function () {
        var btn = document.getElementById("btn");
        var QrImgUrl = "http://fenxiao-guogao.wukongkeyan.com/tm_ca6379f807f89f5e96ebbce4c81203f7_20220719155912.png"
        var AvatarImgUrl = "http://wechatavator-1252524126.file.myqcloud.com/appzissbssa6278/image/compress/u_api_61edfdfd1c284_PWGOuVUIpM.png"
        var GoodsImgUrl = "http://wechatapppro-1252524126.file.myqcloud.com/appzissbssa6278/image/b_u_6170d73860ce7_73CZtcAY/l4z0x8yo02hu.jpg"

        btn.onclick = function () {
            var all = {avatarImg: null, goodsImg: null, qrImg: null}

            var QrImg = new Promise(function (resolve, reject) {
                var qrImg = new Image();
                qrImg.crossOrigin = 'anonymous';
                qrImg.src = QrImgUrl
                qrImg.onload = function () {
                    resolve(qrImg);
                }
            })

            var AvatarImgCircle = new Promise(function (resolve, reject) {
                var cs = document.createElement('canvas');
                var ctx = cs.getContext('2d');
                var avatar = new Image();
                avatar.crossOrigin = 'anonymous';
                avatar.src = AvatarImgUrl
                avatar.onload = function () {
                    cs.width = avatar.width;
                    cs.height = avatar.height;
                    var width = avatar.width;
                    var height = avatar.height;
                    var circle = {x: width / 2, y: height / 2, r: width / 2}
                    ctx.clearRect(0, 0, width, height);
                    ctx.save();
                    ctx.beginPath();
                    ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2, false);
                    ctx.clip();
                    ctx.drawImage(avatar, 0, 0);
                    ctx.restore();
                    // var base64 =
                    var avatarImg = new Image()
                    avatarImg.src = cs.toDataURL("image/png");
                    resolve(avatarImg);
                }
            })
            var GoodsImg = new Promise(function (resolve, reject) {
                var goodsImg = new Image();
                goodsImg.crossOrigin = 'anonymous';
                goodsImg.src = GoodsImgUrl
                goodsImg.onload = function () {
                    resolve(goodsImg);
                }
            })
            Promise.all([QrImg, GoodsImg, AvatarImgCircle]).then(res => {
                all.avatarImg = res[2]
                all.goodsImg = res[1]
                all.qrImg = res[0]
                var bgImg = new Image();
                bgImg.crossOrigin = 'anonymous';
                bgImg.src = 'http://commonresource-1252524126.cdn.xiaoeknow.com/image/l5c2twsm0lm1.png';
                bgImg.onload = function () {
                    canvas.width = 750;
                    canvas.height = 1334;
                    bgImgScaleToFill(bgImg)
                    goodsImgScaleToFit(all.goodsImg, 0, 0)
                    qrImgScaleToFill(all.qrImg, 300, 1060)
                    avatarImgScaleToFill(all.avatarImg, 25, 680, 80, 80)
                    nickname(145, 740)
                    goodsTitle(40, 900)
                    shopMsg(40, 1020)
                    imgMsg(310, 1250)
                    var base64 = canvas.toDataURL();
                    document.getElementById('out').src = base64;
                    document.getElementById('out').style.display = "block";
                }
            })
        }
    }

这个代码主要是进行海报合成,其中注意点事 所有得图片都必须加载完成之后进行合成操作 所以用到了Promise来进行处理

goutil/dump – 打印漂亮易读的go数据

gookit/goutil/dump - 是一个golang数据打印工具包,可以打印出漂亮易读的go slice, map, struct数据。

主要功能:
使用简单,直接调用 dump.P(vars...) 即可
支持所有的基础数据类型
支持slice, map, struct数据结构
支持传入打印多个变量
默认输出调用位置,方便使用
支持自定义部分能力,如 缩进,色彩主题等

打印基础类型

package main

import "github.com/gookit/goutil/dump"

// rum demo:
//     go run ./dump/_examples/basic_types.go
func main() {
    dump.P(
        nil, true,
        12, int8(12), int16(12), int32(12), int64(12),
        uint(22), uint8(22), uint16(22), uint32(22), uint64(22),
        float32(23.78), float64(56.45),
        'c', byte('d'),
        "string",
    )
}

打印slice

打印 array, slice 都会一行一个元素输出,同时会在最后输出长度。

package main

import "github.com/gookit/goutil/dump"

// rum demo:
//     go run ./dump/_examples/slice.go
func main() {
    dump.P(
        []byte("abc"),
        []int{1, 2, 3},
        []string{"ab", "cd"},
        []interface{}{
            "ab",
            234,
            []int{1, 3},
            []string{"ab", "cd"},
        },
    )
}

打印map

打印map数据结构,会一行一个元素输出,同时会在最后输出map长度。

package main

import "github.com/gookit/goutil/dump"

// rum demo:
//     go run ./map.go
//     go run ./dump/_examples/map.go
func main() {
    dump.P(
        map[string]interface{}{
            "key0": 123,
            "key1": "value1",
            "key2": []int{1, 2, 3},
            "key3": map[string]string{
                "k0": "v0",
                "k1": "v1",
            },
        },
    )
}

打印struct

打印struct数据,指针类型会自动打印底层真实数据

package main

import (
    "fmt"

    "github.com/gookit/color"
    "github.com/gookit/goutil/dump"
)

// rum demo:
//     go run ./struct.go
//     go run ./dump/_examples/struct.go
func main() {
    s1 := &struct {
        cannotExport map[string]interface{}
    }{
        cannotExport: map[string]interface{}{
            "key1": 12,
            "key2": "abcd123",
        },
    }

    s2 := struct {
        ab string
        Cd int
    }{
        "ab", 23,
    }

    color.Infoln("- Use fmt.Println:")
    fmt.Println(s1, s2)

    color.Infoln("\n- Use dump.Println:")
    dump.P(
        s1,
        s2,
    )
}

自定义dumper

支持自定义dumper一些选项。如 缩进,色彩主题等

// Options for dump vars
type Options struct {
    // Output the output writer
    Output io.Writer
    // NoType dont show data type TODO
    NoType bool
    // NoColor don't with color
    NoColor bool
    // IndentLen width. default is 2
    IndentLen int
    // IndentChar default is one space
    IndentChar byte
    // MaxDepth for nested print
    MaxDepth int
    // ShowFlag for display caller position
    ShowFlag int
    // MoreLenNL array/slice elements length > MoreLenNL, will wrap new line
    // MoreLenNL int
    // CallerSkip skip for call runtime.Caller()
    CallerSkip int
    // ColorTheme for print result.
    ColorTheme Theme
}

Armbian使用Docker安装IPsec VPN 服务器

项目地址:https://github.com/hwdsl2/setup-ipsec-vpn
首先获取镜像

docker pull hwdsl2/ipsec-vpn-server

创建配置文件config.env

# Note: All the variables to this image are optional.
# See README for more information.
# To use, uncomment and replace with your own values.

# Define IPsec PSK, VPN username and password
# - DO NOT put "" or '' around values, or add space around =
# - DO NOT use these special characters within values: \ " '
VPN_IPSEC_PSK=密钥
VPN_USER=账号
VPN_PASSWORD=密码

# Define additional VPN users
# - DO NOT put "" or '' around values, or add space around =
# - DO NOT use these special characters within values: \ " '
# - Usernames and passwords must be separated by spaces
# VPN_ADDL_USERS=additional_username_1 additional_username_2
# VPN_ADDL_PASSWORDS=additional_password_1 additional_password_2

# Use a DNS name for the VPN server
# - The DNS name must be a fully qualified domain name (FQDN)
# VPN_DNS_NAME=vpn.example.com

# Specify a name for the first IKEv2 client
# - Use one word only, no special characters except '-' and '_'
# - The default is 'vpnclient' if not specified
# VPN_CLIENT_NAME=your_client_name

# Use alternative DNS servers
# - By default, clients are set to use Google Public DNS
# - Example below shows Cloudflare's DNS service
# VPN_DNS_SRV1=1.1.1.1
# VPN_DNS_SRV2=1.0.0.1

# Protect IKEv2 client config files using a password
# - By default, no password is required when importing IKEv2 client configuration
# - Uncomment if you want to protect these files using a random password
# VPN_PROTECT_CONFIG=yes

运行镜像

docker run -d \
    -p 500:500/udp \
    -p 4500:4500/udp \
    --privileged=true \ # 特权模式运行
    --restart=always \  # 开启自启动
    --name ipsec-vpn-server \
    --env-file /home/iuu/Software/Docker/ipsec-vpn-server/config.env \ # 加载配置文件
    -v /home/iuu/Software/Docker/ipsec-vpn-server/ikev2-vpn-data:/etc/ipsec.d \
    -v /lib/modules:/lib/modules:ro \ # 挂载运行库目录 但是只有只读权限
    hwdsl2/ipsec-vpn-server

启动容器后查看容器状态
防火墙开4500 500 UDP端口
如果是内网穿透,路由器也得做对应的端口转发

Mac下PHP编译安装Swoole

该文章记录mac下编译安装swoole步骤
老生常谈
第一步下载swoole源码,解压,进入源码目录执行 phpize 。注意多个php版本别搞错了

iuu@iuudeMac-Studio swoole-src-5.1.1 % /opt/homebrew/opt/php@8.2/bin/phpize    
Configuring for:
PHP Api Version:         20220829
Zend Module Api No:      20220829
Zend Extension Api No:   420220829
config.m4:359: warning: The macro 'AC_PROG_CC_C99' is obsolete.
config.m4:359: You should run autoupdate.
./lib/autoconf/c.m4:1662: AC_PROG_CC_C99 is expanded from...
config.m4:359: the top level

然后配置编译参数

./configure --with-php-config=/opt/homebrew/opt/php@8.2/bin/php-config --enable-openssl  --with-openssl-dir=/opt/homebrew/Cellar/openssl@3/3.2.0_1 --enable-swoole-curl

然后配置编译

make && make install 

M1 下会产生这个错误

/opt/homebrew/Cellar/php@8.2/8.2.15/include/php/ext/pcre/php_pcre.h:23:10: fatal error: 'pcre2.h' file not found
#include "pcre2.h"
         ^~~~~~~~~
1 error generated.
make: *** [ext-src/php_swoole.lo] Error 1

解决方式 (注意版本路径) 两个文件任选其一

ln -s /opt/homebrew/Cellar/pcre2/10.36/include/pcre2.h /opt/homebrew/Cellar/php@7.4/7.4.16/include/php/ext/pcre/pcre2.h
ln -s /opt/homebrew/include/pcre2.h /opt/homebrew/Cellar/php@7.4/7.4.16/include/php/ext/pcre/pcre2.h

清理make 缓存

make clean

然后配置编译

make && make install 

成功之后,将swoole.so加入到php.ini中

# 找到自己的当前php版本的所在ini
php --ini
# 修改加入
[swoole]
extension = swoole.so
swoole.use_shortname = Off
# 查看是否安装成功
php -m
php --ri swoole

苹果Mac下搭建PHP与Nginx的开发环境

记录一下mac搭建php环境的步骤,支持多站点不同PHP版本,终端切换PHP版本

首先介绍一下本文所依赖的目录

站点存放目录

/Users/iuu/Sites/

目录结构如下

Sites
├── php74.test.com # php7.4环境网站
│   ├── 404.html
│   ├── 50x.html
│   ├── index.html
│   ├── index.php
│   └── log # 网站日志存放目录
│       ├── access.log
│       └── error.log
└── php82.test.com  # php8.2环境网站
    ├── 404.html
    ├── 50x.html
    ├── index.html
    ├── index.php
    └── log # 网站日志存放目录
        ├── access.log
        └── error.log

brew services 常用命令

# 启动
 brew services start  xx
# 停止
 brew services stop  xx
 # 状态
 brew services info  xx
 # 服务列表
 brew services list

网站目录配置

在站点目录创建站点文件夹

/Users/iuu/Sites/php70.test.com

在站点目录创建站点日志文件夹

/Users/iuu/Sites/php70.test.com/log/

php安装与配置

添加 shivammathur/php 仓库源

github https://github.com/shivammathur/homebrew-php

brew tap shivammathur/php

搜索可安装的 PHP 版本

brew search shivammathur/php

比如你要这些 PHP 版本就这样操作:

brew install shivammathur/php/php@8.1
brew install shivammathur/php/php@8.2
brew install shivammathur/php/php@8.3

查看php安装信息:

brew info php@7.4

php安装目录为:

/opt/homebrew/opt/php@你的版本/

php-fpm 配置文件为:

/opt/homebrew/etc/php/你的版本/

配置php-fpm:

# 修改/opt/homebrew/etc/php/你的版本/php-fpm.conf 中 error_log 配置:
error_log = log/php-fpm-82.log

# 修改/opt/homebrew/etc/php/你的版本/php-fpm.d/www.conf  中 user 配置:
 user = _www
 group = _www
# 改为:
; eg ; user = _www
; eg ; group = _www
# 修改同文件中的 listen  这里端口我为了统一PHP版本我改的是 90[PHP版本]
listen = 127.0.0.1:9082

php-fpm的启动与停止

# 启动
brew services start shivammathur/php/php@8.2
# 停止
brew services stop shivammathur/php/php@8.2 
# 状态
brew services info shivammathur/php/php@8.2 

终端切换php版本

# 8.2 切 8.3
 brew unlink php 
 brew link --overwrite --force shivammathur/php/php@8.3
# 8.3 切 8.2
  brew unlink php 
  brew link --overwrite --force shivammathur/php/php@8.2
  brew unlink php@8.2 && brew link --force php@8.2
# 8.2切 7.4
 brew unlink php@8.2
 brew link --overwrite --force shivammathur/php/php@7.4 
# 7.4 切 8.3
 brew unlink php@7.4   
 brew link --overwrite --force shivammathur/php/php@8.3

nginx安装与配置

安装 nginx :

brew install nginx 

安装完nginx后 查看配置文件信息:

brew info nginx

默认 nginx 站点目录为 :

/opt/homebrew/var/www

默认nginx 端口与配置文件为:

8080
/opt/homebrew/etc/nginx/nginx.conf 

默认nginx会加载该目录的所有配置文件:
(注意为了统一网站配置文件以后新建站点,站点的配置文件我都放在这里了)

/opt/homebrew/etc/nginx/servers/

贴一下我的nginx 主配置文件

# /opt/homebrew/etc/nginx/nginx.conf 

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.php index.html index.htm;
        }

        error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
           try_files $uri =404;
           root           html;
           fastcgi_pass   127.0.0.1:9082;
           fastcgi_index  index.php;
           fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
           include        fastcgi_params;
        }

       access_log  /opt/homebrew/var/www/log/access.log;
       error_log   /opt/homebrew/var/www/log/error.log;

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }

    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
    include servers/*;
}

接下来创建一个站点:

在 /opt/homebrew/etc/nginx/servers/ 目录下创建一个名称为 php74.test.com.conf 配置文件,内容为:

server {
        listen       80; # 端口
        server_name  php74.test.com; # 站点名称

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /Users/iuu/Sites/php74.test.com;
            index  index.php index.html index.htm;
        }

        error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
             try_files $uri =404;
           root           /Users/iuu/Sites/php74.test.com;
           fastcgi_pass   127.0.0.1:9074;
           fastcgi_index  index.php;
           fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
           include        fastcgi_params;
        }

       access_log  /Users/iuu/Sites/php74.test.com/log/access.log;
       error_log   /Users/iuu/Sites/php74.test.com/log/error.log;
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }

不同人配置不同 视情况修改

nginx 校验配置文件是否有效

nginx -t

nginx 启动 与停止

# 启动
brew services start nginx
# 停止
brew services stop nginx
# 重启
brew services restart nginx
# 状态
brew services info nginx

重新加载配置文件

nginx -s reload

修改hosts 文件实现域名访问

# 新增hosts 记录
127.0.0.1       php70.test.com

启动nginx 启动对应版本的 php-fpm 即可访问对于网址

# nginx 程序日志位置
/opt/homebrew/var/log/nginx
# php-fpm 日志位置
/opt/homebrew/var/log 

(为什么指定了php-fpm日志名称还会出现php-fpm.log文件?)
答:因为brew services 启动 php-fpm 的时候带了一个 StandardErrorPath 字段指定了日志

# 当启动php-fpm 或nginx 可以进这个目录看 brew services 的启动文件
~/Library/LaunchAgents 

参考 https://www.jianshu.com/p/6c3b26490861

1 5 6 7 8 9 11