public Page nextPage() { // walk all pages found in .mcf, return null on last if (_pageIterator >= _pages.Count) { return(null); // handle out of bounds } // the current xml page XmlNode xmlPage = _pages[_pageIterator]; // the reconstructed page (still empty) Page page = new Page(); // keep track which page we are currently processing, every Page object actually consists of // two <fotobook/page> nodes, the left and right side in photobook bool isDouble = false; // need to collect double pages here, in case of cover actually 3 pages. while (xmlPage != null) { // the current page type, later used to handle left/right and special page cases page.type = Page.convert(xmlPage.Attributes.GetNamedItem("type").Value); // store page number if (page.type == Page.Type.Normalpage) { if (isDouble) { page.pageNoRight = getAttributeStr(xmlPage, "pagenr"); } else { page.pageNoLeft = getAttributeStr(xmlPage, "pagenr"); } } // iterate all sub nodes this page contains foreach (XmlNode node in xmlPage.ChildNodes) { switch (node.Name) { // bundlesize is the left & right combined size of the page case "bundlesize": page.bundleSize = new Vector2(getAttributeF(node, "width"), getAttributeF(node, "height")); break; // NOTE: currently only handles background id case "designElementIDs": // store background for left and right individually if (!isDouble) { page.backgroundLeft = getAttributeStr(node, "background"); } else { page.backgroundRight = getAttributeStr(node, "background"); } break; // area is the root class of all content objects // NOTE: currently supports <imagearea> & <textarea> case "area": // get the type of current area string type = node.Attributes.GetNamedItem("areatype").Value; // trick area system... if (type == "spinetextarea") { type = "textarea"; page.spineSize = getAttributeF(node.SelectSingleNode("position"), "height"); } Area newArea; switch (type) { case "imagearea": { // imagearea? image subnode exists! XmlNode image = node.SelectSingleNode("image"); // the image file name stored in .mcf file (in format: "safecontainer:/imageName.jpg) string filename = getAttributeStr(image, "filename"); // replace 'safecontainer:/' with actual path, in case filename does not exist, // store "NULL", will render as magenta outline and print error. string filePath = filename != "" ? filename.Replace("safecontainer:/", _safeContainerPath) : "NULL"; // get & store cutout information XmlNode cutout = image.SelectSingleNode("cutout"); Vector2 cutoutLeftTop = new Vector2(getAttributeF(cutout, "left"), getAttributeF(cutout, "top")); float scale = getAttributeF(cutout, "scale", 1.0f); // construct new area newArea = new ImageArea() { path = filePath, cutout = cutoutLeftTop, scale = scale, }; // get & store border settings XmlNode border = node.SelectSingleNode("decoration/border"); if (border != null) { newArea.border = true; newArea.borderWidth = getAttributeF(border, "width"); newArea.borderColor = getAttributeStr(border, "color"); } break; } case "imagebackgroundarea": { // handle backgroundimages literally just like normal images. // TODO: de-duplicate this code as much as possible XmlNode imgbg = node.SelectSingleNode("imagebackground"); // the image file string filename = getAttributeStr(imgbg, "filename"); // replace 'safecontainer:/' with actual path, in case filename does not exist, // store "NULL", will render as magenta outline and print error. string filePath = filename != "" ? filename.Replace("safecontainer:/", _safeContainerPath) : "NULL"; // get & store cutout information XmlNode cutout = imgbg.SelectSingleNode("cutout"); Vector2 cutoutLeftTop = new Vector2(getAttributeF(cutout, "left"), getAttributeF(cutout, "top")); float scale = getAttributeF(cutout, "scale", 1.0f); string bgPosition = getAttributeStr(imgbg, "backgroundPosition"); ImageBackgroundArea.ImageBackgroundType bgtype = ImageBackgroundArea.ImageBackgroundType.Undefined; if (bgPosition == "LEFT_OR_TOP") { bgtype = ImageBackgroundArea.ImageBackgroundType.Left; } else if (bgPosition == "RIGHT_OR_BOTTOM") { bgtype = ImageBackgroundArea.ImageBackgroundType.Right; } else if (bgPosition == "BUNDLE") { bgtype = ImageBackgroundArea.ImageBackgroundType.Bundle; } else { Log.Error("Unhandled background image position: " + bgPosition); } // construct new area newArea = new ImageBackgroundArea() { path = filePath, cutout = cutoutLeftTop, scale = scale, type = bgtype }; break; } case "textarea": { // in <textarea> these exist: XmlNode text = node.SelectSingleNode("text"); XmlNode textFormat = text.SelectSingleNode("textFormat"); // NOTE: <font> stores several comma-separated values: Fontname,Fonstsize,...and more. Currently only handles these two string[] fontInfo = getAttributeStr(textFormat, "font").Split(","); // get the fontsize, take pdf scale into account and adjust to photobook settings int fontSize = (int)(Convert.ToInt32(fontInfo[1]) * SCALE * FONT); // somewhat matches the result in photobook // text color string color = getAttributeStr(textFormat, "foregroundColor"); // text box background color string bgColor = getAttributeStr(textFormat, "backgroundColor"); // by default align left top string alignLabel = "ALIGNLEFT"; string valignLabel = "ALIGNVTOP"; // NOTE: <align> sometimes holds two comma-separated values (Horizontal and Vertical alignment) // for now only handles second (horizontal). string[] align = getAttributeStr(textFormat, "Alignment").Split(","); alignLabel = align.Last(); if (align.Length > 1) { valignLabel = align.First(); } string str = extractTextFromHTML(text.InnerText, ref color); // construct new area newArea = new TextArea() { textElements = extractTextFromHTMLv2(text.InnerText), text = str, fontsize = fontSize, color = color, font = fontInfo[0], align = alignLabel, valign = valignLabel, backgroundcolor = bgColor, }; break; } default: // there are more areatypes, for now just create an empty area that wont draw anything // and inform user. newArea = new Area(); Log.Warning("Unhandled area type in <page/area> '" + type + "'."); break; } // sanity check, cant be null really :P if (newArea == null) { break; } // all areas contain position information XmlNode position = node.SelectSingleNode("position"); // apply position information to current area newArea.rect = new RectangleF() { X = getAttributeF(position, "left"), Y = getAttributeF(position, "top"), Width = getAttributeF(position, "width"), Height = getAttributeF(position, "height") }; newArea.rotation = getAttributeF(position, "rotation") / SCALE; // undo scale for rotation // store new page in list page.areas.Add(newArea); break; default: // inform user about unhandled node Log.Warning("Unhandled Node in <page> '" + node.Name + "'."); break; } } // Handle all these specific page types and cases if (page.type == Page.Type.Fullcover) { // check if next page is spine, otherwise this was the back side of the cover XmlNode nextPage = _pages[_pageIterator + 1]; Page.Type nextType = Page.convert(nextPage.Attributes.GetNamedItem("type").Value); if (nextType == Page.Type.Spine) { xmlPage = nextPage; } else { xmlPage = null; // cover page is done, proceed } } else if (page.type == Page.Type.Spine) { // check if next page is Fullcover (should be anyway) XmlNode nextPage = _pages[_pageIterator + 1]; Page.Type nextType = Page.convert(nextPage.Attributes.GetNamedItem("type").Value); if (nextType == Page.Type.Fullcover) { xmlPage = nextPage; } else { xmlPage = null; } } else if (page.type == Page.Type.Emptypage) { // check if next page exists... otherwise end of book. if (_pageIterator + 1 < _pages.Count) { XmlNode nextPage = _pages[_pageIterator + 1]; xmlPage = nextPage; isDouble = true; } else { xmlPage = null; } } else if (page.type == Page.Type.Normalpage && !isDouble) { XmlNode nextPage = _pages[_pageIterator + 1]; // next is second half of a double page... xmlPage = nextPage; isDouble = true; } else { // this was second half of a double page, continue with new page xmlPage = null; } // increment to handle next <page> object in list _pageIterator++; } // return the newly constructed page return(page); }
public void writePage(Page pPage) { // page size is given per <fotobook/page>. iTextSharp needs it set before adding page or opening document. _doc.SetPageSize(new Rectangle(0f, 0f, pPage.bundleSize.X, pPage.bundleSize.Y)); // handle first page case try { if (!_doc.IsOpen()) { _doc.Open(); } else { _doc.NewPage(); } } catch (Exception e) { Log.Error("Creating pdf page failed with error: '" + e.Message + "'."); return; } PdfContentByte canvas = _writer.DirectContent; // TOOD: de-duplicate // draw left part of background if (pPage.backgroundLeft != null) { canvas.Rectangle(0, 0, pPage.bundleSize.X / 2, pPage.bundleSize.Y); #if DEBUG || _DEBUG canvas.SetColorFill(BaseColor.CYAN); #else canvas.SetColorFill(BaseColor.WHITE); #endif canvas.Fill(); string id = pPage.backgroundLeft; System.Drawing.Image sysImg = DesignIdConverter.getImageFromID(id); if (sysImg == null) { Log.Error("Background image for id '" + id + "' was null."); #if DEBUG || _DEBUG canvas.SetColorFill(BaseColor.MAGENTA); #else canvas.SetColorFill(BaseColor.WHITE); #endif canvas.Fill(); } else { Image img = sysImageToITextImage(sysImg); float facY = pPage.bundleSize.Y / img.PlainHeight; float facX = pPage.bundleSize.X / img.PlainWidth; float fac = Math.Max(facX, facY); img.ScalePercent(fac * 100f); float yoffset = (img.ScaledHeight - pPage.bundleSize.Y) * -0.5f; float xoffset = (img.ScaledWidth - pPage.bundleSize.X) * -0.5f; img.SetAbsolutePosition(xoffset, yoffset); float width = -xoffset + ((pPage.type == Page.Type.Fullcover) ? pPage.bundleSize.X : pPage.bundleSize.X / 2f); Image imgCropped = cropImage(img, _writer, 0, 0, width, img.ScaledHeight); imgCropped.SetAbsolutePosition(xoffset, yoffset); canvas.AddImage(imgCropped); } } // draw right background if (pPage.backgroundRight != null) { canvas.Rectangle(pPage.bundleSize.X / 2, 0, pPage.bundleSize.X / 2, pPage.bundleSize.Y); #if DEBUG || _DEBUG canvas.SetColorFill(BaseColor.CYAN); canvas.SetColorFill(BaseColor.WHITE); #endif canvas.Fill(); string id = pPage.backgroundRight; System.Drawing.Image sysImg = DesignIdConverter.getImageFromID(id); if (sysImg == null) { Log.Error("Background image for id '" + id + "' was null."); #if DEBUG || _DEBUG canvas.SetColorFill(BaseColor.MAGENTA); #else canvas.SetColorFill(BaseColor.WHITE); #endif canvas.Fill(); canvas.Fill(); } else { Image img = sysImageToITextImage(sysImg); float facY = pPage.bundleSize.Y / img.PlainHeight; float facX = pPage.bundleSize.X / img.PlainWidth; float fac = Math.Max(facX, facY); img.ScalePercent(fac * 100f); float yoffset = (img.ScaledHeight - pPage.bundleSize.Y) * -0.5f; float xoffset = (img.ScaledWidth - pPage.bundleSize.X) * -0.5f; img.SetAbsolutePosition(xoffset, yoffset); Image imgCropped = cropImage(img, _writer, pPage.bundleSize.X / 2f, 0, img.ScaledWidth, img.ScaledHeight); imgCropped.SetAbsolutePosition(xoffset + pPage.bundleSize.X / 2, yoffset); canvas.AddImage(imgCropped); } } // draw all supported content areas stored in this page foreach (Area area in pPage.areas) { // calculate rect dimensions // TODO: de-duplicate? float pX = area.rect.X; float pY = pPage.bundleSize.Y - area.rect.Y - area.rect.Height; // handle rotation canvas.SaveState(); AffineTransform tf = new AffineTransform(); double angle = area.rotation * Math.PI / 180.0; tf.Rotate(-angle, pX + area.rect.Width / 2f, pY + area.rect.Height / 2f); // rotate around center ccw canvas.Transform(tf); if (area is ImageArea || area is ImageBackgroundArea) { // TODO: This is somewhat hacky - there is probably a better way to do this. if (area is ImageBackgroundArea) { ImageBackgroundArea bgArea = (ImageBackgroundArea)area; if (bgArea.type == ImageBackgroundArea.ImageBackgroundType.Right) { bgArea.rect.X += pPage.bundleSize.X / 2f + pPage.spineSize / 2f; } } ImageArea imgArea = (ImageArea)area; // if image path was not valid draw magenta outline and print error if (imgArea.path == "NULL") { #if DEBUG || _DEBUG // calculate rect dimensions Rectangle nullRect = new Rectangle(pX, pY, pX + imgArea.rect.Width, pY + imgArea.rect.Height); // configure border nullRect.Border = 1 | 2 | 4 | 8; nullRect.BorderColor = BaseColor.MAGENTA; nullRect.BorderWidth = 4.0f; // draw to document canvas.Rectangle(nullRect); Log.Error("Image path was null. Probably caused by an empty image area."); canvas.RestoreState(); #endif continue; } // load image file. System.Drawing.Image sysImg; try { sysImg = System.Drawing.Image.FromFile(imgArea.path); } catch (System.Exception e) { if (e is System.IO.FileNotFoundException) { Log.Error("Loading image failed. Image at '" + imgArea.path + "' not found."); } else { Log.Error("Loading image failed wit error: '" + e.Message + "'"); } canvas.RestoreState(); continue; } // fix exif orientation ExifRotate(sysImg); // calculate resizing factor, results in equal pixel density for all images. float scale = 1f / imgArea.scale * Config.ImgScale; // the higher this value, the lower pixel density is. 0.0f = original resolution scale = scale < 1.0f ? 1.0f : scale; // never scale image up System.Drawing.Size newSize = new System.Drawing.Size((int)(sysImg.Width / scale), (int)(sysImg.Height / scale)); // resize image sysImg = (System.Drawing.Image)(new System.Drawing.Bitmap(sysImg, newSize)); Image img = sysImageToITextImage(sysImg); // apply scale as defined in .mcf img.ScalePercent(imgArea.scale * 100.0f * scale); // calculate image position in pdf page float posX = imgArea.rect.X + imgArea.cutout.X; float posY = pPage.bundleSize.Y - imgArea.rect.Y - imgArea.rect.Height; // pdf origin is in lower left, mcf origin is in upper left // yaaaaa... whatever. This way everything fits float cropBottom = img.ScaledHeight - imgArea.rect.Height + imgArea.cutout.Y; // crop image to mcf specified rect Image cropped = cropImage(img, _writer, -imgArea.cutout.X, cropBottom, imgArea.rect.Width, imgArea.rect.Height); // move to mcf specified position cropped.SetAbsolutePosition(imgArea.rect.X, posY); string imgType = area is ImageBackgroundArea ? "ImageBackground" : "Image"; Log.Info("Rendering " + imgType + " (." + imgArea.path.Split(".").Last() + "): " + "original: " + sysImg.Width + "x" + sysImg.Height + "; " + "scaled: " + newSize.Width + "x" + newSize.Height + "; " + "cropped: " + (int)cropped.Width + "x" + (int)cropped.Height + "; " + "at: " + (int)cropped.AbsoluteX + ", " + (int)cropped.AbsoluteY); // draw the image canvas.AddImage(cropped); // draw image border if specified in .mcf if (imgArea.border) { // TODO mcf as an outside property that is currently not taken into account. // seems like all borders are 'outside' in photobook. // iTextSharp draws Borders centered (BorderWidth/2 pixels overlap image) // this should be corrected. // calc border rect Rectangle rect = new Rectangle(pX, pY, pX + imgArea.rect.Width, pY + imgArea.rect.Height); // convert .mcf's html style color hex code to Color, based on: https://stackoverflow.com/a/2109904 int argb = Int32.Parse(imgArea.borderColor.Replace("#", ""), System.Globalization.NumberStyles.HexNumber); System.Drawing.Color clr = System.Drawing.Color.FromArgb(argb); // configure border rect.Border = 1 | 2 | 4 | 8; rect.BorderColor = new BaseColor(clr); rect.BorderWidth = imgArea.borderWidth; // draw border canvas.Rectangle(rect); } } else if (area is TextArea) { TextArea textArea = (TextArea)area; // Render text background if not transparent if (!textArea.backgroundcolor.EndsWith("00")) { Log.Info("Rendering Text background: color=" + textArea.backgroundcolor); canvas.Rectangle(pX, pY, textArea.rect.Width, textArea.rect.Height); canvas.SetColorFill(argb2BaseColor(textArea.backgroundcolor)); canvas.Fill(); } // just in case something went wrong if (String.IsNullOrWhiteSpace(textArea.text)) { Log.Error("Text was empty."); canvas.RestoreState(); continue; } else { Log.Info("Rendering Text: font=" + textArea.font + "; size=" + textArea.fontsize + "; align=" + textArea.align + "; valign=" + textArea.valign); } // iTextSharp textbox ColumnText colText = new ColumnText(canvas); // calculate rect float llx = textArea.rect.X; float lly = pPage.bundleSize.Y - textArea.rect.Y - textArea.rect.Height; float urx = llx + textArea.rect.Width; float ury = lly + textArea.rect.Height; Rectangle textRect = new Rectangle(llx, lly, urx, ury); // apply rect to textbox colText.SetSimpleColumn(textRect); // The actual text object Paragraph par = new Paragraph(); // magic number that closely matches photobook // TODO there is probably more information in the .mcf's css part par.SetLeading(0, 1.3f); // apply corrent alignment if (textArea.align == "ALIGNHCENTER") { par.Alignment = Element.ALIGN_CENTER; } else if (textArea.align == "ALIGNLEFT") { par.Alignment = Element.ALIGN_LEFT; } else if (textArea.align == "ALIGNRIGHT") { par.Alignment = Element.ALIGN_RIGHT; } else if (textArea.align == "ALIGNJUSTIFY") { par.Alignment = Element.ALIGN_JUSTIFIED; } else { Log.Warning("Unhandled text align: '" + textArea.align + "'."); } // add text chunks foreach (TextElement elem in textArea.textElements) { int style = 0; style += elem.bold ? Font.BOLD : 0; style += elem.italic ? Font.ITALIC : 0; style += elem.underlined ? Font.UNDERLINE : 0; Font fnt = FontFactory.GetFont(elem.family, elem.size, style, argb2BaseColor(elem.color)); par.Add(new Chunk(elem.text + (elem.newline ? "\n" : " "), fnt)); } int valign = 0; if (textArea.valign == "ALIGNVCENTER") { valign = Element.ALIGN_MIDDLE; } else if (textArea.valign == "ALIGNVTOP") { valign = Element.ALIGN_TOP; } else if (textArea.valign == "ALIGNVBOTTOM") { valign = Element.ALIGN_BOTTOM; } else { Log.Warning("Unhandled text vertical align: '" + textArea.valign + "'."); } // v align needs a table... PdfPTable table = new PdfPTable(1); table.SetWidths(new int[] { 1 }); table.WidthPercentage = 100; table.AddCell(new PdfPCell(par) { HorizontalAlignment = par.Alignment, VerticalAlignment = valign, FixedHeight = textArea.rect.Height, Border = 0, }); // add paragraph to textbox colText.AddElement(table); // draw textbox colText.Go(); } // restore canvas transform before rotation canvas.RestoreState(); } // draw pagenumbers // TODO remove magic numbers, at least comment const float PAGE_NR_Y_OFFSET = -4.0f; const float PAGE_NR_X_OFFSET = 0.0f; float PAGE_NR_FONT_SIZE = Page.pageNoFontSize * 1.1f; float PAGE_NR_HEIGHT = PAGE_NR_FONT_SIZE + 12.0f; // add some extra space... this is needed. float PAGE_Y_POS = Page.pageNoMargin.Y + PAGE_NR_Y_OFFSET; // TODO de-duplicate all these conversions and move to helper method // convert .mcf's html style color hex code to Color, based on: https://stackoverflow.com/a/2109904 int argb_ = Int32.Parse(Page.pageNoColor.Replace("#", ""), System.Globalization.NumberStyles.HexNumber); System.Drawing.Color clr_ = System.Drawing.Color.FromArgb(argb_); // left Paragraph pageNoLeft = new Paragraph(pPage.pageNoLeft, FontFactory.GetFont(Page.pageNoFont, PAGE_NR_FONT_SIZE, new BaseColor(clr_))); pageNoLeft.Alignment = Element.ALIGN_LEFT + Element.ALIGN_BOTTOM; ColumnText leftNo = new ColumnText(_writer.DirectContent); Rectangle leftNoRect = new Rectangle(Page.pageNoMargin.X + PAGE_NR_X_OFFSET, PAGE_Y_POS, 500, PAGE_Y_POS + PAGE_NR_HEIGHT); leftNo.SetSimpleColumn(leftNoRect); leftNo.AddElement(pageNoLeft); leftNo.Go(); //leftNoRect.Border = 1 | 2 | 4 | 8; //leftNoRect.BorderColor = BaseColor.GREEN; //leftNoRect.BorderWidth = 1.0f; //_writer.DirectContent.Rectangle(leftNoRect); // right Paragraph pageNoRight = new Paragraph(pPage.pageNoRight, FontFactory.GetFont(Page.pageNoFont, PAGE_NR_FONT_SIZE, new BaseColor(clr_))); pageNoRight.Alignment = Element.ALIGN_RIGHT; ColumnText rightNo = new ColumnText(_writer.DirectContent); Rectangle rightNoRect = new Rectangle(pPage.bundleSize.X - Page.pageNoMargin.X - PAGE_NR_X_OFFSET - 500, PAGE_Y_POS, pPage.bundleSize.X - Page.pageNoMargin.X - PAGE_NR_X_OFFSET, PAGE_Y_POS + PAGE_NR_HEIGHT); rightNo.SetSimpleColumn(rightNoRect); rightNo.AddElement(pageNoRight); rightNo.Go(); //rightNoRect.Border = 1 | 2 | 4 | 8; //rightNoRect.BorderColor = BaseColor.YELLOW; //rightNoRect.BorderWidth = 1.0f; //_writer.DirectContent.Rectangle(rightNoRect); //Console.WriteLine("Page drawn: " + pPage.type.ToString() + " left: " + pPage.pageNoLeft + "; right: " + pPage.pageNoRight + "!"); }