How To Remove Whitespace on Merge - c#

I have some code that takes 3 different PDF byte arrays and merges them. This code works great. The issue (some people) are having is that each PDF is considered to be a full page (if printed) even if there is only say 4 inches of content on it, thus leaving 7 inches of white space vertically. Then the middle document gets put in and may or may not have vertical white space at the end of it. Then the footer gets put on its own page as well.
Here is the code:
byte[] Bytes = rv.LocalReport.Render("PDF", null, out MimeType, out Encoding, out Extension, out StreamIDs, out Warnings);
List<byte[]> MergeSets = // This is filled prior to this code
// Append any other pages to this primary letter
if (MergeSets.Count > 0) {
MemoryStream ms = new MemoryStream();
Document document = new Document();
PdfCopy copy = new PdfCopy(document, ms);
document.Open();
PdfImportedPage page;
PdfReader reader = new PdfReader(Bytes); // read the generated primary Letter
int pages = reader.NumberOfPages;
for (int i = 0; i < pages; ) {
page = copy.GetImportedPage(reader, ++i);
copy.AddPage(page);
} // foreach of the pages in the Cover Letter
// Now append the merge sets
foreach (byte[] ba in MergeSets) {
reader = new PdfReader(ba);
pages = reader.NumberOfPages;
for (int i = 0; i < pages; ) {
page = copy.GetImportedPage(reader, ++i);
copy.AddPage(page);
} // foreach of the pages in the current merge set
} // foreach of the sets of data
document.Close();
ServerSaved = SaveGeneratedLetter(ms.GetBuffer(), DateTime.Now.Year, hl.LetterName, SaveName);
} // if there is anything to merge
Is there a way when I am merging each page to clip/remove/erase the vertical white space at the end of each pdf so it appears as one seamless document?
UPDATE:
Here are some sample .pdf files I am trying to merge.
header, body, footer
UPDATE 2: USING THE ANSWER:
I have converted #mkl's code to C# and here it is.
The tool class:
public class PdfVeryDenseMergeTool {
private Rectangle PageSize;
private float TopMargin;
private float BottomMargin;
private float Gap;
private Document Document = null;
private PdfWriter Writer = null;
private float YPosition = 0;
public PdfVeryDenseMergeTool(Rectangle size, float top, float bottom, float gap) {
this.PageSize = size;
this.TopMargin = top;
this.BottomMargin = bottom;
this.Gap = gap;
} // PdfVeryDenseMergeTool
public void Merge(MemoryStream outputStream, List<PdfReader> inputs) {
try {
this.OpenDocument(outputStream);
foreach (PdfReader reader in inputs) {
this.Merge(reader);
} // foreach of the PDF files to merge
} finally {
this.CloseDocument();
} // try-catch-finally
} // Merge
public void OpenDocument(MemoryStream outputStream) {
this.Document = new Document(PageSize, 36, 36, this.TopMargin, this.BottomMargin);
this.Writer = PdfWriter.GetInstance(Document, outputStream);
this.Document.Open();
this.NewPage();
} // OpenDocument
public void CloseDocument() {
try {
this.Document.Close();
} finally {
this.Document = null;
this.Writer = null;
this.YPosition = 0;
} // try-finally
} // CloseDocument
public void NewPage() {
this.Document.NewPage();
this.YPosition = PageSize.GetTop(this.TopMargin);
} // Merge
public void Merge(PdfReader reader) {
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
for (int pageIndex = 1; pageIndex <= reader.NumberOfPages; pageIndex++) {
this.Merge(reader, parser, pageIndex);
} // foreach of the pages of the current PDF
} // Merge
public void Merge(PdfReader reader, PdfReaderContentParser parser, int pageIndex) {
PdfImportedPage importedPage = Writer.GetImportedPage(reader, pageIndex);
PdfContentByte directContent = Writer.DirectContent;
PageVerticalAnalyzer finder = parser.ProcessContent(pageIndex, new PageVerticalAnalyzer());
if (finder.VerticalFlips.Count < 2)
return;
Rectangle pageSizeToImport = reader.GetPageSize(pageIndex);
int startFlip = finder.VerticalFlips.Count - 1;
bool first = true;
while (startFlip > 0) {
if (!first)
this.NewPage();
float freeSpace = this.YPosition - PageSize.GetBottom(BottomMargin);
int endFlip = startFlip + 1;
while ((endFlip > 1) && (finder.VerticalFlips[startFlip] - finder.VerticalFlips[endFlip - 2] < freeSpace))
endFlip -= 2;
if (endFlip < startFlip) {
float height = finder.VerticalFlips[startFlip] - finder.VerticalFlips[endFlip];
directContent.SaveState();
directContent.Rectangle(0, this.YPosition - height, pageSizeToImport.Width, height);
directContent.Clip();
directContent.NewPath();
this.Writer.DirectContent.AddTemplate(importedPage, 0, this.YPosition - (finder.VerticalFlips[startFlip] - pageSizeToImport.Bottom));
directContent.RestoreState();
this.YPosition -= height + this.Gap;
startFlip = endFlip - 1;
} else if (!first) {
throw new ArgumentException(string.Format("Page {0} content too large", pageIndex));
} // if
first = false;
} // while
} // Merge
} // PdfVeryDenseMergeTool
The RenderListener class:
UPDATE 3: FIXED 1 LINE OF CODE AND IT WORKS: See comment in code
public class PageVerticalAnalyzer : IRenderListener {
public PageVerticalAnalyzer() { }
public List<float> VerticalFlips = new List<float>();
public void AddVerticalUseSection(float from, float to) {
if (to < from) {
float temp = to;
to = from;
from = temp;
}
int i = 0;
int j = 0;
for (i = 0; i < VerticalFlips.Count; i++) {
float flip = VerticalFlips[i];
if (flip < from)
continue;
for (j = i; j < VerticalFlips.Count; j++) {
flip = VerticalFlips[j];
if (flip < to)
continue;
break;
}
break;
} // foreach of the vertical flips
bool fromOutsideInterval = i % 2 == 0;
bool toOutsideInterval = j % 2 == 0;
while (j-- > i)
VerticalFlips.RemoveAt(j); // This was the problem line with just .Remove(j)
if (toOutsideInterval)
VerticalFlips.Insert(i, to);
if (fromOutsideInterval)
VerticalFlips.Insert(i, from);
} // AddVerticalUseSection
public void BeginTextBlock() { /* Do nothing */ }
public void EndTextBlock() { /* Do nothing */ }
public void RenderImage(ImageRenderInfo renderInfo) {
Matrix ctm = renderInfo.GetImageCTM();
List<float> YCoords = new List<float>(4) { 0, 0, 0, 0 };
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 2; y++) {
Vector corner = new Vector(x, y, 1).Cross(ctm);
YCoords[2 * x + y] = corner[Vector.I2];
}
}
YCoords.Sort();
AddVerticalUseSection(YCoords[0], YCoords[3]);
} // RenderImage
public void RenderText(TextRenderInfo renderInfo) {
LineSegment ascentLine = renderInfo.GetAscentLine();
LineSegment descentLine = renderInfo.GetDescentLine();
List<float> YCoords = new List<float>(4) {
ascentLine.GetStartPoint()[Vector.I2],
ascentLine.GetEndPoint()[Vector.I2],
descentLine.GetStartPoint()[Vector.I2],
descentLine.GetEndPoint()[Vector.I2],
};
YCoords.Sort();
AddVerticalUseSection(YCoords[0], YCoords[3]);
} // RenderText
} // PageVericalAnalyzer
Code to gather files and run the tool:
public void TestMergeDocuments() {
PdfVeryDenseMergeTool tool = new PdfVeryDenseMergeTool(iTextSharp.text.PageSize.A4, 18, 18, 10);
List<byte[]> Files = new List<byte[]>();
// Code to load each of the 3 files I need into this byte array list
using (MemoryStream ms = new MemoryStream()) {
List<PdfReader> files = new List<PdfReader>();
foreach (byte[] ba in Files) {
files.Add(new PdfReader(ba));
} // foreach of the sets of data
tool.Merge(ms, files);
// Save the file using: ms.GetBuffer()
} // using the memory stream
} // TestMergeDocuments

The following sample tool has been implemented along the ideas of the tool PdfDenseMergeTool from this answer which the OP has commented to be SO close to what [he] NEEDs. Just like PdfDenseMergeTool this tool here is implemented in Java/iText which I'm more at home with than C#/iTextSharp. As the OP has already translated PdfDenseMergeTool to C#/iTextSharp, translating this tool here also should not be too great a problem.
PdfVeryDenseMergeTool
This tool similarly to PdfDenseMergeTool takes the page contents of pages from a number of PdfReader instances and tries to merge them densely, i.e. putting contents of multiple source pages onto a single target page if there is enough free space to do so. In contrast to that earlier tool, this tool even splits source page contents to allow for an even denser merge.
Just like that other tool the PdfVeryDenseMergeTool does not take vector graphics into account because the iText(Sharp) parsing API does only forward text and bitmap images
The PdfVeryDenseMergeTool splits source pages which do not completely fit onto a target page at a horizontal line which is not intersected by the bounding boxes of text glyphs or bitmap graphics.
The tool class:
public class PdfVeryDenseMergeTool
{
public PdfVeryDenseMergeTool(Rectangle size, float top, float bottom, float gap)
{
this.pageSize = size;
this.topMargin = top;
this.bottomMargin = bottom;
this.gap = gap;
}
public void merge(OutputStream outputStream, Iterable<PdfReader> inputs) throws DocumentException, IOException
{
try
{
openDocument(outputStream);
for (PdfReader reader: inputs)
{
merge(reader);
}
}
finally
{
closeDocument();
}
}
void openDocument(OutputStream outputStream) throws DocumentException
{
final Document document = new Document(pageSize, 36, 36, topMargin, bottomMargin);
final PdfWriter writer = PdfWriter.getInstance(document, outputStream);
document.open();
this.document = document;
this.writer = writer;
newPage();
}
void closeDocument()
{
try
{
document.close();
}
finally
{
this.document = null;
this.writer = null;
this.yPosition = 0;
}
}
void newPage()
{
document.newPage();
yPosition = pageSize.getTop(topMargin);
}
void merge(PdfReader reader) throws IOException
{
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
for (int page = 1; page <= reader.getNumberOfPages(); page++)
{
merge(reader, parser, page);
}
}
void merge(PdfReader reader, PdfReaderContentParser parser, int page) throws IOException
{
PdfImportedPage importedPage = writer.getImportedPage(reader, page);
PdfContentByte directContent = writer.getDirectContent();
PageVerticalAnalyzer finder = parser.processContent(page, new PageVerticalAnalyzer());
if (finder.verticalFlips.size() < 2)
return;
Rectangle pageSizeToImport = reader.getPageSize(page);
int startFlip = finder.verticalFlips.size() - 1;
boolean first = true;
while (startFlip > 0)
{
if (!first)
newPage();
float freeSpace = yPosition - pageSize.getBottom(bottomMargin);
int endFlip = startFlip + 1;
while ((endFlip > 1) && (finder.verticalFlips.get(startFlip) - finder.verticalFlips.get(endFlip - 2) < freeSpace))
endFlip -=2;
if (endFlip < startFlip)
{
float height = finder.verticalFlips.get(startFlip) - finder.verticalFlips.get(endFlip);
directContent.saveState();
directContent.rectangle(0, yPosition - height, pageSizeToImport.getWidth(), height);
directContent.clip();
directContent.newPath();
writer.getDirectContent().addTemplate(importedPage, 0, yPosition - (finder.verticalFlips.get(startFlip) - pageSizeToImport.getBottom()));
directContent.restoreState();
yPosition -= height + gap;
startFlip = endFlip - 1;
}
else if (!first)
throw new IllegalArgumentException(String.format("Page %s content sections too large.", page));
first = false;
}
}
Document document = null;
PdfWriter writer = null;
float yPosition = 0;
final Rectangle pageSize;
final float topMargin;
final float bottomMargin;
final float gap;
}
(PdfVeryDenseMergeTool.java)
This tool makes use of a custom RenderListener for use with the iText parser API:
public class PageVerticalAnalyzer implements RenderListener
{
#Override
public void beginTextBlock() { }
#Override
public void endTextBlock() { }
/*
* #see RenderListener#renderText(TextRenderInfo)
*/
#Override
public void renderText(TextRenderInfo renderInfo)
{
LineSegment ascentLine = renderInfo.getAscentLine();
LineSegment descentLine = renderInfo.getDescentLine();
float[] yCoords = new float[]{
ascentLine.getStartPoint().get(Vector.I2),
ascentLine.getEndPoint().get(Vector.I2),
descentLine.getStartPoint().get(Vector.I2),
descentLine.getEndPoint().get(Vector.I2)
};
Arrays.sort(yCoords);
addVerticalUseSection(yCoords[0], yCoords[3]);
}
/*
* #see RenderListener#renderImage(ImageRenderInfo)
*/
#Override
public void renderImage(ImageRenderInfo renderInfo)
{
Matrix ctm = renderInfo.getImageCTM();
float[] yCoords = new float[4];
for (int x=0; x < 2; x++)
for (int y=0; y < 2; y++)
{
Vector corner = new Vector(x, y, 1).cross(ctm);
yCoords[2*x+y] = corner.get(Vector.I2);
}
Arrays.sort(yCoords);
addVerticalUseSection(yCoords[0], yCoords[3]);
}
/**
* This method marks the given interval as used.
*/
void addVerticalUseSection(float from, float to)
{
if (to < from)
{
float temp = to;
to = from;
from = temp;
}
int i=0, j=0;
for (; i<verticalFlips.size(); i++)
{
float flip = verticalFlips.get(i);
if (flip < from)
continue;
for (j=i; j<verticalFlips.size(); j++)
{
flip = verticalFlips.get(j);
if (flip < to)
continue;
break;
}
break;
}
boolean fromOutsideInterval = i%2==0;
boolean toOutsideInterval = j%2==0;
while (j-- > i)
verticalFlips.remove(j);
if (toOutsideInterval)
verticalFlips.add(i, to);
if (fromOutsideInterval)
verticalFlips.add(i, from);
}
final List<Float> verticalFlips = new ArrayList<Float>();
}
(PageVerticalAnalyzer.java)
It is used like this:
PdfVeryDenseMergeTool tool = new PdfVeryDenseMergeTool(PageSize.A4, 18, 18, 5);
tool.merge(output, inputs);
(VeryDenseMerging.java)
Applied to the OP's sample documents
Header.pdf
Body.pdf
Footer.pdf
it generates
If one defines the target document page size to be A5 landscape:
PdfVeryDenseMergeTool tool = new PdfVeryDenseMergeTool(new RectangleReadOnly(595,421), 18, 18, 5);
tool.merge(output, inputs);
(VeryDenseMerging.java)
it generates this:
Beware! This is only a proof of concept and it does not consider all possibilities. E.g. the case of source or target pages with a non-trivial Rotate value is not properly handled. Thus, it is not ready for production use yet.
Improvement in current (5.5.6 SNAPSHOT) iText version
The current iText development version towards 5.5.6 enhances the parser functionality to also signal vector graphics. Thus, I extended the PageVerticalAnalyzer to make use of this:
public class PageVerticalAnalyzer implements ExtRenderListener
{
#Override
public void beginTextBlock() { }
#Override
public void endTextBlock() { }
#Override
public void clipPath(int rule) { }
...
static class SubPathSection
{
public SubPathSection(float x, float y, Matrix m)
{
float effectiveY = getTransformedY(x, y, m);
pathFromY = effectiveY;
pathToY = effectiveY;
}
void extendTo(float x, float y, Matrix m)
{
float effectiveY = getTransformedY(x, y, m);
if (effectiveY < pathFromY)
pathFromY = effectiveY;
else if (effectiveY > pathToY)
pathToY = effectiveY;
}
float getTransformedY(float x, float y, Matrix m)
{
return new Vector(x, y, 1).cross(m).get(Vector.I2);
}
float getFromY()
{
return pathFromY;
}
float getToY()
{
return pathToY;
}
private float pathFromY;
private float pathToY;
}
/*
* Beware: The implementation is not correct as it includes the control points of curves
* which may be far outside the actual curve.
*
* #see ExtRenderListener#modifyPath(PathConstructionRenderInfo)
*/
#Override
public void modifyPath(PathConstructionRenderInfo renderInfo)
{
Matrix ctm = renderInfo.getCtm();
List<Float> segmentData = renderInfo.getSegmentData();
switch (renderInfo.getOperation())
{
case PathConstructionRenderInfo.MOVETO:
subPath = null;
case PathConstructionRenderInfo.LINETO:
case PathConstructionRenderInfo.CURVE_123:
case PathConstructionRenderInfo.CURVE_13:
case PathConstructionRenderInfo.CURVE_23:
for (int i = 0; i < segmentData.size()-1; i+=2)
{
if (subPath == null)
{
subPath = new SubPathSection(segmentData.get(i), segmentData.get(i+1), ctm);
path.add(subPath);
}
else
subPath.extendTo(segmentData.get(i), segmentData.get(i+1), ctm);
}
break;
case PathConstructionRenderInfo.RECT:
float x = segmentData.get(0);
float y = segmentData.get(1);
float w = segmentData.get(2);
float h = segmentData.get(3);
SubPathSection section = new SubPathSection(x, y, ctm);
section.extendTo(x+w, y, ctm);
section.extendTo(x, y+h, ctm);
section.extendTo(x+w, y+h, ctm);
path.add(section);
case PathConstructionRenderInfo.CLOSE:
subPath = null;
break;
default:
}
}
/*
* #see ExtRenderListener#renderPath(PathPaintingRenderInfo)
*/
#Override
public Path renderPath(PathPaintingRenderInfo renderInfo)
{
if (renderInfo.getOperation() != PathPaintingRenderInfo.NO_OP)
{
for (SubPathSection section : path)
addVerticalUseSection(section.getFromY(), section.getToY());
}
path.clear();
subPath = null;
return null;
}
List<SubPathSection> path = new ArrayList<SubPathSection>();
SubPathSection subPath = null;
...
}
(PageVerticalAnalyzer.java)
A simple test (VeryDenseMerging.java method testMergeOnlyGraphics) merges these files
into this:
But once again beware: this is a mere proof of concept. Especially modifyPath() needs to be improved, the implementation is not correct as it includes the control points of curves which may be far outside the actual curve.

Related

argument out of range exception in a memory puzzle game

I'm following along a course on udemy which is for free it's a memory game (https://www.udemy.com/xamarin-native-ios-memory-game-csharp/learn/v4/overview)
Now with the randomizer method i get a new problem with the same out of range error
using System;
using System.Collections;
using System.Collections.Generic;
using UIKit;
namespace iOSMemoryGame
{
public partial class ViewController : UIViewController
{
#region vars
List<String> imgArr = new List<String>();
float gameViewWidth;
int gridSize = 6;
ArrayList tilesArr = new ArrayList();
ArrayList coordsArr = new ArrayList();
#endregion
public ViewController(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Perform any additional setup after loading the view, typically from a nib.
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
// make sure Game View is Laid Out
gameView.LayoutIfNeeded();
gameViewWidth = (float)gameView.Frame.Size.Width;
// let's load all of our images into an array
for (int i = 1; i <= 18; i++)
{
imgArr.Add("img_" + i.ToString() + ".png");
}
// let's make a call to tileMaker
tileMaker();
// let's call the randomizer
randomizer();
}
private void randomizer()
{
// we are gonna go through our tiles in ORDER
// and we are gonna assign a new center to them RANDOMLY
foreach (UIView any in tilesArr)
{
// UIImageView thisTile = (UIImageView)tilesArr[i];
Random myRand = new Random();
int randomIndex = myRand.Next(0, coordsArr.Count);
CoreGraphics.CGPoint newRandCenter = (CoreGraphics.CGPoint)coordsArr[randomIndex];
any.Center = newRandCenter;
coordsArr.RemoveAt(randomIndex);
}
}
private void tileMaker()
{
float tileWidth = gameViewWidth / gridSize;
float xCenter = tileWidth / 2;
float yCenter = tileWidth / 2;
int imgCounter = 0;
for (int h = 0; h < gridSize; h++)
{
for (int v = 0; v < gridSize; v++)
{
UIImageView tileImgView = new UIImageView();
CoreGraphics.CGPoint newCenter = new CoreGraphics.CGPoint(xCenter, yCenter);
tileImgView.Frame = new CoreGraphics.CGRect(0, 0, tileWidth - 5, tileWidth - 5);
String imgName = imgArr[imgCounter];
tileImgView.Image = new UIImage(imgName);
tileImgView.Center = newCenter;
// user CAN interact with this image view
tileImgView.UserInteractionEnabled = true;
// adding the new image view to the array
tilesArr.Add(tileImgView);
gameView.AddSubview(tileImgView);
xCenter = xCenter + tileWidth;
imgCounter++;
if (imgCounter == gridSize * gridSize / 2)
{
imgCounter = 0;
}
}
xCenter = tileWidth / 2;
yCenter = yCenter + tileWidth;
}
}
public override void DidReceiveMemoryWarning()
{
base.DidReceiveMemoryWarning();
// Release any cached data, images, etc that aren't in use.
}
partial void Rst4Button_TouchUpInside(UIButton sender)
{
throw new NotImplementedException();
}
partial void Rst6Button_TouchUpInside(UIButton sender)
{
throw new NotImplementedException();
}
}
}
Something is wrong with the randomizer method but i dont know what.
it gives me again a out of range error.
without the randomizer method it works fine
This line
for (int i = 1; i < 18; i++)
creates 17 images, not 18 as expected by the following loops.
// 0-5 = 6 loops
for (int h = 0; h < gridSize; h++)
{
// 0-5 = 6 loops (h*v=18)
for (int v = 0; v < gridSize; v++)
{
....
You need to write
for (int i = 0; i < 18; i++)
imgArr.Add("img_" + (i+1).ToString() + ".png");

From C# serverside, Is there anyway to generate a treemap and save as an image?

I have been using this javascript library to create treemap on webpages and it works great. The issue now is that I need to include this in a powerpoint presentation that I am generating on the server side (I am generating the powerpoint using aspose.slides for .net)
The easiest thing I thought of was to try to somehow build a treemap on the server and save as an image (as adding an image into the powerpoint presentation is quite simple) but after googling, I don't see any solution that from C# serverside can generate a treemap as an image.
Does something like this exist where I can create a treemap as an image from a server side C# app.
Given that algorithms are known, it's not hard to just draw a bitmap with a treemap. At the moment I don't have enough time to write code myself, but I have enough time to (almost) mindlessly port some existing code to C# :) Let's take this javascript implementation. It uses algorithm described in this paper. I found some problems in that implementation, which are fixed in C# version. Javascript version works with pure arrays (and arrays of arrays of arrays) of integers. We define some class instead:
public class TreemapItem {
private TreemapItem() {
FillBrush = Brushes.White;
BorderBrush = Brushes.Black;
TextBrush = Brushes.Black;
}
public TreemapItem(string label, int area, Brush fillBrush) : this() {
Label = label;
Area = area;
FillBrush = fillBrush;
Children = null;
}
public TreemapItem(params TreemapItem[] children) : this() {
// in this implementation if there are children - all other properies are ignored
// but this can be changed in future
Children = children;
}
// Label to write on rectangle
public string Label { get; set; }
// color to fill rectangle with
public Brush FillBrush { get; set; }
// color to fill rectangle border with
public Brush BorderBrush { get; set; }
// color of label
public Brush TextBrush { get; set; }
// area
public int Area { get; set; }
// children
public TreemapItem[] Children { get; set; }
}
Then starting to port. First Container class:
class Container {
public Container(int x, int y, int width, int height) {
X = x;
Y = y;
Width = width;
Height = height;
}
public int X { get; }
public int Y { get; }
public int Width { get; }
public int Height { get; }
public int ShortestEdge => Math.Min(Width, Height);
public IDictionary<TreemapItem, Rectangle> GetCoordinates(TreemapItem[] row) {
// getCoordinates - for a row of boxes which we've placed
// return an array of their cartesian coordinates
var coordinates = new Dictionary<TreemapItem, Rectangle>();
var subx = this.X;
var suby = this.Y;
var areaWidth = row.Select(c => c.Area).Sum()/(float) Height;
var areaHeight = row.Select(c => c.Area).Sum()/(float) Width;
if (Width >= Height) {
for (int i = 0; i < row.Length; i++) {
var rect = new Rectangle(subx, suby, (int) (areaWidth), (int) (row[i].Area/areaWidth));
coordinates.Add(row[i], rect);
suby += (int) (row[i].Area/areaWidth);
}
}
else {
for (int i = 0; i < row.Length; i++) {
var rect = new Rectangle(subx, suby, (int) (row[i].Area/areaHeight), (int) (areaHeight));
coordinates.Add(row[i], rect);
subx += (int) (row[i].Area/areaHeight);
}
}
return coordinates;
}
public Container CutArea(int area) {
// cutArea - once we've placed some boxes into an row we then need to identify the remaining area,
// this function takes the area of the boxes we've placed and calculates the location and
// dimensions of the remaining space and returns a container box defined by the remaining area
if (Width >= Height) {
var areaWidth = area/(float) Height;
var newWidth = Width - areaWidth;
return new Container((int) (X + areaWidth), Y, (int) newWidth, Height);
}
else {
var areaHeight = area/(float) Width;
var newHeight = Height - areaHeight;
return new Container(X, (int) (Y + areaHeight), Width, (int) newHeight);
}
}
}
Then Treemap class which builds actual Bitmap
public class Treemap {
public Bitmap Build(TreemapItem[] items, int width, int height) {
var map = BuildMultidimensional(items, width, height, 0, 0);
var bmp = new Bitmap(width, height);
var g = Graphics.FromImage(bmp);
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
foreach (var kv in map) {
var item = kv.Key;
var rect = kv.Value;
// fill rectangle
g.FillRectangle(item.FillBrush, rect);
// draw border
g.DrawRectangle(new Pen(item.BorderBrush, 1), rect);
if (!String.IsNullOrWhiteSpace(item.Label)) {
// draw text
var format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
var font = new Font("Arial", 16);
g.DrawString(item.Label, font, item.TextBrush, new RectangleF(rect.X, rect.Y, rect.Width, rect.Height), format);
}
}
return bmp;
}
private Dictionary<TreemapItem, Rectangle> BuildMultidimensional(TreemapItem[] items, int width, int height, int x, int y) {
var results = new Dictionary<TreemapItem, Rectangle>();
var mergedData = new TreemapItem[items.Length];
for (int i = 0; i < items.Length; i++) {
// calculate total area of children - current item's area is ignored
mergedData[i] = SumChildren(items[i]);
}
// build a map for this merged items (merged because their area is sum of areas of their children)
var mergedMap = BuildFlat(mergedData, width, height, x, y);
for (int i = 0; i < items.Length; i++) {
var mergedChild = mergedMap[mergedData[i]];
// inspect children of children in the same way
if (items[i].Children != null) {
var headerRect = new Rectangle(mergedChild.X, mergedChild.Y, mergedChild.Width, 20);
results.Add(mergedData[i], headerRect);
// reserve 20 pixels of height for header
foreach (var kv in BuildMultidimensional(items[i].Children, mergedChild.Width, mergedChild.Height - 20, mergedChild.X, mergedChild.Y + 20)) {
results.Add(kv.Key, kv.Value);
}
}
else {
results.Add(mergedData[i], mergedChild);
}
}
return results;
}
private Dictionary<TreemapItem, Rectangle> BuildFlat(TreemapItem[] items, int width, int height, int x, int y) {
// normalize all area values for given width and height
Normalize(items, width*height);
var result = new Dictionary<TreemapItem, Rectangle>();
Squarify(items, new TreemapItem[0], new Container(x, y, width, height), result);
return result;
}
private void Normalize(TreemapItem[] data, int area) {
var sum = data.Select(c => c.Area).Sum();
var multi = area/(float) sum;
foreach (var item in data) {
item.Area = (int) (item.Area*multi);
}
}
private void Squarify(TreemapItem[] data, TreemapItem[] currentRow, Container container, Dictionary<TreemapItem, Rectangle> stack) {
if (data.Length == 0) {
foreach (var kv in container.GetCoordinates(currentRow)) {
stack.Add(kv.Key, kv.Value);
}
return;
}
var length = container.ShortestEdge;
var nextPoint = data[0];
if (ImprovesRatio(currentRow, nextPoint, length)) {
currentRow = currentRow.Concat(new[] {nextPoint}).ToArray();
Squarify(data.Skip(1).ToArray(), currentRow, container, stack);
}
else {
var newContainer = container.CutArea(currentRow.Select(c => c.Area).Sum());
foreach (var kv in container.GetCoordinates(currentRow)) {
stack.Add(kv.Key, kv.Value);
}
Squarify(data, new TreemapItem[0], newContainer, stack);
}
}
private bool ImprovesRatio(TreemapItem[] currentRow, TreemapItem nextNode, int length) {
// if adding nextNode
if (currentRow.Length == 0)
return true;
var newRow = currentRow.Concat(new[] {nextNode}).ToArray();
var currentRatio = CalculateRatio(currentRow, length);
var newRatio = CalculateRatio(newRow, length);
return currentRatio >= newRatio;
}
private int CalculateRatio(TreemapItem[] row, int length) {
var min = row.Select(c => c.Area).Min();
var max = row.Select(c => c.Area).Max();
var sum = row.Select(c => c.Area).Sum();
return (int) Math.Max(Math.Pow(length, 2)*max/Math.Pow(sum, 2), Math.Pow(sum, 2)/(Math.Pow(length, 2)*min));
}
private TreemapItem SumChildren(TreemapItem item) {
int total = 0;
if (item.Children?.Length > 0) {
total += item.Children.Sum(c => c.Area);
foreach (var child in item.Children) {
total += SumChildren(child).Area;
}
}
else {
total = item.Area;
}
return new TreemapItem(item.Label, total, item.FillBrush);
}
}
Now let's try to use and see how it goes:
var map = new[] {
new TreemapItem("ItemA", 0, Brushes.DarkGray) {
Children = new[] {
new TreemapItem("ItemA-1", 200, Brushes.White),
new TreemapItem("ItemA-2", 500, Brushes.BurlyWood),
new TreemapItem("ItemA-3", 600, Brushes.Purple),
}
},
new TreemapItem("ItemB", 1000, Brushes.Yellow) {
},
new TreemapItem("ItemC", 0, Brushes.Red) {
Children = new[] {
new TreemapItem("ItemC-1", 200, Brushes.White),
new TreemapItem("ItemC-2", 500, Brushes.BurlyWood),
new TreemapItem("ItemC-3", 600, Brushes.Purple),
}
},
new TreemapItem("ItemD", 2400, Brushes.Blue) {
},
new TreemapItem("ItemE", 0, Brushes.Cyan) {
Children = new[] {
new TreemapItem("ItemE-1", 200, Brushes.White),
new TreemapItem("ItemE-2", 500, Brushes.BurlyWood),
new TreemapItem("ItemE-3", 600, Brushes.Purple),
}
},
};
using (var bmp = new Treemap().Build(map, 1024, 1024)) {
bmp.Save("output.bmp", ImageFormat.Bmp);
}
Output:
This can be extended in multiple ways, and code quality can certainly be improved significantly. But if you would go this way, it can at least give you a good start. Benefit is that it's fast and no external dependencies involved.
If you would want to use it and find some issues or it does not match some of your requirements - feel free to ask and I will improve it when will have more time.
Using the GDI+ api may be your only choice, with good support cross platform. However, there are several potential issues you need to be aware of when doing anything with GDI+ on the server side. It's worth reading this as it explains the current state of drawing Graphics in DotNet and has a point on Server-side processing:
https://github.com/imazen/Graphics-vNext
Having said that; there's this article that deals with what you're asking for:
OutOfMemory Exception when recursively drawing rectangles in GDI+ (It's specifically talking about generating a TreeMap with GDI+ and if you read the comments and answer you'll avoid a lot of the pitfalls)
Once you've generated your image, it's a trivial process to save it to disk somewhere and then, hopefully, embed it within your presentation; you have options to write to streams as well, so it may be possible to directly embed it in the powerpoint file without first saving it to disk.
You can use WPF rendering: http://lordzoltan.blogspot.co.uk/2010/09/using-wpf-to-render-bitmaps.html but it's not without its drawbacks.
(That's a link to my own old blog - but if you search for 'using wpf to generate images' you'll get lots of other examples - many of which are better than mine!)
Generating a tree in WPF is going to be, well, challenging, though - although it can be done, since the WPF drawing primitives are hierarchical in nature.
It might not be suitable, bou could also consider GraphViz - https://github.com/JamieDixon/GraphViz-C-Sharp-Wrapper however I don't know how much luck you'll have executing the command line in a web server.
There are - I expect - bound to be paid-for libraries to do this, as well, as it's a common need.
The Eindhoven University of Technology has published a paper on the algorithm of squarified treemaps. Pascal Laurin has turned this into C#. There is also a Code Project article that has a section about treemaps.
Of course there are also commercial solutions like the one from .NET Charting, Infragistics or Telerik. The downside of those is probably that they are designed as Controls which need to be painted, so you might need some sort of UI thread.
There is also a question here on Stack Overflow that already asked for treemap implementations in C#. Just in case you don't remember.
Since you are already generating the JS and the HTML version of everything, one thing you might want to check out is :
http://www.nrecosite.com/html_to_image_generator_net.aspx
I use this to generate high res-graphic reports straight from my generated pages. It used WKHTML to render it and you can pass a ton of parameters to it to really fine tune it. Its free to most things and it works great. Multi-threading is kind of a pain in the butt with it but I haven't run into to many issues. If you use the NRECO PDf library you can even do batches of stuff also.
With this all you would have to do is render the page like you already are, pipe it through the library and insert into your PPT and all should be good.
Since all you need to do is to extract a screenshot of the web page, it would be more convenient to capture the web page as an Image.
This free library is capable of extracting screenshot from your web page, and it supports Javascript / CSS.
I'm basing the following solution in this project about treemaps in WPF.
Using the data in your link, you can define your model (only with necesary data) like this:
class Data
{
[JsonProperty("$area")]
public float Area { get; set; }
[JsonProperty("$color")]
public Color Color { get; set; }
}
class Item
{
public string Name { get; set; }
public Data Data { get; set; }
public IEnumerable<Item> Children { get; set; }
internal TreeMapData TMData { get; set; }
internal int GetDepth()
{
return Children.Select(c => c.GetDepth()).DefaultIfEmpty().Max() + 1;
}
}
Adding an extra property TreeMapData with some values used in the solution:
class TreeMapData
{
public float Area { get; set; }
public SizeF Size { get; set; }
public PointF Location { get; set; }
}
Now, defining a TreeMap class with the following public members:
class TreeMap
{
public IEnumerable<Item> Items { get; private set; }
public TreeMap(params Item[] items) :
this(items.AsEnumerable()) { }
public TreeMap(IEnumerable<Item> items)
{
Items = items.OrderByDescending(t => t.Data.Area).ThenByDescending(t => t.Children.Count());
}
public Bitmap Draw(int width, int height)
{
var bmp = new Bitmap(width + 1, height + 1);
using (var g = Graphics.FromImage(bmp))
{
DrawIn(g, 0, 0, width, height);
g.Flush();
}
return bmp;
}
//Private members
}
So, you can use it like this:
var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);
And the private/helper members:
private RectangleF emptyArea;
private void DrawIn(Graphics g, float x, float y, float width, float height)
{
Measure(width, height);
foreach (var item in Items)
{
var sFormat = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
if (item.Children.Count() > 0)
{
g.FillRectangle(Brushes.DimGray, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15);
g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.LightGray, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, 15), sFormat);
var treeMap = new TreeMap(item.Children);
treeMap.DrawIn(g, x + item.TMData.Location.X, y + item.TMData.Location.Y + 15, item.TMData.Size.Width, item.TMData.Size.Height - 15);
}
else
{
g.FillRectangle(new SolidBrush(item.Data.Color), x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
g.DrawString(item.Name, SystemFonts.DefaultFont, Brushes.Black, new RectangleF(x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height), sFormat);
}
var pen = new Pen(Color.Black, item.GetDepth() * 1.5f);
g.DrawRectangle(pen, x + item.TMData.Location.X, y + item.TMData.Location.Y, item.TMData.Size.Width, item.TMData.Size.Height);
}
g.Flush();
}
private void Measure(float width, float height)
{
emptyArea = new RectangleF(0, 0, width, height);
var area = width * height;
var sum = Items.Sum(t => t.Data.Area + 1);
foreach (var item in Items)
{
item.TMData = new TreeMapData();
item.TMData.Area = area * (item.Data.Area + 1) / sum;
}
Squarify(Items, new List<Item>(), ShortestSide());
foreach (var child in Items)
if (!IsValidSize(child.TMData.Size))
child.TMData.Size = new Size(0, 0);
}
private void Squarify(IEnumerable<Item> items, IEnumerable<Item> row, float sideLength)
{
if (items.Count() == 0)
{
ComputeTreeMaps(row);
return;
}
var item = items.First();
List<Item> row2 = new List<Item>(row);
row2.Add(item);
List<Item> items2 = new List<Item>(items);
items2.RemoveAt(0);
float worst1 = Worst(row, sideLength);
float worst2 = Worst(row2, sideLength);
if (row.Count() == 0 || worst1 > worst2)
Squarify(items2, row2, sideLength);
else
{
ComputeTreeMaps(row);
Squarify(items, new List<Item>(), ShortestSide());
}
}
private void ComputeTreeMaps(IEnumerable<Item> items)
{
var orientation = this.GetOrientation();
float areaSum = 0;
foreach (var item in items)
areaSum += item.TMData.Area;
RectangleF currentRow;
if (orientation == RowOrientation.Horizontal)
{
currentRow = new RectangleF(emptyArea.X, emptyArea.Y, areaSum / emptyArea.Height, emptyArea.Height);
emptyArea = new RectangleF(emptyArea.X + currentRow.Width, emptyArea.Y, Math.Max(0, emptyArea.Width - currentRow.Width), emptyArea.Height);
}
else
{
currentRow = new RectangleF(emptyArea.X, emptyArea.Y, emptyArea.Width, areaSum / emptyArea.Width);
emptyArea = new RectangleF(emptyArea.X, emptyArea.Y + currentRow.Height, emptyArea.Width, Math.Max(0, emptyArea.Height - currentRow.Height));
}
float prevX = currentRow.X;
float prevY = currentRow.Y;
foreach (var item in items)
{
var rect = GetRectangle(orientation, item, prevX, prevY, currentRow.Width, currentRow.Height);
item.TMData.Size = rect.Size;
item.TMData.Location = rect.Location;
ComputeNextPosition(orientation, ref prevX, ref prevY, rect.Width, rect.Height);
}
}
private RectangleF GetRectangle(RowOrientation orientation, Item item, float x, float y, float width, float height)
{
if (orientation == RowOrientation.Horizontal)
return new RectangleF(x, y, width, item.TMData.Area / width);
else
return new RectangleF(x, y, item.TMData.Area / height, height);
}
private void ComputeNextPosition(RowOrientation orientation, ref float xPos, ref float yPos, float width, float height)
{
if (orientation == RowOrientation.Horizontal)
yPos += height;
else
xPos += width;
}
private RowOrientation GetOrientation()
{
return emptyArea.Width > emptyArea.Height ? RowOrientation.Horizontal : RowOrientation.Vertical;
}
private float Worst(IEnumerable<Item> row, float sideLength)
{
if (row.Count() == 0) return 0;
float maxArea = 0;
float minArea = float.MaxValue;
float totalArea = 0;
foreach (var item in row)
{
maxArea = Math.Max(maxArea, item.TMData.Area);
minArea = Math.Min(minArea, item.TMData.Area);
totalArea += item.TMData.Area;
}
if (minArea == float.MaxValue) minArea = 0;
float val1 = (sideLength * sideLength * maxArea) / (totalArea * totalArea);
float val2 = (totalArea * totalArea) / (sideLength * sideLength * minArea);
return Math.Max(val1, val2);
}
private float ShortestSide()
{
return Math.Min(emptyArea.Width, emptyArea.Height);
}
private bool IsValidSize(SizeF size)
{
return (!size.IsEmpty && size.Width > 0 && size.Width != float.NaN && size.Height > 0 && size.Height != float.NaN);
}
private enum RowOrientation
{
Horizontal,
Vertical
}
Finally, to parse and draw the json in the example I'm doing this:
var json = File.ReadAllText(#"treemap.json");
var items = JsonConvert.DeserializeObject<Item>(json);
var treeMap = new TreeMap(items);
var bmp = treeMap.Draw(1366, 768);
bmp.Save("treemap.png", ImageFormat.Png);
And the resulting image:
Actually I don't know if the following can help you or not since you aren't using vsto, AND AS SAID IN THE COMMENTS PROBABLY IS A BAD IDEA.
Starting in Office 2016, treemaps are incorporated as charts. You can read this to see how create treemaps from datasets in Excel.
So, you can generate the chart in Excel and pass it to PowerPoint:
//Start an hidden excel application
var appExcel = new Excel.Application { Visible = false };
var workbook = appExcel.Workbooks.Add();
var sheet = workbook.ActiveSheet;
//Generate some random data
Random r = new Random();
for (int i = 1; i <= 10; i++)
{
sheet.Cells[i, 1].Value2 = ((char)('A' + i - 1)).ToString();
sheet.Cells[i, 2].Value2 = r.Next(1, 20);
}
//Select the data to use in the treemap
var range = sheet.Cells.Range["A1", "B10"];
range.Select();
range.Activate();
//Generate the chart
var shape = sheet.Shapes.AddChart2(-1, (Office.XlChartType)117, 200, 25, 300, 300, null);
shape.Chart.ChartTitle.Caption = "Generated TreeMap Chart";
//Copy the chart
shape.Copy();
appExcel.Quit();
//Start a Powerpoint application
var appPpoint = new Point.Application { Visible = Office.MsoTriState.msoTrue };
var presentation = appPpoint.Presentations.Add();
//Add a blank slide
var master = presentation.SlideMaster;
var slide = presentation.Slides.AddSlide(1, master.CustomLayouts[7]);
//Paste the treemap
slide.Shapes.Paste();
Treemap chart in the slide:
Probably you can generate the treemap using the first part (Excel part) and paste the chart using the tool you said, or save the Powerpoint file with the chart generated in VSTO and open it with the tool.
The benefits are that these objects are real charts not just images, so you can change or add colors, styles, effects easily.

PictureBox updates only on resize

Trying to display a graph on a Form application.
Created a PictureBox control and initialized this class with its value.
On resize, graph always updates; on mouse scroll, it hardly does.
It's GraphBox , PictureBox control, inside a GraphBoxPanel, Panel control.
This the class:
public struct DLT_measure_item
{
public DateTime ts;
public float value;
public int id;
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct dlt_ser_meas
{
public byte msg_id; // 'D'
public byte meas_count; // Number of measures
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] port; // Module ID (4b) + Port ID (4b)
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public float[] meas; // measure
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public byte[] msg_end;
}
public class manageGraph
{
private PictureBox box;
public bool displayGrid = true;
private int horAxisMin_p = 0;
private int horAxisMax_p = 300;
private float verAxisMin_p = 0;
private float verAxisMax_p = 40;
public int horAxisMin
{
get { return this.horAxisMin_p; }
set
{
if (value < horAxisMax_p)
{
this.horAxisMin_p = value;
reDraw();
}
}
}
public int horAxisMax
{
get { return this.horAxisMax_p; }
set
{
if (value > horAxisMin_p)
{
this.horAxisMax_p = value;
reDraw();
}
}
}
public float verAxisMin
{
get { return this.verAxisMin_p; }
set
{
if (value < verAxisMax_p)
{
this.verAxisMin_p = value;
verPointPerUnit = graphArea.Height / (verAxisMax_p - this.verAxisMin_p);
}
}
}
public float verAxisMax
{
get { return this.verAxisMax_p; }
set
{
if (value > verAxisMin_p)
{
this.verAxisMax_p = value;
verPointPerUnit = graphArea.Height / (this.verAxisMax_p - verAxisMin_p);
}
}
}
Pen axes = new Pen(Color.Black, (float)1.5);
public int horAxisSpacing = 30;
public int verAxisSpacing = 20;
public int horAxis = 20;
public int verAxis = 20;
private float horPointPerUnit = 1;
private float verPointPerUnit = 1;
public int horAxisTickLen = 5;
public int verAxisTickLen = 5;
public bool horAxisShowTime = false;
private Rectangle graphArea = new Rectangle();
public void reDraw()
{
box.Image.Dispose();
Bitmap GraphBlankImage = new Bitmap(box.Width, box.Height);
box.Image = GraphBlankImage;
updatePointPerUnit();
drawGrid();
box.Refresh();
}
public manageGraph(PictureBox targetImageBoxbox)
{
box = targetImageBoxbox;
horAxisMin_p = 0;
horAxisMax_p = 300;
verAxisMin_p = 0F;
verAxisMax_p = 50F;
updatePointPerUnit();
}
private Point measToPoint(DLT_measure_item measure) {
Point coords = new Point();
coords.X = graphArea.Width - (int)(
((DateTime.Now - measure.ts).TotalSeconds + horAxisMin_p) * horPointPerUnit ) ;
coords.Y = graphArea.Height - (int)(
((measure.value - verAxisMin_p) * verPointPerUnit));
return coords;
}
public manageGraph(PictureBox targetImageBoxbox,
int xmin, int xmax, float ymin, float ymax)
{
box = targetImageBoxbox;
horAxisMin_p = xmin;
horAxisMax_p = xmax;
verAxisMin_p = ymin;
verAxisMax_p = ymax;
updatePointPerUnit();
}
private void updateGraphArea()
{
graphArea = new Rectangle(0, 0, box.Width - horAxis, box.Height - verAxis);
}
private void updatePointPerUnit()
{
updateGraphArea();
horPointPerUnit = graphArea.Width / (horAxisMax_p - horAxisMin_p);
verPointPerUnit = graphArea.Height / (verAxisMax_p - verAxisMin_p);
}
public void drawGrid()
{
//updatePointPerUnit();
using (Graphics g = Graphics.FromImage(box.Image))
{
// X axis
g.DrawLine(axes, graphArea.Left, graphArea.Bottom, box.Width, graphArea.Bottom);
// Y axis
g.DrawLine(axes, graphArea.Right + 1, graphArea.Top, graphArea.Right +1, graphArea.Bottom);
using (Font ArialFont = new Font("Arial", 10))
{
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
// Put x labels
for (int i = 1; i <= (graphArea.Width / horPointPerUnit); i = i + (int)(horAxisSpacing / horPointPerUnit ) + 1)
{
g.DrawString((i).ToString(), ArialFont, Brushes.Black, graphArea.Width - ( i * horPointPerUnit) - 5, graphArea.Bottom +5);
g.DrawLine(axes, graphArea.Width - (i * horPointPerUnit), graphArea.Bottom + (horAxisTickLen / 2), graphArea.Width - (i * horPointPerUnit), graphArea.Bottom - (horAxisTickLen / 2));
}
// Put y labels
for (int i = 1; i <= (graphArea.Height / verPointPerUnit); i = i + (int)(verAxisSpacing / verPointPerUnit) +1)
{
g.DrawString((i).ToString(), ArialFont, Brushes.Black, graphArea.Right + 1 , graphArea.Height - (i * verPointPerUnit) - 8);
g.DrawLine(axes, graphArea.Width - (verAxisTickLen / 2), (i * verPointPerUnit), graphArea.Width + (verAxisTickLen / 2), (i * verPointPerUnit));
}
}
/*Put some random data*/
DLT_measure_item testmeas = new DLT_measure_item();
Point testGraphPoint = new Point();
testmeas.ts = DateTime.Now;
testmeas.value = 0;
testGraphPoint = measToPoint(testmeas);
g.FillEllipse(Brushes.Blue, testGraphPoint.X, testGraphPoint.Y, 4, 4);
for (double i = 0; i < 300; i++)
{
double x = i;
double freq = 10;
double y = 30 - (i/10);
testmeas.value = (float)y;
testmeas.ts = DateTime.Now.AddSeconds(-1 * i);
testGraphPoint = measToPoint(testmeas);
g.FillEllipse(Brushes.Red, testGraphPoint.X, testGraphPoint.Y, 2,2);
}
}
}
}
The initialization:
public DLThermalogMainForm()
{
InitializeComponent();
Bitmap GraphBlankImage = new Bitmap(GraphBox.Width, GraphBox.Height);
GraphBox.Image = GraphBlankImage;
myGraph = new manageGraph(GraphBox);
myGraph.drawGrid();
}
Those the handlers:
private void Form1_ResizeEnd(object sender, EventArgs e)
{
myGraph.reDraw();
OutputTextBox.AppendText("Resize." + Environment.NewLine);
}
private void GraphBox_MouseMove(object sender, MouseEventArgs e)
{
//Get the focus to have the wheel working
GraphBoxPanel.Focus();
}
private void GraphBoxPanel_MouseWheel(object sender, MouseEventArgs e)
{
// Change x axis max value to zoom in/out the graph
myGraph.horAxisMax += e.Delta/ 120;
myGraph.reDraw();
}
On resize event, it always redraws; on mouse wheel, it does quickly only with small horAxisMax values (does it makes sense???), but for larger, it takes many seconds to update, or doesn't at all.
Thank you very much
Change reDraw like this:
public void reDraw()
{
box.Image.Dispose();
Bitmap GraphBlankImage = new Bitmap(box.ClientSize.Width, box.ClientSize.Height);
updatePointPerUnit();
drawGrid(GraphBlankImage);
box.Image = GraphBlankImage;
}
and drawGrid like this:
public void drawGrid(Bitmap bmp)
{
//updatePointPerUnit(); //??
using (Graphics g = Graphics.FromImage(bmp))
{
...
...
...
}
}
Now the Bitmap with the grid should immediately show up in the PictureBox.
As mentioned a Graphics object is a tool to change an associated Bitmap. To pick it up the Bitmap should be assigned to the PictureBoxe's Image.
Also note, that unless your PictureBox has no Border, there is a small difference between the outer size (aka Bounds) and the inner size, the ClientRectangle / ClientSize. The Image should have the ClientSize
You may wonder why your original code doesn't work? After all an Image is a reference type, so changing it, as you did should be enough..
But looking deeper into the source code we find the reason:
The PictureBox's Image is a property and in its setter there is a call to InstallNewImage:
public Image Image {
get {
return image;
}
set {
InstallNewImage(value, ImageInstallationType.DirectlySpecified);
}
}
The same call is also in a few other places like Load or in the setter of ImageLocation. But changing the Image behind the scene alone will not force the PictureBox to make that call. A Refresh() should also do it.. And, as you found out, resizing it will also cause the PictureBox to pick up the changed data in the Image Bitmap..
The easiest way to force updating is simply to invalidate the control.
timer = new Timer();
timer.Interval = 200; //refreshes every 200 ms
timer.Tick += (sender,e) => targetImageBoxbox.Invalidate();
timer.Start();

Is there a way to find progress completion of ObjImporter drawing mesh in Unity C#?

In my Unity project, I want to have a progress bar that starts loading when I import a .obj model during runtime.
How I want to it work is when I press "spacebar", the ObjImporter would start importing/drawing the assigned model and during that period of time while it's drawing, there would be a progress bar somewhere that shows the completion of the importing+/drawing progress, so the user could see the estimated amount of time neededto complete and not get impatient.
I'm using a slightly modified ObjImporter.cs from unity wiki:
/* This version of ObjImporter first reads through the entire file, getting a count of how large
* the final arrays will be, and then uses standard arrays for everything (as opposed to ArrayLists
* or any other fancy things).
*/
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
public class ObjImporter : MonoBehaviour {
public GameObject spawnPoint;
public GameObject emptyPrefabWithMeshRenderer;
public string meshPath;
public GameObject spawnedPrefab;
public float progressPercentage;
int noOfLines;
void Start ()
{
progressPercentage = 0;
}
void Update ()
{
if(Input.GetKeyDown("space")){
Mesh importedMesh = GetComponent<ObjImporter>().ImportFile(meshPath);
noOfLines = TotalLines(meshPath);
spawnedPrefab=Instantiate(emptyPrefabWithMeshRenderer,spawnPoint.transform.position,spawnPoint.transform.rotation) as GameObject;
spawnedPrefab.GetComponent<MeshFilter>().mesh=importedMesh;
spawnedPrefab.GetComponent<Renderer>().material.color = new Color(Random.value, Random.value, Random.value, 1.0f);
}
}
private struct meshStruct
{
public Vector3[] vertices;
public Vector3[] normals;
public Vector2[] uv;
public Vector2[] uv1;
public Vector2[] uv2;
public int[] triangles;
public int[] faceVerts;
public int[] faceUVs;
public Vector3[] faceData;
public string name;
public string fileName;
}
// Use this for initialization
public Mesh ImportFile (string filePath){
meshStruct newMesh = createMeshStruct(filePath);
populateMeshStruct(ref newMesh);
Vector3[] newVerts = new Vector3[newMesh.faceData.Length];
Vector2[] newUVs = new Vector2[newMesh.faceData.Length];
Vector3[] newNormals = new Vector3[newMesh.faceData.Length];
int i = 0;
/* The following foreach loops through the facedata and assigns the appropriate vertex, uv, or normal
* for the appropriate Unity mesh array.
*/
foreach (Vector3 v in newMesh.faceData)
{
newVerts[i] = newMesh.vertices[(int)v.x - 1];
if (v.y >= 1)
newUVs[i] = newMesh.uv[(int)v.y - 1];
if (v.z >= 1)
newNormals[i] = newMesh.normals[(int)v.z - 1];
i++;
}
Mesh mesh = new Mesh();
mesh.vertices = newVerts;
mesh.uv = newUVs;
mesh.normals = newNormals;
mesh.triangles = newMesh.triangles;
mesh.RecalculateBounds();
mesh.Optimize ();
return mesh;
}
private int TotalLines(string filePath)
{
using (StreamReader r = new StreamReader(filePath))
{
int i = 0;
while (r.ReadLine() != null)
{
i++;
}
return i;
}
}
private static meshStruct createMeshStruct(string filename)
{
int triangles = 0;
int vertices = 0;
int vt = 0;
int vn = 0;
int face = 0;
meshStruct mesh = new meshStruct();
mesh.fileName = filename;
// Read and retrieve all the text in the file.
StreamReader stream = File.OpenText(filename);
string entireText = stream.ReadToEnd();
stream.Close(); // End of stream.
// Going through the retrieved text.
using (StringReader reader = new StringReader(entireText))
{
string currentText = reader.ReadLine();
char[] splitIdentifier = { ' ' };
string[] brokenString;
while (currentText != null)
{
if (!currentText.StartsWith("f ") && !currentText.StartsWith("v ") && !currentText.StartsWith("vt ")
&& !currentText.StartsWith("vn "))
{
currentText = reader.ReadLine();
if (currentText != null)
{
currentText = currentText.Replace(" ", " ");
}
}
else
{
currentText = currentText.Trim(); // Trim the current line
brokenString = currentText.Split(splitIdentifier, 50); // Split the line into an array, separating the original line by blank spaces
switch (brokenString[0])
{
case "v":
vertices++;
break;
case "vt":
vt++;
break;
case "vn":
vn++;
break;
case "f":
face = face + brokenString.Length - 1;
triangles = triangles + 3 * (brokenString.Length - 2); /*brokenString.Length is 3 or greater since a face must have at least
3 vertices. For each additional vertice, there is an additional
triangle in the mesh (hence this formula).*/
break;
}
currentText = reader.ReadLine();
if (currentText != null)
{
currentText = currentText.Replace(" ", " ");
}
}
}
}
mesh.triangles = new int[triangles];
mesh.vertices = new Vector3[vertices];
mesh.uv = new Vector2[vt];
mesh.normals = new Vector3[vn];
mesh.faceData = new Vector3[face];
return mesh;
}
private static void populateMeshStruct(ref meshStruct mesh)
{
StreamReader stream = File.OpenText(mesh.fileName);
string entireText = stream.ReadToEnd();
stream.Close();
using (StringReader reader = new StringReader(entireText))
{
string currentText = reader.ReadLine();
char[] splitIdentifier = { ' ' };
char[] splitIdentifier2 = { '/' };
string[] brokenString;
string[] brokenBrokenString;
int f = 0;
int f2 = 0;
int v = 0;
int vn = 0;
int vt = 0;
int vt1 = 0;
int vt2 = 0;
while (currentText != null)
{
if (!currentText.StartsWith("f ") && !currentText.StartsWith("v ") && !currentText.StartsWith("vt ") &&
!currentText.StartsWith("vn ") && !currentText.StartsWith("g ") && !currentText.StartsWith("usemtl ") &&
!currentText.StartsWith("mtllib ") && !currentText.StartsWith("vt1 ") && !currentText.StartsWith("vt2 ") &&
!currentText.StartsWith("vc ") && !currentText.StartsWith("usemap "))
{
currentText = reader.ReadLine();
if (currentText != null)
{
currentText = currentText.Replace(" ", " ");
}
}
else
{
currentText = currentText.Trim();
brokenString = currentText.Split(splitIdentifier, 50);
switch (brokenString[0])
{
case "g":
break;
case "usemtl":
break;
case "usemap":
break;
case "mtllib":
break;
case "v":
mesh.vertices[v] = new Vector3(System.Convert.ToSingle(brokenString[1]), System.Convert.ToSingle(brokenString[2]),
System.Convert.ToSingle(brokenString[3]));
v++;
break;
case "vt":
mesh.uv[vt] = new Vector2(System.Convert.ToSingle(brokenString[1]), System.Convert.ToSingle(brokenString[2]));
vt++;
break;
case "vt1":
mesh.uv[vt1] = new Vector2(System.Convert.ToSingle(brokenString[1]), System.Convert.ToSingle(brokenString[2]));
vt1++;
break;
case "vt2":
mesh.uv[vt2] = new Vector2(System.Convert.ToSingle(brokenString[1]), System.Convert.ToSingle(brokenString[2]));
vt2++;
break;
case "vn":
mesh.normals[vn] = new Vector3(System.Convert.ToSingle(brokenString[1]), System.Convert.ToSingle(brokenString[2]),
System.Convert.ToSingle(brokenString[3]));
vn++;
break;
case "vc":
break;
case "f":
int j = 1;
List<int> intArray = new List<int>();
while (j < brokenString.Length && ("" + brokenString[j]).Length > 0)
{
Vector3 temp = new Vector3();
brokenBrokenString = brokenString[j].Split(splitIdentifier2, 3); //Separate the face into individual components (vert, uv, normal)
temp.x = System.Convert.ToInt32(brokenBrokenString[0]);
if (brokenBrokenString.Length > 1) //Some .obj files skip UV and normal
{
if (brokenBrokenString[1] != "") //Some .obj files skip the uv and not the normal
{
temp.y = System.Convert.ToInt32(brokenBrokenString[1]);
}
temp.z = System.Convert.ToInt32(brokenBrokenString[2]);
}
j++;
mesh.faceData[f2] = temp;
intArray.Add(f2);
f2++;
}
j = 1;
while (j + 2 < brokenString.Length) //Create triangles out of the face data. There will generally be more than 1 triangle per face.
{
mesh.triangles[f] = intArray[0];
f++;
mesh.triangles[f] = intArray[j];
f++;
mesh.triangles[f] = intArray[j+1];
f++;
j++;
}
break;
}
currentText = reader.ReadLine();
if (currentText != null)
{
currentText = currentText.Replace(" ", " "); //Some .obj files insert double spaces, this removes them.
}
}
}
}
}
void OnGUI()
{
GUI.Label (new Rect (20, 20, 100, 100), noOfLines.ToString());
}
}
This is a simple ProgressBar.cs I wrote:
using UnityEngine;
using System.Collections;
public class ProgressBar : MonoBehaviour {
float progress;
Vector2 pos;
Vector2 size;
public Texture2D barBG;
public Texture2D barLoad;
// Use this for initialization
void Start () {
progress = 0;
pos = new Vector2 (20, 40);
size = new Vector2 (100, 10);
}
// Update is called once per frame
void Update () {
progress = Time.time * 0.05f; // Place holder.
}
void OnGUI ()
{
GUI.DrawTexture(new Rect(pos.x, pos.y, size.x, size.y), barBG);
GUI.DrawTexture(new Rect(pos.x, pos.y, size.x * Mathf.Clamp01(progress), size.y), barLoad);
}
}
I have looked everywhere for a method in finding out progress completion of file reading+drawing of mesh and couldn't find one.
I've tried having the progressBar load respectively to the number of lines in the .obj file, but it isn't working too well as reading is way faster than the actual drawing of the mesh.
I'm out of ideas, anyone know of any way? Thanks in advance!

Centering an pdfimportedpage in iTextSharp

I am appending PDFs together using the function below via iTextSharp. Its working fine. The only problem is that PDFs that are larger than the set size of the document (A4), ends up being scaled and placed at the bottom left corner of the document. I would like to centre it. Can anyone point me in the right direction to achieving this? Cheers.
private void appendPDF(appendDoc doc)
{
PdfContentByte pdfContentByte = pdfWriter.DirectContent;
PdfReader pdfReader = null;
if (doc.MemoryStream != null && doc.MemoryStream.CanRead)
{
pdfReader = new PdfReader(doc.MemoryStream);
}
else if (File.Exists(doc.FullFilePath))
{
pdfReader = new PdfReader(doc.FullFilePath);
}
if (pdfReader != null)
{
for (int pageIndex = 1; pageIndex <= pdfReader.NumberOfPages; pageIndex++)
{
PdfImportedPage importedPage = pdfWriter.GetImportedPage(pdfReader, pageIndex);
float importedPageXYRatio = importedPage.Width / importedPage.Height;
if (XYRatio > 1f)
{
iTextDocument.SetPageSize(PageSize.A4.Rotate());
}
else
{
iTextDocument.SetPageSize(PageSize.A4);
}
iTextDocument.NewPage();
pdfContentByte.AddTemplate(importedPage, 0, 0);
}
}
}
Edit:
This was the solution I ended up using.
private void appendPDF(appendDoc doc)
{
PdfContentByte pdfContentByte = pdfWriter.DirectContent;
PdfReader pdfReader = null;
if (doc.MemoryStream != null && doc.MemoryStream.CanRead)
{
pdfReader = new PdfReader(doc.MemoryStream);
}
else if (File.Exists(doc.FullFilePath))
{
pdfReader = new PdfReader(doc.FullFilePath);
}
if (pdfReader != null)
{
for (int pageIndex = 1; pageIndex <= pdfReader.NumberOfPages; pageIndex++)
{
PdfImportedPage importedPage = pdfWriter.GetImportedPage(pdfReader, pageIndex);
float importedPageXYRatio = importedPage.Width / importedPage.Height;
if (XYRatio > 1f)
{
iTextDocument.SetPageSize(PageSize.A4.Rotate());
}
else
{
iTextDocument.SetPageSize(PageSize.A4);
}
iTextDocument.NewPage();
var truePageWidth = iTextDocument.PageSize.Width - iTextDocument.LeftMargin - iTextDocument.RightMargin;
var truePageHeight = iTextDocument.PageSize.Height - iTextDocument.TopMargin - iTextDocument.BottomMargin;
var x = (truePageWidth - importedPage.Width) / 2 + iTextDocument.RightMargin;
var y = (truePageHeight - importedPage.Height) / 2 + iTextDocument.BottomMargin;
pdfContentByte.AddTemplate(importedPage, x, y);
}
}
}
Can you set the x-coordinate when you call AddTemplate?
Float offset = 0;
if(importedPage.width < iTextDocument.PageSize.Width) {
offset = (iTextDocument.PageSize.Width - importedPage.width)/2;
}
pdfContentByte.AddTemplate(importedPage, offset, 0);
Or does it do the scaling in AddTemplate so you don't know the final width?

Categories