首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WebAssembly实战-在浏览器中使用ImageMagick

WebAssembly实战-在浏览器中使用ImageMagick

作者头像
WecTeam
发布于 2019-12-16 14:39:28
发布于 2019-12-16 14:39:28
8.3K2
举报
文章被收录于专栏:WecTeamWecTeam

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) 提供的环境搭建方式:

代码语言:javascript
复制

# 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):

代码语言:javascript
复制
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 库我们安装时,一般流程就是:

代码语言:javascript
复制
./configure  # 检查系统环境,判断当前环境是否满足编译条件

make # 执行编译

make install  # 将二进制应用程序安装的指定位置

详细的 CMake 、 autotool 使用可以写一本书,本次我们主要关注生成静态库、查找依赖、查找头文件的配置。

建立项目

环境:win10 下的 virtualbox6.0.4 内的 Ubuntu 18.10

本文源码链接:https://github.com/mk33mk333/wasm-im

项目结构:

代码语言:javascript
复制
# 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 里。

代码语言:javascript
复制
# 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 用的编译参数。

代码语言:javascript
复制
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,并无复杂配置,也没有外部依赖,并且已经做了生成静态库的配置。

代码语言:javascript
复制
# zlib/CMakeLists.txt
...
add_library(zlibstatic STATIC ${ZLIB_SRCS} ${ZLIB_ASMS} ${ZLIB_PUBLIC_HDRS} ${ZLIB_PRIVATE_HDRS})
...

因此直接在 build-item.sh 中加入

代码语言:javascript
复制
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 环境下不能自动被搜索到,因此需要自己配置。

代码语言:javascript
复制
# 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 中加入

代码语言:javascript
复制
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,发现编译静态库选项默认关闭,需要手动开启。

代码语言:javascript
复制
# libjpeg/CMakeLists.txt
...
OPTION(BUILD_STATIC OFF)
OPTION(BUILD_EXECUTABLES ON)
OPTION(BUILD_TESTS ON)
...

在 build-item.sh 中加入

代码语言:javascript
复制
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 做如下修改:

代码语言:javascript
复制
# 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 中加入

代码语言:javascript
复制

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 的配置选项:

代码语言:javascript
复制
#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 中加入

代码语言:javascript
复制
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()来查找组件是否存在,例如:

代码语言:javascript
复制
#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 中加入

代码语言:javascript
复制

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 模块的命令类似这样:

代码语言:javascript
复制
# 输出运行时方法,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 中加入

代码语言:javascript
复制

# --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。

代码语言:javascript
复制

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 对象来进行文件操作。例如:

代码语言:javascript
复制
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模块。

代码语言:javascript
复制
# 背景颜色: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 源码:

代码语言:javascript
复制

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 的参数即可。

代码语言:javascript
复制
// 把图片换成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 前,我们需要把四张原图在文件系统中准备好。

代码语言:javascript
复制
// 用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 后,我们可以读取出生成的文件。

代码语言:javascript
复制
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 这种成型库,要大幅修改构建脚本,有兴趣的同学可以尝试。

可用内置库如下:

代码语言:javascript
复制
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.csdn.net/lusic01/article/details/78645316)

pkg-config 详解 (https://blog.csdn.net/newchenxf/article/details/51750239)

WebAssembly进阶系列三:微信小程序支持webP的WebAssembly方案 (https://juejin.im/post/5d32e2c2f265da1b897b0b57)

ImageMagick中文站 (http://www.imagemagick.com.cn/)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • WebAssembly & ImageMagick
  • Emscripten工具链
  • 环境搭建
    • 常规的环境搭建方式
    • 使用docker快速搭建环境
  • ImageMagick依赖分析
  • 编译依赖库
    • 建立项目
      • 建立编译脚本入口
      • 增加全局编译参数
    • 编译zlib
    • 编译libpng
    • 编译libjpeg
    • 编译libwebp
    • 编译ImageMagick
  • 编写js侧的调用代码
    • 使用虚拟文件系统
    • 像在linux中执行命令那样使用ImageMagick
    • 在worker中执行命令
    • 其他构建方式
  • 浏览器兼容性
  • 总结
  • 参考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档