1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16#define LOG_TAG "PdfEditor"
17
18#include <sys/types.h>
19#include <unistd.h>
20
21#include <vector>
22
23#include <log/log.h>
24#include <utils/Log.h>
25
26#include "PdfUtils.h"
27
28#include "jni.h"
29#include "JNIHelp.h"
30
31#pragma GCC diagnostic push
32#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
33#include "fpdfview.h"
34#include "fpdf_edit.h"
35#include "fpdf_save.h"
36#include "fpdf_transformpage.h"
37#pragma GCC diagnostic pop
38
39#include "SkMatrix.h"
40
41#include <core_jni_helpers.h>
42
43namespace android {
44
45enum PageBox {PAGE_BOX_MEDIA, PAGE_BOX_CROP};
46
47static struct {
48    jfieldID x;
49    jfieldID y;
50} gPointClassInfo;
51
52static struct {
53    jfieldID left;
54    jfieldID top;
55    jfieldID right;
56    jfieldID bottom;
57} gRectClassInfo;
58
59static jint nativeRemovePage(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex) {
60    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
61
62    FPDFPage_Delete(document, pageIndex);
63    HANDLE_PDFIUM_ERROR_STATE_WITH_RET_CODE(env, -1)
64
65    int pageCount = FPDF_GetPageCount(document);
66    HANDLE_PDFIUM_ERROR_STATE_WITH_RET_CODE(env, -1)
67
68    return pageCount;
69}
70
71struct PdfToFdWriter : FPDF_FILEWRITE {
72    int dstFd;
73};
74
75static bool writeAllBytes(const int fd, const void* buffer, const size_t byteCount) {
76    char* writeBuffer = static_cast<char*>(const_cast<void*>(buffer));
77    size_t remainingBytes = byteCount;
78    while (remainingBytes > 0) {
79        ssize_t writtenByteCount = write(fd, writeBuffer, remainingBytes);
80        if (writtenByteCount == -1) {
81            if (errno == EINTR) {
82                continue;
83            }
84            ALOGE("Error writing to buffer: %d", errno);
85            return false;
86        }
87        remainingBytes -= writtenByteCount;
88        writeBuffer += writtenByteCount;
89    }
90    return true;
91}
92
93static int writeBlock(FPDF_FILEWRITE* owner, const void* buffer, unsigned long size) {
94    const PdfToFdWriter* writer = reinterpret_cast<PdfToFdWriter*>(owner);
95    const bool success = writeAllBytes(writer->dstFd, buffer, size);
96    if (!success) {
97        ALOGE("Cannot write to file descriptor. Error:%d", errno);
98        return 0;
99    }
100    return 1;
101}
102
103static void nativeWrite(JNIEnv* env, jclass thiz, jlong documentPtr, jint fd) {
104    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
105    PdfToFdWriter writer;
106    writer.dstFd = fd;
107    writer.WriteBlock = &writeBlock;
108    const bool success = FPDF_SaveAsCopy(document, &writer, FPDF_NO_INCREMENTAL);
109    if (!success) {
110        jniThrowExceptionFmt(env, "java/io/IOException",
111                "cannot write to fd. Error: %d", errno);
112    }
113    HANDLE_PDFIUM_ERROR_STATE(env)
114}
115
116static void nativeSetTransformAndClip(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
117        jlong transformPtr, jint clipLeft, jint clipTop, jint clipRight, jint clipBottom) {
118    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
119
120    FPDF_PAGE* page = (FPDF_PAGE*) FPDF_LoadPage(document, pageIndex);
121    if (!page) {
122        jniThrowException(env, "java/lang/IllegalStateException",
123                "cannot open page");
124        return;
125    }
126    HANDLE_PDFIUM_ERROR_STATE(env);
127
128    double width = 0;
129    double height = 0;
130
131    const int result = FPDF_GetPageSizeByIndex(document, pageIndex, &width, &height);
132    if (!result) {
133        jniThrowException(env, "java/lang/IllegalStateException",
134                    "cannot get page size");
135        return;
136    }
137    bool isExceptionPending = forwardPdfiumError(env);
138    if (isExceptionPending) {
139        FPDF_ClosePage(page);
140        return;
141    }
142
143    // PDF's coordinate system origin is left-bottom while in graphics it
144    // is the top-left. So, translate the PDF coordinates to ours.
145    SkMatrix reflectOnX = SkMatrix::MakeScale(1, -1);
146    SkMatrix moveUp = SkMatrix::MakeTrans(0, FPDF_GetPageHeight(page));
147    SkMatrix coordinateChange = SkMatrix::Concat(moveUp, reflectOnX);
148
149    // Apply the transformation what was created in our coordinates.
150    SkMatrix matrix = SkMatrix::Concat(*reinterpret_cast<SkMatrix*>(transformPtr),
151            coordinateChange);
152
153    // Translate the result back to PDF coordinates.
154    matrix.setConcat(coordinateChange, matrix);
155
156    SkScalar transformValues[6];
157    if (!matrix.asAffine(transformValues)) {
158        FPDF_ClosePage(page);
159
160        jniThrowException(env, "java/lang/IllegalArgumentException",
161                "transform matrix has perspective. Only affine matrices are allowed.");
162        return;
163    }
164
165    FS_MATRIX transform = {transformValues[SkMatrix::kAScaleX], transformValues[SkMatrix::kASkewY],
166                           transformValues[SkMatrix::kASkewX], transformValues[SkMatrix::kAScaleY],
167                           transformValues[SkMatrix::kATransX],
168                           transformValues[SkMatrix::kATransY]};
169
170    FS_RECTF clip = {(float) clipLeft, (float) clipTop, (float) clipRight, (float) clipBottom};
171
172    FPDFPage_TransFormWithClip(page, &transform, &clip);
173    isExceptionPending = forwardPdfiumError(env);
174    if (isExceptionPending) {
175        FPDF_ClosePage(page);
176        return;
177    }
178
179    FPDF_ClosePage(page);
180    HANDLE_PDFIUM_ERROR_STATE(env);
181}
182
183static void nativeGetPageSize(JNIEnv* env, jclass thiz, jlong documentPtr,
184        jint pageIndex, jobject outSize) {
185    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
186
187    FPDF_PAGE page = FPDF_LoadPage(document, pageIndex);
188    if (!page) {
189        jniThrowException(env, "java/lang/IllegalStateException",
190                "cannot open page");
191        return;
192    }
193    HANDLE_PDFIUM_ERROR_STATE(env);
194
195    double width = 0;
196    double height = 0;
197
198    const int result = FPDF_GetPageSizeByIndex(document, pageIndex, &width, &height);
199    if (!result) {
200        jniThrowException(env, "java/lang/IllegalStateException",
201                    "cannot get page size");
202        return;
203    }
204    bool isExceptionPending = forwardPdfiumError(env);
205    if (isExceptionPending) {
206        FPDF_ClosePage(page);
207        return;
208    }
209
210    env->SetIntField(outSize, gPointClassInfo.x, width);
211    env->SetIntField(outSize, gPointClassInfo.y, height);
212
213    FPDF_ClosePage(page);
214    HANDLE_PDFIUM_ERROR_STATE(env);
215}
216
217static bool nativeGetPageBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
218        PageBox pageBox, jobject outBox) {
219    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
220
221    FPDF_PAGE page = FPDF_LoadPage(document, pageIndex);
222    if (!page) {
223        jniThrowException(env, "java/lang/IllegalStateException",
224                "cannot open page");
225        return false;
226    }
227    HANDLE_PDFIUM_ERROR_STATE_WITH_RET_CODE(env, false);
228
229    float left;
230    float top;
231    float right;
232    float bottom;
233
234    const FPDF_BOOL success = (pageBox == PAGE_BOX_MEDIA)
235        ? FPDFPage_GetMediaBox(page, &left, &top, &right, &bottom)
236        : FPDFPage_GetCropBox(page, &left, &top, &right, &bottom);
237    bool isExceptionPending = forwardPdfiumError(env);
238    if (isExceptionPending) {
239        FPDF_ClosePage(page);
240        return false;
241    }
242
243    FPDF_ClosePage(page);
244    HANDLE_PDFIUM_ERROR_STATE_WITH_RET_CODE(env, false);
245
246    if (!success) {
247        return false;
248    }
249
250    env->SetIntField(outBox, gRectClassInfo.left, (int) left);
251    env->SetIntField(outBox, gRectClassInfo.top, (int) top);
252    env->SetIntField(outBox, gRectClassInfo.right, (int) right);
253    env->SetIntField(outBox, gRectClassInfo.bottom, (int) bottom);
254
255    return true;
256}
257
258static jboolean nativeGetPageMediaBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
259        jobject outMediaBox) {
260    const bool success = nativeGetPageBox(env, thiz, documentPtr, pageIndex, PAGE_BOX_MEDIA,
261            outMediaBox);
262    return success ? JNI_TRUE : JNI_FALSE;
263}
264
265static jboolean nativeGetPageCropBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
266        jobject outMediaBox) {
267    const bool success = nativeGetPageBox(env, thiz, documentPtr, pageIndex, PAGE_BOX_CROP,
268         outMediaBox);
269    return success ? JNI_TRUE : JNI_FALSE;
270}
271
272static void nativeSetPageBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
273        PageBox pageBox, jobject box) {
274    FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
275
276    FPDF_PAGE page = FPDF_LoadPage(document, pageIndex);
277    if (!page) {
278        jniThrowException(env, "java/lang/IllegalStateException",
279                "cannot open page");
280        return;
281    }
282    HANDLE_PDFIUM_ERROR_STATE(env);
283
284    const int left = env->GetIntField(box, gRectClassInfo.left);
285    const int top = env->GetIntField(box, gRectClassInfo.top);
286    const int right = env->GetIntField(box, gRectClassInfo.right);
287    const int bottom = env->GetIntField(box, gRectClassInfo.bottom);
288
289    if (pageBox == PAGE_BOX_MEDIA) {
290        FPDFPage_SetMediaBox(page, left, top, right, bottom);
291    } else {
292        FPDFPage_SetCropBox(page, left, top, right, bottom);
293    }
294    bool isExceptionPending = forwardPdfiumError(env);
295    if (isExceptionPending) {
296        FPDF_ClosePage(page);
297        return;
298    }
299
300    FPDF_ClosePage(page);
301    HANDLE_PDFIUM_ERROR_STATE(env);
302}
303
304static void nativeSetPageMediaBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
305        jobject mediaBox) {
306    nativeSetPageBox(env, thiz, documentPtr, pageIndex, PAGE_BOX_MEDIA, mediaBox);
307}
308
309static void nativeSetPageCropBox(JNIEnv* env, jclass thiz, jlong documentPtr, jint pageIndex,
310        jobject mediaBox) {
311    nativeSetPageBox(env, thiz, documentPtr, pageIndex, PAGE_BOX_CROP, mediaBox);
312}
313
314static const JNINativeMethod gPdfEditor_Methods[] = {
315    {"nativeOpen", "(IJ)J", (void*) nativeOpen},
316    {"nativeClose", "(J)V", (void*) nativeClose},
317    {"nativeGetPageCount", "(J)I", (void*) nativeGetPageCount},
318    {"nativeRemovePage", "(JI)I", (void*) nativeRemovePage},
319    {"nativeWrite", "(JI)V", (void*) nativeWrite},
320    {"nativeSetTransformAndClip", "(JIJIIII)V", (void*) nativeSetTransformAndClip},
321    {"nativeGetPageSize", "(JILandroid/graphics/Point;)V", (void*) nativeGetPageSize},
322    {"nativeScaleForPrinting", "(J)Z", (void*) nativeScaleForPrinting},
323    {"nativeGetPageMediaBox", "(JILandroid/graphics/Rect;)Z", (void*) nativeGetPageMediaBox},
324    {"nativeSetPageMediaBox", "(JILandroid/graphics/Rect;)V", (void*) nativeSetPageMediaBox},
325    {"nativeGetPageCropBox", "(JILandroid/graphics/Rect;)Z", (void*) nativeGetPageCropBox},
326    {"nativeSetPageCropBox", "(JILandroid/graphics/Rect;)V", (void*) nativeSetPageCropBox}
327};
328
329int register_android_graphics_pdf_PdfEditor(JNIEnv* env) {
330    const int result = RegisterMethodsOrDie(
331            env, "android/graphics/pdf/PdfEditor", gPdfEditor_Methods,
332            NELEM(gPdfEditor_Methods));
333
334    jclass pointClass = FindClassOrDie(env, "android/graphics/Point");
335    gPointClassInfo.x = GetFieldIDOrDie(env, pointClass, "x", "I");
336    gPointClassInfo.y = GetFieldIDOrDie(env, pointClass, "y", "I");
337
338    jclass rectClass = FindClassOrDie(env, "android/graphics/Rect");
339    gRectClassInfo.left = GetFieldIDOrDie(env, rectClass, "left", "I");
340    gRectClassInfo.top = GetFieldIDOrDie(env, rectClass, "top", "I");
341    gRectClassInfo.right = GetFieldIDOrDie(env, rectClass, "right", "I");
342    gRectClassInfo.bottom = GetFieldIDOrDie(env, rectClass, "bottom", "I");
343
344    return result;
345};
346
347};
348