[背景]
忘れるのでメモ。
Andoiroid上でJavascriptで日本語PDFを生成していたが、NDKを使ってみたかったのと、
Android上で、フリーのピュアJavaの日本語PDF生成ライブラリが見つけられなかった為。
(ApacheのPDFのライブラリのAndroidで動くよう調整したものが、github上に公開されているが、
調整元のバージョンが古いので日本語に対応していない)
[前提]
・androidstudioに、NDKのbuild環境を導入済み
・macにはcmakeを導入済み
・zlibはAndroidのものを使用
・pnglibは、githubにあるAndroid用に調整されたソースを使用
(設定がわからなかったので、自分でconfigureできなかった。。。)
・haruは、githubのソースを使用
・実行環境は5.1以上とする
(別に拘りはないが、持ってる実機で一番古いのが5.1の為。。。)
・ソースはutf8、出力するPDFはsjis(HARUの日本語出力が、sjisかeucの為)
・生成したPDFの確認はIntent経由でAcrobatで表示する。なので、最終的には実機で確認する(とーぜん、Acrobatはインストール済み)
[前準備]
1.日本語フォントのDL
IPAのttfを使うことにした。
とりあえず、IPA明朝を取得。
https://ipafont.ipa.go.jp/old/ipafont/download.html
適当なフォルダに解凍しておく。
2.HARU
githubの方から取得した。
https://github.com/libharu/
適当なフォルダに解凍しておく。
3.libpng
HARUに必要。(libpngはzlib依存だが、androidのzlibを使用する)
android用に調整されたものが、githubに置かれているので、
それを取得した。
https://github.com/julienr/libpng-android
適当なフォルダに解凍しておく。
[プロジェクトの生成]
1.androidstudioのIDEを開き、プロジェクトの新規作成する。
C++サポートのチェックはこの段階では要らない。
5.1の実機があるので、5.1にした。。。
とりあえず、emptyで生成。
あとは、デフォルトのまま。
生成直後。。。
プロジェクトのappフォルダを開き、jniフォルダを作成しておく
[Cソースの準備]
2.libpngの解凍ファイル内のjniフォルダの中身を前述libpngフォルダにコピー。
ただし、「Android.mk」、「Application.mk」 は不要。
(armフォルダとその中身、c,hソースをコピーする)
4.libharuの解凍ファイル内のjniフォルダの中身を前述libharuフォルダにコピー。
(不要なファイルもあるが、とりあえず、そのままコピーする)
5.コンソールを起動し、libharuフォルダに移動。
フォルダ直下で、cmake実行
cmake ./
6.前述jniフォルダにharu_wrapperフォルダ作成
(HARUにjniでアクセスする関数を作成するソースの格納フォルダ)
7.haru_wrapperフォルダ下で、ソース、「native-test.cpp」を新規作成し、
以下のように記述。
#include <jni.h>
#include <string>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <android/log.h>
// haru-lib include
#include "hpdf.h"
// JNI初めてのよびだし。。。
extern "C" JNIEXPORT jstring
JNICALL
Java_gkgkdrink_com_fugaharu_MainActivity_stringTest(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from First C++!";
return env->NewStringUTF(hello.c_str());
}
※疎通確認のため、とりあえず、jni経由で文字列を返すだけの関数を追加。
8.前述jni下フォルダに「Android.mk」、「Application.mk」ファイルを新規作成。
9.「Android.mk」を以下のように記述して保存
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libpng
LOCAL_SRC_FILES := $(subst $(LOCAL_PATH)/,, $(wildcard $(LOCAL_PATH)/libpng/*.c)) \
$(subst $(LOCAL_PATH)/,, $(wildcard $(LOCAL_PATH)/libpng/arm/*.c))
LOCAL_EXPORT_LDLIBS := -lz
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libpng
LOCAL_MODULE := png
APP_STL:=gnustl_static
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/libpng \
$(LOCAL_PATH)/libharu/include \
$(LOCAL_PATH)/libharu/src \
$(LOCAL_PATH)/haru_wrapper
LOCAL_SRC_FILES := $(subst $(LOCAL_PATH)/,, $(wildcard $(LOCAL_PATH)/libharu/src/*.c))
LOCAL_LDLIBS := -lz -lm -llog
LOCAL_CFLAGS += -O2 -D__ANDROID__ -fPIC
LOCAL_MODULE := haru
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/zlib \
$(LOCAL_PATH)/libpng \
$(LOCAL_PATH)/libharu/include \
$(LOCAL_PATH)/libharu/src \
$(LOCAL_PATH)/haru-wrapper
LOCAL_SRC_FILES := $(subst $(LOCAL_PATH)/,, $(wildcard $(LOCAL_PATH)/haru-wrapper/*.cpp))
LOCAL_LDLIBS := -lz -lm -llog
LOCAL_CFLAGS += -O2 -D__ANDROID__ -fPIC
LOCAL_MODULE := fuga
ifeq ($(TARGET_ARCH_ABI),$(filter $(TARGET_ARCH_ABI), arm64-v8a mips64 x86_64))
LOCAL_CFLAGS += -D__ANDROID64__
endif
LOCAL_STATIC_LIBRARIES := png haru
include $(BUILD_SHARED_LIBRARY)
10.「Applicatio.mk」を以下のように記述して保存
APP_PLATFORM := android-22
APP_ABI := armeabi armeabi-v7a x86 mips arm64-v8a x86_64 mips64
APP_STL:=stlport_static
APP_MODULES := fuga
[NDKのビルド設定と疎通確認]
1.androidstudioのプロジェクトペインで、「app」を選択して右クリックし、
メニューから「Link C++ Project with Gradle」を選択し、実行。
2.ダイアログが表示されるので、
「Build System」-> 「ndk-build」
「Build System」-> 作成した「Android.mk」のパス
を選択し、「OK」ボタンをクリック。
ビルドが始まる。
正常に終わる(ハズ。。。)なので、おわると、プロジェクトペインは
以下のようになる。
Cのフォルダと、Cのmakeファイルが、現れる。
「build.gradle」も、
externalNativeBuild {
ndkBuild {
path 'jni/Android.mk'
}
}
が、追記されている。(ハズ。。。)
3.疎通確認のため用意したtestString()を呼び出す為、Java側ソースを編集する。
デフォルトでプロジェクトを作成しているので、レイアウトにはラベルが存在するはずなんで、
それにidを付与する。
layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/txtFuga"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Activity表示時に、jni経由で文字列を取ってきてラベルに表示する。
gkgkdrink.com.fugaharu.MainActivity.java
package gkgkdrink.com.fugaharu;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// HaruのラッパーLib
static {
System.loadLibrary("fuga");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// jni疎通確認。。。
TextView txt = findViewById(R.id.txtFuga);
txt.setText(this.stringTest());
}
// 疎通確認用native関数
public native String stringTest();
}
staticライブラリのfugaをロード、stringTest()をnativeメソッドとして定義し、画面ロード時にラベルに設定するよう記述した。
クリーンしてリビルド後、エミュレータで実行してみる。
レイアウトに設定されている「Hello Wold!」から「Hello from First C++!」が表示されいるので、
Cのライブラリの疎通が確認出来た。
[日本語PDFの出力]
1.プロジェクトにダウンロードしたIPAの明朝フォントを取り込む。
finder等でプロジェクトの「app/src/main」フォルダを開き、「assets」フォルダを作成する。
assetsフォルダに解凍した明朝ttfファイルをコピー
androidstudioに戻ると、assetsがプロジェクトペインにある
(ハズ。。。なければ、リビルドするか、Sync With FileSystemする。。。)
HARUは、外部フォントをパスを指定してロードする。
この為、assetsに置いただけでは使えない。
(Streamで取れる為。ここら辺のクソ仕様はなんとかならんものか。。。)
なので、ローカルフォルダにフォントをコピーするコードをJava側で記述する。
「build.gradle」に以下の設定を追加する。
(前述、externalNativeBuildの上らへん。。。)
aaptOptions {
noCompress "ttf"
}
※これをしておかないと、ローカルにコピーした時に、
「java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed」
が発生するという、クソ仕様。。。
「AndroidManifest.xml」にアクセス権限を追加する。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gkgkdrink.com.fugaharu">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
MainActivityに以下処理を追加
//〜省略〜
// jni疎通確認。。。
TextView txt = findViewById(R.id.txtFuga);
txt.setText(this.stringTest());
// Font準備
String fontDir = this.getApplicationContext().getExternalFilesDir(null) + "/fonts/";
File fontDirFp = new File(fontDir);
if (!fontDirFp.exists()) {
fontDirFp.mkdir();
}
File font1Fp = new File(fontDirFp.getAbsolutePath() + "/ipaexm.ttf");
if (!font1Fp.exists()) {
AssetManager am = getApplicationContext().getAssets();
AssetFileDescriptor afd = null;
try {
if (!font1Fp.exists()) {
afd = am.openFd( "ipaexm.ttf");
Log.i("FUGA", "copy start ipaexm.ttf");
copyFile(afd, font1Fp);
Log.i("FUGA", "copy success ipaexm.ttf");
} else {
Log.i("FUGA", "font ipaexm.ttf exists.");
}
} catch (Exception e) {
Log.e("FUGA", "font load failed...", e);
}
}
//〜省略〜
private void copyFile(AssetFileDescriptor src, File dst) throws Exception {
byte[] buf = new byte[1024];
long len = 0;
InputStream is = null;
FileOutputStream os = null;
try {
is = src.createInputStream();
os = new FileOutputStream(dst);
// ファイルの終わりまで読み込む
while((len = is.read(buf)) != -1){
os.write(buf);
}
os.flush();
} finally {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
}
}
//〜省略〜
アプリの内部ストレージにフォントフォルダを作成し、
assetsのttfをコピーしているだけ。
とりあえず、エミュレーターで確認。
05-12 08:03:25.431 31258-31258/gkgkdrink.com.fugaharu I/FUGA: copy success ipaexm.ttf
ローカルに実ファイルとして、コピーした事を確認。。。
2.C側に文字列SJIS変換関数を用意する。
前述のとおり、HARUはShift_JIS、もしくは、EUC-JPしか受け付けない。
Android側から送られる文字列は、当然、UTF-8の為、変換が必要になる。そこで、C側にSJIS変換関数を、native-test.cppに用意した。
// 〜中略
// SJIS変換関数
char* GetStringLocalChars(JNIEnv *env, jstring str){
jstring enc = env->NewStringUTF("Shift_JIS");
jclass clazz = env->FindClass("java/lang/String");
jmethodID getBytes = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray bytes = (jbyteArray)env->CallObjectMethod(str, getBytes, enc);
jsize len = env->GetArrayLength(bytes);
char *s = (char*)malloc(len+1);
jbyte *bs = env->GetByteArrayElements(bytes, NULL);
memcpy(s, bs, len);
env->ReleaseByteArrayElements(bytes, bs, 0);
s[len] = '\0';
return s;
}
// 文字列のメモリ解放
void ReleaseStringLocalChars(char* str){
free(str);
}
// 〜中略
処理内容は、JavaのStringをsjisで作り直して、getBytesメソッドで取り出し、コピーしているだけ。ただし、mallocしているので、使ったら解放が必要になる。
3.C側にHARUにてPDFを生成するラッパー関数を用意する。
エラーハンドラを用意。
// 〜中略
// エラーハンドラ
#ifdef HPDF_DLL
void __stdcall
#else
void
#endif
error_handler (HPDF_STATUS error_no,
HPDF_STATUS detail_no,
void *user_data)
{
__android_log_print(ANDROID_LOG_ERROR, "FUGA", "ERROR: error_no=%04X, detail_no=%u\n",
(HPDF_UINT)error_no,
(HPDF_UINT)detail_no);
}
// 〜中略
特に、何もしない。。。メッセージを表示するのみとした。
次にPDF出力関数。
// 〜中略
// 日本語PDFを出力
extern "C" JNIEXPORT void JNICALL Java_gkgkdrink_com_fugaharu_MainActivity_fugaPdf(
JNIEnv *env,
jobject,
jstring pdf_path,
jstring fontpath,
jstring writemsg) {
HPDF_Doc pdf;
HPDF_Font font;
HPDF_Page page;
HPDF_STATUS status;
const char *detail_font_name;
jboolean isCopy;
// PDFドキュメントの作成
pdf = HPDF_New (error_handler, NULL);
if (!pdf) {
printf ("error: cannot create PdfDoc object\n");
return ;
}
// 日本語エンコーディングを使用する
status = HPDF_UseJPEncodings(pdf);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf usejpencodings result %lu", status);
// Fontパス変換
const char *font_path = env->GetStringUTFChars(fontpath,&isCopy);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","load font start ");
// 生成PDFへフォントを設定
detail_font_name = HPDF_LoadTTFontFromFile(pdf, font_path, HPDF_TRUE);
if (strlen(detail_font_name) > 0) {
__android_log_print(ANDROID_LOG_DEBUG,"TAG","load font :[%s]", detail_font_name);
}
__android_log_print(ANDROID_LOG_DEBUG,"TAG","load font end");
/* set compression mode */
status = HPDF_SetCompressionMode (pdf, HPDF_COMP_ALL);
/* create default-font */
// Fontを取得
font = HPDF_GetFont (pdf, detail_font_name, "90msp-RKSJ-H");
if (font == NULL) {
__android_log_print(ANDROID_LOG_DEBUG,"TAG","get font failed ");
} else {
__android_log_print(ANDROID_LOG_DEBUG,"TAG","get font success.. ");
}
// 新規ページ作成
page = HPDF_AddPage (pdf);
// 新規ページは、A4縦とする
status = HPDF_Page_SetSize(page,
HPDF_PAGE_SIZE_A4 ,
HPDF_PAGE_PORTRAIT);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf setsize result %lu", status);
// 引数で渡された出力文字列をSJISに変換する
char *helloMsg = GetStringLocalChars(env, writemsg);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","hello msg %s", helloMsg);
// 出力するフォントのサイズを指定
status = HPDF_Page_SetFontAndSize (page, font, 24);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf setfontsize result %lu", status);
// 出力文字から幅を見積もる
float tw = HPDF_Page_TextWidth (page, helloMsg);
// ページへ文字列出力開始
status = HPDF_Page_BeginText (page);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf begin result %lu", status);
// 日本語文字の出力
HPDF_Page_TextOut (page, (HPDF_Page_GetWidth(page) - tw) / 2,
HPDF_Page_GetHeight (page) - 50, helloMsg);
// SJIS文字列の解放
ReleaseStringLocalChars(helloMsg);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","free alloc pdf msg...");
// ページ終了
HPDF_Page_EndText (page);
// 指定パスへPDFを出力
const char *save_path = env->GetStringUTFChars(pdf_path, &isCopy);
status = HPDF_SaveToFile(pdf, save_path);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write path %s", save_path);
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write result %lu", status);
// 結果チェック
switch (status)
{
case HPDF_OK:
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write result success");
break;
case HPDF_INVALID_DOCUMENT:
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write result invalid document");
break;
case HPDF_FAILD_TO_ALLOC_MEM:
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write result failed alloc mem");
break;
case HPDF_FILE_IO_ERROR:
__android_log_print(ANDROID_LOG_DEBUG,"TAG","pdf write result io error");
break;
default:
break;
}
// PDF解放
HPDF_Free (pdf);
return ;
}
// 〜中略
引数は、
PDF出力ファイルパス、
明朝フォントファイルパス、
出力文字列(日本語混入)
とした。エラー処理は特に実装していない。
リビルドすると、何事もなく終了する(ハズ。。。)
4.Java側にラッパー関数を呼び出す処理を「MainActivity.java」に、追加する。
ネイティブ関数呼び出しメソッドの宣言。
// 〜中略
// 日本語PDFを出力
public native void fugaPdf(String pdf_path, String fontpath, String writemsg);
// 〜中略
メソッドのインターフェースは、前述のとおり。
日本語PDF作成処理の追加。
追加箇所は、「onCreate()」の続きで行った。
テスト出力なので、特に非同期とかにはしない。。。
// 〜中略
// PDF出力フォルダの準備
String pdfDir = this.getApplicationContext().getExternalFilesDir(null)
+ "/share_pdf/";
File fp = new File(pdfDir);
if (!fp.exists()) {
fp.mkdir();
Log.i("FUGA", pdfDir + " created...");
} else {
Log.i("FUGA", pdfDir + " exists.");
}
// 出力先PDFパス
String pdfPath = pdfDir + "hogehoge.pdf";
// PDFに出力する日本語付きメッセージ
String jpmsg = "HARUで日本語出力orz";
// 出力実行
this.fugaPdf(pdfPath, font1Fp.getAbsolutePath(), jpmsg);
Log.d("FUGA","write pdf:" + pdfPath);
// 〜中略
リビルドし、エミュレーターで実行してみる。
05-13 09:05:42.281 4670-4670/gkgkdrink.com.fugaharu D/FUGA: write pdf:/storage/emulated/0/Android/data/gkgkdrink.com.fugaharu/files/share_pdf/hogehoge.pdf
正常に終了する。。。
画面を追加するのがメンド臭いので、ここからは、実機で実行する。
作成したPDFをIntent経由で実機にインストールされているAcrobatで表示し、
日本語が出力されるか確認する。
// 〜中略
// 外部アプリに渡すURIを生成
File pdfFp = new File(pdfPath);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = FileProvider.getUriForFile(this,
"gkgkdrink.com.fugaharu.fileprovider", pdfFp);
// Intent起動...
intent.setDataAndType(uri, "application/pdf");
intent.addFlags(
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, 0/*request code*/);
Log.d("FUGA","pdf first test");
// 〜中略
URI作成時にFileProviderを使用するので、AndroidManifest.xmlに、設定を追加する。
// 〜中略
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="gkgkdrink.com.fugaharu.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
「application」タグ内に記述する。file_pathsが無いので新規に作成する(res/xml/file_paths.xml)
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="share_pdf" path="Android/data/gkgkdrink.com.fugaharu/files/share_pdf" />
</paths>
リビルドして、実機で実行する。
アプリ起動時、インテントで他アプリを起動しようとする。
ので、Acrobatを選択して、起動する。
設定した日本語付きメッセージが、PDFへ出力されていることが
確認出来る。
[まとめ]
とりあえず、NDKでビルドする、日本語PDFを出力する的な事は、
メンド臭い感じで出来る事が確認出来た。
SJIS変換とか、フォント渡すところとか、ほんとにメンド臭い。。。
公開しているアプリはwebview-javascriptで無理やりPDFを生成してるので、
これで書き直すと思う。
以下、TODO....
TODO:kotlinから呼び出すよう、書き直す。
TODO:ReactNativeとか、Fultter経由だとどう実装するか。。。