using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Text; namespace BoxPacker3D { public static class PdfExporter { private const float PageW = 842f, PageH = 595f, Margin = 36f; public static void Export(string path, List solutions, List overviews, List layerBitmaps, List<(double YMin, double YMax)> layers, double wallPad, double itemPad, List unpackedItems = null) { var pages = new List(); // Layer pages foreach (var bmp in layerBitmaps) pages.Add(BitmapToJpeg(bmp)); // Overview pages foreach (var bmp in overviews) pages.Add(BitmapToJpeg(bmp)); // Add unpacked items text page if needed bool hasUnpackedPage = unpackedItems != null && unpackedItems.Count > 0; using (var fs = new FileStream(path, FileMode.Create)) { var w = new PdfWriter(fs); w.Raw("%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"); int n = pages.Count + (hasUnpackedPage ? 1 : 0); // Total pages including text page int basePage = 3; int baseContent = basePage + n; int baseImage = baseContent + n; int objFont1 = baseImage + pages.Count; // After image objects int objFont2 = objFont1 + 1; var offsets = new long[objFont2 + 1]; // Object 1: Catalog offsets[1] = w.Pos; w.Obj(1); w.Raw("<< /Type /Catalog /Pages 2 0 R >>\n"); w.EndObj(); // Object 2: Pages var kids = string.Join(" ", Enumerable.Range(basePage, n).Select(i => $"{i} 0 R")); offsets[2] = w.Pos; w.Obj(2); w.Raw($"<< /Type /Pages /Kids [{kids}] /Count {n} >>\n"); w.EndObj(); // Per-page objects (image pages) for (int i = 0; i < pages.Count; i++) { bool isLayer = (i < layerBitmaps.Count); string pageTitle; if (isLayer) { int layIdx = i; pageTitle = $"Vrstva {layIdx + 1} / {layerBitmaps.Count}" + (layIdx < layers.Count ? $" (Y: {layers[layIdx].YMin:F1} – {layers[layIdx].YMax:F1} mm)" : ""); } else { int boxIdx = i - layerBitmaps.Count; var sol = solutions[boxIdx]; pageTitle = $"Škatuľa {boxIdx + 1} / {solutions.Count} " + $"({sol.Box.W}×{sol.Box.D}×{sol.Box.H} mm)"; } int imgObj = baseImage + i; int contentObj= baseContent + i; int pageObj = basePage + i; var imgBytes = pages[i]; int imgW, imgH; using (var ms = new MemoryStream(imgBytes)) using (var bmp = new Bitmap(ms)) { imgW = bmp.Width; imgH = bmp.Height; } float headerH = 52f, footerH = 22f; float availW = PageW - 2 * Margin; float availH = PageH - headerH - footerH - Margin; float sc = Math.Min(availW / imgW, availH / imgH); float scaledW = imgW * sc, scaledH = imgH * sc; float imgX = Margin + (availW - scaledW) / 2; float imgY = PageH - headerH - scaledH - 4; var cs = new StringBuilder(); // White background cs.AppendLine("1 1 1 rg"); cs.AppendLine($"0 0 {PageW:F2} {PageH:F2} re f"); // Header bar cs.AppendLine("0.06 0.35 0.72 rg"); cs.AppendLine($"0 {PageH - 44:F2} {PageW:F2} 44 re f"); // Title cs.AppendLine("1 1 1 rg BT /F1 14 Tf"); cs.AppendLine($"{Margin:F2} {PageH - 30:F2} Td"); cs.AppendLine($"({EscPdf(pageTitle)}) Tj ET"); // Date cs.AppendLine("0.70 0.80 0.90 rg BT /F2 8 Tf"); cs.AppendLine($"{PageW - 200:F2} {PageH - 30:F2} Td"); cs.AppendLine($"(Strana {i + 1} / {n} {DateTime.Now:dd.MM.yyyy}) Tj ET"); // Box info if (isLayer && i < solutions.Count) { var sol = solutions[0]; // assume first box for layer pages cs.AppendLine("0.40 0.40 0.50 rg BT /F2 8 Tf"); cs.AppendLine($"{Margin:F2} {PageH - 44:F2} Td"); string infoLine = $"Skatula: {sol.Box.W}×{sol.Box.H}×{sol.Box.D} mm | " + $"Stenova izolacia: {wallPad} mm | Medikusova: {itemPad} mm"; cs.AppendLine($"({EscPdf(infoLine)}) Tj ET"); } // Image cs.AppendLine("q"); cs.AppendLine($"{scaledW:F2} 0 0 {scaledH:F2} {imgX:F2} {imgY:F2} cm"); cs.AppendLine($"/Im{i} Do Q"); // Footer cs.AppendLine("0.35 0.35 0.45 rg BT /F2 7 Tf"); cs.AppendLine($"{Margin:F2} 10 Td"); cs.AppendLine("(Generovane programom 3D Box Packer) Tj ET"); byte[] csBytes = Encoding.Latin1.GetBytes(cs.ToString()); // Page offsets[pageObj] = w.Pos; w.Obj(pageObj); w.Raw($"<< /Type /Page /Parent 2 0 R\n"); w.Raw($" /MediaBox [0 0 {PageW:F2} {PageH:F2}]\n"); w.Raw($" /Contents {contentObj} 0 R\n"); w.Raw($" /Resources << /Font << /F1 {objFont1} 0 R /F2 {objFont2} 0 R >>"); w.Raw($" /XObject << /Im{i} {imgObj} 0 R >> >>\n>>\n"); w.EndObj(); // Content offsets[contentObj] = w.Pos; w.Obj(contentObj); w.Raw($"<< /Length {csBytes.Length} >>\nstream\n"); w.Bytes(csBytes); w.Raw("\nendstream\n"); w.EndObj(); // Image offsets[imgObj] = w.Pos; w.Obj(imgObj); w.Raw($"<< /Type /XObject /Subtype /Image /Width {imgW} /Height {imgH}"); w.Raw($" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode"); w.Raw($" /Length {imgBytes.Length} >>\nstream\n"); w.Bytes(imgBytes); w.Raw("\nendstream\n"); w.EndObj(); } // Unpacked items text page (if any) if (hasUnpackedPage) { int pageIdx = pages.Count; int pageObj = basePage + pageIdx; int contentObj = baseContent + pageIdx; // Group unpacked items by name var grouped = unpackedItems .GroupBy(it => it.Name) .Select(g => new { Name = g.Key, Count = g.Count(), W = g.First().W, H = g.First().H, D = g.First().D }) .OrderBy(x => x.Name) .ToList(); var cs = new StringBuilder(); // White background cs.AppendLine("1 1 1 rg"); cs.AppendLine($"0 0 {PageW:F2} {PageH:F2} re f"); // Header bar (red for warning) cs.AppendLine("0.83 0.18 0.18 rg"); cs.AppendLine($"0 {PageH - 44:F2} {PageW:F2} 44 re f"); // Title cs.AppendLine("1 1 1 rg BT /F1 14 Tf"); cs.AppendLine($"{Margin:F2} {PageH - 30:F2} Td"); cs.AppendLine($"(NEZABALENE KUSY) Tj ET"); // Date cs.AppendLine("1 1 1 rg BT /F2 8 Tf"); cs.AppendLine($"{PageW - 200:F2} {PageH - 30:F2} Td"); cs.AppendLine($"(Strana {n} / {n} {DateTime.Now:dd.MM.yyyy}) Tj ET"); // Summary cs.AppendLine("0.20 0.20 0.30 rg BT /F1 11 Tf"); cs.AppendLine($"{Margin:F2} {PageH - 80:F2} Td"); cs.AppendLine($"(Nasledujuce kusy sa nezmestili do skatul:) Tj ET"); // Table header float y = PageH - 120; cs.AppendLine("0.85 0.85 0.90 rg"); cs.AppendLine($"{Margin:F2} {y:F2} {PageW - 2*Margin:F2} 20 re f"); cs.AppendLine("0.25 0.25 0.35 rg BT /F1 9 Tf"); cs.AppendLine($"{Margin + 10:F2} {y + 6:F2} Td"); cs.AppendLine("(Nazov) Tj"); cs.AppendLine($"{250:F2} {0:F2} Td (Rozmery \\(mm\\)) Tj"); cs.AppendLine($"{160:F2} {0:F2} Td (Pocet) Tj ET"); y -= 25; // Table rows int rowNum = 0; foreach (var item in grouped) { if (y < 60) break; // Don't overflow page // Alternating row background if (rowNum % 2 == 0) { cs.AppendLine("0.96 0.96 0.98 rg"); cs.AppendLine($"{Margin:F2} {y:F2} {PageW - 2*Margin:F2} 18 re f"); } cs.AppendLine("0.20 0.20 0.30 rg BT /F2 8 Tf"); cs.AppendLine($"{Margin + 10:F2} {y + 5:F2} Td"); cs.AppendLine($"({EscPdf(item.Name)}) Tj"); cs.AppendLine($"{250:F2} {0:F2} Td ({item.W:F0} x {item.H:F0} x {item.D:F0}) Tj"); cs.AppendLine($"{160:F2} {0:F2} Td ({item.Count}) Tj ET"); y -= 18; rowNum++; } if (grouped.Count > rowNum) { cs.AppendLine("0.50 0.50 0.60 rg BT /F2 7 Tf"); cs.AppendLine($"{Margin + 10:F2} {y:F2} Td"); cs.AppendLine($"(... a dalsich {grouped.Count - rowNum} typov kusov) Tj ET"); } // Footer cs.AppendLine("0.35 0.35 0.45 rg BT /F2 7 Tf"); cs.AppendLine($"{Margin:F2} 10 Td"); cs.AppendLine("(Generovane programom 3D Box Packer) Tj ET"); byte[] csBytes = Encoding.Latin1.GetBytes(cs.ToString()); // Page offsets[pageObj] = w.Pos; w.Obj(pageObj); w.Raw($"<< /Type /Page /Parent 2 0 R\n"); w.Raw($" /MediaBox [0 0 {PageW:F2} {PageH:F2}]\n"); w.Raw($" /Contents {contentObj} 0 R\n"); w.Raw($" /Resources << /Font << /F1 {objFont1} 0 R /F2 {objFont2} 0 R >> >>\n>>\n"); w.EndObj(); // Content offsets[contentObj] = w.Pos; w.Obj(contentObj); w.Raw($"<< /Length {csBytes.Length} >>\nstream\n"); w.Bytes(csBytes); w.Raw("\nendstream\n"); w.EndObj(); } // Fonts offsets[objFont1] = w.Pos; w.Obj(objFont1); w.Raw("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\n"); w.EndObj(); offsets[objFont2] = w.Pos; w.Obj(objFont2); w.Raw("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\n"); w.EndObj(); // xref long xrefPos = w.Pos; w.Raw($"xref\n0 {offsets.Length}\n"); w.Raw("0000000000 65535 f \n"); for (int i = 1; i < offsets.Length; i++) w.Raw($"{offsets[i]:D10} 00000 n \n"); w.Raw($"trailer\n<< /Size {offsets.Length} /Root 1 0 R >>\n"); w.Raw($"startxref\n{xrefPos}\n%%EOF\n"); } } private static byte[] BitmapToJpeg(Bitmap bmp) { using (var ms = new MemoryStream()) { var enc = GetJpegEncoder(); var prms = new EncoderParameters(1); prms.Param[0] = new EncoderParameter( System.Drawing.Imaging.Encoder.Quality, (long)88); bmp.Save(ms, enc, prms); return ms.ToArray(); } } private static ImageCodecInfo GetJpegEncoder() => ImageCodecInfo.GetImageEncoders().First(c => c.MimeType == "image/jpeg"); private static string EscPdf(string s) => s.Replace("\\", "\\\\").Replace("(", "\\(").Replace(")", "\\)"); private class PdfWriter { private readonly FileStream _fs; public long Pos => _fs.Position; public PdfWriter(FileStream fs) { _fs = fs; } public void Raw(string s) { var b = Encoding.Latin1.GetBytes(s); _fs.Write(b, 0, b.Length); } public void Bytes(byte[] b) { _fs.Write(b, 0, b.Length); } public void Obj(int n) { Raw($"{n} 0 obj\n"); } public void EndObj() { Raw("endobj\n"); } } } }