Skip to content

Commit

Permalink
feat: [Android] PdfDocument
Browse files Browse the repository at this point in the history
Inport initial implementation from PR unoplatform#1796
  • Loading branch information
artemious7 authored and workgroupengineering committed Mar 24, 2023
1 parent 9f57900 commit a9db106
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 131 deletions.
27 changes: 25 additions & 2 deletions build/PackageDiffIgnore.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9389,11 +9389,34 @@
<Member fullName="System.Void Windows.UI.Xaml.Controls.TextBox.OnIsSpellCheckEnabledChanged(Windows.UI.Xaml.DependencyPropertyChangedEventArgs e)" reason="Changed parameter type and made private." />
<Member fullName="System.Void Windows.UI.Xaml.Controls.TextBox.OnIsTextPredictionEnabledChanged(Windows.UI.Xaml.DependencyPropertyChangedEventArgs e)" reason="Changed parameter type and made private." />
<Member fullName="System.Void Windows.UI.Xaml.Controls.TextBox.OnTextAlignmentChanged(Windows.UI.Xaml.DependencyPropertyChangedEventArgs e)" reason="Changed parameter type and made private." />
<Member fullName="System.Void Windows.Devices.Lights.Lamp..ctor()" reason="Not present in Windows" />
<Member fullName="System.Void Windows.Devices.Lights.Lamp..ctor()" reason="Not present in Windows" />
</Methods>
</IgnoreSet>

<!--
<IgnoreSet baseVersion="4.8">
<Types>
</Types>

<Events>
</Events>

<Fields>
</Fields>

<Properties>
</Properties>

<Methods>
<!-- BEGIN Android PDFDocument -->
<Member fullName="System.Void Windows.Data.Pdf.PdfDocument..ctor()"
reason="Parameter-less ctor does not exist in UWP"/>
<Member fullName="System.Void Windows.Data.Pdf.PdfPage..ctor()"
reason="Parameter-less ctor does not exist in UWP"/>
<!-- END Android PDFDocument -->
</Methods>
</IgnoreSet>

<!--
Supported nodes (please keep at the bottom of this file):
<IgnoreSet baseVersion="0.0">
<Types>
Expand Down
10 changes: 2 additions & 8 deletions src/Uno.Foundation/Rect.Android.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;

namespace Windows.Foundation;
namespace Windows.Foundation;

public partial struct Rect
{
Expand All @@ -14,5 +8,5 @@ public partial struct Rect

public static implicit operator Rect(Android.Graphics.RectF rect) => new Rect(rect.Left, rect.Top, rect.Width(), rect.Height());

public static implicit operator Android.Graphics.RectF(Rect rect) => new Android.Graphics.RectF((int)rect.X, (int)rect.Y, (int)(rect.X + rect.Width), (int)(rect.Y + rect.Height));
public static implicit operator Android.Graphics.RectF(Rect rect) => new Android.Graphics.RectF((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom);
}
70 changes: 60 additions & 10 deletions src/Uno.UI.RuntimeTests/Helpers/ImageAssert.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using NUnit.Framework;
using Uno.UITest;
using Windows.UI;
using static System.Math;

using Rectangle = System.Drawing.Rectangle;
using Size = System.Drawing.Size;
using Point = System.Drawing.Point;
using SamplesApp.UITests;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml;

namespace Uno.UI.RuntimeTests.Helpers;

/// <summary>
/// Screenshot based assertions, to validate individual colors of an image
/// Screen shot based assertions, to validate individual colors of an image
/// </summary>
public static partial class ImageAssert
{
Expand Down Expand Up @@ -161,4 +152,63 @@ public static void HasPixels(RawBitmap actual, params ExpectedPixels[] expectati
}
}
#endregion

/// <summary>
/// Asserts that two image are equal within the given <see href="https://en.wikipedia.org/wiki/Root-mean-square_deviation">RMSE</see>
/// The method it based roughly on ImageMagick implementation to ensure consistency.
/// If the error is greater than or equal to 0.022, the differences are visible to human eyes.
/// <paramref name="expected">Reference image.</paramref>
/// <paramref name="actual">The image to compare with reference</paramref>
/// <paramref name="imperceptibilityThreshold">It is the threshold beyond which the compared images are not considered equal. Default value is 0.022.</paramref>>
/// </summary>
public static async Task AreEqual(RawBitmap expected, RawBitmap actual, double imperceptibilityThreshold = 0.022)
{
await actual.Populate();
await expected.Populate();

using var assertionScope = new AssertionScope("ImageAssert");

if (actual.Width != expected.Width || actual.Height != expected.Height)
{
assertionScope.FailWith($"Images have different resolutions. {Environment.NewLine}expected:({expected.Width},{expected.Height}){Environment.NewLine}actual :({actual.Width},{actual.Height})");
}

var quantity = actual.Width * actual.Height;
double squaresError = 0;

const double scale = 1 / 255d;

for (var x = 0; x < actual.Width; x++)
{
double localError = 0;

for (var y = 0; y < actual.Height; y++)
{
var expectedAlpha = expected[x, y].A * scale;
var actualAlpha = actual[x, y].A * scale;

var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
var a = expectedAlpha - actualAlpha;

var error = r * r + g * g + b * b + a * a;

localError += error;
}

squaresError += localError;
}

var meanSquaresError = squaresError / quantity;

const int channelCount = 4;

meanSquaresError = meanSquaresError / channelCount;
var sqrtMeanSquaresError = Sqrt(meanSquaresError);
if (sqrtMeanSquaresError >= imperceptibilityThreshold)
{
assertionScope.FailWith($"the actual image is not the same as the expected one.{Environment.NewLine}actual RSMD: {sqrtMeanSquaresError}{Environment.NewLine}threshold: {imperceptibilityThreshold}");
}
}
}
3 changes: 3 additions & 0 deletions src/Uno.UI.RuntimeTests/Helpers/RawBitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public Color GetPixel(int x, int y)
return Color.FromArgb(a, r, g, b);
}

public Color this[int x, int y] =>
GetPixel(x, y);

/// <summary>
/// Enables the <see cref="GetPixel(int, int)"/> method.
/// </summary>
Expand Down
112 changes: 112 additions & 0 deletions src/Uno.UWP/Data/Pdf/PdfDocument.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Android.Graphics.Pdf;
using Android.OS;
using Windows.Foundation;

namespace Windows.Data.Pdf;

public partial class PdfDocument : IDisposable
{
private readonly PdfRenderer _pdfRenderer;

private PdfDocument(PdfRenderer pdfRenderer)
{
this._pdfRenderer = pdfRenderer ?? throw new ArgumentNullException(nameof(pdfRenderer));
}

public bool IsPasswordProtected => false;

public uint PageCount => (uint)_pdfRenderer.PageCount;

public PdfPage GetPage(uint pageIndex)
{
if (pageIndex >= _pdfRenderer.PageCount)
{
throw new ArgumentOutOfRangeException(nameof(pageIndex), $"In this document the page index cannot be {pageIndex}. Page count is {_pdfRenderer.PageCount}");
}

var pdfPage = _pdfRenderer.OpenPage((int)pageIndex);
return new PdfPage(pdfPage);
}

public static IAsyncOperation<PdfDocument> LoadFromFileAsync(Storage.IStorageFile file)
{
return LoadFromFileAsync(file, null);
}

public static IAsyncOperation<PdfDocument> LoadFromFileAsync(Storage.IStorageFile file, string password)
{
if (!string.IsNullOrEmpty(password))
{
// password protected PDF files are not supported by PdfRenderer in Android
throw new NotImplementedException("The member IAsyncOperation<PdfDocument> PdfDocument.LoadFromFileAsync(IStorageFile file, string password) is not implemented in Uno.");
}

if (file is null)
{
throw new ArgumentNullException(nameof(file));
}

var localpath = file.Path;
var fileDescriptor = localpath.StartsWith('/')
? ParcelFileDescriptor.Open(new Java.IO.File(localpath), ParcelFileMode.ReadOnly)
: Android.App.Application.Context.ContentResolver.OpenFileDescriptor(Android.Net.Uri.Parse(localpath), "r");
var pdfRenderer = new PdfRenderer(fileDescriptor);
var pdfDocument = new PdfDocument(pdfRenderer);

return Task.FromResult(pdfDocument).AsAsyncOperation();
}

public static IAsyncOperation<PdfDocument> LoadFromStreamAsync(Storage.Streams.IRandomAccessStream inputStream)
{
return LoadFromStreamAsync(inputStream, default);
}

public static IAsyncOperation<PdfDocument> LoadFromStreamAsync(Storage.Streams.IRandomAccessStream inputStream, string password)
{
if (!string.IsNullOrEmpty(password))
{
// password protected PDF files are not supported by PdfRenderer in Android
throw new NotImplementedException("The member IAsyncOperation<PdfDocument> PdfDocument.LoadFromStreamAsync(IRandomAccessStream inputStream, string password) is not implemented in Uno.");
}
return AsyncOperation.FromTask(async ct =>
{
var parcel = await GetParcelFileDescriptorFromStreamAsync(inputStream.AsStream(), ct);
var pdfRenderer = new PdfRenderer(parcel);
var pdfDocument = new PdfDocument(pdfRenderer);
return pdfDocument;
});
}

public void Dispose()
{
_pdfRenderer.Dispose();
}

private static Task<ParcelFileDescriptor> GetParcelFileDescriptorFromStreamAsync(Stream input, CancellationToken token) =>
Task.Run(() =>
{
/* PdfRenderer https://developer.android.com/reference/android/graphics/pdf/PdfRenderer#PdfRenderer(android.os.ParcelFileDescriptor)
* PdfRenderer needs a seek-able ParcelFileDescriptor,
* to get it from a stream (without using hacks),
* you need to copy the stream to a temporary file.
*/
var fileName = Path.GetTempFileName();
var file = new Java.IO.File(fileName);
file.DeleteOnExit();
var outputStream = new Java.IO.FileOutputStream(file);
byte[] buf = new byte[4096];
var len = 0;
while (!token.IsCancellationRequested && (len = input.Read(buf, 0, buf.Length)) > 0)
{
outputStream.Write(buf, 0, len);
}
outputStream.Flush();
outputStream.Close();
return ParcelFileDescriptor.Open(file, ParcelFileMode.ReadOnly);
}, token);
}
Loading

0 comments on commit a9db106

Please sign in to comment.