前言

目前,网上关于android 移植打印机驱动的基本是以下几个方向:

  • ghostscript : gs 本身主要是一个文件转换器 ,对打印机的支持比较有限,需要借助其他filter
    如 hpijs foo2zjs
  • cups :cups 的功能非常强大 也是linux 上非常通用的 打印系统,但是正因为强大,所以东西太复杂,不利于移植,搞到一半实在搞不下去
  • ghostscript+ foo2zjs : 这种方案太老了,支持的打印机非常有限,要支持新的打印机基本不可能,而且foo2zjs 的源码都难找 (需要的话 可以去github上搜到)
  • ghostscript+ hpijs :这也是很老的方案,新打印机很多都不支持 ,hpijs 被合并到hplip中已经不再更新

我的学习:
本来想一步到位 ,直接移植cups ,但发现依赖是在太多,7.1 的内核包含的库少 而且旧 ,cusp静态编译又一堆问题,最后放弃。
然后尝试ghostscript+hplip ,但是hplip也很庞杂,而且严重依赖python,但是在研究的过程中发现,hplip 中包含了hpijs 的最新代码,所以就采用了 ghostscript+hpijs(hplip)的方案

前置条件

1 android7.1  的编译环境  交叉编译用
 2 ubuntu 18 以上,(我用的18 ),最好不要用虚拟机,编译太慢 
 3 vscode  : linux 上看代码必备,
 4 源码下载:ghostscript 10,  hplip3.23.3 这两项代码都好下载,下载最新的即可

编译ghostscript

1 配置;./configure --host=aarch64-linux
2 修改makefile :找到STDLIBS 在最后加上 -static
3 make 生成./bin/gs
ghostscript 的编译非常简单,生成后用readelf -d ./bin/gs 查看

Dynamic section at offset 0x1cae260 contains 34 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libXt.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libX11.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libm.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libdl.so.2]
 0x0000000000000001 (NEEDED)             共享库:[libpthread.so.0]
 0x0000000000000001 (NEEDED)             共享库:[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]

非静态编译会显示依赖库,这是无法在android的linux 环境运行的

There is no dynamic section in this file.

静态编译会提示该信息

最好做两份 一份pc版,一份arm版,pc版主要后面调试用到。先在pc上调通,然后移植到arm板上,直接在板子上调是不太现实的。

  • 运行 :
    生成的可执行文件为./bin/gs,拷贝到android 的/system/bin下
    执行
    gs -h
    会有打印消息

编译hplip (hpijs)

  • 配置
./configure --host=aarch64-linux --enable-network-build=no --enable-libusb01_build --enable-scan-build=no --enable-dbus-build=no --enable-fax-build=no --enable-hpijs-only-build --enable-hpijs-install

主要是编译hpijs 所以用到–enable-hpijs-only-build
同时编译一份pc版,调试主要是在pc上调,通了后移植到板子上就只剩编译问题了。

pc上的配置 将–host=aarch64-linux去掉,也可以将–enable-hpijs-only-build 去掉,编译出来有很多工具,有兴趣可以研究研究。

  • 修改makefile 设置静态编译
    找到 hpijs_LINK ,在-o 前面加上 -all-static
hpijs_LINK = $(LIBTOOL) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) \
	--mode=link $(CXXLD) $(hpijs_CXXFLAGS) $(CXXFLAGS) \
	$(AM_LDFLAGS) $(LDFLAGS) -all-static -o $@

pc版可以不设置,pc上没有库依赖的问题

  • make
    编译完成后,在当前目录或./.lib 下生成hpijs ,注意:如果没有用-enable-hpijs-only-build 当前目录下生成的hpijs 是脚本文件,可执行文件在./.lib 下。同样可以用readelf 查看生成文件的属性。
  • 运行
    运行 ./hpijs -h
HP Co. Inkjet Server 3.23.3
Copyright (c) 2001-2004, HP Co.

出现该消息 说明编译成功 ,android 需要将bin文件放到 system/bin 下,

  • 打印
    android 上要打印还需要将内核配置成支持USB打印机:
Device Drivers --->

         [*] USB support --->

                     <*> USB Printer support

如此,在插上打印机后 会产生/dev/usb/lp0节点 ,有些打印机需要下载固件,在下载固件之前,它可能映射成其它设备,无法生存该节点,这种打印机目前没有办法。

运行命令,需要root权限,pc 上需要先sudo -i , sudo好像不行.

gs -sDEVICE=ijs -sIjsServer=hpijs -dIjsUseOutputFD -sDeviceManufacturer="HEWLETT-PACKARD" -sDeviceModel="DESKJET 825" -dNOPAUSE -dBATCH -dPARANOIDSAFER -dQUIET -sOutputFile="/dev/usb/lp0" ./examples/waterfal.ps
  • -sDEVICE=ijs -sIjsServer=hpijs
    这两个选项是配置 gs 的ijs插件,为hpijs,
  • -sDeviceManufacturer=“HEWLETT-PACKARD”
    hpijs 用到的参数,只能设置为 HEWLETT-PACKARD APOLLO HP 三者之一,但其实没什么意义,只是单纯的判断以下:
else if (!strcmp (key, "DeviceManufacturer"))    {
       if ((strncasecmp(svalue, "HEWLETT-PACKARD", 15) != 0) &&
           (strncasecmp(svalue, "APOLLO", 6) != 0) && (strncasecmp(svalue, "HP", 2) != 0))
       {
           BUG("unable to set DeviceManufacturer=%s\n", svalue);
           status = -1;
       }    }
  • -sDeviceModel=“DESKJET 825”
    hpijs用到的参数,该参数非常重要,设置不对打印不了。其实就是设置具体打印机。
    想要知道hpijs 是否支持,在hplip代码中搜索对应型号,如:HP LaserJet P1102,找到:
class LJZjsMonoProxy : public PrinterProxy
{
public:
    LJZjsMonoProxy() : PrinterProxy(
        "LJZjsMono",
        "HP LaserJet 1000\0"
        "HP LaserJet 1005\0"
        "HP LaserJet 1018\0"
        "HP LaserJet 1020\0"
        "HP LaserJet 1022\0"
        "HP LaserJet P2035\0"
        "HP LaserJet P1102\0" //**<--搜索到该打印机 说明支持**
		"HP LaserJet P1566\0"
		"HP LaserJet P1606\0"
        "HP LaserJet Professional M1136\0"
        "HP LaserJet Professional M1132\0"
        "HP LaserJet Professional M1212nf\0"
    ) {m_iPrinterType = eLJZjsMono;}
    inline Printer* CreatePrinter(SystemServices* pSS) const { return new LJZjsMono(pSS); }
	inline PRINTER_TYPE GetPrinterType() const { return eLJZjsMono;}
	inline unsigned int GetModelBit() const { return 0x40;}
};

不过可惜的是 HP LaserJet P1102 就是上面提到的需要下载固件的打印机,android上无法产生/dev/usb/lp0 节点。
关于该参数后面会详细分析。到这里很可能还是打印不成功的。

  • -dQUIET
    完成后 退出gs 程序,否则会一直卡在gs 程序里,需要ctrl+c 退出。android上用app调用shell命令,该参数非常重要。
  • -sOutputFile=“/dev/usb/lp0”
    指向 打印机节点。
  • ./examples/waterfal.ps
    命令最后是输入文件,该文件为 ghostscript 中examples 的一个postscript的示例,输入文件也可以是pdf文件

ijs服务概要

前面的工作做完了,但是打印不一定会成功,最主要的是DeviceModel设置的问题,那到底该参数是如何作用的呢?
既然代码都有,那就直接分析分析代码好了。了解以下ijs服务,对后期调试也很有帮助。

ijs 是一个比较简单的服务,主要函数就下面几个:

ijs_server_init();
ijs_server_install_status_cb ();
ijs_server_install_list_cb ();
ijs_server_install_enum_cb ();
ijs_server_install_set_cb ();
ijs_server_install_get_cb ();
ijs_server_get_page_header()

还有一个最核心的函数表:

ijs_server_proc ijs_server_procs[] = {
  ijs_server_proc_ack,
  ijs_server_proc_nak,
  ijs_server_proc_ping,
  ijs_server_proc_pong,
  ijs_server_proc_open,
  ijs_server_proc_close,
  ijs_server_proc_begin_job,
  ijs_server_proc_end_job,
  ijs_server_proc_cancel_job,
  ijs_server_proc_query_status,
  ijs_server_proc_list_params,
  ijs_server_proc_enum_param,
  ijs_server_proc_set_param,
  ijs_server_proc_get_param,
  ijs_server_proc_begin_page,
  ijs_server_proc_send_data_block,
  ijs_server_proc_end_page,
  ijs_server_proc_exit
};
  • ijs_server_init();
fd_from = 0;
  fd_to = 1;
  ijs_recv_init (&ctx->recv_chan, fd_from);
  ijs_send_init (&ctx->send_chan, fd_to);

初始化函数 主要工作就是创建了两个文件接口,一个收一个发,分别对应fd 0,fd 1。

-ijs_server_get_page_header()
该函数会读取fd_from 文件 接收从客户端发送的消息。

while (1)
   {
      if ((ret = ijs_server_get_page_header(ctx, &pSS->ph)) < 0)
      {
         BUG("unable to read client data err=%d\n", ret);
         goto BUGOUT;
      }
     .............
     ....
     ....
 }

如是,就启动了ijs常驻服务。
ijs_server_get_page_header 函数非常简单:

ijs_server_get_page_header (IjsServerCtx *ctx, IjsPageHeader *ph)
{
  int status;

  ctx->ph = ph;
  ctx->in_page = FALSE;

  do 
    {
      status = ijs_server_iter (ctx);
    }
  while (status == 0 && !ctx->in_page);

  ctx->ph = NULL;
  return status;
}
  • ijs_server_iter

该函数接收客户端消息,然后解析得到cmd_num ,然后查ijs_server_procs表,并调用相应处理函数。
这就是ijs 的总体处理结构,相当简单的服务。

int
ijs_server_iter (IjsServerCtx *ctx)
{
  int cmd_num;
  int status;

  status = ijs_recv_buf (&ctx->recv_chan);

  if (status < 0)
    return status;

  cmd_num = ijs_get_int (ctx->recv_chan.buf);
#ifdef VERBOSE
  fprintf (stderr, "command %d, %d bytes\n", cmd_num, ctx->recv_chan.buf_size);
#endif
  if (cmd_num < 0 ||
      cmd_num >= (int)sizeof(ijs_server_procs) / sizeof(ijs_server_procs[0]))
    return -1;
  return ijs_server_procs[cmd_num] (ctx);
}
  • ijs_server_proc_set_param

当客户端发送设置参数的命令时,就会调用ijs_server_procs 中的ijs_server_proc_set_param函数
该函数主要做一些会话验证工作,然后调用到ijs_server_set_param 函数;此函数处理ijs自身的参数设置命令,以及第三方参数设置命令。最主要的就是第三方设置命令,代码这里补贴了。

return ctx->set_cb (ctx->set_cb_data, ctx, job_id, key, value, value_size);

set_cb 就是第三方参数设置的回调函数接口,该结构在ijs_server_install_set_cb 中设置,hpijs的设置:

ijs_server_install_set_cb (ctx, hpijs_set_cb, pSS)
  • hpijs_set_cb
    从该函数中得知,hpijs 可以设置的参数很多,这里分析DeviceModel, 该参数就是在gs命名中 用-sDeviceModel设置
(r = pSS->pPC->SelectDevice(svalue)

当设置的打印机 在支持列表中时,设置正确,然后就可以打印了(至少现在是这么认为的)

ijs 的服务层 大概就是这样子了。

  • hpijs 打印机管理框架
    hpijs 的打印机管理非常简单粗暴,就是一个PrinterFactory 的单例,也就是全局变量。
#define pPFI PrinterFactory::GetInstance()

PrinterFactory 中存放了所有支持的打印机的 family 每个family 对应一个PrinterProxy
比如:

class LJM1005Proxy : public PrinterProxy
{
public:
    LJM1005Proxy() : PrinterProxy(
        "LJM1005",
        "LaserJet M1005\0"
        "HP LaserJet M1005\0"
        "HP LaserJet M1120\0"
        "HP LaserJet M1319\0"
        "HP LaserJet P1505\0"
        "HP LaserJet P2010\0"
        "HP LaserJet P2014\0"
        "HP LaserJet P2014n\0"
        "M1005\0"
    ) {m_iPrinterType = eLJM1005;}
    inline Printer* CreatePrinter(SystemServices* pSS) const { return new LJM1005(pSS); }
    inline PRINTER_TYPE GetPrinterType() const { return eLJM1005;}
    inline unsigned int GetModelBit() const { return 0x40;}
};

每一个family 有一个family name 如:LJM1005,和szModelNames ,szModelNames是一个字符串数组,对应的就是具体打印机,而devicemodel 既可以是 family name 也可以是modename

所有的PrinterProxy 都以静态全局变量的形式 定义在 internal.h中 ,总共有如下family:

family:AP2xxx 
family:AP21xx 
family:AP2560 
family:DJ3320 
family:DJD2600 
family:DJ4100 
family:DJ3600 
family:DJ350 
family:DJ540 
family:DJ600 
family:DJ630 
family:DJ6xx 
family:DJ6xxPhoto 
family:DJ850 
family:DJ890 
family:DJ8x5 
family:DJ8xx 
family:OJProKx50 
family:DJ9xxVIP 
family:DJ9xx 
family:DJ55xx 
family:GenericVIP 
family:PS470 
family:PS100 
family:LJP1XXX 
family:LJM1005 
family:LJZjsColor 
family:LJZjsMono 
family:LJFastRaster 
family:LJJetReady 
family:ColorLaser 
family:Mono Laser

PrinterProxy 在构造的时候 ,会调用PrinterFactory::Register(this); 将自己添加到支持列表中,
也就是前面提到的全局单例中(#define pPFI PrinterFactory::GetInstance())

PrinterProxy::PrinterProxy
(
    const char* szFamilyName,
    const char* szModelNames
) : m_szFamilyName(szFamilyName),
    m_szModelNames(szModelNames)
{
    unsigned int uCount = 0;
    char* tPtr = const_cast<char*>(m_szModelNames);
    while (*tPtr)
    {
        tPtr += strlen(tPtr)+1;
        uCount++;
    }
    m_uModelCount = uCount;

    PrinterFactory::Register(this);// <--将自己添加到支持列表中
    TRACE("PP::PP family is %s and supports %d models\n", GetFamilyName(), GetModelCount());
} //PrinterProxy

基本结构就大致如此。

hp-plugin

理论上讲,只要打印机在支持列表中,那么gs的打印命令中 -sDeviceModel 设置正确 就可以打印成功, 那这篇文章也就不存在了。因为我的HP LaserJet M1005 打印不成功,没办法只能 read the fucking source code。

明明列表中有HP LaserJet M1005 ,为啥不能打印?gs 提供的错误消息有限 ,而hpijs 作为服务端,无任何提示消息,hpijs.cpp在main中的一句话,给了一点点方向:

openlog("hpijs", LOG_PID,  LOG_DAEMON);

然后:

#define BUG(args...) syslog(LOG_ERR, __FILE__ " " STRINGIZE(__LINE__) ": " args)

因此 它肯定会在/var/log/下留下痕迹
于是在/var/log 下 grep -r -i “hpijs”
"unable to set device=HP LaserJet M1005, err=48

if ((r = pSS->pPC->SelectDevice(svalue)) != NO_ERROR)
{
     if (r == PLUGIN_LIBRARY_MISSING)
     {
         // call dbus here
         const char    *user_name = " ";
         const char    *title     = " ";
       const char *device_uri = getenv ("DEVICE_URI");
       const char *printer = getenv ("PRINTER");
         int     job_id = 0;

       if (device_uri == NULL)
            device_uri = "";
       if (printer == NULL)
            printer = "";

       SendDbusMessage (device_uri, printer,
 	                  EVENT_PRINT_FAILED_MISSING_PLUGIN,
		               user_name, job_id, title);
        BUG("unable to set device=%s, err=%d\n", svalue, r); //<====这里
        status = -1;
     }
     else
     {
        /* OfficeJet LX is not very unique, do separate check here. */
        if (!strncmp(svalue,"OfficeJet", 10))
        r = pSS->pPC->SelectDevice("DESKJET 540");
     }
}

PLUGIN_LIBRARY_MISSING的定义正好是0x30(48)

PLUGIN_LIBRARY_MISSING = 0x30,   //!< a required plugin (dynamic) library is missing

提示plugin 的库找不到
PLUGIN_LIBRARY_MISSING 用到的地方不多,最终确认是走的ljzjs.cpp

LJZjs::LJZjs (SystemServices* pSS, int numfonts, BOOL proto)
    : Printer(pSS, numfonts, proto)
{

    CMYMap = NULL;
#ifdef  APDK_AUTODUPLEX
    m_bRotateBackPage = FALSE;  // Lasers don't require back side image to be rotated
#endif
    m_pszInputRasterData = NULL;
    m_dwCurrentRaster = 0;
    m_bStartPageSent = FALSE;
    
    
    m_iPrinterType = UNSUPPORTED;
#ifdef HAVE_LIBDL
    HPLJJBGCompress = NULL;
    m_hHPLibHandle = NULL;
    m_hHPLibHandle = LoadPlugin ("lj.so");
    if (m_hHPLibHandle)
    {
        dlerror ();
        *(void **) (&HPLJJBGCompress) = dlsym (m_hHPLibHandle, "hp_encode_bits_to_jbig");
        *(void **) (&HPLJSoInit) = dlsym (m_hHPLibHandle, "hp_init_lib");
        if (!HPLJSoInit || (HPLJSoInit && !HPLJSoInit (1)))
        {
            constructor_error = PLUGIN_LIBRARY_MISSING;
        }
    }
#endif
    if (HPLJJBGCompress == NULL)
    {
        constructor_error = PLUGIN_LIBRARY_MISSING;
    }
	//Issue: LJZJSMono class printers not printing in RHEL 
	//Cause: Since start page is common for LJZJSMono and LJZJSColor class, the items of 
	//LJZJSColor-2 format was used for LJZJSMono due to below variable not initialised 
	//Fix: Added initialisation so that correct LJZJSMono items are used.  
	//Variable is updated in LJZJSColor. 
	m_bLJZjsColor2Printer = FALSE; 

}

LJZjs是一个printer 类 ,很多打印机继承与该类,HP LaserJet M1005 就是其中之一,这类打印机会加载lj.so库
m_hHPLibHandle = LoadPlugin (“lj.so”);
添加打印消息,确定全路径是./usr/share/hplip/prnt/plugins/lj.so
库是存在的,那就是 初始化失败

if (!HPLJSoInit || (HPLJSoInit && !HPLJSoInit (1)))

这就没有办法了,没有源码。

看代码发现仅仅用到了这个库中的一个函数 “hp_encode_bits_to_jbig” ,jbig是压缩编码,那是不是可以找到 jbig 的源码,然后实现这个函数呢?

jbig-android/jbig-android-library/src/main/jni/ 目录下即 jbig 压缩源码,然而拿到jbig源码也不知道从何下手,因为不知道hp到底做了些什么处理。

然而幸运的是,公司以前移植过 apdk ,apdk中 有这一段源码,于是搬过来直接用了,接下来就是添加编译改错,编译成功后终于HP LaserJet M1005 打印成功!

由于HPLJJBGCompress 的源码并非我的工作成果,HP也将其做成插件 未开放,所以不能上传。与上面的jbig源码相差不大,通过它来解决这个问题也许 可行。

然而,接下来还有一个问题,另一款打印机hp-laserjet_pro_mfp_m126a 不在支持列表,但是PPD文件中有他的ppd文件,其中有一句话

FoomaticRIPOptionSetting Model=HP-Color_LaserJet_2600n: " -sDeviceManufactur&&
er="HEWLETT-PACKARD" -sDeviceModel="HP Color LaserJet 2600n""

就是说这款打印机的DeviceModel 对应的是HP Color LaserJet 2600n ,但设置为这个后,打印不成功,在 /var/log/下有消息:

prnt/hpijs/services.cpp 389: unable to write to output, fd=6, count=4096: No such device

android java shell

gs 打印shell 命令:

gs  -dIjsUseOutputFD -dNOPAUSE -dBATCH -dPARANOIDSAFER  -dQUIET -sDEVICE=ijs -sIjsServer=hpijs -sOutputFile=/dev/usb/lp0 -sDeviceManufacturer=\"HEWLETT-PACKARD\" -sDeviceModel=\"HP LaserJet M1005\"  test.pdf

test.pdf 是测试用的PDF文件,该命令在adb shell 下执行正常
但是用java 调用shell 命令的方法就是打印不出来:

String printerId = "";
Process process = null;
DataInputStream in = null;
DataOutputStream os = null;
mSupportPrinters.clear();
cmd ="gs  -dIjsUseOutputFD -dNOPAUSE -dBATCH -dPARANOIDSAFER  -dQUIET -sDEVICE=ijs -sIjsServer=hpijs -sOutputFile=/dev/usb/lp0 -sDeviceManufacturer="HEWLETT-PACKARD" -sDeviceModel="HP LaserJet M1005"  test.pdf \n"
try {
    process = Runtime.getRuntime().exec("su"); //切换到root帐号
    os = new DataOutputStream(process.getOutputStream());
    in = new DataInputStream(process.getInputStream());
    os.writeBytes(cmd);
    os.flush();


    while (true) {
        printerId = in.readLine();//获取输入流
        if (printerId ==null)
            break;
        if(printerId.contains("END")){
            printerId = in.readLine();
            break;
        }

        mSupportPrinters.add(printerId.replace("ModeName:",""));
        //printerId = printerId + in.readLine();

    }

    os.writeBytes("exit\n");
    os.flush();

    process.waitFor();
} catch (Exception e) {

} finally {
    try {
        if (os != null) {
            os.close();
        }
        if (in != null) {
            in.close();
        }
        process.destroy();
    } catch (Exception e) {
    }
}

摸索了很久后发现,java调用shell时,会用到临时文件夹“/tmp/”,但是Android没有这个目录,于是用jni函数写了一个设置$TEMPDIR 环境变量的函数:

#include <stdlib.h>
JNIEXPORT jint JNICALL Java_jni_fpgargb_1lib_setenv
        (JNIEnv *env, jclass,jstring key, jstring val){
    jboolean iscopy;
    //jobject mFileDescriptor;

    const char *key_val = (*env).GetStringUTFChars(key, &iscopy);
    const char *val_val = (*env).GetStringUTFChars(val, &iscopy);

    setenv(key_val,val_val,1);
    (*env).ReleaseStringUTFChars(key, key_val);
    (*env).ReleaseStringUTFChars(val, val_val);
    return 0;
 }
 /
///调用,将$TEMPDIR 设置为当前app的临时目录
  fpgargb_lib.setenv("TEMPDIR",getContext().getCacheDir().getAbsolutePath());

打印成功