// ----------------------------------------- // Using Directives // ----------------------------------------- using System.Security.Cryptography; using System.Collections.Generic; using System.Drawing; using System.Text; using System.IO; using System; // ----------------------------------------- // RGB // ----------------------------------------- public class RGB { public RGB(byte red, byte green, byte blue) { Red = red; Green = green; Blue = blue; } public byte Red { get; private set; } public byte Green { get; private set; } public byte Blue { get; private set; } public byte[] AsBytes() { return(new byte[] { Red, Green, Blue } ); } } // ----------------------------------------- // ImageInfo // ----------------------------------------- public class ImageInfo { public ImageInfo(int objectID, string name, int width, int height) { ObjectID = objectID; Name = name; Width = width; Height = height; } public int ObjectID { get; private set; } public string Name { get; private set; } public int Width { get; private set; } public int Height { get; private set; } } // ----------------------------------------- // ImageEntry // ----------------------------------------- public class ImageEntry { public ImageEntry(List> image, ImageInfo info) { Image = image; Info = info; } public List> Image { get; private set; } public ImageInfo Info { get; private set; } } // ----------------------------------------- // PDF // ----------------------------------------- public class PDF { public enum Font { NONE, COURIER, COURIER_BOLD, COURIER_OBLIQUE, COURIER_BOLD_OBLIQUE, HELVETICA, HELVETICA_BOLD, HELVETICA_OBLIQUE, HELVETICA_BOLD_OBLIQUE, SYMBOL, TIMES, TIMES_BOLD, TIMES_ITALIC, TIMES_BOLD_ITALIC, ZAPF_DINGBATS } public enum LineCap { BUTT_CAP, ROUND_CAP, PROJECTING_SQUARE_CAP } public enum LineJoin { MITER_JOIN, ROUND_JOIN, BEVEL_JOIN } public static readonly string[] mFonts = { "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique", "Symbol", "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic", "ZapfDingbats" }; public static readonly int DEFAULT_WIDTH = 612; public static readonly int DEFAULT_HEIGHT = 792; public PDF() : this(DEFAULT_WIDTH, DEFAULT_HEIGHT) { } public PDF(int width, int height) { mWidth = width; mHeight = height; } public int GetWidth() { return(mWidth); } public int GetHeight() { return(mHeight); } private static int GetImageObjectID(int index) { // Images are stored just after our fonts return(mFonts.Length + index + 1); } public override string ToString() { var offsets = new List(); StringBuilder output = new StringBuilder(); // --------------------------------------------- // Firstly, the string to identify // this as a pdf file // --------------------------------------------- output.Append("%PDF-1.1" + "\n\n"); // --------------------------------------------- // Now, our standard fonts // --------------------------------------------- for(int i = 0; i < mFonts.Length; i ++) { offsets.Add(output.Length); output.Append( (1 + i) + " 0 obj" + "\n" + "<<" + "\n" + " /Type /Font" + "\n" + " /Subtype /Type1" + "\n" + " /BaseFont /" + mFonts[i] + "\n" + " /Encoding /WinAnsiEncoding" + "\n" + ">>" + "\n" + "endobj" + "\n\n" ); } // Store our images (if we have any) int theID = GetImageObjectID(0); for(int i = 0; i < mEntries.Count; i ++) { offsets.Add(output.Length); theID = mEntries[i].Info.ObjectID; int height = mEntries[i].Image.Count; int width = (height > 0 ? mEntries[i].Image[0].Count : 0); // We don't count the final newline in our /Length value int length = (3 * 2 * width + 1) * height - 1; output.Append( theID + " 0 obj" + "\n" + "<<" + "\n" + " /Type /XObject" + "\n" + " /Subtype /Image" + "\n" + " /Width " + width + "\n" + " /Height " + height + "\n" + " /BitsPerComponent 8" + "\n" + " /ColorSpace /DeviceRGB" + "\n" + " /Filter /ASCIIHexDecode" + "\n" + " /Length " + length + "\n" + ">>" + "\n" ); output.Append("stream" + "\n"); for(int y = 0; y < height; y ++) { for(int x = 0; x < width; x ++) { int r = mEntries[i].Image[y][x].Red; int g = mEntries[i].Image[y][x].Green; int b = mEntries[i].Image[y][x].Blue; output.Append(r.ToString("X2") + g.ToString("X2") + b.ToString("X2")); } output.Append("\n"); } output.Append("endstream" + "\n" + "endobj" + "\n\n"); } if(mEntries.Count > 0) theID++; // Now, set up an obj for our common resources, // first our fonts and then any images offsets.Add(output.Length); int idResources = theID; output.Append( idResources + " 0 obj" + "\n" + "<<" + "\n" + " /Font <<" + "\n" ); for(int i = 0; i < mFonts.Length; i ++) output.Append(" /F" + (i + 1) + " " + (i + 1) + " 0 R" + "\n"); output.Append(" >>" + "\n"); if(mEntries.Count > 0) { output.Append(" /XObject <<" + "\n"); for(int i = 0; i < mEntries.Count; i ++) { output.Append(" " + mEntries[i].Info.Name + " " + mEntries[i].Info.ObjectID + " 0 R" + "\n" ); } output.Append(" >>" + "\n"); } output.Append(">>" + "\n" + "endobj" + "\n\n"); // Now, our top-level object offsets.Add(output.Length); int idCatalog = (idResources + 1); int idPages = (idCatalog + 1); output.Append( idCatalog + " 0 obj" + "\n" + "<<" + "\n" + " /Type /Catalog" + "\n" + " /Pages " + idPages + " 0 R" + "\n" + ">>" + "\n" + "endobj" + "\n\n" ); // Prepare our pages var pages = new List(); foreach(var p in mPageContents) pages.Add(p); // Append the current page but only if it // is not empty if(mCurrentPageContents != "") pages.Add(mCurrentPageContents); // Now our pages object offsets.Add(output.Length); output.Append( idPages + " 0 obj" + "\n" + "<<" + "\n" + " /Type /Pages" + "\n" + " /MediaBox " ); output.Append("[ 0 0 " + mWidth + " " + mHeight + " ]" + "\n"); output.Append(" /Count " + pages.Count + "\n" + " /Kids ["); for(int i = 0; i < pages.Count; i ++) output.Append(" " + (1 + idPages + i) + " 0 R"); output.Append( " ]" + "\n" + ">>" + "\n" + "endobj" + "\n\n" ); // Now our pages for(int i = 0; i < pages.Count; i ++) { offsets.Add(output.Length); int idPage = (1 + idPages + i); output.Append( idPage + " 0 obj" + "\n" + "<<" + "\n" + " /Type /Page" + "\n" + " /Parent " + idPages + " 0 R" + "\n" + " /Contents " + (idPage + pages.Count) + " 0 R" + "\n" + " /Resources " + idResources + " 0 R" + "\n" + ">>" + "\n" + "endobj" + "\n\n" ); } // Now our page contents int idPageContent = idPages + pages.Count + 1; for(int i = 0; i < pages.Count; i ++) { string theContents = pages[i]; theContents = OP_BEGIN_TEXT + "\n" + theContents + OP_END_TEXT; offsets.Add(output.Length); output.Append( idPageContent + " 0 obj" + "\n" + "<<" + "\n" + " /Length " + theContents.Length + "\n" + ">>" + "\n" + "stream" + "\n" + theContents + "\n" + "endstream" + "\n" + "endobj" + "\n\n" ); idPageContent++; } int xrefOffset = output.Length; int theSize = (offsets.Count + 1); output.Append( "xref" + "\n" + "0 " + theSize + "\n" + "0000000000 65535 f" + " " + "\n" ); for(int i = 0; i < offsets.Count; i ++) { output.Append(new String('0', 10 - ("" + offsets[i]).Length)); output.Append(offsets[i] + " 00000 n" + " " + "\n"); } output.Append( "trailer" + "\n" + "<<" + "\n" + " /Size " + theSize + "\n" + " /Root " + idCatalog + " 0 R" + "\n" + ">>" + "\n" + "startxref" + "\n" + xrefOffset + "\n" + "%%EOF\n" ); return(output.ToString()); } public void WriteToFile(string fileName) { using(StreamWriter writer = new StreamWriter(fileName)) writer.WriteLine(this); } public static List> GetImage(string fileName) { Bitmap theImage = (Bitmap)Image.FromFile(fileName); var data = new List>(); for(int y = 0; y < theImage.Height; y ++) { var row = new List(); for(int x = 0; x < theImage.Width; x ++) { Color c = theImage.GetPixel(x, y); row.Add(new RGB(c.R, c.G, c.B)); } data.Add(row); } theImage.Dispose(); return(data); } public ImageInfo ProcessImage(string fileName) { return(ProcessImage(GetImage(fileName))); } public ImageInfo ProcessImage(List> theImage) { if(theImage == null || theImage.Count == 0 || theImage[0].Count == 0) return(null); byte[] theBytes = new byte[theImage.Count * theImage[0].Count * 3]; int offset = 0; foreach(var row in theImage) foreach(var entry in row) foreach(var currentByte in entry.AsBytes()) theBytes[offset++] = currentByte; string hash = PDF.GetHash(theBytes); if(mImageCache.ContainsKey(hash)) return(mImageCache[hash]); int id = GetImageObjectID(mEntries.Count); var theInfo = new ImageInfo( id, "/Img" + id, theImage[0].Count, theImage.Count ); mEntries.Add(new ImageEntry(theImage, theInfo)); // Associate our ImageInfo object with its checksum mImageCache[hash] = theInfo; return(theInfo); } public void NewPage() { mPageContents.Add(mCurrentPageContents); mCurrentPageContents = ""; mCurrentFont = Font.NONE; mCurrentFontSize = 0; } public void ShowImage(ImageInfo info, int x, int y, double xScale, double yScale) { int width = (int)(info.Width * xScale + 0.5); int height = (int)(info.Height * yScale + 0.5); string output = OP_SAVE + " " + width + " " + 0 + " " + 0 + " " + height + " " + x + " " + y + " " + OP_CONCAT_MATRIX + " " + info.Name + " " + OP_DO + " " + OP_RESTORE + "\n"; mCurrentPageContents += output; } public void ShowImage(ImageInfo info, int x, int y, double scale) { ShowImage(info, x, y, scale, scale); } public void ShowImage(ImageInfo info, int x, int y) { ShowImage(info, x, y, 1.0, 1.0); } public static int StringWidth(Font currentFont, int currentSize, string text) { int[] metrics = Metrics.Get(currentFont); if(metrics == null) return(0); double dWidth = 0.0; for(int i = 0; i < text.Length; i ++) { double value = metrics[text[i]] / 1000.0; dWidth += currentSize * value; } return((int)Math.Round(dWidth)); } public Font CurrentFont() { return(mCurrentFont); } public int CurrentFontSize() { return(mCurrentFontSize); } public int StringWidth(string text) { return(StringWidth(mCurrentFont, mCurrentFontSize, text)); } public void SetFont(Font theFont, int size) { mCurrentPageContents += String.Format("/F{0} {1} {2}\n", (int)theFont, size, OP_SET_FONT); mCurrentFont = theFont; mCurrentFontSize = size; } public void SetTextCharSpace(int value) { mCurrentPageContents += String.Format("{0} {1}\n", value, OP_CHAR_SPACE); } public void SetTextWordSpace(int value) { mCurrentPageContents += String.Format("{0} {1}\n", value, OP_WORD_SPACE); } public void SetTextHorizontalScaling(int value) { mCurrentPageContents += String.Format("{0} {1}\n", value, OP_HORIZ_SCALE); } public void ShowTextXY(string text, int x, int y) { string theText = ""; foreach(char c in text) { if(c == '(' || c == ')') theText += "\\"; theText += c; } mCurrentPageContents += String.Format( "1 0 0 1 {0} {1} {2}\n" + "({3}) {4}\n", x, y, OP_TEXT_MATRIX, theText, OP_TEXT_SHOW ); } public void RightJustifyTextXY(string text, int x, int y) { ShowTextXY(text, x - StringWidth(text), y); } public List WrapText( string text, int maxWidth, bool rightJustify, string indent = "" ) { var paragraphs = ToParagraphs(text); var result = new List(); for(int i = 0; i < paragraphs.Count; i ++) { if(i > 0) result.Add(""); foreach(var s in DoWrap(this, paragraphs[i], maxWidth, rightJustify, indent)) result.Add(s); } return(result); } public void SetLineWidth(int value) { mCurrentPageContents += String.Format("{0} {1}\n", value, OP_WIDTH); } public void SetLineCap(LineCap cap) { mCurrentPageContents += String.Format("{0} {1}\n", (int)cap, OP_CAP); } public void SetLineJoin(LineJoin join) { mCurrentPageContents += String.Format("{0} {1}\n", (int)join, OP_JOIN); } public void SetMiterLimit(double value) { mCurrentPageContents += String.Format("{0} {1}\n", value, OP_MITER); } public void SetDash(int[] pattern, int offset = 0) { string s = ""; foreach(int value in pattern) s += (s == "" ? "" : " ") + value; mCurrentPageContents += String.Format("[ {0} ] {1} {2}\n", s, offset, OP_DASH); } public void DrawLine(int x0, int y0, int x1, int y1) { DrawLine(new List() { new Point(x0, y0), new Point(x1, y1) }); } public void DrawLine(int x0, int y0, int x1, int y1, int x2, int y2) { DrawLine(new List() { new Point(x0, y0), new Point(x1, y1), new Point(x2, y2) }); } public void DrawLine(List points) { string output = ""; for(int i = 0; i < points.Count; i ++) { output += String.Format( "{0} {1} {2}\n", points[i].X, points[i].Y, i == 0 ? OP_MOVETO : OP_LINETO ); } output += OP_STROKE + "\n"; mCurrentPageContents += output; } public void DrawRect(int x, int y, int width, int height) { mCurrentPageContents += HandleRectangle(x, y, width, height, OP_STROKE); } public void FillRect(int x, int y, int width, int height) { mCurrentPageContents += HandleRectangle(x, y, width, height, OP_FILL); } public void DrawPolygon(List points) { mCurrentPageContents += HandlePolygon(points, OP_STROKE); } public void FillPolygon(List points) { mCurrentPageContents += HandlePolygon(points, OP_FILL); } public void DrawEllipse( int xCenter, int yCenter, int xRadius, int yRadius ) { mCurrentPageContents += HandlePolygon( StrokeEllipse(xCenter, yCenter, xRadius, yRadius), OP_STROKE ); } public void FillEllipse( int xCenter, int yCenter, int xRadius, int yRadius ) { mCurrentPageContents += HandlePolygon( StrokeEllipse(xCenter, yCenter, xRadius, yRadius), OP_FILL ); } public void DrawCircle(int xCenter, int yCenter, int radius) { DrawEllipse(xCenter, yCenter, radius, radius); } public void FillCircle(int xCenter, int yCenter, int radius) { FillEllipse(xCenter, yCenter, radius, radius); } public void SetLineColor(byte red, byte green, byte blue) { mCurrentPageContents += HandleColor(red, green, blue, OP_LINE_COLOR); } public void SetFillColor(byte red, byte green, byte blue) { mCurrentPageContents += HandleColor(red, green, blue, OP_FILL_COLOR); } private static string HandlePolygon(List points, string theOperator) { if(points.Count <= 2) return(""); string output = ""; for(int i = 0; i < points.Count; i ++) { output += points[i].X + " " + points[i].Y + " " + (i == 0 ? OP_MOVETO : OP_LINETO) + "\n"; } // Close the polygon if it's not already closed if(points[0].X != points[points.Count - 1].X || points[0].Y != points[points.Count - 1].Y) { output += points[0].X + " " + points[0].Y + " " + OP_LINETO + "\n"; } return(output + theOperator + "\n"); } private static string HandleRectangle( int x, int y, int width, int height, string theOperator ) { return( String.Format( "{0} {1} {2} {3} {4} {5}\n", x, y, width, height, OP_RECTANGLE, theOperator ) ); } private static string HandleColor( byte red, byte green, byte blue, string theOperator ) { return( String.Format( "{0} {1} {2} {3}\n", red / 255.0, green / 255.0, blue / 255.0, theOperator ) ); } private static List StrokeEllipse( int xCenter, int yCenter, int xRadius, int yRadius ) { // ---------------- // // Formula: // // 2 2 // X Y // - + - = 1 // 2 2 // A B // // ---------------- double a2 = xRadius * xRadius; double b2 = yRadius * yRadius; var points = new List(); for(int i = xRadius; i >= 0; i --) { double x = i; double y = Math.Sqrt(b2 * (1 - x*x / a2)); points.Add( new Point( (int)(xCenter + x + 0.5), (int)(yCenter + y + 0.5) ) ); } for(int i = points.Count - 2; i >= 0; i --) { points.Add( new Point( xCenter - (points[i].X - xCenter), points[i].Y ) ); } for(int i = points.Count - 2; i >= 0; i --) { points.Add( new Point( points[i].X, yCenter - (points[i].Y - yCenter) ) ); } return(points); } private static string RightJustifyLine(PDF pdf, string line, int maxWidth) { string theLine = line; string aSpace = " "; int spaceWidth = pdf.StringWidth(aSpace); // Right justify the line by interspersing additional spaces // between the words until adding an additional space would make // the line too long. if(pdf.StringWidth(theLine) + spaceWidth <= maxWidth) { // Store off our indices, the locations in 'theLine' of the // first space after each word: // // here is a string of some words // ^ ^ ^ ^ ^ ^ var indices = new List(); for(int j = 0; j < theLine.Length; j ++) { if(j > 0 && !char.IsWhiteSpace(theLine[j - 1]) && char.IsWhiteSpace(theLine[j])) indices.Add(j); } int nIndices = indices.Count; // We only add spaces between words - so unless // there are 2+ words there's nothing to do if(nIndices > 0) { // We first insert a space at indices[0], then // at indices[1], etc. The variable 'theIndex' // indices which offset in 'indices' we're using int theIndex = 0; while(pdf.StringWidth(theLine) + spaceWidth <= maxWidth) { theLine = theLine.Substring(0, indices[theIndex]) + aSpace + theLine.Substring(indices[theIndex]); // Because we added a space to the string after // indices[theIndex], we need to add one to all // our offsets after 'theIndex' to reflect the // addition of the space for(int j = theIndex + 1; j < nIndices; j ++) indices[j]++; // If we've hit our last two words, roll back // around to the first two words to add the // next space if(++theIndex == nIndices) theIndex = 0; } } } return(theLine); } private static List ToParagraphs(string s) { // First, handle any platform/newline issues: // // a) convert \r\n -> \n // b) convert \r -> \n string theString = ""; for(int i = 0, n = s.Length; i < n; i ++) { if(s[i] == '\r') { if(i + 1 == n || s[i + 1] != '\n') theString += "\n"; } else theString += s[i]; } // Now, split into paragraphs when we find // two consecutive newlines var paragraphs = new List(); string current = ""; for(int i = 0, n = theString.Length; i < n; i ++) { bool isLast = (i + 1 == n); if(theString[i] == '\n' && !isLast && theString[i + 1] == '\n') { paragraphs.Add(current); current = ""; i++; } else { current += theString[i]; if(isLast) paragraphs.Add(current); } } return(paragraphs); } private static List DoWrap( PDF pdf, string text, int maxWidth, bool rightJustify, string indent ) { // First, break 'text' up into separate words var words = new List(); string current = ""; for(int i = 0; i < text.Length; i ++) { if(!char.IsWhiteSpace(text[i])) { current += text[i]; if(i + 1 == text.Length || char.IsWhiteSpace(text[i + 1])) { words.Add(current); current = ""; } } } var result = new List(); string currentLine = ""; for(int i = 0; i < words.Count; i ++) { string s = currentLine; if(currentLine != "") { switch(currentLine[currentLine.Length - 1]) { case '.': case '?': case '!': s += " "; break; } s += " "; } else if(i == 0) { s += indent; } s += words[i]; if(pdf.StringWidth(s) > maxWidth) { result.Add(currentLine); currentLine = words[i]; } else currentLine = s; } result.Add(currentLine); // If so requested, right justify each line if(rightJustify) { for(int i = 0; i < result.Count; i ++) result[i] = RightJustifyLine(pdf, result[i], maxWidth); } return(result); } // ------------------------------------------------- // Return an md5 hash (32 hex chars) of 'bytes' // ------------------------------------------------- public static string GetHash(byte[] bytes) { var md5 = MD5.Create(); byte[] hash = md5.ComputeHash(bytes); StringBuilder sb = new StringBuilder(); for(int i = 0; i < hash.Length; i ++) sb.Append(hash[i].ToString("X2")); return(sb.ToString()); } private int mWidth = 0; private int mHeight = 0; private List mPageContents = new List(); private string mCurrentPageContents = ""; private List mEntries = new List(); private Font mCurrentFont = Font.NONE; private int mCurrentFontSize = 0; private Dictionary mImageCache = new Dictionary(); private static readonly string OP_BEGIN_TEXT = "BT"; private static readonly string OP_CONCAT_MATRIX = "cm"; private static readonly string OP_DO = "Do"; private static readonly string OP_END_TEXT = "ET"; private static readonly string OP_FILL = "f"; private static readonly string OP_LINETO = "l"; private static readonly string OP_MOVETO = "m"; private static readonly string OP_SAVE = "q"; private static readonly string OP_RESTORE = "Q"; private static readonly string OP_LINE_COLOR = "RG"; private static readonly string OP_FILL_COLOR = "rg"; private static readonly string OP_STROKE = "S"; private static readonly string OP_SET_FONT = "Tf"; private static readonly string OP_TEXT_SHOW = "Tj"; private static readonly string OP_TEXT_MATRIX = "Tm"; private static readonly string OP_RECTANGLE = "re"; private static readonly string OP_WIDTH = "w"; private static readonly string OP_CAP = "J"; private static readonly string OP_JOIN = "j"; private static readonly string OP_MITER = "M"; private static readonly string OP_DASH = "d"; private static readonly string OP_CHAR_SPACE = "Tc"; private static readonly string OP_WORD_SPACE = "Tw"; private static readonly string OP_HORIZ_SCALE = "Tz"; }