apache2 開發C++模塊 —— 基於cximage實現圖片、縮略圖下載功能

      之前的博客中有提到基於apache2的模塊開發,提供了下載文件的demo。但後來發現不僅僅是文件(準確說是圖片)下載,還需要根據相關參數,提供圖片縮略圖下載、裁剪圖下載或原圖下載等功能。


一、編譯問題:    

    種種原因,選用cximage庫來提供圖片處理功能。但坑爹的是這個庫是C++的,而apache是c的,apxs怎麼才能編譯C++代碼呢? 當然可以把cximage封裝一下,提供一個C接口的庫。但本人比較懶,還是希望直接將cximage編譯到apache2模塊中。主要解決步驟如下:

1、 模塊代碼中包含cximage的頭文件、直接定義cximage類對象,以C++方式調用相關接口。

2、 代碼編寫完成後通過apxs編譯。

#apxs編譯代碼的命令中需要指定c++頭文件,並通過-S參數將gcc替換成g++
/usr/local/apache2/bin/apxs -L /usr/include/c++/ -L /usr/include/c++/3.4.6/x86_64-redhat-linux/  -i -c -a -S CC=g++  mod_api.c


3、 執行上述編譯命令仍然報錯:

libCxImage.a(ximatran.o): relocation R_X86_64_32S against `.rodata' can not be used when making a shared object; recompile with -fPIC
/home/apache-module/api/libCxImage.a: could not read symbols: Bad value

   這應該不是一個普遍問題,主要是編譯cximage時沒有指定-fPIC,添加-fPIC選項重新編譯cximage庫,就可以了。

4、編譯成功後,修改httpd.conf,加載mod_api.so,並設置handler.

實際上通過apxs編譯的時候,應該會自動添加LoadModule的配置,但是 SetHandler還是需要自己手動加的。

<Location /api>  
      SetHandler api  
</Location>  


5、重啓apache2服務,發現加載mod_api.so報錯:

httpd: Syntax error on line 56 of /usr/local/apache2/conf/httpd.conf: Cannot load /usr/local/apache2/modules/mod_api.so into server: /usr/local/apache2/modules/mod_api.so: undefined symbol: jpeg_resync_to_restart

解決方法:應該是編譯mod_api.so是缺少cximage相關的.a文件。用find cximage -name *.a 找出所有的.a文件,然後逐個添加到編譯選項中測試,最終確定需要以下幾個.a(順序上被依賴的庫放在後面,需要依賴別人的庫放在前面): 

-lCxImage -ljasper -lpng -ltiff -lzlib -ljpeg

所以最終的編譯命令如下:

/usr/local/apache2/bin/apxs -L /usr/include/c++/ -L /usr/include/c++/3.4.6/x86_64-redhat-linux/  -i -c -a -S CC=g++  mod_api.c -L./lib -lCxImage -ljasper -lpng -ltiff -lzlib -ljpeg

PS:

另外網上還有說需要在httpd.conf的LoadModule之前加上一句LoadFile,已加載C++模塊:

LoadFile /usr/lib64/libstdc++.so.6
LoadModule api_module   modules/mod_api.so
但實際測試了一下,不加這句似乎也沒有什麼問題。


二、模塊代碼編寫

      概括的說,主要有三類需求:首先從調用我們內部的SDK從其他內部平臺下載圖片原始文件,然後根據參數

1) 返回原圖;2)返回縮略圖;3)裁剪指定範圍的圖片

這裏忽略了調用sdk的步驟,假設圖片就在服務器本地,且直接在url路徑中表示本地路徑,設計的url規則如下:

url參數定義: 
以下參數中 file、mode 是必須參數,其他參數是否需要視mode取值而定。 
格式爲: file=xxx&mode=xxx&ratio=xxx 參數組合的順序可以任意。

file 文件路徑和名稱
mode 圖片下載模式,有以下三種取值:
normal 原圖下載,忽略除file、mode之外的其他參數;
thumbnail 縮略圖,還需要提供壓縮比例: ratio,此時忽略圖片裁剪相關的參數;
crop 圖片裁剪,還需提供參裁剪範圍參數: left,top,right,bottom, 此時忽略縮略圖相關參數
ratio 圖片壓縮比例
left 圖片裁剪區域左上角x座標
top 圖片裁剪區域左上角y座標
right 圖片裁剪區域右下角x座標
bottom 圖片裁剪區域右下角y座標

原圖

http://192.168.1.100:8080/api?file=/home/tmp/test-img.jpeg&mode=normal
縮略圖
http://192.168.1.100:8080/api?file=/home/tmp/test-img.jpeg&mode=thumbnail&ratio=2
圖片裁剪
http://192.168.1.100:8080/api?file=/home/tmp/test-img.jpeg&mode=crop&left=50&top=80&right=300&bottom=600


2.1 參數解析

參考apache2.4官方文檔 ,可以使用 ap_args_to_table 和 apr_table_get 解析參數,前者將query中的參數都解析到一個列表中,後者從該列表中查找指定的參數。

示例代碼:

    /* parse args from uri */
    apr_table_t *GET;
    apr_array_header_t *POST;
    ap_args_to_table(r, &GET);
    ap_parse_form_data(r, NULL, &POST, -1, MAX_ARGS_BUF);
    /*http://192.168.1.100:8080/api?file=/home/tmp/test-img.jpeg&mode=crop&left=50&top=80&right=300&bottom=600*/
    const char* file_path = apr_table_get(GET, "file");
    const char* mode = apr_table_get(GET, "mode");

另外,如果url中還有子目錄,例如 192.168.1.100:8080/api/downloads?XXXXXX ,那麼downloads 字段會體現在request_rec結構的uri和path_info中:(以下是gdb調試時打印出來的request_rec結構體部分字段)

  unparsed_uri = 0x8ed008 "/api/downloads?file=abc.txt&type=2", 
  uri = 0x8ed028 "/api/downloads", 
  filename = 0x8e38a8 "/usr/local/apache2/htdocs/api", 
  canonical_filename = 0x8e38a8 "/usr/local/apache2/htdocs/api", 
  path_info = 0x8e37ec "downloads", 
  args = 0x8ed038 "file=/home/abc.txt&type=2", 

PS:如果用的是apache 2.2 的版本,那麼是沒有這兩個函數的。可以參照2.4的代碼,將這兩個函數以及其依賴的代碼移植到2.2中。


2.2 圖片處理:縮略圖提取或裁剪

由於這部分是比較定製化的需求,與apache2沒有必然關係,這裏僅爲了邏輯的完整性貼出調用cximage處理圖片的示例代碼,不做過多解釋。

	CxImage src_img;
	CxImage dest_img;
	long img_size = 0;
	BYTE* img_buf = NULL;
    if ( 0 == strcmp("normal", mode)) {              // 原圖下載
		img_buf = (BYTE *)read_buf;
		img_size = len;
    }else if ( 0 == strcmp("thumbnail", mode) ) {    // 縮略圖下載
    	/* parse resample rate from uri */
    	const char* ratio_str = apr_table_get(GET, "ratio");
		if ( NULL == ratio_str ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse resample ratio from uri query : %s", query);
    		return HTTP_BAD_REQUEST;
		}
    	int  ratio = atoi(ratio_str);
		if ( 0 == ratio ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "invalid resample rate : %s", ratio_str);
    		return HTTP_BAD_REQUEST;
		}
		if ( !src_img.Decode((BYTE *)read_buf, sizeof(read_buf), CXIMAGE_FORMAT_JPG) ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to decode picture");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
		if (src_img.Resample(src_img.GetWidth()/ratio,src_img.GetHeight()/ratio, 1, &dest_img)) {
			dest_img.Encode( img_buf, img_size, CXIMAGE_FORMAT_JPG );
		} else {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to resample picture : %s, ratio : %d", file_path, ratio);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
    }else if ( 0 == strcmp("crop", mode) ) {         // 圖片裁剪
		src_img.Decode((BYTE *)read_buf, sizeof(read_buf) ,CXIMAGE_FORMAT_JPG);
    	const char* left_str = apr_table_get(GET, "left");
    	const char* top_str = apr_table_get(GET, "top");
    	const char* right_str = apr_table_get(GET, "right");
    	const char* bottom_str = apr_table_get(GET, "bottom");
    	if (NULL == left_str || NULL == top_str || NULL == right_str || NULL == bottom_str ) {
    		ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse crop region from uri query : %s", query);
    		return HTTP_BAD_REQUEST;
    	}
		int left   = atoi(left_str);
		int top    = atoi(top_str);
		int right  = atoi(right_str);
		int bottom = atoi(bottom_str);
		if (left < 0 || top < 0 || right < 0 || bottom < 0) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "invalid crop region: (%d, %d), (%d, %d)", left, top, right, bottom);
    		return HTTP_BAD_REQUEST;
		}
		if ( !src_img.Decode((BYTE *)read_buf, sizeof(read_buf), CXIMAGE_FORMAT_JPG) ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to decode picture");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
		if (src_img.Crop( left, top, right, bottom, &dest_img )) {
			dest_img.Encode( img_buf, img_size, CXIMAGE_FORMAT_JPG );
		}else {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to crop picture : %s, (%s, %s), (%s, %s)", file_path, left_str, top_str, right_str, bottom_str);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
    }

2.3  下載:將圖片數據發送給客戶端

這一部分與上一篇《c apache2模塊開發--根據自定義業務邏輯實現文件下載》中相同:

調用 ap_rwrite 接口循環發送數據。 稍微有點區別,我們改成在發送數據之前才設置Content-Length,因爲結合前面的邏輯,數據的長度只有待縮略圖/剪切圖生成後才能最終確定。其他的就沒什麼變化了。

	/* we can get file length only after converted picture format */
    /* Content-Length:xxxx */
    char file_len_str[MAX_FILE_LEN_DIGITS];
    memset(file_len_str, 0, sizeof(file_len_str));
    snprintf(file_len_str, sizeof(file_len_str)-1, "%d", img_size);
    apr_table_add(r->headers_out,"Content-Length", file_len_str);
	
	int send_bytes = 0;
	while( send_bytes < img_size ){
		/*ap_flush_conn(r->connection);*/
		int send_ret = ap_rwrite( img_buf, img_size - send_bytes, r );
		if( send_ret >= 0 ) {
			send_bytes += send_ret;
		} else {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to send buffer to client");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
	}





完整示例代碼如下:

/* 
**  mod_api.c -- Apache sample api module
**  [Autogenerated via ``apxs -n api -g'']
**
**  To play with this sample module first compile it into a
**  DSO file and install it into Apache's modules directory 
**  by running:
**
**    $ apxs -c -i mod_api.c
**
**  Then activate it in Apache's httpd.conf file for instance
**  for the URL /api in as follows:
**
**    #   httpd.conf
**    LoadModule api_module modules/mod_api.so
**    <Location /api>
**    SetHandler api
**    </Location>
**
**  Then after restarting Apache via
**
**    $ apachectl restart
**
**  you immediately can request the URL /api and watch for the
**  output of this module. This can be achieved for instance via:
**
**    $ lynx -mime_header http://localhost/api 
**
**  The output should be similar to the following one:
**
**    HTTP/1.1 200 OK
**    Date: Tue, 31 Mar 1998 14:42:22 GMT
**    Server: Apache/1.3.4 (Unix)
**    Connection: close
**    Content-Type: text/html
**  
**    The sample page from mod_api.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "ap_config.h"
/*#include "http_connection.h"*/
#include "ap_regex.h"
#include "http_log.h"
#include "util_script.h"
#include "ximage.h"
#include <stdio.h>

#define  MAX_PATH_LEN          256
#define  MAX_FILE_LEN_DIGITS   64
#define  FILE_BUF_SIZE         1024
#define  MAX_ARGS_BUF          8192


/* get file name from the abolute path
 * eg:  input   /home/downloads/test.so
 *      output  test.so
 */
const char* get_file_name(const char* path)
{
    if (NULL == path) {
        return NULL;
    }
    int  path_len = strlen(path);
    const char *pos = path + path_len;
    while (*pos != '/' && pos != path) {
        pos--;
    }
    if (pos == path) {
        return path+1;
    }else {
        int len = len - (pos - path);
        return (pos + 1);
    }
}

int get_file_length(const char* file_path, request_rec *r)
{
    int len = 0;
    apr_finfo_t  info;
    apr_stat(&info, file_path, APR_FINFO_SIZE, r->pool);
    len = (apr_size_t)info.size;
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0,r->server, "file :%s, len:%d", file_path, len);
    return len;
}

/* The sample content handler */
static int api_handler(request_rec *r)
{
    if (strcmp(r->handler, "api")) {
        return DECLINED;
    }

    /* only support GET or POST request */
    if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
        return HTTP_METHOD_NOT_ALLOWED;
    }

    /* full url : http://192.168.1.100:8080/helloworld?file=/home/test.txt&type=2*/
    /* r->parsed_uri.query : file=/home/test.txt&type=2 */
    if ( NULL == r->parsed_uri.query ){
        ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server,"uri param is empty");
        return HTTP_BAD_REQUEST;
    }
	const char* query = r->parsed_uri.query;
	
    /* parse args from uri */
    apr_table_t *GET;
    apr_array_header_t *POST;
    ap_args_to_table(r, &GET);
    ap_parse_form_data(r, NULL, &POST, -1, MAX_ARGS_BUF);
    /* http://192.168.1.100:8080/api?file=/home/tmp/test-img.jpeg&type=1&left=0&top=0&right=100&bottom=100&resizerate=3 */
    //const char* bucket = apr_table_get(GET, "bucket");
    const char* file_path = apr_table_get(GET, "file");
    const char* mode = apr_table_get(GET, "mode");

    /* file,mode should not be empty */
    if ( NULL == file_path || NULL == mode ) {
    	ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse file/mode from uri query: %s", query);
    	return HTTP_BAD_REQUEST;
    }

    /* set response headers */
    /* Content-Type:application/octet-stream */
    r->content_type = "application/octet-stream";                   /* "text/html" */
    /* Content-Disposition:attachment;filename=test.txt */
    char file_name[24 + (MAX_PATH_LEN)] = {0};   /* length of "attachment;filename=" is 20 */
    sprintf(file_name, "attachment;filename=%s", get_file_name(file_path));
    apr_table_add(r->headers_out,"Content-Disposition", file_name);
	/* we can get file length only after converted picture format */
    /* Content-Length:xxxx */

	
	/* TODO: call our SDK to get picture */
    /* load picture from file to memory */
	/* suppose that we have already downloaded files from other platform, and all the files are in the memory.
     * so just return the memory data to client */
    FILE*  fp = fopen(file_path, "rb");
    if ( NULL == fp ) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server,"failed to open file %s", file_path);
        return HTTP_NON_AUTHORITATIVE;
    }
    int read_ret = 0;
	int len = get_file_length(file_path, r);
    char read_buf[len];
	//while( !feof( fp ) ){
		memset( read_buf, 0, sizeof( read_buf ) );
		read_ret = fread( read_buf, 1, len, fp );
		if( ferror( fp ) ){
			/* todo log error */
			fclose(fp);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
	//}
	fclose(fp);
	
   
	// for test
	CxImage src_img;
	CxImage dest_img;
	long img_size = 0;
	BYTE* img_buf = NULL;
    if ( 0 == strcmp("normal", mode)) {              // 原圖下載
		img_buf = (BYTE *)read_buf;
		img_size = len;
    }else if ( 0 == strcmp("thumbnail", mode) ) {    // 縮略圖下載
    	/* parse resample rate from uri */
    	const char* ratio_str = apr_table_get(GET, "ratio");
		if ( NULL == ratio_str ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse resample ratio from uri query : %s", query);
    		return HTTP_BAD_REQUEST;
		}
    	int  ratio = atoi(ratio_str);
		if ( 0 == ratio ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "invalid resample rate : %s", ratio_str);
    		return HTTP_BAD_REQUEST;
		}
		if ( !src_img.Decode((BYTE *)read_buf, sizeof(read_buf), CXIMAGE_FORMAT_JPG) ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to decode picture");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
		if (src_img.Resample(src_img.GetWidth()/ratio,src_img.GetHeight()/ratio, 1, &dest_img)) {
			dest_img.Encode( img_buf, img_size, CXIMAGE_FORMAT_JPG );
		} else {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to resample picture : %s, ratio : %d", file_path, ratio);
			// todo :失敗的情況下是否需要調用 dest_img.FreeMemory(img_buf);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
    }else if ( 0 == strcmp("crop", mode) ) {         // 圖片裁剪
		src_img.Decode((BYTE *)read_buf, sizeof(read_buf) ,CXIMAGE_FORMAT_JPG);
    	const char* left_str = apr_table_get(GET, "left");
    	const char* top_str = apr_table_get(GET, "top");
    	const char* right_str = apr_table_get(GET, "right");
    	const char* bottom_str = apr_table_get(GET, "bottom");
    	if (NULL == left_str || NULL == top_str || NULL == right_str || NULL == bottom_str ) {
    		ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to parse crop region from uri query : %s", query);
    		return HTTP_BAD_REQUEST;
    	}
		int left   = atoi(left_str);
		int top    = atoi(top_str);
		int right  = atoi(right_str);
		int bottom = atoi(bottom_str);
		if (left < 0 || top < 0 || right < 0 || bottom < 0) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "invalid crop region: (%d, %d), (%d, %d)", left, top, right, bottom);
    		return HTTP_BAD_REQUEST;
		}
		if ( !src_img.Decode((BYTE *)read_buf, sizeof(read_buf), CXIMAGE_FORMAT_JPG) ) {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to decode picture");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
		if (src_img.Crop( left, top, right, bottom, &dest_img )) {
			dest_img.Encode( img_buf, img_size, CXIMAGE_FORMAT_JPG );
		}else {
			// todo :失敗的情況下是否需要調用 dest_img.FreeMemory(img_buf);
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to crop picture : %s, (%s, %s), (%s, %s)", file_path, left_str, top_str, right_str, bottom_str);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
    }
    
	/* we can get file length only after converted picture format */
    /* Content-Length:xxxx */
    char file_len_str[MAX_FILE_LEN_DIGITS];
    memset(file_len_str, 0, sizeof(file_len_str));
    snprintf(file_len_str, sizeof(file_len_str)-1, "%d", img_size);
    apr_table_add(r->headers_out,"Content-Length", file_len_str);
	
	int send_bytes = 0;
	while( send_bytes < img_size ){
		/*ap_flush_conn(r->connection);*/
		int send_ret = ap_rwrite( img_buf, img_size - send_bytes, r );
		if( send_ret >= 0 ) {
			send_bytes += send_ret;
		} else {
			ap_log_error(APLOG_MARK, APLOG_ERR, 0,r->server, "failed to send buffer to client");
			return HTTP_INTERNAL_SERVER_ERROR;
		}
	}
	if (img_buf != (BYTE*)read_buf) {            /* (img_buf == read_buf)  means download the original picture, and haven't call dest_img.Encode() */
		dest_img.FreeMemory(img_buf);
		img_buf = NULL;
	}	
    return OK;
}

static void api_register_hooks(apr_pool_t *p)
{
    ap_hook_handler(api_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA api_module = {
    STANDARD20_MODULE_STUFF, 
    NULL,                  /* create per-dir    config structures */
    NULL,                  /* merge  per-dir    config structures */
    NULL,                  /* create per-server config structures */
    NULL,                  /* merge  per-server config structures */
    NULL,                  /* table of config file commands       */
    api_register_hooks  /* register hooks                      */
};








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章