Files
BoxPacker3D/PdfExporter.cs
2026-06-05 22:02:32 +02:00

331 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<BoxSolution> solutions,
List<Bitmap> overviews,
List<Bitmap> layerBitmaps,
List<(double YMin, double YMax)> layers,
double wallPad, double itemPad,
List<Item> unpackedItems = null)
{
var pages = new List<byte[]>();
// 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"); }
}
}
}