How to Merge Images into PDF on Android using Java

Sometimes, we may need to merge multiple images into a single PDF file. For example, we scan the front side and the back side of an ID card and want to store them in a single PDF file.

In this article, we are going to talk about how to build an Android app using Java to merge images into a PDF file with the help of Dynamsoft Document Normalizer.

New Project

Open Android Studio and create a new Empty Views Activity.

Add Dependencies

Next, add Dynamsoft Document Normalizer and its dependencies.

  1. Add the following to settings.gradle.

    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            maven {
                url "https://download2.dynamsoft.com/maven/aar"
            }
        }
    }
    
  2. Add the following to build.gradle.

    implementation 'com.dynamsoft:dynamsoftcapturevisionrouter:2.2.30'
    implementation 'com.dynamsoft:dynamsoftcore:3.2.30'
    implementation 'com.dynamsoft:dynamsoftdocumentnormalizer:2.2.11'
    implementation 'com.dynamsoft:dynamsoftimageprocessing:2.2.30'
    implementation 'com.dynamsoft:dynamsoftlicense:3.2.20'
    implementation 'com.dynamsoft:dynamsoftutility:1.2.20'
    

Layout Design

Open activity_main.xml and add the following:

<Button
    android:id="@+id/selectImagesButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Select Images for Merging into PDF"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<CheckBox
    android:id="@+id/enableAutoCroppingCheckBox"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Enable Auto Cropping"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/selectImagesButton" />

<RadioGroup
    android:id="@+id/radioGroup"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="151dp"
    android:layout_marginEnd="148dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/enableAutoCroppingCheckBox">

    <RadioButton
        android:id="@+id/blackAndWhiteRadioButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Black and White" />

    <RadioButton
        android:id="@+id/grayRadioButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Grayscale" />

    <RadioButton
        android:id="@+id/colorRadioButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:checked="true"
        android:text="Color" />
</RadioGroup>
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="15dp"
    android:textAlignment="center"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/radioGroup" />

The layout is like the following screenshot. It contains a button to select images from the gallery, a checkbox to enable auto cropping of the document in the image and radio buttons to select which color mode to use for the images in the PDF.

screenshot

Create a new ActivityResultLauncher and launch it for selecting the images after selectImagesButton is clicked. We can get the list of URIs in onActivityResult.

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "DDN";
    private ActivityResultLauncher<String[]> galleryActivityLauncher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Button selectImagesButton = findViewById(R.id.selectImagesButton);
        selectImagesButton.setOnClickListener((view)->{
            galleryActivityLauncher.launch(new String[]{"image/*"});
        });
        galleryActivityLauncher = registerForActivityResult(new ActivityResultContracts.OpenMultipleDocuments(), new ActivityResultCallback<List<Uri>>() {
            @Override
            public void onActivityResult(List<Uri> results) {
                if (results != null) {
                    // perform desired operations using the result Uri
                    Log.d(TAG,"selected "+results.size()+" files");
                    textView.setText("Merging...");
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mergeImagesToPDF(results);
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }).start();
                } else {
                    Log.d(TAG, "onActivityResult: the result is null for some reason");
                }
            }
        });
    }
}

Use Dynamsoft Document Normalizer to Merge Images into PDF

  1. Initialize the license for Dynamsoft Document Normalizer. You can apply for a license here.

    private static final String LICENSE = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
       
    protected void onCreate(Bundle savedInstanceState) {
        LicenseManager.initLicense(LICENSE, this, (isSuccess, error) -> {
            if (!isSuccess) {
                Log.e(TAG, "InitLicense Error: " + error);
            }
        });
    }
       
    
  2. Create an instance of Capture Vision Router to call Dynamsoft Document Normalizer.

    private CaptureVisionRouter mRouter;
    protected void onCreate(Bundle savedInstanceState) {
        mRouter = new CaptureVisionRouter(MainActivity.this);
    }
       
    
  3. Process images and save them to a PDF file. It uses Capture Vision Router to process the images and uses Image Manager to save them to a PDF file.

    private void mergeImagesToPDF(List<Uri> results) throws Exception {
        String templateName = EnumPresetTemplate.PT_NORMALIZE_DOCUMENT;
        if (enableAutoCroppingCheckBox.isChecked()) {
            templateName = EnumPresetTemplate.PT_DETECT_AND_NORMALIZE_DOCUMENT;
        }else{
            templateName = EnumPresetTemplate.PT_NORMALIZE_DOCUMENT;
            SimplifiedCaptureVisionSettings settings = mRouter.getSimplifiedSettings(EnumPresetTemplate.PT_NORMALIZE_DOCUMENT);
            settings.roiMeasuredInPercentage = true;
            settings.roi = new Quadrilateral(new Point(0,0),new Point(100,0),new Point(100,100),new Point(0,100)); //process full image
            mRouter.updateSettings(EnumPresetTemplate.PT_NORMALIZE_DOCUMENT,settings
        }
           
        ImageManager imageManager = new ImageManager();
        File externalFilesDir = this.getApplicationContext().getExternalFilesDir("");
        String filename = new Date().getTime()+".pdf";
        File outputFile = new File(externalFilesDir,filename);
        for (Uri result:results) {
            InputStream inp = this.getApplicationContext().getContentResolver().openInputStream(result);
            if (inp != null) {
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                int nRead;
                byte[] data = new byte[16384];
                while ((nRead = inp.read(data, 0, data.length)) != -1) {
                    buffer.write(data, 0, nRead);
                }
                CapturedResult capturedResult = mRouter.capture(buffer.toByteArray(), templateName);
                NormalizedImagesResult normalizedImagesResult = capturedResult.getNormalizedImagesResult();
                if (normalizedImagesResult != null) {
                    NormalizedImageResultItem[] items = normalizedImagesResult.getItems();
                    if (items != null && items.length>0) {
                        ImageData imageData = items[0].getImageData();
                        imageManager.saveToFile(imageData,outputFile.getPath(),true); //will append images to a PDF file
                    }
                }
            }
        }
        Log.d(TAG,"run on ui thread");
        runOnUiThread(()->{
            if (outputFile.exists()) {
                textView.setText("PDF written to "+outputFile.getAbsolutePath());
            }else{
                textView.setText("Failed");
            }
        });
    }
    
  4. The color mode is modified with the following code. Converting an image to black and white can clean the background and save the file’s size while converting to grayscale has a balance of details and size.

    private void updateColorMode() throws CaptureVisionRouterException {
        int colorMode;
        if (blackAndWhiteRadioButton.isChecked()){
            colorMode = EnumImageColourMode.ICM_BINARY;
        }else if (grayRadioButton.isChecked()){
            colorMode = EnumImageColourMode.ICM_GRAYSCALE;
        }else{
            colorMode = EnumImageColourMode.ICM_COLOUR;
        }
        SimplifiedCaptureVisionSettings settings = mRouter.getSimplifiedSettings(templateName);
        settings.documentSettings.colourMode = mode;
        mRouter.updateSettings(templateName,settings);
    }
    

Screenshot of converted files:

diagram

Source Code

You can check out the source code to have a try: https://github.com/tony-xlh/Merge-Images-to-PDF/tree/main/Android/PDFCreator