package co.smartreceipts.android.workers.reports.pdf.renderer.text;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.google.common.base.Preconditions;
import com.tom_roush.pdfbox.pdmodel.PDDocument;
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle;
import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory;
import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject;
import com.tom_roush.pdfbox.util.awt.AWTColor;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import co.smartreceipts.android.workers.reports.pdf.fonts.PdfFontSpec;
import co.smartreceipts.android.workers.reports.pdf.pdfbox.PdfBoxImageUtils;
import co.smartreceipts.android.workers.reports.pdf.pdfbox.PdfBoxWriter;
import co.smartreceipts.android.workers.reports.pdf.renderer.Renderer;
import co.smartreceipts.android.workers.reports.pdf.renderer.constraints.WidthConstraint;
import co.smartreceipts.android.workers.reports.pdf.renderer.constraints.XPositionConstraint;
import co.smartreceipts.android.workers.reports.pdf.renderer.constraints.YPositionConstraint;
import co.smartreceipts.android.workers.reports.pdf.renderer.formatting.Alignment;
import co.smartreceipts.android.workers.reports.pdf.renderer.formatting.BackgroundColor;
import co.smartreceipts.android.workers.reports.pdf.renderer.formatting.Color;
import co.smartreceipts.android.workers.reports.pdf.renderer.formatting.Font;
import co.smartreceipts.android.workers.reports.pdf.renderer.formatting.Padding;
import wb.android.image.ImageUtils;
/**
* A fallback implementation, which should be used in conjunction with the {@link TextRenderer} class
* in order to handle situations in which our base pdf doesn't have a font with which to render a
* character glyph (common amongst non-Western languages, since we preset Roboto and not the Noto
* fonts).
*/
public class FallbackTextRenderer extends Renderer {
private static final int WIDTH_SCALE_FACTOR = 3;
private static final int UI_THREAD_WAITING_TIME_SECONDS = 5;
private static int HEIGHT_MEASURE_SPEC = ViewGroup.LayoutParams.WRAP_CONTENT;
private final Context context;
private final PDDocument pdDocument;
private final String string;;
private final Handler uiThreadRunner = new Handler(Looper.getMainLooper());
public FallbackTextRenderer(@NonNull Context context, @NonNull PDDocument pdDocument, @NonNull String string) {
this.context = Preconditions.checkNotNull(context.getApplicationContext());
this.pdDocument = Preconditions.checkNotNull(pdDocument);
this.string = Preconditions.checkNotNull(string);
this.width = WRAP_CONTENT;
this.height = WRAP_CONTENT;
}
@NonNull
@Override
public Renderer copy() {
final FallbackTextRenderer copy = new FallbackTextRenderer(this.context, this.pdDocument, this.string);
copy.height = this.height;
copy.width = this.width;
copy.getRenderingConstraints().setConstraints(this.getRenderingConstraints());
copy.getRenderingFormatting().setFormatting(this.getRenderingFormatting());
return copy;
}
@Override
public void measure() throws IOException {
final PdfFontSpec fontSpec = Preconditions.checkNotNull(getRenderingFormatting().getFormatting(Font.class));
final Float widthConstraint = getRenderingConstraints().getConstraint(WidthConstraint.class);
final float padding = getRenderingFormatting().getFormatting(Padding.class, 0f);
final TextView unscaledTextView = createTextView(fontSpec.getSize());
final int widthMeasureSpec;
if (widthConstraint != null) {
// Note: we remove the padding, since we manage that internally for this fallback view
widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((int) (widthConstraint - 2 * padding), View.MeasureSpec.EXACTLY);
} else {
widthMeasureSpec = ViewGroup.LayoutParams.WRAP_CONTENT;
}
synchronouslyRunUiThreadOperation(new Runnable() {
@Override
public void run() {
unscaledTextView.measure(widthMeasureSpec, HEIGHT_MEASURE_SPEC);
unscaledTextView.layout(0, 0, unscaledTextView.getMeasuredWidth(), unscaledTextView.getMeasuredHeight());
}
});
width = unscaledTextView.getWidth();
height = unscaledTextView.getHeight();
}
@Override
public void render(@NonNull PdfBoxWriter writer) throws IOException {
final PdfFontSpec fontSpec = Preconditions.checkNotNull(getRenderingFormatting().getFormatting(Font.class));
final Float widthConstraint = getRenderingConstraints().getConstraint(WidthConstraint.class);
final float x = Preconditions.checkNotNull(getRenderingConstraints().getConstraint(XPositionConstraint.class));
final float y = Preconditions.checkNotNull(getRenderingConstraints().getConstraint(YPositionConstraint.class));
final float padding = getRenderingFormatting().getFormatting(Padding.class, 0f);
final TextView textView = createTextView(fontSpec.getSize() * WIDTH_SCALE_FACTOR);
final int widthMeasureSpec;
if (widthConstraint != null) {
widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((int) ((widthConstraint - 2 * padding) * WIDTH_SCALE_FACTOR), View.MeasureSpec.EXACTLY);
} else {
widthMeasureSpec = ViewGroup.LayoutParams.WRAP_CONTENT;
}
final AtomicReference<Bitmap> bitmapReference = new AtomicReference<>();
final AtomicReference<IOException> ioExceptionReference = new AtomicReference<>();
try {
synchronouslyRunUiThreadOperation(new Runnable() {
@Override
public void run() {
textView.measure(widthMeasureSpec, HEIGHT_MEASURE_SPEC);
textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight());
Bitmap bitmap = ImageUtils.drawView(textView);
bitmap = ImageUtils.applyWhiteBackground(bitmap);
try {
bitmap = ImageUtils.changeCodec(bitmap, Bitmap.CompressFormat.JPEG, 100);
bitmapReference.set(bitmap);
} catch (IOException e) {
ioExceptionReference.set(e);
}
}
});
final IOException ioException = ioExceptionReference.get();
if (ioException != null) {
throw ioException;
} else {
final Bitmap bitmap = bitmapReference.get();
final PDImageXObject imageXObject = JPEGFactory.createFromImage(pdDocument, bitmap, 1);
float availableHeight = height;
float availableWidth = width;
final PDRectangle rectangle = new PDRectangle(x + padding, y + padding, availableWidth, availableHeight);
final PDRectangle resizedRec = PdfBoxImageUtils.scaleImageInsideRectangle(imageXObject, rectangle);
writer.printPDImageXObject(imageXObject, resizedRec.getLowerLeftX(), resizedRec.getLowerLeftY(), resizedRec.getWidth(), resizedRec.getHeight());
}
} finally {
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
bitmap.recycle();
}
}
}
/**
* All the {@link android.widget.TextView} stuff has to be run on the main thread, but this requires some fanciful
* thread management. To handle (pun intended) in a relatively simple manner, we use a {@link Handler}
* and {@link CountDownLatch} to ensure the UI operation completes before continuing.
*
* @param runnable the {@link Runnable} operation to perform
* @throws IOException if this operation fails to complete in under {@link #UI_THREAD_WAITING_TIME_SECONDS}
*/
private void synchronouslyRunUiThreadOperation(@NonNull final Runnable runnable) throws IOException {
final CountDownLatch uiOperationLatch = new CountDownLatch(1);
final Runnable runnableWrapper = new Runnable() {
@Override
public void run() {
runnable.run();
uiOperationLatch.countDown();
}
};
uiThreadRunner.post(runnableWrapper);
try {
uiOperationLatch.await(UI_THREAD_WAITING_TIME_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new IOException("Failed to load the pdf", e);
}
}
@NonNull
private TextView createTextView(int fontSizePx) {
final AWTColor color = Preconditions.checkNotNull(getRenderingFormatting().getFormatting(Color.class));
final AWTColor backgroundColor = getRenderingFormatting().getFormatting(BackgroundColor.class);
final Alignment.Type alignment = getRenderingFormatting().getFormatting(Alignment.class, Alignment.Type.Centered);
final TextView textView = new TextView(context);
textView.setText(string);
textView.setTextColor(color.color);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx);
if (alignment == Alignment.Type.Centered) {
textView.setGravity(Gravity.CENTER);
} else {
textView.setGravity(Gravity.START);
}
if (backgroundColor != null) {
textView.setBackgroundColor(backgroundColor.color);
}
return textView;
}
@VisibleForTesting
public static void setHeightMeasureSpec(int measureSpec) {
HEIGHT_MEASURE_SPEC = measureSpec;
}
@VisibleForTesting
public static void resetHeightMeasureSpec() {
HEIGHT_MEASURE_SPEC = ViewGroup.LayoutParams.WRAP_CONTENT;
}
}