Add project files.

This commit is contained in:
roman
2026-06-05 22:02:32 +02:00
parent f09ad1b7c6
commit 274d607d54
25 changed files with 5462 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
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"); }
}
}
}