C# Rendering Performance Difference - c#

I am using Dynamic Data Display to draw some plots.
It provides a class called MarkerPointsGraph to render markers to the screen. The OnRender method is called during Render and this in turn calls the appropriate marker's (Circle, Triangle, etc.) render method.
protected override void OnRenderCore(DrawingContext dc, RenderState state)
{
if (DataSource == null) return;
if (Marker == null) return;
var transform = Plotter2D.Viewport.Transform;
DataRect bounds = DataRect.Empty;
using (IPointEnumerator enumerator = DataSource.GetEnumerator(GetContext()))
{
Point point = new Point();
while (enumerator.MoveNext())
{
enumerator.GetCurrent(ref point);
enumerator.ApplyMappings(Marker);
//Point screenPoint = point.Transform(state.Visible, state.Output);
Point screenPoint = point.DataToScreen(transform);
bounds = DataRect.Union(bounds, point);
Marker.Render(dc, screenPoint);
}
}
Viewport2D.SetContentBounds(this, bounds);
}
/// <summary>Renders circle around each point of graph</summary>
public class CirclePointMarker : ShapePointMarker {
public override void Render(DrawingContext dc, Point screenPoint) {
dc.DrawEllipse(Fill, Pen, screenPoint, Size / 2, Size / 2);
}
}
Now, my calling code is as below.
plotter.AddLineGraph(
data.Data,
new Pen
{
Brush = Brushes.Violet,
DashStyle = DashStyles.DashDot,
Thickness = 3
},
GetMarker(data),
new StandardDescription(data.Title));
The main difference I noticed in performance is in the methods below.
Referring to data from the ChartData class
private ShapePointMarker GetMarker(ChartData data)
{
ShapePointMarker marker = null;
switch (data.MarkerShape)
{
case Shapes.Circle:
marker = new CirclePointMarker();
break;
case Shapes.Triangle:
marker = new TrianglePointMarker();
break;
case Shapes.None:
default:
marker = null;
break;
}
if (marker != null)
{
//Referring to stuff here.
marker.Fill = new SolidColorBrush(data.MarkerColor);
marker.Size = data.MarkerSize;
}
return marker;
}
Hardcoding the data in place of the reference.
private ShapePointMarker GetMarker(ChartData data)
{
ShapePointMarker marker = null;
switch (data.MarkerShape)
{
case Shapes.Circle:
marker = new CirclePointMarker();
break;
case Shapes.Triangle:
marker = new TrianglePointMarker();
break;
case Shapes.None:
default:
marker = null;
break;
}
if (marker != null)
{
//Hard coding stuff here.
marker.Fill = Brushes.Red;
marker.Size = 5;
}
return marker;
}
I see an improvement of nearly 100x in the render times merely by changing the reference based data in the GetMarker method to a hard coded value.
What behaviour of C# causes this difference?
EDIT: The MarkerPointsGraph does not create an object of Marker for each point. It merely calls the render method.

You could try freezing your Brush:
if (marker != null)
{
Brush b = new SolidColorBrush(data.MarkerColor);
b.Freeze();
marker.Fill = b;
marker.Size = data.MarkerSize;
}
Brushes.Red is already Frozen that might be why you see the performance increase.

Without knowing too much more about your setup, your issue likely lies in the fact that you are creating a SolidColorBrush in a relatively tight loop (and not disposing them, but that's another issue). That object has a fair amount of overhead associated with creating a new one, whereas using Brushes.Red uses the same instance and so doesn't have the same issues. If you were to create your own brush once I don't think you'd see the same problems.

Related

DirectX 11 render BGRA32 Frame

First time trying to render something and I have big troubles... I am using DirectN library and SwapChainSurface class from KlearTouch.MediaPlayer. I am trying to render BGRA32 frame using D3D11Device.
For this I have slightly modified OnNewSurfaceAvailable:
public void OnNewSurfaceAvailable2(Action<ID3D11Device, ID3D11DeviceContext> updateSurface)
{
if (rendering)
{
return;
}
try
{
if (this.swapChain is null || swapChainComObject is null)
{
return;
}
swapChainComObject.GetDesc(out var swapChainDesc).ThrowOnError();
if (swapChainDesc.BufferDesc.Width != PanelWidth || swapChainDesc.BufferDesc.Height != PanelHeight)
{
swapChainComObject.ResizeBuffers(2, PanelWidth, PanelHeight, DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, 0).ThrowOnError();
}
var device = swapChain.Object.GetDevice1().Object.As<ID3D11Device>();
device.GetImmediateContext(out var context);
// context.ClearRenderTargetView(renderTargetView.Object, new []{0f, 1f, 1f, 1f});
updateSurface(device, context);
swapChainComObject.Present(1, 0).ThrowOnError();
}
catch (ObjectDisposedException)
{
Reinitialize();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("\nException: " + ex, nameof(SwapChainSurface) + '.' + nameof(OnNewSurfaceAvailable));
}
rendering = false;
}
OnSurfaceAvailable2 is called from:
void VideoFrameArrived(Bgra32VideoFrame frame)
{
DispatcherQueue.TryEnqueue(() =>
{
previewSurface.OnNewSurfaceAvailable2((device, context) =>
{
var size = frame.m_height * frame.m_height * 4;
D3D11_TEXTURE2D_DESC td;
td.ArraySize = 1;
td.BindFlags = (uint) D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE;
td.Usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
td.CPUAccessFlags = (uint) D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE;
td.Format = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
td.Height = (uint) frame.m_height;
td.Width = (uint) frame.m_width;
td.MipLevels = 1;
td.MiscFlags = 0;
td.SampleDesc.Count = 1;
td.SampleDesc.Quality = 0;
D3D11_SUBRESOURCE_DATA srd;
srd.pSysMem = frame.m_pixelBuffer;
srd.SysMemPitch = (uint) frame.m_height;
srd.SysMemSlicePitch = 0;
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
var mappedResource = context.Map(texture.Object, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD);
var mappedData = mappedResource.pData;
unsafe
{
Buffer.MemoryCopy(frame.m_pixelBuffer.ToPointer(), mappedData.ToPointer(), size, size);
}
// Just for debug
var pixelsInFrame = new byte[size];
var pixelsInResource = new byte[size];
Marshal.Copy(frame.m_pixelBuffer, pixelsInFrame, 0, size);
Marshal.Copy(mappedResource.pData, pixelsInResource, 0, size);
context.Unmap(texture.Object, 0);
});
});
}
Problem is that I can't see anything rendered and surface stay black and I assume it should not be.
Update: Project repository
Update 2:
I solved my issue. I had too little knowledge about DX11 so I had to study more how things work there. With this knowledge I updated repository which can display preview from black magic design card. It is just example with many issues so be careful and feel free to look for or inspiration there.
There's a various amount of issues here.
First on frame arrived, you have
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
So your create a texture, but you do not use it anywhere, it needs to be blitted to the swapchain (can do a CopyResource on device context or draw a full screen triangle/quad).
Note that CopyResource will only work if your swapchain has the same size as your incoming texture, which is rather unlikely, so you will have to draw a blit with a shader most likely.
Also you actually copying the data in the texture twice :
var texture = device.CreateTexture2D<ID3D11Texture2D>(td, new []{srd});
Since you provide initial data, the content is already there.
also, pitch is incorrect :
srd.SysMemPitch = (uint) frame.m_height;
pitch is the length (in bytes) of a line, so it should be :
srd.SysMemPitch = frame.GetRowBytes();
Please also note that in case of a non converted Decklink frame,
GetRowBytes can be different from width*4 (they can align row size to multiple of 16/32 or other values).
Next, in the case of resource map, the following is also incorrect :
unsafe
{
Buffer.MemoryCopy(frame.m_pixelBuffer.ToPointer(), mappedData.ToPointer(), size, size);
}
You are not checking the pitch/stride requirement of a texture (which can be different as well),
so you need to do :
if (mappedResource.RowPitch == frame.GetRowBytes())
{
//here you can use a direct copy as above
}
else
{
//here you need to copy data line per line
}

How to make Win2D BlendEffect apply to current drawing surface (background)?

I want to draw some images on to existing canvas using multiply blend mode. However, I don't know how to do it as the BlendEffect class require me to assign the Background variable but that is suppose to be the canvas which I could not put there.
private void OnDrawCanvas(CanvasControl sender, CanvasDrawEventArgs args)
{
var list = new LinkedList<ImageNode>();
mRootNode.GetTraverseList(list, false);
foreach (var item in list)
{
if (!item.treeVisible)
continue;
if (item.mLayerPixels != null)
{
if (item.mLayer.BlendModeKey == BlendModeType.MULTIPLY)
{
var blendEffect = new BlendEffect()
{
//Background = ???, // what to put????
Foreground = item.mLayerPixels,
Mode = BlendEffectMode.Multiply
};
args.DrawingSession.DrawImage(blendEffect, item.mLayer.Left, item.mLayer.Top);
}
else
{
args.DrawingSession.DrawImage(item.mLayerPixels, item.mLayer.Left, item.mLayer.Top);
}
}
}
}
I ended up creating an offscreen CanvasRenderTarget to do the blending. When all the drawing is done, I create a CanvasBitmap from CanvasRenderTarget which allow me to draw the final result to the UI with args.DrawingSession.DrawImage();

Unable to locate cause of memory leak in Android app

I built out this whole app thinking that the garbage collector handled memory clean-up just fine, which was incredibly stupid and naive of me, but hey, it was my first time every using Xamarin to build an app, and my first time ever building an app, so what's a guy to do? Every screen seems to leak memory, but the screens that leak the most are screens that have bitmaps, generating a memory dump and analyzing it in MAT, I found the following:
So there are 4 potential culprits, 2 are bitmaps, 2 are byte arrays. This is a heap dump for the main menu of the app, if I go into my list view activity for listing out elements, I get 5 potential leaks from bitmaps. Here is the code for the activity:
AssetManager assets = Assets;
Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
var topPanel = FindViewById<TextView>(Resource.Id.topPanel);
topPanel.Text = service.GetLanguageValue("use recommendations - top bar heading");
topPanel.Dispose();
var lowerPanel = FindViewById<TextView>(Resource.Id.recommendationsPanel);
lowerPanel.Text = service.GetLanguageValue("title upper - recommendations by variety");
Shared.ScaleTextToOneLine(lowerPanel, lowerPanel.Text, Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp()), 1.0f);
lowerPanel.Dispose();
// Read html file and replace it's contents with apple data
string html = "";
using (StreamReader sr = new StreamReader(Assets.Open("apple-variety-detail.html")))
{
html = sr.ReadToEnd();
}
html = ReplaceAppleDetailsHtml(html);
var webview = FindViewById<WebView>(Resource.Id.recommendationsMessage);
CleanWebView();
webview.LoadDataWithBaseURL("file:///android_asset/",
html,
"text/html", "UTF-8", null);
if (Shared.currentApple != null)
{
// Setup apple image
using (var imageView = FindViewById<ImageView>(Resource.Id.recommendationsImage))
{
var apple = this.apples.Where(a => a.Id == Shared.currentApple.AppleId).Select(a => a).First();
var imgName = apple.Identifier.First().ToString().ToUpper() + apple.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
using (var bitmap = Shared.decodeSampledBitmapFromResource(ApplicationContext.Resources,
Resources.GetIdentifier(fullImageName.ToLower(), "drawable", PackageName),
200, 200))
{
imageView.SetImageBitmap(bitmap);
}
}
// Setup apple name
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = Shared.currentApple.Name;
}
else
{
FindViewById<TextView>(Resource.Id.appleNameTextView).Text = "Not Found!";
}
// Setup list menu for apples
AppleListView = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Scale details and list to fit on the same screen if the screen size permits
if (Shared.GetViewportWidthInDp() >= Shared.minPhoneLandscapeWidth)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(240);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
// Here, we either need to hide the list view if an apple was selected,
// or set it to be 100% of the screen if it wasn't selected.
if(!Shared.appleSelected)
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(Shared.GetViewportWidthInDp());
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
else
{
var listViewParams = AppleListView.LayoutParameters;
// Scales list view to a set width
listViewParams.Width = Shared.ScaleFloatToDensityPixels(0);
listViewParams.Height = Shared.ScaleFloatToDensityPixels(Shared.GetViewportHeightInDp());
AppleListView.LayoutParameters = listViewParams;
}
}
// Set listview adapter
if(AppleListView.Adapter == null)
{
AppleListView.Adapter = new Adapters.AppleListAdapter(this, (List<Apple>)apples, this);
}
AppleListView.FastScrollEnabled = true;
// Set the currently active view for the slide menu
var frag = (SlideMenuFragment)FragmentManager.FindFragmentById<SlideMenuFragment>(Resource.Id.SlideMenuFragment);
frag.SetSelectedLink(FindViewById<TextView>(Resource.Id.SlideMenuRecommendations));
// Replace fonts for entire view
Typeface tf = Typeface.CreateFromAsset(assets, "fonts/MuseoSansRounded-300.otf");
FontCrawler fc = new FontCrawler(tf);
fc.replaceFonts((ViewGroup)this.FindViewById(Android.Resource.Id.recommendationsRootLayout));
tf.Dispose();
}
The important part to note about this is the way this activity works is it loads an adapter, and when it displays it shows a list of items, when an item is clicked, it reloads this same activity, and it computes the screen size, shrinks down the list to show only the webview off to the side, and displays details about the item, thus simulating 2 screens, the reason I did this is because when the screen size is larger, it needs to show all of this as one single view, so on larger screens it will actually show both the listview and the webview, but still reload the activity to load new data.
The adapter code is probably what is giving me a hard time, but I'm not sure, I've tried quite a few things, but nothing seems to help, here's the adapter code:
public class AppleListAdapter : BaseAdapter<Apple>
{
List<Apple> items;
Activity context;
ApplicationService service = AgroFreshApp.Current.ApplicationService;
private Context appContext;
private Typeface tf;
static AppleRowViewHolder holder = null;
public AppleListAdapter(Activity context, List<Apple> items, Context appContext): base ()
{
this.context = context;
this.items = items;
this.appContext = appContext;
context.FindViewById<ListView>(Resource.Id.ApplesListMenu).ChoiceMode = ChoiceMode.Single;
tf = Typeface.CreateFromAsset(context.Assets, "fonts/MuseoSansRounded-300.otf");
}
public override long GetItemId(int position)
{
return position;
}
public override Apple this[int position]
{
get { return items[position]; }
}
public override int Count
{
get
{
return items.Count;
}
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
var view = convertView;
var imgName = item.Identifier.First().ToString().ToUpper() + item.Identifier.Substring(1);
var fullImageName = "SF_" + imgName;
if (view == null)
{
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
}
if (view != null)
{
holder = view.Tag as AppleRowViewHolder;
}
if(holder == null)
{
holder = new AppleRowViewHolder();
view = context.LayoutInflater.Inflate(Resource.Layout.appleRowView, null);
holder.AppleImage = view.FindViewById<ImageView>(Resource.Id.iconImageView);
holder.AppleName = view.FindViewById<TextView>(Resource.Id.nameTextView);
view.Tag = holder;
}
using (var bitmap = Shared.decodeSampledBitmapFromResource(context.Resources,
context.Resources.GetIdentifier(fullImageName.ToLower(), "drawable", context.PackageName),
25, 25))
{
holder.AppleImage.SetImageBitmap(bitmap);
}
holder.AppleName.Text = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a => a.AppleId == item.Id).Select(a => a.Name).FirstOrDefault();
holder.AppleName.SetTypeface(tf, TypefaceStyle.Normal);
view.Click += (object sender, EventArgs e) =>
{
var apple = AgroFreshApp.Current.AppleManager.Get(item.Id);
Shared.currentApple = AgroFreshApp.Current.AppleDetailManager.GetAll().Where(a=>a.AppleId == item.Id && a.LanguageId == service.UserSettings.LanguageId).Select(a=>a).FirstOrDefault();
Shared.appleSelected = true;
Intent intent = new Intent(appContext, typeof(RecommendationsActivity));
intent.SetFlags(flags: ActivityFlags.NoHistory | ActivityFlags.NewTask);
appContext.StartActivity(intent);
};
return view;
}
}
So I'm using the viewholder pattern here, and assigning click events to each list item as they get generated, with nohistory and newtask as the intent flags so that the pages refreshes properly. To clean up the bitmaps, I have been using these two methods:
This cleans the large image on the details webview:
public void CleanBitmap()
{
// Clean recommendations bitmap
ImageView imageView = (ImageView)FindViewById(Resource.Id.recommendationsImage);
Drawable drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
Java.Lang.JavaSystem.Gc();
}
And this cleans the bitmaps stored in each listview item:
public void CleanListViewBitmaps()
{
var parent = FindViewById<ListView>(Resource.Id.ApplesListMenu);
// Clean listview bitmaps
for (int i = 0; i < parent.ChildCount; i++)
{
var tempView = parent.GetChildAt(i);
// If the tag is null, this no longer holds a reference to the view, so
// just leave it.
if(tempView.Tag != null)
{
AppleRowViewHolder tempHolder = (AppleRowViewHolder)tempView.Tag;
var imageView = tempHolder.AppleImage;
var drawable = imageView.Drawable;
if (drawable is BitmapDrawable)
{
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if (bitmapDrawable.Bitmap != null)
{
Bitmap bitmap = bitmapDrawable.Bitmap;
if (!bitmap.IsRecycled)
{
imageView.SetImageBitmap(null);
bitmap.Recycle();
bitmap = null;
}
}
}
}
}
Java.Lang.JavaSystem.Gc();
}
They then get called in the activities ondestroy method like so:
protected override void OnDestroy()
{
base.OnDestroy();
CleanBitmap();
CleanListViewBitmaps();
Shared.appleSelected = false;
}
I'm also using a shared class with static variables to essentially track view states like if something was selected or no, but it only stores primitives, it doesn't store any view objects or anything like that, so I don't think that is the problem like I said it looks like bitmaps aren't getting cleaned correctly, and it seems to happen on every view, but this one in particular is bad.
I also on each view load 2 fragments, one is a slide menu fragment in a frame layout, and the other is a navbar fragment that just holds 2 bitmaps for a logo and menu handle, so those could be culprits too I suppose. Here's the navbar fragment:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
var view = inflater.Inflate(Resource.Layout.navbar, container, false);
var navLogo = view.FindViewById(Resource.Id.navbarLogo);
var menuHandle = view.FindViewById(Resource.Id.menuHandle);
var navSpacer = view.FindViewById(Resource.Id.navSpacer);
((ImageButton)(menuHandle)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((ImageButton)(menuHandle)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
((ImageButton)(menuHandle)).Click += (object sender, EventArgs e) =>
{
var slideMenu = FragmentManager.FindFragmentById(Resource.Id.SlideMenuFragment);
if (slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Show(slideMenu).Commit();
}
else if (!slideMenu.IsHidden)
{
FragmentManager.BeginTransaction().Hide(slideMenu).Commit();
}
};
var navLogoParams = navLogo.LayoutParameters;
// Account for the padding offset of the handle to center logo truly in the center of the screen
navLogoParams.Width = global::Android.Content.Res.Resources.System.DisplayMetrics.WidthPixels - (((ImageButton)(menuHandle)).MaxWidth * 2);
navLogoParams.Height = (Shared.GenerateProportionalHeight(.25f, 30));
navLogo.LayoutParameters = navLogoParams;
// Spacer puts the logo in the middle of the screen, by making it's size the same as the handle on the opposite side to force-center the logo
((Button)(navSpacer)).SetMaxWidth(Shared.GenerateProportionalWidth(.25f, 50));
((Button)(navSpacer)).SetMaxHeight(Shared.GenerateProportionalHeight(.25f, 50));
return view;
}
Does anyone see any obvious or stupid mistake that I'm making? I feel like it has to just be sheer inexperience that's causing me to miss something really obvious, or I'm doing something completely wrong, either way.
EDIT #1:
1 of the bitmaps leaking was the menu handle button in the navigation fragment, so that drops the leak down from 300kb to 200kb, but I still need to figure out how to clean it properly.
EDIT #2:
Here is my code that scales bitmaps down
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight)
{
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeResource(res, resId, options);
// Calculate inSampleSize
options.InSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)
{
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
int halfHeight = height / 2;
int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth)
{
inSampleSize *= 2;
}
}
return inSampleSize;
}
For anyone wondering, I've figured out the problem. Xamarin is a c# wrapper around native java, so at runtime there is the native Java runtime, and the mono runtime as well, so any object like a bitmap that you want to cleanup, you need to cleanup the native Java object, but you also need to clean up the c# handle to the native object, because what happens is the garbage collector goes to see if it should clean your resource, sees a handle associated with the resource, and moves on. My solution was to call the c# dispose after I cleaned up the native Java object, and then call both the c# and Java garbage collector, I'm not sure if calling both garbage collectors is explicitly needed, but I chose to do it anyway. Seriously hope this helps someone out, I do not envy people who have to hunt down these problems.
Sometimes Bitmaps ar not garbage collected correctly, and generete the outofmemory exception.
my suggestion if you're working with bitmaps is to call
System.gc();
to recycle bitmaps from memory correctly

Reading legacy Word forms checkboxes converted to PDF

Our customers sends us orders as PDF forms which is generated from a Word document built with legacy forms.
Currently people at our customer center is punching the orders into our system, but we have decided to try and automate this task.
I'm able to read the content of the PDF with a simple PdfReader per page:
public static string GetPdfText(string path)
{
var text = string.Empty;
using (var reader = new PdfReader(path))
{
for (var page = 1; page <= reader.NumberOfPages; page++)
{
text += PdfTextExtractor.GetTextFromPage(reader, page);
}
}
return text;
}
But not the checkboxes...
I am able to detect the checkboxes as dictionaries while running through every object in the PDF, but I'm unable to distinguish them from other objects or read the value...
public static IEnumerable<PdfDictionary> ReadCheckboxes(string path)
{
using (var reader = new PdfReader(path))
{
var checkboxes = new List<PdfDictionary>();
for (var i = 0; i < reader.XrefSize; i++)
{
var pdfObject = reader.GetPdfObject(i);
checkboxes.Add((PdfDictionary) pdfObject);
}
return checkboxes;
}
}
What am I missing? I've also tried reading the AcroFields, but they're empty...
I have uploaded a sample PDF with legacy checkboxes here.
Currently there is not option to integrate between our systems or do any changes to the underlying PDF or Word document.
The OP indicated in comments that a solution which returns an output like "checkbox at position x0, y0, checked; checkbox at position x1, y1, not checked; ..." would suffice, i.e. his "forms" are static enough so that these positions allow identification of the meaning of the respective checkboxes. Thus, here an implementation of this variant.
I just saw that the question is tagged c# while I have implemented the search using Java. This should not be too big a problem, the code should be easy to port. If there are problems porting, I'll add a C# version here.
As the checkboxes are drawn using vector graphics, the text extraction already used by the OP does not find them. Fortunately, though, the iText parsing framework can also be used to look for vector graphics.
Thus, we first need an ExtRenderListener (IExtRenderListener in iTextSharp) which collects the boxes. It only has non-trivial implementations of the interface methods modifyPath and renderPath:
#Override
public void modifyPath(PathConstructionRenderInfo renderInfo)
{
switch (renderInfo.getOperation())
{
case PathConstructionRenderInfo.RECT:
{
float x = renderInfo.getSegmentData().get(0);
float y = renderInfo.getSegmentData().get(1);
float w = renderInfo.getSegmentData().get(2);
float h = renderInfo.getSegmentData().get(3);
rectangle = new Rectangle(x, y, x+w, y+h);
}
case PathConstructionRenderInfo.MOVETO:
{
float x = renderInfo.getSegmentData().get(0);
float y = renderInfo.getSegmentData().get(1);
moveToVector = new Vector(x, y, 1);
lineToVector = null;
break;
}
case PathConstructionRenderInfo.LINETO:
{
if (moveToVector != null)
{
float x = renderInfo.getSegmentData().get(0);
float y = renderInfo.getSegmentData().get(1);
lineToVector = new Vector(x, y, 1);
}
break;
}
default:
moveToVector = null;
lineToVector = null;
}
}
#Override
public Path renderPath(PathPaintingRenderInfo renderInfo)
{
if (renderInfo.getOperation() != PathPaintingRenderInfo.NO_OP)
{
if (rectangle != null)
{
Vector a = new Vector(rectangle.getLeft(), rectangle.getBottom(), 1).cross(renderInfo.getCtm());
Vector b = new Vector(rectangle.getRight(), rectangle.getBottom(), 1).cross(renderInfo.getCtm());
Vector c = new Vector(rectangle.getRight(), rectangle.getTop(), 1).cross(renderInfo.getCtm());
Vector d = new Vector(rectangle.getLeft(), rectangle.getTop(), 1).cross(renderInfo.getCtm());
Box box = new Box(new LineSegment(a, c), new LineSegment(b, d));
boxes.add(box);
}
if (moveToVector != null && lineToVector != null)
{
if (!boxes.isEmpty())
{
Vector from = moveToVector.cross(renderInfo.getCtm());
Vector to = lineToVector.cross(renderInfo.getCtm());
boxes.get(boxes.size() - 1).selectDiagonal(new LineSegment(from, to));
}
}
}
moveToVector = null;
lineToVector = null;
rectangle = null;
return null;
}
Vector moveToVector = null;
Vector lineToVector = null;
Rectangle rectangle = null;
public Iterable<Box> getBoxes()
{
return boxes;
}
final List<Box> boxes = new ArrayList<Box>();
(from CheckBoxExtractionStrategy.java)
It uses a helper class Box which models the checkboxes using their respective diagonals:
public class Box
{
public LineSegment getDiagonal()
{
return diagonalA;
}
public boolean isChecked()
{
return selectedA && selectedB;
}
Box(LineSegment diagonalA, LineSegment diagonalB)
{
this.diagonalA = diagonalA;
this.diagonalB = diagonalB;
}
void selectDiagonal(LineSegment diagonal)
{
if (approximatelyEquals(diagonal, diagonalA))
selectedA = true;
else if (approximatelyEquals(diagonal, diagonalB))
selectedB = true;
}
boolean approximatelyEquals(LineSegment a, LineSegment b)
{
float permissiveness = a.getLength() / 10.0f;
if (approximatelyEquals(a.getStartPoint(), b.getStartPoint(), permissiveness) &&
approximatelyEquals(a.getEndPoint(), b.getEndPoint(), permissiveness))
return true;
if (approximatelyEquals(a.getStartPoint(), b.getEndPoint(), permissiveness) &&
approximatelyEquals(a.getEndPoint(), b.getStartPoint(), permissiveness))
return true;
return false;
}
boolean approximatelyEquals(Vector a, Vector b, float permissiveness)
{
return a.subtract(b).length() < permissiveness;
}
boolean selectedA = false;
boolean selectedB = false;
final LineSegment diagonalA, diagonalB;
}
(Inner class in CheckBoxExtractionStrategy.java)
Applying it like this to the sample document:
for (int page = 1; page <= pdfReader.getNumberOfPages(); page++)
{
System.out.printf("\nPage %s\n====\n", page);
CheckBoxExtractionStrategy strategy = new CheckBoxExtractionStrategy();
PdfReaderContentParser parser = new PdfReaderContentParser(pdfReader);
parser.processContent(page, strategy);
for (Box box : strategy.getBoxes())
{
Vector basePoint = box.getDiagonal().getStartPoint();
System.out.printf("at %s, %s - %s\n", basePoint.get(Vector.I1), basePoint.get(Vector.I2),
box.isChecked() ? "checked" : "unchecked");
}
}
one gets the output
Page 1
====
at 73.104, 757.8 - checked
at 86.544, 757.8 - checked
at 99.984, 757.8 - unchecked
for the OP's document

Can't Beat 'Out Of Memory' Exceptions

I am developing an app with Mono for Android.
I have been struggling with Out of memory exceptions for the last few days and am starting to lose hope!
I have a ListView displaying anything from 200 to 600 items. These items consist of a bitmap thumbnail and some text.
I am decoding the Bitmap asynchronously using an AsyncTask, here is the code:
public class BitmapWorkerTask : AsyncTask
{
private WeakReference imageViewReference;
public string thisURL = "";
private int sampleSize = 0;
private int reqHeight = 0;
private int reqWidht = 0;
public BitmapWorkerTask(ImageView imageView, int pSampleSize, int pReqWidth, int pReqHeight)
{
//_____________________________________________________________________
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
reqHeight = pReqHeight;
reqWidht = pReqWidth;
sampleSize = pSampleSize;
}
protected override Java.Lang.Object DoInBackground(params Java.Lang.Object[] #params)
{
string strUrl = #params[0].ToString();
try
{
return DecodeSampleBitmapFromStream(strUrl, reqWidht, reqHeight);
}
catch (Exception ex)
{
return null;
}
}
protected override void OnPostExecute(Java.Lang.Object result)
{
base.OnPostExecute(result);
if (IsCancelled)
{
result = null;
Log.Debug("TT", "OnPostExecute - Task Cancelled");
}
else
{
Bitmap bmpResult = result as Bitmap;
if (imageViewReference != null && bmpResult != null)
{
ImageView view = imageViewReference.Target as ImageView;
if (view != null)
{
view.SetImageBitmap(bmpResult);
}
}
}
}
public static int CalculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight)
{
//_____________________________
// Raw height and width of image
int height = options.OutHeight;
int width = options.OutWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
{
if (width > height)
{
inSampleSize = (int)Math.Round((float)height / (float)reqHeight);
}
else
{
inSampleSize = (int)Math.Round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
public static Bitmap DecodeSampleBitmapFromStream(string URL, int reqWidth, int reqHeight)
{
URL url = new URL(URL);
try
{
//______________________________________________________________
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeStream(url.OpenConnection().InputStream, null, options);
//______________________
// Calculate inSampleSize
options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
//____________________________________
// Decode bitmap with inSampleSize set
options.InJustDecodeBounds = false;
return BitmapFactory.DecodeStream(url.OpenConnection().InputStream, null, options);
}
catch (Exception ex)
{
return null;
}
finally
{
url.Dispose();
}
}
I am starting this AsyncTask from the Lists GetView() function using this Method:
public void loadBitmap(string url, ImageView imageView)
{
if (Common.cancelPotentialWork(url, imageView))
{
BitmapWorkerTask task = new BitmapWorkerTask(imageView, 2, 80,80);
AsyncDrawable asyncDrawable = new AsyncDrawable(null, null, task);
imageView.SetImageDrawable(asyncDrawable);
task.Execute(url);
}
}
Everything works as expected for a period of time, but if I continuously scroll up and down through my list I eventually start getting OutOfMemoryExceptions and the app crashes. My understanding of how the Android list works is it disposes of the ListItem views as they move off screen, but it feels as though this is not happening!
It feels like all those bitmaps I am decoding as I scroll through the list are for whatever reason being held in memory? What could I be missing here that is preventing those bitmaps from being disposed of? Where could I implement a call to Bitmap.Recycle() to ensure the bitmaps are cleared?
I did a test whereby I made a call to GC.Collect on every call to GetView which did seem to keep my memory usage fairly consistent, but I know this shouldn't be needed and it affects scrolling performance.
Why when I scroll through my list without the call to GC.Collect() am I not seeing those garbage collection message indicating that the system is, in fact, doing routine collections?
Any help is appreciated, I am losing the will to code!
My understanding of how the Android list works is it disposes of the List item views as they move off screen, but It feels as though this is not happening!
This isn't correct.
What Android does is it hold on to the set of item views and it tries to reuse them after they have gone off screen. This is what the convertView parameter is for.
I can't see your Adapter code posted in the question, so I'm not sure what your code is for using the convertView parameter, but I'd guess what it should do in the case of a convertView is:
it should cancel any existing async image fetch/conversion
it should start a new one
The MvvmCross code may be a little too complicated for you as a reference/example here, but you can at least see convertView in use in MvxBindableListAdapter.cs - see protected virtual View GetBindableView(View convertView, object source, int templateId)

Categories