GSoC '23: Summary of work done over first coding period

Hello and welcome back to my blog! This time I will be reviewing the work I've done during the first coding period of GSoC '23. This blog is written as part of my work for GSoC '23, to detail all the work I have done. Let's get started!

Challenges faced

Some of the challenges I faced are:

  • Time: Sometimes it felt like time was not on my side. Between college, assignments, exams, and family time, I found it hard to find time to concentrate on GSoC. However, I'm hoping I can improve my time management to remove this issue.

  • Lack of Documentation: For Android-NDK, there was a lack of documentation for things that are considered simple when developing a traditional Java app for Android. This made it more annoying to deal with Android libraries (especially since I'm using CMake). Sometimes I had this feeling while dealing with Poppler as well, but luckily my mentor helped me out massively.

  • Lack of experience in Android development: Going into the project, I didn't have much experience with Android development, much less Android-NDK based development. This proved to be a hindrance at times, as I did not know how to do simple things due to my lack of knowledge.

Work done

My goal for the first coding period was to implement a font-fetching API in Poppler, so that if a document has unembedded fonts, Okular can still display the document by using similar fonts found in the system.

I've successfully implemented this into Poppler along with help from my mentor, Albert Astals Cid (). To do so I had to implement multiple things:

The AFontMatcher API

The Afontmatcher functionality was introduced in Android-NDK around Android API level 29. It can be used to fetch a font that best matches the font family and the text to be rendered.

To use this API to implement font-matching capabilities, I had to do just a few things:

  • Add the Android library to the CMakeLists.txt for Poppler

  • Import the required header files into poppler/GlobalParams.cc. According to the documentation, these are the required header files:

    • <font.h>

    • <font_matcher.h>

    • <system_fonts.h>

  • Implement the AFontMatcher API in the GlobalParams::findSystemFontFile() method inside GlobalParams.cc

To implement the AFontMatcher API, I implemented GlobalParams::findSystemFontFile() in the following way:

  • Create a new AFontMatcher object using AFontMatcher_create()

  • Set the font weight and italics for the AFontMatcher object by using AFontMatcher_setStyle(), and the methods GfxFont::getWeight() for font-weight, as well as GfxFont::isItalic() for font italics.

  • Get the generic family name of the required font using the GfxFont::isSerif(), and GfxFont::isFixedWidth()

  • Match the font using AFontMatcher_match() to get an AFont object

  • Use the AFont object to get a font path for the font.

  • Use the font file extension to set the font type, which depends on the font format. Since the fonts can be in .ttf, .otf, .otc, or .ttc format, we check the font file extension to set the font type.

  • Create a GooString object using the path and return it.

  • Before returning the path, close the AFontMatcher and AFont objects using AFontMatcher_destroy() and AFont_close() to prevent memory leaks.

Setting up the Base-14 fonts

The Base-14 fonts are a special subset of fonts used in PDFs. Wikipedia describes them as:

Fourteen typefaces, known as the standard 14 fonts, have a special significance in PDF documents:

- Times (v3) (in regular, italic, bold, and bold italic) - Courier (in regular, oblique, bold and bold oblique) - Helvetica (v3) (in regular, oblique, bold and bold oblique) - Symbol - Zapf Dingbats

These fonts are sometimes called the base fourteen fonts. These fonts, or suitable substitute fonts with the same metrics, should be available in most PDF readers, but they are not guaranteed to be available in the reader, and may only display correctly if the system has them installed. Fonts may be substituted if they are not embedded in a PDF.

-- Wikipedia page on PDF file format

These fonts are usually substituted since they are licensed fonts, and permission is required to use them. So we use substitute fonts for them, which are either packaged along with the application or can be found in the system

Since Android systems have a limited set of fonts that can be fetched by AFontMatcher, we'll use substitute font files for the base-14 fonts. Thankfully, these are already packaged inside Okular's APK file, in the assets/share/fonts folder.

Poppler uses the GlobalParams::setupBaseFonts() method to set up these base fonts and create a mapping between the base-14 font names and their font file paths within the filesystem.

However, since these fonts are packaged inside the APK, they cannot be accessed using regular methods. So to access the fonts, I implemented a font-copying mechanism that copies all base-14 fonts into the fonts folder of the application's internal storage. This is described in the next section.

Then I had to create an array of structs with the base-14 font name and the name of the substitute font file. Here it is:

static struct
{
    const char *name;
    const char *otFileName;
} displayFontTab[] = { { "Courier", "NimbusMonoPS-Regular.otf" },
                       { "Courier-Bold", "NimbusMonoPS-Bold.otf" },
                       { "Courier-BoldOblique", "NimbusMonoPS-BoldItalic.otf" },
                       { "Courier-Oblique", "NimbusMonoPS-Italic.otf" },
                       { "Helvetica", "NimbusSans-Regular.otf" },
                       { "Helvetica-Bold", "NimbusSans-Bold.otf" },
                       { "Helvetica-BoldOblique", "NimbusSans-BoldItalic.otf" },
                       { "Helvetica-Oblique", "NimbusSans-Italic.otf" },
                       { "Symbol", "StandardSymbolsPS.otf" },
                       { "Times-Bold", "NimbusRoman-Bold.otf" },
                       { "Times-BoldItalic", "NimbusRoman-BoldItalic.otf" },
                       { "Times-Italic", "NimbusRoman-Italic.otf" },
                       { "Times-Roman", "NimbusRoman-Regular.otf" },
                       { "ZapfDingbats", "D050000L.otf" },
                       { nullptr, nullptr } };

The GlobalParams::setupBaseFonts() method would then loop over this array and set a mapping between base-14 font names and the path of their substitute font files. This mapping is then used by other methods such as GlobalParams::findFontFile() to return the font file path for a particular font. However, if there is no such font, then Poppler will fall back on GlobalParams::findSystemFontFile, which on Android uses the AFontMatcher API.

Mechanism to copy font files from the APK

While the above features worked, running the GlobalParams::setupBaseFonts() method required me to copy the font files manually, using adb push. However, the end user should never have to manually intervene in the application's files to make it work. Hence I began working on copying font files from the APK automatically.

To do so I needed a way to do 4 things:

  1. To get the path of the internal storage of the application. I needed to get the path programmatically, since other apps may use Poppler for PDF rendering, and those apps would have their own internal storage paths. Fortunately, Qt has a component called QStandardPaths which allows the user to retrieve the paths of standard directories. In this case, I used QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) to retrieve the path of the app's internal storage directory.

  2. To get the font files from within the assets folder inside the APK. Fortunately, Qt allows access to these assets through a special syntax - by prefixing the path with assets:/ and specifying the file path relative to the assets folder. So for example, if you wanted to access the file present in assets/exampledir/examplefile, you would need to specify it as "assets:/exampledir/examplefile".

  3. A way to copy the font files into the fonts directory inside the internal storage of the application. For this purpose, I used a QDirIterator along with the file paths from earlier and copied every font file into the app's internal storage.

  4. To set the path of the font directory inside the GlobalParams.cc file so that setupBaseFonts() can create the proper font mappings. For this purpose, I created a static function in GlobalParams.cc called setFontDir().

The Results

Before

Here is a video demonstrating a pdf with unembedded fonts before the new Android font-matching functionality was added:

After

And here is one demonstrating the same pdf after implementation of the new Android font matching functionality:

Remaining work

  • More thorough testing of the code: I've only done some basic testing of the new features and with a single pdf that had unembedded fonts. To find any bugs that are still hiding in the code, more thorough testing should be done with other PDFs.

  • Making the assets font directory configurable using CMake flags: The path of the assets directory that holds the fonts is currently hard coded in the DocumentData::init() method of the qt5/src/poppler-private.cc file. To enable users of the library to change this, a CMake commandline may be added to configure it.

  • Smoothening the text rendering: While I may have finished my work on setting up an Android-specific font-matching interface, the rendering of the text in the PDF still leaves something to be desired. The rendering is a bit pixelated when zoomed out, and becomes clear only when zoomed in.


The Journey

This section describes my journey as I worked on implementing AFontMatcher. It includes setting up my development environment, the components I worked on, as well as everything I tried, what worked, and what didn't. If you are interested, read on!

The Beginning

Getting up to speed with the code

Without knowing what code I had to change, I couldn't start working on my project. I approached my mentor, Albert Astals Cid () for help, and he pointed me toward the poppler/GlobalParams.cc file, and that I had to re-implement the GlobalParams::findSystemFontFile() and maybe GlobalParams::findBase14FontFile() so that it would work on the Android platform.

Setting up my development environment

Now I had to set up my development environment. No developer can do their work without some setup after all :)

Craft

First, I set up an environment to compile Okular for Android. For this, I used KDE's Craft tool, which is used for cross-compiling across OSs and architectures.

To set up Craft to compile for Android, I referenced the following guide: develop.kde.org/docs/packaging/android/buil..

I edited the arguments a bit, so my setup involved using the following commands:

mkdir -p $HOME/craft
docker run -ti --rm -v $HOME/craft:/home/user/ kdeorg/android-qt515 bash
python3 -c "$(curl https://raw.githubusercontent.com/KDE/craft/master/setup/CraftBootstrap.py)" --prefix ~/CraftRoot

I then setup the environment to build for the arm32 architecture. To build Okular for android and package it as an apk, I had to execute the following commands (exclude the docker command if you're already inside the Craft docker container):

# Start the docker container if not already inside it
docker run -ti --rm -v $HOME/craft:/home/user/ kdeorg/android-qt515 bash

# Init the craft environment 
source ~/CraftRoot/craft/craftenv.sh

# Craft okular for the first time
craft okular

# Package okular as an apk 
craft --package okular

# cd into ~/CraftRoot/tmp, where the packaged apk is
cd ~/CraftRoot/tmp

# Align the apk using zipalign
 /opt/android-sdk/build-tools/30.0.2/zipalign -p -f -v 4 okularkirigami-armeabi-v7a.apk okularkirigami-armeabi-v7a.signed.apk 

# Generate the keys for signing the apk (must only be done the first time)
keytool -genkey -noprompt -keystore key.keystore -keypass 123456  -dname "CN=None, OU=None, O=None, L=None, S=None, C=XY" -alias mykey -keyalg RSA -keysize 2048 -validity 10000 -storepass 123456

# Finally, sign the apk using apksigner
/opt/android-sdk/build-tools/30.0.2/apksigner sign -verbose -ks key.keystore okularkirigami-armeabi-v7a.signed.apk

This will generate a signed apk (.signed.apk extension) in the ~/CraftRoot/tmp directory of the docker image. To find the apk in your computer's filesystem, you must go to the path where you created the craft directory and find the tmp directory within it. For me it was located at ~/craft/CraftRoot/tmp.

Poppler

Next, I had to set up Poppler for cross-compilation to Android arm32.

Initially, this proved to be frustrating - not only was I dealing with lots of CMake flags, but I also had to cross-compile for both the Android AND ARM32 platforms - not fun.

After a couple of days of pulling my hair out in frustration, my mentor advised me to reference the android_build section of Poppler's gitlab CI file, which can be found in the repo at .gitlab-ci.yml.

Based on the Gitlab CI file, I had to use the kdeorg/android-sdk docker image for cross-compiling Poppler. I used the following commands to set up the docker image:

# Create a directory for your source code and git clone the poppler repo into it
mkdir -p ~/kde-android/src
git clone https://gitlab.freedesktop.org/poppler/poppler.git

# Launch the kdeorg/android-sdk container
docker run -ti --rm -v $HOME/kde-android/src:/home/user/src  kdeorg/android-sdk bash
cd

Now my docker container for cross-compiling Poppler was ready. I also created a build script at ~/kde-android/src/build.sh for easily building Poppler:

#!/bin/bash

echo "workaround for ECM Android toolchain wanting all binaries to be shared libraries"

sed -i -e 's/<LINK_FLAGS> <CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS>/<LINK_FLAGS>/g' /opt/nativetooling/share/ECM/toolchain/Android.cmake

mkdir -p /home/user/src/poppler/build
cd /home/user/src/poppler/build

rm -rf *

echo -e "\n\n ##### BUILDING POPPLER ##### \n\n"

ANDROID_ARCH_ABI=armeabi-v7a cmake -G Ninja .. \
    -DCMAKE_ANDROID_API=29 \
    -DCMAKE_PREFIX_PATH="/opt/Qt/;/opt/kdeandroid-arm/" \
    -DCMAKE_BUILD_TYPE=debug \
    -DCMAKE_POSITION_INDEPENDENT_CODE=OFF \
    -DENABLE_DCTDECODER=unmaintained \
    -DENABLE_LIBOPENJPEG=unmaintained \
    -DENABLE_BOOST=OFF \
    -DCMAKE_BUILD_TYPE=debugfull \
    -DCMAKE_CXX_FLAGS="-Wno-deprecated-declarations" \
    -DCMAKE_TOOLCHAIN_FILE=/opt/nativetooling/share/ECM/toolchain/Android.cmake

if [[ $1 == -b ]]; then
    ninja -j4
fi

This script should be run from the docker container. It only runs CMake by default, but it can also compile Poppler when you specify the -b flag.

Building Okular with my custom Poppler library

Now that the environment is ready, I have to figure out how to build Okular with my custom Poppler library. My mentor suggested I replace Poppler's .so files contained in the Craft container with my custom-built libraries. So I figured out all the locations where Poppler's .so files were stored inside the Craft container and made a small script to quickly replace those files with my custom-build Poppler:

#!/bin/bash

popplerdir='/home/shivodit/kde-android/src/poppler/build/android-build/libs/armeabi-v7a'

declare -a paths=(
    '/home/shivodit/craft/CraftRoot/build/qt-libs/poppler/work/build/android-build/libs/armeabi-v7a/'
    '/home/shivodit/craft/CraftRoot/build/qt-libs/poppler/image-Release-23.03.0/lib/'
    '/home/shivodit/craft/CraftRoot/build/kde/applications/okular/work/build/okularkirigami_build_apk/libs/armeabi-v7a/'
    '/home/shivodit/craft/CraftRoot/build/kde/applications/okular/work/build/okularkirigami_build_apk/build/intermediates/merged_jni_libs/release/out/armeabi-v7a/'
    '/home/shivodit/craft/CraftRoot/build/kde/applications/okular/work/build/okularkirigami_build_apk/build/intermediates/merged_native_libs/release/out/lib/armeabi-v7a/'
    '/home/shivodit/craft/CraftRoot/build/kde/applications/okular/work/build/okularkirigami_build_apk/build/intermediates/stripped_native_libs/release/out/lib/armeabi-v7a/'
    '/home/shivodit/craft/CraftRoot/lib/'
)

for i in "${paths[@]}"; do
    cp $popplerdir/* $i
done

This script is run from your host system, not from within any of the docker containers. It simply creates an array of locations where the libraries must be replaced, and copies the custom Poppler to those locations, overwriting the already existing libraries. Pretty neat, right?

Now whenever I want to build Okular, I simply have to execute the following commands after entering the docker container and activating Craft:

craft --compile okular
craft --package okular
 /opt/android-sdk/build-tools/30.0.2/zipalign -p -f -v 4 okularkirigami-armeabi-v7a.apk okularkirigami-armeabi-v7a.signed.apk 
/opt/android-sdk/build-tools/30.0.2/apksigner sign -verbose -ks key.keystore okularkirigami-armeabi-v7a.signed.apk

Now let us move on to the fun part - coding.

Coding the font API

After setting up, I got started with writing the font API. This involved several steps:

Figuring out logging

Since Android is much more locked down than desktop platforms such as Linux, MacOS, Windows, *BSDs, etc. there is a bit of difficulty involved with using a debugger to test your apps. For this very reason, my mentor suggested I use print debugging instead. In hindsight, I do think this was a good decision - setting up the debugger would have taken a long time, especially since I lack much Android development experience.

On Android, the primary tool for viewing logs from apps is Logcat. However, when you use print statements (for example, cout in C++), it does not appear in logcat. Instead of printing, you have to use the logging tools that Android provides. Since I was using C++, I set up android-ndk's logging facility. Its documentation can be found here - developer.android.com/ndk/reference/group/l..

But before using this logging tool, I set up my CMakeLists.txt to include find the log library and link it with the Poppler library, as follows:

# finding the android logging library and storing it in a variable named android-log-lib
    find_library(android-log-lib log)
# some 500 lines of other statements
#Linking the logging library with poppler
target_link_libraries(poppler LINK_PUBLIC ${android-log-lib} LINK_PRIVATE ${poppler_LIBS} LINK_PRIVATE ${android-lib})

It worked! Now all I had to do was import the logging library into the source file and use __android_log_print() to write to logcat.

However, I was still bothered - __android_log_print() took too long to type! So I googled a bit, and I stumbled upon some macros which made things easier. Here they are:

#ifndef MODULE_NAME
#define MODULE_NAME  "AUDIO-APP"
#endif

#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,MODULE_NAME, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,MODULE_NAME, __VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,MODULE_NAME, __VA_ARGS__)

I only used LOGV() since that's all I needed.

Of course, this was just temporary, for debugging. In my final merge request, I removed all the print debugging related stuff.

Figuring out AFontMatcher

The next step was to get a rudimentary version of the AFontMatcher API working. There was one problem though, I had no idea how to build with AFontMatcher. When I tried to include AFontMatcher's header files, the compiler threw a bunch of errors.

It seemed like I had to set up my CMakeLists to include the library that provided AFontMatcher functionality, and I couldn't seem to find it in the android-ndk documentation. I struggled with this problem for a few days, alternating between looking through the android-ndk documentation, googling, and experimenting with different CMakeList statements.

I got the bright idea to google for code that used any of the AFontMatcher functions. I stumbled upon Github's code search, and searched for "AFontMatcher_create". I found a C++ project using AFontMatcher, and looked at its CMakeLists.txt to figure out how to get AFontMatcher to compile.

Turns out I had to use find_library to find the android library, and then link it with Poppler. I added the following lines to the CMakeLists.txt:

# Find android library
find_library(Androidlib NAMES android REQUIRED)

# Check if library is found, if yes then add it to poppler_LIBS, which will 
# be linked during compilation
if(Androidlib) 
  set(poppler_LIBS ${poppler_LIBS} ${Androidlib})
endif()

After adding these lines, Poppler finally compiled successfully!

I then added some code to GlobalParams.cc, inside of the GlobalParams::findSystemFontFile() method to run a rudimentary version of the Afontmatcher API.

It worked; I was thrilled! After 1.5 weeks of struggling with setup and compiling Poppler with AFontMatcher, I finally had a working build! Even though the font didn't account for font-weight or italics, and only searched for serif fonts, I had taken a step forward.

Improving AFontMatcher

Now that I have a working yet rudimentary AFontMatcher implementation, it's time to refine it.

We need to determine the following:

  • Check whether the font is supposed to be sans-serif, serif, or fixed-width.

    • GfxFont::isSerif() checks whether the font is serif, and returns a boolean value.

    • GfxFont::isFixedWidth() checks whether the font is fixed width or not, and returns a boolean value

  • Check the font-weight.

    • Used to set the thickness of the font.

    • Can be retrieved using GfxFont::getWeight().

    • It returns a value from 1 to 9, however, AFontMatcher takes weight in increments of 100, from 0 to 1000. So we must multiply its value by 100.

  • Check whether the font is italic.

    • Gfx::isItalic() checks whether the font is italic, and returns a boolean value.
  • Set the type of the returned font file.

    • GlobalParams.h defines four types of fonts in an enum:
    enum SysFontType
    {
        sysFontPFA,
        sysFontPFB,
        sysFontTTF,
        sysFontTTC
    };
  • Since AFontMatcher returns .otf, .ttf, .otc, or .ttc, we set sysFontTTF for .otf and .ttf, and sysFontTTC for the remaining two.

All of this results in the following code for the AFontMatcher-based API defined in GlobalParams::findSystemFontFile():

GooString *GlobalParams::findSystemFontFile(const GfxFont *font, SysFontType *type, int *fontNum, GooString *substituteFontName, const GooString *base14Name)
{
    GooString *path = nullptr;
    const std::optional<std::string> &fontName = font->getName();

    if (!fontName) {
        return nullptr;
    }

    globalParamsLocker();

    // If font is not found in the default base-14 fonts,
    // use Android-NDK's AFontMatcher API instead. 
    // Documentation for AFontMatcher API can be found at:
    // https://developer.android.com/ndk/reference/group/font
    std::string genericFontFamily = "serif";

    if (!font->isSerif()) {
        genericFontFamily = "sans-serif";
    } else if (font->isFixedWidth()) {
        genericFontFamily = "monospace";
    }

    AFontMatcher *fontmatcher = AFontMatcher_create();

    // Set font weight and italics for the font
    AFontMatcher_setStyle(fontmatcher, font->getWeight() * 100, font->isItalic());

    // Get font match and the font file's path
    AFont *afont = AFontMatcher_match(fontmatcher, genericFontFamily.c_str(), (uint16_t *)u"A", 1, nullptr);
    path = new GooString(AFont_getFontFilePath(afont));

    // Font has been matched and its path has been copied, delete the 
    // AFontMatcher and AFont objects to avoid memory leaks
    AFont_close(afont);
    AFontMatcher_destroy(fontmatcher);

    // Set the type of font. Fonts returned by AFontMatcher are of
    // four possible types - ttf, otf, ttc, otc.
    if (path->endsWith(".ttf") || path->endsWith(".otf")) {
        *type = sysFontTTF;
    } else if (path->endsWith(".ttc") || path->endsWith(".otc")) {
        *type = sysFontTTC;
    }

    return path;
}

setupBaseFonts() and the base-14 fonts

After setting up a rudimentary afontmatcher implementation, I got started on implementing setupBaseFonts().

To start, I simply copied the code that was used for Windows' version of setupBaseFonts(), and edited it a bit to suit my needs.

I also copied the struct and its array containing the font names, and their file names. Without defining these, the setupBaseFonts function would not work. Since the copied struct had font names in .pfb and .ttf formats, I altered the struct and the array so that it only included .otf files, as follows:

static struct
{
    const char *name;
    const char *otFileName;
} displayFontTab[] = { { "Courier", "NimbusMonoPS-Regular.otf" },
                       { "Courier-Bold", "NimbusMonoPS-Bold.otf" },
                       { "Courier-BoldOblique", "NimbusMonoPS-BoldItalic.otf" },
                       { "Courier-Oblique", "NimbusMonoPS-Italic.otf" },
                       { "Helvetica", "NimbusSans-Regular.otf" },
                       { "Helvetica-Bold", "NimbusSans-Bold.otf" },
                       { "Helvetica-BoldOblique", "NimbusSans-BoldItalic.otf" },
                       { "Helvetica-Oblique", "NimbusSans-Italic.otf" },
                       { "Symbol", "StandardSymbolsPS.otf" },
                       { "Times-Bold", "NimbusRoman-Bold.otf" },
                       { "Times-BoldItalic", "NimbusRoman-BoldItalic.otf" },
                       { "Times-Italic", "NimbusRoman-Italic.otf" },
                       { "Times-Roman", "NimbusRoman-Regular.otf" },
                       { "ZapfDingbats", "D050000L.otf" },
                       { nullptr, nullptr } };

I had initially kept the Symbol font's otFileName as nullptr because I didn't know what substitute font it used from the Okular APK's assets folder. This caused setupBaseFonts to crash due to a null pointer dereference. My mentor pointed out the correct name for Symbol's substitute font. After adding its name, the nullptr dereference crash was gone.

Now I had to copy the fonts from the apk to somewhere where Poppler could access it. For the moment, I used adb push to copy the fonts to Androids /data/local/tmp/ directory, inside a folder named font.

I then added the path /data/local/tmp/font to the displayFontDirs array, which setupBaseFonts() would use to search for fonts:

static const char *displayFontDirs[] = { "/data/local/tmp/font", nullptr };

GlobalParams::setupBaseFonts() was now functional. Now I just had to write some code to copy the fonts contained within the apk to the application's data directory.

Writing the font file copying functionality

Since every android app has its own internal storage directory once it is installed, Poppler will need to find out what the exact path is. For example, when running okular, since its fully qualified name is org.kde.okular.kirigami, it's internal directory will be located at /data/user/0/org.kde.okular.kirigami/files/.

In order to find the internal data directory of the app, my mentor and I were looking at ANativeActivity as a way to do so. We also needed to access the fonts inside of the APK assets folder, for which we thought of using android-ndk's Assets API. However both of these seemed way too complex, so we explored other ideas.

While researching I came across QStandardPaths, and discovered that it could get the app directory path by using:

QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)

And to access assets, Qt provides a special font path which can be used with Qt File functions. It simply involves prefixing assets:/ to the path. The path should be relative to the assets directory of the app's apk. For example, in this case to access a file stored in assets/filedir/file.txt, we can use the path: assets:/filedir/file.txt.

Since these Qt-based solutions seemed more elegant and straightforward than the Android-NDK ANativeActivity and Asset API, my mentor agreed that I should use these instead.

Using the above features, I used a QDirIterator to loop over the assets:/share/fonts directory and copied the font files one by one to a folder named fonts, located within Okular's internal storage directory. I wrote the code like this:

QString assetsFontDir = QStringLiteral("assets:/share/fonts");
    QString fontsdir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/fonts");
    QDir fontPath = QDir(fontsdir);

    if (fontPath.mkpath(fontPath.absolutePath())) {
        QDirIterator iterator(assetsFontDir, QDir::NoFilter, QDirIterator::Subdirectories);

        while (iterator.hasNext()) {
            iterator.next();
            QFileInfo fontFileInfo = iterator.fileInfo();
            QString fontFilePath = assetsFontDir + QStringLiteral("/") + fontFileInfo.fileName();
            QString destPath = fontPath.absolutePath() + QStringLiteral("/") + fontFileInfo.fileName();
            QFile::copy(fontFilePath, destPath);
        }
    }

Initially I was implementing this not in poppler, but instead in the Okular code, in mobile/app/main.cpp. This was because we thought that since Poppler is a library that is used by Okular, it wouldn't be able to get its internal directory path.

However when implementing this, I realized I also had to set the font directory inside of Poppler.

So I asked my mentor how I could access poppler's GlobalParams.cc through Okular. He said that GlobalParams class is private to poppler, and cannot be used outside of the library. He recommended that instead of implementing this font-copying functionality in Okular, I should do it in the qt side of poppler. More specifically, in the DocumentData constructors defined in qt5/poppler-private.h.

Initially I had copy pasted the above snippet in each constructor, but later I placed it in the DocumentData::init() function defined within qt5/poppler-private.cc. This is because the init() function is called by all DocumentData constructors, and having the code in one place reduces redundancy and improves readability.

All of this was working well, all the font files were getting copied successfully. The next step was to set the font directory path inside of GlobalParams.cc, so that GlobalParams::setupBaseFonts() could search for fonts in the correct path.

To do this, I defined an Android-only static function, GlobalParams::setFontDir() in GlobalParams.h and provided its definition in GlobalParams.cc. I also replaced the static const char *displayFontDirs[] array with a static std::string variable named displayFontDir.

The GlobalParams::setFontDir() method takes an std::string as an argument. When called, it sets the displayFontDir variable to the string that was passed to it. This successfully sets the font directory. I then called this function inside the init() method, when the fonts are copied:

QString assetsFontDir = QStringLiteral("assets:/share/fonts");
    QString fontsdir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/fonts");
    QDir fontPath = QDir(fontsdir);

    if (fontPath.mkpath(fontPath.absolutePath())) {
        GlobalParams::setFontDir(fontPath.absolutePath().toStdString());
        QDirIterator iterator(assetsFontDir, QDir::NoFilter, QDirIterator::Subdirectories);

        while (iterator.hasNext()) {
            iterator.next();
            QFileInfo fontFileInfo = iterator.fileInfo();
            QString fontFilePath = assetsFontDir + QStringLiteral("/") + fontFileInfo.fileName();
            QString destPath = fontPath.absolutePath() + QStringLiteral("/") + fontFileInfo.fileName();
            QFile::copy(fontFilePath, destPath);
        }
    } else {
        GlobalParams::setFontDir("");
    }

This causes the displayFontDir variable to be set correctly.

Since displayFontDir is now an std::string instead of a const char array, we also need to modify GlobalParams::setupBaseFonts() to treat it like an std::string. Hence we remove the loop that would have looped over the earlier char array, and add a check to see if the font dir is empty.

void GlobalParams::setupBaseFonts(const char *dir)
{
    FILE *f;
    int i;

    for (i = 0; displayFontTab[i].name; ++i) {
        if (fontFiles.count(displayFontTab[i].name) > 0) {
            continue;
        }

        std::unique_ptr<GooString> fontName = std::make_unique<GooString>(displayFontTab[i].name);
        std::unique_ptr<GooString> fileName;
        if (dir) {
            fileName.reset(appendToPath(new GooString(dir), displayFontTab[i].otFileName));
            if ((f = openFile(fileName->c_str(), "rb"))) {
                fclose(f);
            } else {
                fileName.reset();
            }
        }
        if (!displayFontDir.empty()) {
            fileName.reset(appendToPath(new GooString(displayFontDir), displayFontTab[i].otFileName));
            if ((f = openFile(fileName->c_str(), "rb"))) {
                fclose(f);
            } else {
                fileName.reset();
            }
        }
        if (!fileName) {
            error(errConfig, -1, "No display font for '{0:s}'", displayFontTab[i].name);
            continue;
        }
        addFontFile(fontName->toStr(), fileName->toStr());
    }
}

Finished!

Finally, the android-specific font matching API is successfully finished. I sincerely hope this new feature is useful for users.

I had initially thought that most of the work would be in implementing AFontMatcher, however base-14 fonts took much more effort.

I'd like to thank my mentor, without his support I would still be stuck at trying to build Poppler. :D