实操指南:在浏览器内运用ImageMagick的WebAssembly技术
WebAssembly & ImageMagick
WebAssembly
为前端开发带来了新的可能性,一些原本由 C/C++ 开发的库经过很简单的改造,就可以编译到 WebAssembly
,在 Node.js、浏览器端使用。
对于 Node.js,我们之前已经有了 node-ffi
等方式来调用 C++ 库,但是 node-ffi
并不能用在浏览器里,WebAssembly
使在浏览器环境使用 C/C++ 库成为可能。
WebAssembly
一样受浏览器沙箱限制,并没有比普通 js 更多的能力,但是在计算密集型任务中拥有比普通 js 更好的性能表现,否则移植 C/C++ 库也没有意义。
ImageMagick 是著名的 C/C++ 图形工具库,有命令行上的 PhotoShop
之称,支持包括 psd,ai 等超过 200 种格式图像的各种处理,本次我们把 ImageMagick
移植到前端,用它来在浏览器中完成各种图像处理操作。
移植主要使用基于 LLVM
的 Emscripten
工具链。
Emscripten工具链
LLVM
是一个开源编译器平台,以 LLVM Intermediate Representation (LLVM IR)
作为中间代码,实现了多种语言编译到多种目标平台。
如图所示:
LLVM示意图
一种编程语言只要实现 LLVM
前端,就可以支持x86、arm等目标平台。
一个新的目标平台只要实现 LLVM
后端,C/C++、haskell 等语言就可以编译到此平台。
WebAssembly
就是一个新的目标平台。而 Emscripten
则是一个 LLVM
后端,能够把 LLVM IR
编译到 WebAssembly
。
工作流程
Emscripten
工具链包括 emcc
,emconfigure
等工具,借助这些工具,可以把 C/C++ 库编译、构建成 WebAssembly
文件。
配合其他 LLVM
相关工具,可以把更多语言编译到 WebAssembly
,例如 AssemblyScript,它可以把 TypeScript
编译到 WebAssembly
。
环境搭建
常规的环境搭建方式
官网 (https://emscripten.org/docs/getting_started/downloads.html) 提供的环境搭建方式:
# Get the emsdk repo
git clone https://github.com/emscripten-core/emsdk.git
# Enter that directory
cd emsdk
# Fetch the latest version of the emsdk (not needed the first time you clone)
git pull
# Download and install the latest SDK tools.
./emsdk install latest
# Make the "latest" SDK "active" for the current user. (writes ~/.emscripten file)
./emsdk activate latest
# Activate PATH and other environment variables in the current terminal
# On Windows, run emsdk instead of ./emsdk, and emsdk_env.bat instead of source ./emsdk_env.sh.
source ./emsdk_env.sh
除此之外,还需要安装 CMake
、 autoconf
、 libtool
、 pkg-config
工具。
使用docker快速搭建环境
常规的环境搭建方式,可能会遇到下载慢、下载失败的问题,甚至导致环境有部分组件缺失。因此强烈建议使用 docker
,从 Docker Hub
下载已经搭建好的环境。
本文使用 docker
搭建环境。
Docker Hub
上使用最多的 emscripten
镜像是 trzeci/emscripten,除了 emsdk
外,还安装了 CMake
、 make
等构建工具。
但是对于我们想构建 ImageMagick
,这些工具还不够,因此我以 trzeci/emscripten 为基础镜像,构建了新的镜像 mk33mk33/wasm-base,在 trzeci/emscripten 的基础上,安装了 autoconf
、 libtool
、 pkg-config
三个构建工具。
安装命令如下(没有 docker
的同学请先安装 docker
):
docker pull mk33mk33/wasm-base
对 docker 构建过程有兴趣的同学可以查看以上两个镜像的 Dockfile
了解镜像构建细节。
trzeci/emscripten
:https://github.com/trzecieu/emscripten-docker/blob/master/docker/trzeci/emscripten/Dockerfile
mk33mk33/wasm-base
:https://github.com/mk33mk333/wasm-im/blob/master/docker/wasm-base/Dockerfile
ImageMagick依赖分析
ImageMagick
功能强大,依赖库也众多,但是大部分都是可选的,我们可以根据我们需要的功能选择使用哪些依赖。
本次我们选择最常用的 jpg、png、webp 支持。
支持jpg格式需要 libjpeg
库,支持 png 格式需要 libpng
库,另外 libpng
需要依赖 zlib
,支持 webp 需要 libwebp
库,libwebp
依赖前面的所有库。
因此我们需要先把 libjpeg
、 libpng
、 zlib
、 libwebp
用 emsdk
编译成目标平台为 WebAssembly
的静态库。
然后用 ImageMagick
和以上静态库,一起编译成最终的 wasm 文件。
编译依赖库
C项目一般使用 make
工具链进行构建,主要是根据当前环境,对源码进行编译、链接,生成动态库、静态库和二进制应用程序。
项目庞大时会使用 autotool
、 CMake
等工具辅助生成 Makefile
,Makefile
就是 make
工具执行构建使用的脚本。
如此构建的 C 库我们安装时,一般流程就是:
./configure # 检查系统环境,判断当前环境是否满足编译条件
make # 执行编译
make install # 将二进制应用程序安装的指定位置
详细的 CMake
、 autotool
使用可以写一本书,本次我们主要关注生成静态库、查找依赖、查找头文件的配置。
建立项目
环境:win10 下的 virtualbox6.0.4 内的 Ubuntu 18.10
本文源码链接:https://github.com/mk33mk333/wasm-im
项目结构:
# tree -L 2
.
├── docker # 存放docker文件
│ ├── emscripten
│ └── wasm-base
├── external
│ ├── build-item.sh # 构建脚本
│ ├── build.sh # 构建脚本入口
│ ├── dist # 生成的js和wasm
│ ├── ImageMagick # 依赖库
│ ├── libjpeg # 依赖库
│ ├── libpng # 依赖库
│ ├── libwebp # 依赖库
│ ├── README.md
│ └── zlib # 依赖库
├── README.md
├── todo.md
└── web # 验证页面
├── img
├── index.html
├── README.md
├── wasm # 存放wasm模块和胶水js
├── webp-wasm.html #webp 测试
└── worker.js
建立编译脚本入口
首先进入存放外部依赖和编译脚本的目录 external
。
因为编译都要在docker内进行,因此先建立一个编译脚本入口文件 build.sh
。
具体的编译流程写在 build-item.sh
里。
# build.sh
docker run --rm --workdir /wasm -v $(pwd):/wasm mk33mk33/wasm-base bash ./build-item.sh
将当前目录映射到docker中的 /wasm 目录下,执行 build-item.sh
。
增加全局编译参数
首先在 build-item.sh
中加入 wasm 用的编译参数。
export CFLAGS="-O3 -s BINARYEN_TRAP_MODE=clamp -s ALLOW_MEMORY_GROWTH=1 -s USE_PTHREADS=0"
export CXXFLAGS="-O3 -s BINARYEN_TRAP_MODE=clamp -s ALLOW_MEMORY_GROWTH=1 -s USE_PTHREADS=0"
CFLAGS
为编译 c 语言文件的编译参数,CXXFLAGS
为编译 C++ 文件的编译参数。
-O3 为生产环境的优化级别。
ALLOW_MEMORY_GROWTH=1 允许 wasm 使用的堆动态增加,如果现有的大小不足,可以重新改变堆的大小,以满足程序运行过程中不断扩充的内存使用。
BINARYEN_TRAP_MODE=clamp 可以避免一些数字导致的错误。
USE_PTHREADS=0 暂不使用多线程。
make 工具会直接使用上述环境变量。
编译zlib
git clone
源码 (https://github.com/madler/zlib) 到 external
目录下,进入 zlib
源码目录,可以看到有 CMakeLists.txt
,因此我们可以使用 CMake
工具来构建 zlib
。
查看 CMakeLists.txt
,并无复杂配置,也没有外部依赖,并且已经做了生成静态库的配置。
# zlib/CMakeLists.txt
...
add_library(zlibstatic STATIC ${ZLIB_SRCS} ${ZLIB_ASMS} ${ZLIB_PUBLIC_HDRS} ${ZLIB_PRIVATE_HDRS})
...
因此直接在 build-item.sh
中加入
cd /wasm/zlib # 在docker环境下进入映射的源码目录
emconfigure cmake . # 使用 emconfigure 调用 cmake 生成 makefile
emmake make # 使用 emmake 调用 make 生成 libz.a
执行 sh build.sh
,编译成功后,我们可以在 zlib
目录下看到 libz.a 文件。
注意:cmake 执行后会生成缓存文件,包括 CMakeCache.txt、CMakeFiles 目录等,修改配置后需要删掉缓存文件再执行构建。
编译libpng
git clone
源码 (https://github.com/glennrp/libpng) 并进入源码目录,有 CMakeLists.txt
,也可以用 CMake
来构建。
查看 CMakeLists.txt
,发现里面有生成动态库的选项 PNG_SHARED
和测试的选项 PNG_TESTS
,都可以不用。另外有两个外部依赖 zlib
和 m
,m
在 ubuntu 环境下不能自动被搜索到,因此需要自己配置。
# libpng/CMakeLists.txt
...
option(PNG_BUILD_ZLIB "Custom zlib Location, else find_package is used" OFF)
if(NOT PNG_BUILD_ZLIB)
find_package(ZLIB REQUIRED)
include_directories(${ZLIB_INCLUDE_DIR})
endif()
if(UNIX AND NOT APPLE AND NOT BEOS AND NOT HAIKU)
find_library(M_LIBRARY m)
else()
# libm is not needed and/or not available
set(M_LIBRARY "")
endif()
# COMMAND LINE OPTIONS
option(PNG_SHARED "Build shared lib" ON)
option(PNG_STATIC "Build static lib" ON)
option(PNG_TESTS "Build libpng tests" ON)
...
在 build-item.sh
中加入
cd /wasm/libpng
emconfigure cmake . -DPNG_SHARED=OFF -DPNG_TESTS=OFF -DZLIB_INCLUDE_DIR=/wasm/zlib -DZLIB_LIBRARY=/wasm/zlib -DM_LIBRARY=/usr/lib/x86_64-linux-gnu # -D<key>=<value>是在命令行上给cmake添加变量的格式
emmake make
编译libjpeg
git clone
源码 (https://github.com/LuaDist/libjpeg) 并进入源码目录,查看 CMakeLists.txt
,发现编译静态库选项默认关闭,需要手动开启。
# libjpeg/CMakeLists.txt
...
OPTION(BUILD_STATIC OFF)
OPTION(BUILD_EXECUTABLES ON)
OPTION(BUILD_TESTS ON)
...
在 build-item.sh
中加入
cd /wasm/libjpeg
emconfigure cmake . -DBUILD_STATIC=ON
emmake make
编译libwebp
git clone
源码 (https://github.com/webmproject/libwebp) 并进入源码目录,查看 CMakeLists.txt
,libwebp
为编译到 WebAssembly
增加了专门的编译选项 WEBP_BUILD_WEBP_JS
,需要我们手动开启。另外需要提供 zlib
,libpng
,libjpeg
的库路径和头文件路径。
但是之后从 ImageMagick
的编译检查项中发现,ImageMagick
不但需要 libwebp
还需要 libwebpmux
,目前的 CMakeLists.txt
在开启 WEBP_BUILD_WEBP_JS
时不会编译出 libwebpmux
,因此我们对 CMakeLists.txt
做如下修改:
# libwebp/CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(WebP C)
# Options for coder / decoder executables.
option(WEBP_BUILD_LIBWEBPMUX "Build the libwebpmux." ON) # 增加此行选项,用于开启对libwebpmux的编译
option(WEBP_ENABLE_SIMD "Enable any SIMD optimization." ON)
option(WEBP_BUILD_ANIM_UTILS "Build animation utilities." ON)
...
if(WEBP_BUILD_GIF2WEBP OR WEBP_BUILD_IMG2WEBP OR WEBP_BUILD_LIBWEBPMUX) # 修改此行,增加 OR WEBP_BUILD_LIBWEBPMUX 此if为true就会编译libwebpmux
parse_makefile_am(${CMAKE_CURRENT_SOURCE_DIR}/src/mux "WEBP_MUX_SRCS" "")
add_library(libwebpmux ${WEBP_MUX_SRCS})
target_link_libraries(libwebpmux webp)
...
然后在 build-item.sh
中加入
cd /wasm/libwebp
emconfigure cmake . -DWEBP_BUILD_WEBP_JS=ON -DZLIB_INCLUDE_DIR=/wasm/zlib -DZLIB_LIBRARY=/wasm/zlib -DPNG_LIBRARY=/wasm/libpng -DPNG_PNG_INCLUDE_DIR=/wasm/libpng -DJPEG_LIBRARY=/wasm/libjpeg -DJPEG_INCLUDE_DIR=/wasm/libjpeg
emmake make
提示:
libwebp
在编译时会生成webp_wasm.js
和webp_wasm.wasm
,使用此模块能够使不支持webp的浏览器显示webp图片,我把此模块和官方示例也提取了出来,地址为 https://mk33mk333.github.io/wasm-im/webp-wasm.html,有兴趣的同学可以研究。
编译ImageMagick
前面几个库相对比较小,编译比较简单,ImageMagick
的编译比较复杂。
首先下载源码,进入源码目录。ImageMagick
没有支持 CMake
,所以我们只能用 autotool
工具来编译。
autotool
比 CMake
复杂些,流程如下:
autotool生成流程
简单的说,就是
- 编写
configure.ac
,执行autoconf
生成configure
- 编写
Makefile.in
,执行./configure
生成Makefile
make && make install
我们主要关注 configure.ac
中的配置。
在 configure.ac
文件的结尾处,我们可以看到 ImageMagick
的配置选项:
#ImageMagick/configure.ac
...
# ==============================================================================
# ImageMagick Configuration
# ==============================================================================
AC_MSG_NOTICE([
==============================================================================
${PACKAGE_NAME} ${PACKAGE_VERSION}${PACKAGE_VERSION_ADDENDUM} is configured as follows. Please verify that this
configuration matches your expectations.
Host system type: $host
Build system type: $build
Option Value
------------------------------------------------------------------------------
Shared libraries --enable-shared=$enable_shared $libtool_build_shared_libs
Static libraries --enable-static=$enable_static $libtool_build_static_libs
Build utilities --with-utilities=$with_utilities $with_utilities
Module support --with-modules=$build_modules $build_modules
GNU ld --with-gnu-ld=$with_gnu_ld $lt_cv_prog_gnu_ld
Quantum depth --with-quantum-depth=$with_quantum_depth $with_quantum_depth
High Dynamic Range Imagery
--enable-hdri=$enable_hdri $enable_hdri
Install documentation: $wantdocs
Memory allocation library:
JEMalloc --with-jemalloc=$with_jemalloc $have_jemalloc
TCMalloc --with-tcmalloc=$with_tcmalloc $have_tcmalloc
UMem --with-umem=$with_umem $have_umem
Delegate library configuration:
BZLIB --with-bzlib=$with_bzlib $have_bzlib
Autotrace --with-autotrace=$with_autotrace $have_autotrace
DJVU --with-djvu=$with_djvu $have_djvu
DPS --with-dps=$with_dps $have_dps
FFTW --with-fftw=$with_fftw $have_fftw
FLIF --with-flif=$with_flif $have_flif
FlashPIX --with-fpx=$with_fpx $have_fpx
FontConfig --with-fontconfig=$with_fontconfig $have_fontconfig
FreeType --with-freetype=$with_freetype $have_freetype
Ghostscript lib --with-gslib=$with_gslib $have_gslib
Graphviz --with-gvc=$with_gvc $have_gvc
HEIC --with-heic=$with_heic $have_heic
JBIG --with-jbig=$with_jbig $have_jbig
JPEG v1 --with-jpeg=$with_jpeg $have_jpeg
JPEG XL --with-jxl=$with_jxl $have_jxl
LCMS --with-lcms=$with_lcms $have_lcms
LQR --with-lqr=$with_lqr $have_lqr
LTDL --with-ltdl=$with_ltdl $have_ltdl
LZMA --with-lzma=$with_lzma $have_lzma
Magick++ --with-magick-plus-plus=$with_magick_plus_plus $have_magick_plus_plus
OpenEXR --with-openexr=$with_openexr $have_openexr
OpenJP2 --with-openjp2=$with_openjp2 $have_openjp2
PANGO --with-pango=$with_pango $have_pango
PERL --with-perl=$with_perl $have_perl
PNG --with-png=$with_png $have_png
RAQM --with-raqm=$with_raqm $have_raqm
RAW --with-raw=$with_raw $have_raw
RSVG --with-rsvg=$with_rsvg $have_rsvg
TIFF --with-tiff=$with_tiff $have_tiff
WEBP --with-webp=$with_webp $have_webp
WMF --with-wmf=$with_wmf $have_wmf
X11 --with-x=$with_x $have_x
XML --with-xml=$with_xml $have_xml
ZLIB --with-zlib=$with_zlib $have_zlib
ZSTD --with-zstd=$with_zstd $have_zstd
Delegate program configuration:
GhostPCL None $PCLDelegate ($PCLVersion)
GhostXPS None $XPSDelegate ($XPSVersion)
Ghostscript None $PSDelegate ($GSVersion)
Font configuration:
Apple fonts --with-apple-font-dir=$with_apple_font_dir $result_apple_font_dir
Dejavu fonts --with-dejavu-font-dir=$with_dejavu_font_dir $result_dejavu_font_dir
Ghostscript fonts --with-gs-font-dir=$with_gs_font_dir $result_ghostscript_font_dir
URW-base35 fonts --with-urw-base35-font-dir=$with_urw_base35_font_dir $result_urw_base35_font_dir
Windows fonts --with-windows-font-dir=$with_windows_font_dir $result_windows_font_dir
X11 configuration:
X_CFLAGS = $X_CFLAGS
X_PRE_LIBS = $X_PRE_LIBS
X_LIBS = $X_LIBS
X_EXTRA_LIBS = $X_EXTRA_LIBS
Options used to compile and link:
PREFIX = $PREFIX_DIR
EXEC-PREFIX = $EXEC_PREFIX_DIR
VERSION = $PACKAGE_VERSION
CC = $CC
CFLAGS = $CFLAGS
CPPFLAGS = $CPPFLAGS
PCFLAGS = $PCFLAGS
DEFS = $DEFS
LDFLAGS = $LDFLAGS
LIBS = $MAGICK_DEP_LIBS
CXX = $CXX
CXXFLAGS = $CXXFLAGS
FEATURES = $MAGICK_FEATURES
DELEGATES = $MAGICK_DELEGATES
==============================================================================
])
...
如果需要更详细的说明,可以参考 advanced-unix-installation
(https://imagemagick.org/script/advanced-unix-installation.php)。
我们主要关注:
- Shared libraries && Static libraries(生成动态库/静态库)
- Delegate library configuration(可选组件)
- Options used to compile and link(编译参数)
我们需要禁用掉没有编译的可选组件,并且添加 CPPFLAG
、 LDFLAG
环境变量。
CPPFLAG
:预编译参数,预编译主要涉及头文件的查找,因此我们要把之前编译的几个库的头文件路径添加进去。
LDFLAG
:链接器参数,链接需要找到所需的库文件和对象文件的位置,因此要把之前编译的几个库的库文件路径添加进去。
在 build-item.sh
中加入
export CPPFLAGS="-I/wasm/libpng -I/wasm/zlib -I/wasm/libjpeg -I/wasm/libwebp/src"
export LDFLAGS="-L/wasm/zlib -L/wasm/libpng -L/wasm/libpng/.libs -L/wasm/libjpeg -L/wasm/libwebp"
除此之外,我们从 configure.ac
中可以发现,它使用 PKG_CHECK_MODULES()
来查找组件是否存在,例如:
#ImageMagick/configure.ac
...
#
# Check for ZLIB
#
AC_ARG_WITH([zlib],
[AC_HELP_STRING([--without-zlib],
[disable ZLIB support])],
[with_zlib=$withval],
[with_zlib='yes'])
if test "$with_zlib" != 'yes'; then
DISTCHECK_CONFIG_FLAGS="${DISTCHECK_CONFIG_FLAGS} --with-zlib=$with_zlib "
fi
have_zlib='no'
ZLIB_CFLAGS=""
ZLIB_LIBS=""
ZLIB_PKG=""
if test "x$with_zlib" = "xyes"; then
AC_MSG_RESULT([-------------------------------------------------------------])
PKG_CHECK_MODULES(ZLIB,[zlib >= 1.0.0], have_zlib=yes, have_zlib=no)
AC_MSG_RESULT([])
fi
if test "$have_zlib" = 'yes'; then
AC_DEFINE(ZLIB_DELEGATE,1,Define if you have ZLIB library)
CFLAGS="$ZLIB_CFLAGS $CFLAGS"
LIBS="$ZLIB_LIBS $LIBS"
fi
AM_CONDITIONAL(ZLIB_DELEGATE, test "$have_zlib" = 'yes')
AC_SUBST(ZLIB_CFLAGS)
AC_SUBST(ZLIB_LIBS)
...
PKG_CHECK_MODULES()
调用 pkg-config 工具查找格式为 *.pc 的配置文件,从中获取组件信息。我们可以看到,zlib
,libpng
等下面都有生成的 *.pc 文件。因此我们只要给 pkg-config
指定搜索路径就可以了,指定搜索路径的方式是添加参数 PKG_CONFIG_PATH
。
在 build-item.sh
中加入
cd /wasm/ImageMagick #进入源码目录
autoreconf -fi # 重新用configure.ac生成configure文件
# 指定PKG_CONFIG_PATH、禁用未编译的组件、不生成动态库
emconfigure ./configure --prefix=/ --disable-shared --without-threads --without-magick-plus-plus --without-perl --without-x --disable-largefile --disable-openmp --without-bzlib --without-dps --without-freetype --without-jbig --without-openjp2 --without-lcms --without-wmf --without-xml --without-fftw --without-flif --without-fpx --without-djvu --without-fontconfig --without-raqm --without-gslib --without-gvc --without-heic --without-lqr --without-openexr --without-pango --without-raw --without-rsvg --without-xml PKG_CONFIG_PATH="/wasm/libpng:/wasm/zlib:/wasm/libjpeg:/wasm/libwebp/src:/wasm/libwebp/src/mux:/wasm/libwebp/src/demux:"
emmake make
make 以后,没有生成 *.a 文件,却生成了 *.la 文件,对于 *.la 文件,我们需要用 libtool 工具来处理,另外,我们最终需要一个能够执行的程序,而不只是一个库文件,因此,我们要把库文件和带有 main 方法的入口一起编译,最后生成我们可用的 wasm 模块。
如果我们想在 js 中像在 linux 中执行命令那样使用 ImageMagick
,需要有调用 main 方法的能力,按照官方文档 (https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html) 提供的方式,生成 wasm 模块的命令类似这样:
# 输出运行时方法,ccall和cwrap,输出模块里的全局方法int_sqrt
./emcc tests/hello_function.cpp -o function.js -s EXPORTED_FUNCTIONS='["_int_sqrt"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'
因此,在 build-item.sh
中加入
# --tag 标明源码为C语言,--mode 表示此次动作为链接
# libMagickCore-7.Q16HDRI.la 为 ImageMagick 底层库
# libMagickWand-7.Q16HDRI.la 为 ImageMagick 上层库
# magick.o 为含有main方法和命令行调用逻辑的入口对象
# 在dist目录下生成wasm模块和js胶水文件,命名为wasm-im.js和wasm-im.wasm
# 导出main方法
/bin/bash ./libtool --tag=CC --mode=link emcc $CFLAGS $LDFLAGS -o /wasm/dist/wasm-im.js -s EXTRA_EXPORTED_RUNTIME_METHODS='["callMain"]' utilities/magick.o MagickCore/libMagickCore-7.Q16HDRI.la MagickWand/libMagickWand-7.Q16HDRI.la
编写js侧的调用代码
现在我们已经构建好的 wasm-im.wasm
,同时获得了一个 wasm-im.js
。此文件是一个胶水 js,封装了 wasm 模块的一些基本功能,我们可以直接使用它。
wasm-im.js
向外暴露了 Module 对象,我们对 wasm 模块的一切调用都可以通过 Module
对象完成。
Module
对象的官方说明在这里 (https://emscripten.org/docs/api_reference/module.html)。
我们可以在 Module.onRuntimeInitialized
的回调函数中使用 Module.callMain
来调用 ImageMagick
。
Module.onRuntimeInitialized = function () {
// 调用main方法
Module.callMain(command)
}
使用虚拟文件系统
ImageMagick
处理图片,自然会涉及到文件的读取、写入,浏览器并没有广泛支持的文件系统API,浏览器本身有沙箱限制,也不能访问操作系统本地的文件系统。
WebAssembly
同样受到沙箱限制,因此提供了虚拟文件系统来适配C/C++程序对于文件系统的调用。
WebAssembly
提供了 MEMFS
,NODEFS
,IDBFS
三种虚拟文件系统,其中 NODEFS
专门供 Node.js 环境使用,直接调用本地文件系统。MEMFS
是在内存中建立的虚拟文件系统。IDBFS
是基于 IndexDB
的虚拟文件系统。胶水 js wasm-im.js
中同样包含了对虚拟文件系统的封装,默认使用 MEMFS
。我们可以使用 FS
对象来进行文件操作。例如:
FS.mkdir('/im');// mkdir im
FS.currentPath = '/im'; // cd im
FS.open('/im');// 打开文件夹
FS.readFile('animation.gif') // 读取文件内容
像在linux中执行命令那样使用ImageMagick
我们可以用官方的命令行实例 (https://imagemagick.org/Usage/anim_basics/#gif_anim)来验证测试wasm模块。
# 背景颜色:SkyBlue 间隔100ms 分别在位置5,10展示balloon.gif 35,30展示medical.gif 62,50展示present.gif 10,55展示shading.gif
# 无限次循环 在当前目录下生成文件为 animation.gif
convert -delay 100 -size 100x100 xc:SkyBlue
-page +5+10 balloon.gif -page +35+30 medical.gif
-page +62+50 present.gif -page +10+55 shading.gif
-loop 0 animation.gif
我们知道main函数签名为 int main(int argc,char **argv)
,然后来看 Module.callMain
源码:
function callMain(args) {
args = args || [];
var argc = args.length + 1;
var argv = stackAlloc((argc + 1) * 4);
HEAP32[argv >> 2] = allocateUTF8OnStack(thisProgram);
for (var i = 1; i < argc; i++) {
HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i - 1])
}
HEAP32[(argv >> 2) + argc] = 0;
try {
var ret = Module["_main"](argc, argv);
exit(ret, true)
} catch (e) {
if (e instanceof ExitStatus) {
return
} else if (e == "SimulateInfiniteLoop") {
noExitRuntime = true;
return
} else {
var toLog = e;
if (e && typeof e === "object" && e.stack) {
toLog = [e, e.stack]
}
err("exception thrown: " + toLog);
quit_(1, e)
}
} finally {
calledMain = true
}
}
显然,我们只要把需要执行的命令用空格分隔成字符串数组作为 callMain
的参数即可。
// 把图片换成jpg
var command = ['convert','-delay', '100', '-size', '100x100' ,'xc:SkyBlue',
'-page','+5+10','1.jpg','-page','+35+30','2.jpg',
'-page','+62+50','3.jpg','-page','+10+55','4.jpg',
'-loop','0','animation.gif']
执行 calledMain
前,我们需要把四张原图在文件系统中准备好。
// 用fetch获取四张图片
FS.writeFile('1.jpg', new Uint8Array(await(await fetch("1.jpg")).arrayBuffer()))
FS.writeFile('2.jpg', new Uint8Array(await(await fetch("2.jpg")).arrayBuffer()))
FS.writeFile('3.jpg', new Uint8Array(await(await fetch("3.jpg")).arrayBuffer()))
FS.writeFile('4.jpg', new Uint8Array(await(await fetch("4.jpg")).arrayBuffer()))
执行 calledMain
后,我们可以读取出生成的文件。
var gif = FS.readFile('animation.gif')
var gifBlob = new Blob([gif]);
var img = document.createElement('img');
img.src = URL.createObjectURL(gifBlob);
document.body.appendChild(img)
在worker中执行命令
对 ImageMagick
的调用很适合放到 worker
中执行。需要注意的是,postMessage
时如果传递了存放文件内容的 arrayBuffer
,需要将其放进 transferList,避免无谓的数据拷贝。
完整的示例在 https://mk33mk333.github.io/wasm-im/
其他构建方式
Emscripten
工具链提供了 Emscripten Ports,内置了一批常用库,其中包括了 zlib
、 libpng
、 libjpeg
等。使用方式是在编译参数上增加对应的变量,比如想链接 libpng
,就添加-s USE_LIBPNG=1
。但是对于编译 libwebp
、 ImageMagick
这种成型库,要大幅修改构建脚本,有兴趣的同学可以尝试。
可用内置库如下:
Available ports:
Boost headers v1.70.0 (USE_BOOST_HEADERS=1; Boost license)
icu (USE_ICU=1; Unicode License)
zlib (USE_ZLIB=1; zlib license)
bzip2 (USE_BZIP2=1; BSD license)
libjpeg (USE_LIBJPEG=1; BSD license)
libpng (USE_LIBPNG=1; zlib license)
SDL2 (USE_SDL=2; zlib license)
SDL2_image (USE_SDL_IMAGE=2; zlib license)
SDL2_gfx (zlib license)
ogg (USE_OGG=1; zlib license)
vorbis (USE_VORBIS=1; zlib license)
SDL2_mixer (USE_SDL_MIXER=2; zlib license)
bullet (USE_BULLET=1; zlib license)
freetype (USE_FREETYPE=1; freetype license)
harfbuzz (USE_HARFBUZZ=1; MIT license)
SDL2_ttf (USE_SDL_TTF=2; zlib license)
SDL2_net (zlib license)
cocos2d
regal (USE_REGAL=1; Regal license)
浏览器兼容性
wasm兼容性
从图中可以看到,除了 IE 完全不支持,其他主流浏览器已经支持 WebAssembly
。
对于 IE,我们可以考虑使用 asm.js
或者其他降级方式,需要根据实际需求来决定。
总结
本次我们把 ImageMagick
编译成 wasm
模块,并运行在浏览器中。但是我们只使用了最简单的功能:调用 main 方法。
没有写一行 C/C++ 代码,更没有涉及到 js/C++ 方法互调、js/C++ 对象绑定等更复杂的实践。
之后我们会深入研究更复杂的应用和实践。
如果对在浏览器中使用 ImageMagick
的成熟方案感兴趣,可以关注WASM-imageMagick (https://github.com/KnicKnic/WASM-ImageMagick),在 js 侧使用 Typescript
进行了完善的封装,提供了 Typescript API
。
参考
WebAssembly MDN
(https://developer.mozilla.org/zh-CN/docs/WebAssembly)
emscripten
(https://emscripten.org/index.html)
RuntimeError: integer overflow when convert double to int in wasm mode
(https://github.com/emscripten-core/emscripten/issues/5404)
makefile编译选项CC与CXX/CPPFLAGS,CFLAGS与CXXFLAGS/LDFLAGS
(https://blog.****.net/lusic01/article/details/78645316)
pkg-config 详解
(https://blog.****.net/newchenxf/article/details/51750239)
WebAssembly进阶系列三:微信小程序支持webP的WebAssembly方案
(https://juejin.im/post/5d32e2c2f265da1b897b0b57)
ImageMagick中文站
(http://www.imagemagick.com.cn/)