Files
Better-Seewo/Manager/Services/Conversion/PptxImporter.cs

619 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Xml.Linq;
namespace BetterSeewo.Manager.Services.Conversion
{
public class PptxImporter
{
private static readonly XNamespace A = "http://schemas.openxmlformats.org/drawingml/2006/main";
private static readonly XNamespace P = "http://schemas.openxmlformats.org/presentationml/2006/main";
private static readonly XNamespace R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
private int _nextId;
public SmfDocument Import(string pptxPath)
{
using var package = Package.Open(pptxPath, FileMode.Open, FileAccess.Read);
return Import(package);
}
public SmfDocument Import(Package pptx)
{
_nextId = 1;
var doc = new SmfDocument();
var pres = GetPresentation(pptx);
var size = GetSlideSize(pres);
doc.Properties.SlideWidth = size.Item1;
doc.Properties.SlideHeight = size.Item2;
doc.Properties.Title = GetProperty(pres, P + "title") ?? "";
ExtractMediaFiles(pptx, doc);
var slideRels = GetSlideRelations(pptx, pres);
foreach (var (relId, target, slideNum) in slideRels)
{
var slidePart = pptx.GetPart(PackUriHelper.CreatePartUri(new Uri(target, UriKind.Relative)));
var slideXml = XDocument.Load(slidePart.GetStream());
var slide = new SmfSlide { Id = $"slide_{slideNum}" };
ParseBackground(slideXml, slide, pptx);
ParseElements(slideXml, slide, pptx, doc);
doc.Slides.Add(slide);
}
return doc;
}
private void ExtractMediaFiles(Package pptx, SmfDocument doc)
{
foreach (var part in pptx.GetParts())
{
var uri = part.Uri.ToString();
if (uri.StartsWith("/ppt/media/", StringComparison.OrdinalIgnoreCase))
{
var fileName = Path.GetFileName(uri);
using var stream = part.GetStream();
var data = new byte[stream.Length];
stream.Read(data, 0, data.Length);
doc.MediaFiles[fileName] = data;
doc.MediaContentTypes[fileName] = part.ContentType;
}
}
}
private void ParseBackground(XDocument slideXml, SmfSlide slide, Package pptx)
{
var bg = slideXml.Root?.Element(P + "cSld")?.Element(P + "bg");
if (bg == null) return;
var bgPr = bg.Element(P + "bgPr");
if (bgPr == null) return;
var solidFill = bgPr.Element(A + "solidFill");
if (solidFill != null)
{
slide.Background.Type = SmfFillType.Solid;
slide.Background.SolidColor = ParseColor(solidFill);
return;
}
var gradFill = bgPr.Element(A + "gradFill");
if (gradFill != null)
{
slide.Background.Type = SmfFillType.Gradient;
slide.Background.Gradient = ParseGradient(gradFill);
return;
}
var blipFill = bgPr.Element(A + "blipFill");
if (blipFill != null)
{
var blip = blipFill.Element(A + "blip");
var embed = blip?.Attribute(R + "embed")?.Value;
if (embed != null)
{
var relsPart = GetSlideRelsPart(pptx, slide.Id);
if (relsPart != null)
{
var rels = XDocument.Load(relsPart.GetStream());
var rel = rels.Root?.Elements("Relationship")
.FirstOrDefault(r => (string)r.Attribute("Id") == embed);
if (rel != null)
{
var target = (string)rel.Attribute("Target");
slide.Background.Type = SmfFillType.Image;
slide.Background.ImageRef = Path.GetFileName(target);
}
}
}
}
}
private PackagePart? GetSlideRelsPart(Package pptx, string slideId)
{
var num = slideId.Replace("slide_", "");
var relsUri = PackUriHelper.CreatePartUri(
new Uri($"/ppt/slides/_rels/slide{num}.xml.rels", UriKind.Relative));
if (pptx.PartExists(relsUri))
return pptx.GetPart(relsUri);
return null;
}
private void ParseElements(XDocument slideXml, SmfSlide slide, Package pptx, SmfDocument doc)
{
var spTree = slideXml.Root?.Element(P + "cSld")?.Element(P + "spTree");
if (spTree == null) return;
foreach (var elem in spTree.Elements())
{
string elemName = elem.Name.LocalName;
switch (elemName)
{
case "sp":
ParseShapeElement(elem, slide, pptx);
break;
case "pic":
ParseImageElement(elem, slide, pptx);
break;
case "graphicFrame":
ParseTableElement(elem, slide);
break;
case "grpSp":
break;
}
}
}
private void ParseShapeElement(XElement sp, SmfSlide slide, Package pptx)
{
var nvSpPr = sp.Element(P + "nvSpPr");
var cNvSpPr = nvSpPr?.Element(P + "cNvSpPr");
var isTextBox = cNvSpPr?.Attribute("txBox") != null;
var txBody = sp.Element(P + "txBody");
var xfrm = sp.Element(P + "spPr")?.Element(A + "xfrm");
if (xfrm == null) return;
var bounds = ParseBounds(xfrm);
var rotation = (double?)xfrm?.Attribute("rot") ?? 0;
var spPr = sp.Element(P + "spPr");
var prstGeom = spPr?.Element(A + "prstGeom");
var shapeType = prstGeom != null ? ParseShapeType((string)prstGeom.Attribute("prst") ?? "") : SmfShapeType.NoShape;
var fill = ParseFill(spPr);
var stroke = ParseStroke(spPr);
if (isTextBox || (txBody != null && shapeType == SmfShapeType.NoShape))
{
var elem = new SmfTextElement
{
Id = NextId(),
Bounds = bounds,
Rotation = rotation / 60000.0,
RichText = ParseRichText(txBody),
BackgroundFill = fill,
Outline = stroke,
};
slide.Elements.Add(elem);
}
else
{
var elem = new SmfShapeElement
{
Id = NextId(),
Bounds = bounds,
Rotation = rotation / 60000.0,
ShapeType = shapeType,
Fill = fill,
Stroke = stroke,
};
if (txBody != null)
elem.TextContent = ParseRichText(txBody);
slide.Elements.Add(elem);
}
}
private void ParseImageElement(XElement pic, SmfSlide slide, Package pptx)
{
var spPr = pic.Element(P + "spPr");
var prXfrm = spPr?.Element(A + "xfrm");
if (prXfrm == null) return;
var bounds = ParseBounds(prXfrm);
var rotation = (double?)prXfrm?.Attribute("rot") ?? 0;
var blip = pic.Element(P + "blipFill")?.Element(A + "blip");
var embed = blip?.Attribute(R + "embed")?.Value;
if (embed == null) return;
var relsPart = GetSlideRelsPart(pptx, slide.Id);
if (relsPart == null) return;
var rels = XDocument.Load(relsPart.GetStream());
var rel = rels.Root?.Elements("Relationship")
.FirstOrDefault(r => (string)r.Attribute("Id") == embed);
if (rel == null) return;
var target = (string)rel.Attribute("Target");
if (target.StartsWith("../media/", StringComparison.OrdinalIgnoreCase))
target = target.Substring("../".Length);
var fileName = Path.GetFileName(target);
var elem = new SmfImageElement
{
Id = NextId(),
Bounds = bounds,
Rotation = rotation / 60000.0,
Source = fileName,
ContentType = guessContentType(fileName),
};
var cropRect = blip?.Element(A + "srcRect");
if (cropRect != null)
{
elem.Crop = new SmfCropInfo
{
Left = (double?)cropRect.Attribute("l") ?? 0,
Right = (double?)cropRect.Attribute("r") ?? 0,
Top = (double?)cropRect.Attribute("t") ?? 0,
Bottom = (double?)cropRect.Attribute("b") ?? 0,
};
}
slide.Elements.Add(elem);
}
private void ParseTableElement(XElement graphicFrame, SmfSlide slide)
{
var tbl = graphicFrame.Descendants(A + "tbl").FirstOrDefault();
if (tbl == null) return;
var xfrm = graphicFrame.Element(P + "xfrm") ?? graphicFrame.Element(P + "spPr")?.Element(A + "xfrm");
if (xfrm == null) return;
var bounds = ParseBounds(xfrm);
var table = new SmfTableElement
{
Id = NextId(),
Bounds = bounds,
};
var gridCol = tbl.Element(A + "tblGrid");
if (gridCol != null)
{
foreach (var col in gridCol.Elements(A + "gridCol"))
table.ColumnWidths.Add((double)col.Attribute("w"));
}
foreach (var tr in tbl.Elements(A + "tr"))
{
var row = new SmfTableRow { Height = (double)tr.Attribute("h") };
foreach (var tc in tr.Elements(A + "tc"))
{
var cell = new SmfTableCell();
var txBody = tc.Element(A + "txBody");
if (txBody != null)
cell.Content = ParseRichText(txBody);
var tcPr = tc.Element(A + "tcPr");
if (tcPr != null)
{
cell.RowSpan = (int?)tcPr.Attribute("rowSpan") ?? 1;
cell.ColSpan = (int?)tcPr.Attribute("gridSpan") ?? 1;
cell.LeftMargin = (double?)tcPr.Attribute("marL") ?? 45720;
cell.RightMargin = (double?)tcPr.Attribute("marR") ?? 45720;
cell.TopMargin = (double?)tcPr.Attribute("marT") ?? 0;
cell.BottomMargin = (double?)tcPr.Attribute("marB") ?? 0;
var anchor = (string)tcPr.Attribute("anchor") ?? "t";
cell.VerticalAlign = anchor switch
{
"t" => SmfVerticalAlignment.Top,
"m" => SmfVerticalAlignment.Middle,
"b" => SmfVerticalAlignment.Bottom,
_ => SmfVerticalAlignment.Top,
};
var solidFill = tcPr.Element(A + "solidFill");
if (solidFill != null)
cell.Background = new SmfFill { Type = SmfFillType.Solid, SolidColor = ParseColor(solidFill) };
}
row.Cells.Add(cell);
}
table.Rows.Add(row);
}
slide.Elements.Add(table);
}
private SmfBounds ParseBounds(XElement xfrm)
{
var off = xfrm.Element(A + "off");
var ext = xfrm.Element(A + "ext");
return new SmfBounds
{
X = off != null ? (double)off.Attribute("x") : 0,
Y = off != null ? (double)off.Attribute("y") : 0,
Width = ext != null ? (double)ext.Attribute("cx") : 0,
Height = ext != null ? (double)ext.Attribute("cy") : 0,
};
}
private SmfRichText ParseRichText(XElement? txBody)
{
var rt = new SmfRichText();
if (txBody == null) return rt;
var bodyPr = txBody.Element(A + "bodyPr");
if (bodyPr != null)
{
rt.LeftMargin = (double?)bodyPr.Attribute("lIns") ?? 91440;
rt.RightMargin = (double?)bodyPr.Attribute("rIns") ?? 91440;
rt.TopMargin = (double?)bodyPr.Attribute("tIns") ?? 91440;
rt.BottomMargin = (double?)bodyPr.Attribute("bIns") ?? 91440;
rt.WordWrap = (string)bodyPr.Attribute("wrap") != "none";
var anchor = (string)bodyPr.Attribute("anchor") ?? "t";
rt.VerticalAlignment = anchor switch
{
"t" => SmfVerticalAlignment.Top,
"m" => SmfVerticalAlignment.Middle,
"b" => SmfVerticalAlignment.Bottom,
_ => SmfVerticalAlignment.Top,
};
}
foreach (var para in txBody.Elements(A + "p"))
{
var pElem = new SmfParagraph();
var pPr = para.Element(A + "pPr");
if (pPr != null)
{
var algn = (string)pPr.Attribute("algn");
pElem.Alignment = algn switch
{
"l" => SmfHorizontalAlignment.Left,
"ctr" => SmfHorizontalAlignment.Center,
"r" => SmfHorizontalAlignment.Right,
"just" => SmfHorizontalAlignment.Justify,
_ => null,
};
}
foreach (var run in para.Elements(A + "r"))
{
var rElem = new SmfRun
{
Text = run.Element(A + "t")?.Value ?? "",
Formatting = ParseFormatting(run.Element(A + "rPr")),
};
pElem.Runs.Add(rElem);
}
if (pElem.Runs.Count > 0 || para.Elements().Any())
rt.Paragraphs.Add(pElem);
}
return rt;
}
private SmfFormatting? ParseFormatting(XElement? rPr)
{
if (rPr == null) return null;
var fmt = new SmfFormatting();
var sz = rPr.Attribute("sz");
if (sz != null) fmt.Size = (double)sz / 100.0;
var b = rPr.Attribute("b");
if (b != null) fmt.Bold = (string)b == "1" || (string)b == "true";
var i = rPr.Attribute("i");
if (i != null) fmt.Italic = (string)i == "1" || (string)i == "true";
var u = rPr.Attribute("u");
if (u != null) fmt.Underline = (string)u != "none";
var strike = rPr.Attribute("strike");
if (strike != null) fmt.Strikethrough = (string)strike == "sngStrike" || (string)strike == "dblStrike";
var solidFill = rPr.Element(A + "solidFill");
if (solidFill != null) fmt.Color = ParseColor(solidFill);
var latin = rPr.Element(A + "latin");
if (latin != null) fmt.Font = (string)latin.Attribute("typeface");
if (fmt.Size == null && fmt.Bold == null && fmt.Italic == null &&
fmt.Underline == null && fmt.Color == null && fmt.Font == null)
return null;
return fmt;
}
private SmfColor? ParseColor(XElement? parent)
{
if (parent == null) return null;
var srgbClr = parent.Element(A + "srgbClr");
if (srgbClr != null)
return new SmfColor { Value = (string)srgbClr.Attribute("val") ?? "000000" };
var schemeClr = parent.Element(A + "schemeClr");
if (schemeClr != null)
return new SmfColor { Value = SchemeToRgb((string)schemeClr.Attribute("val") ?? "dk1") };
return null;
}
private static string SchemeToRgb(string scheme) => scheme switch
{
"dk1" => "000000", "lt1" => "FFFFFF", "dk2" => "1F1F1F", "lt2" => "FFFFFF",
"accent1" => "4472C4", "accent2" => "ED7D31", "accent3" => "A5A5A5",
"accent4" => "FFC000", "accent5" => "5B9BD5", "accent6" => "70AD47",
"hlink" => "0563C1", "folHlink" => "954F72", _ => "000000",
};
private SmfFill ParseFill(XElement? spPr)
{
if (spPr == null) return new SmfFill { Type = SmfFillType.None };
var fill = new SmfFill();
if (spPr.Element(A + "noFill") != null) { fill.Type = SmfFillType.None; return fill; }
var solidFill = spPr.Element(A + "solidFill");
if (solidFill != null)
{
fill.Type = SmfFillType.Solid;
fill.SolidColor = ParseColor(solidFill);
return fill;
}
var gradFill = spPr.Element(A + "gradFill");
if (gradFill != null)
{
fill.Type = SmfFillType.Gradient;
fill.Gradient = ParseGradient(gradFill);
return fill;
}
return fill;
}
private SmfGradientFill? ParseGradient(XElement gradFill)
{
var g = new SmfGradientFill();
var gsLst = gradFill.Element(A + "gsLst");
if (gsLst != null)
{
foreach (var gs in gsLst.Elements(A + "gs"))
{
var pos = (double)gs.Attribute("pos") / 1000.0;
var color = ParseColor(gs);
if (color != null)
g.Stops.Add(new SmfGradientStop { Position = pos, Color = color });
}
}
var lin = gradFill.Element(A + "lin");
if (lin != null)
{
g.Type = SmfGradientType.Linear;
g.Angle = (double)lin.Attribute("ang") / 60000.0;
}
return g;
}
private SmfStroke? ParseStroke(XElement? spPr)
{
if (spPr == null) return null;
var ln = spPr.Element(A + "ln");
if (ln == null) return null;
var stroke = new SmfStroke
{
Width = (double?)ln.Attribute("w") ?? 1,
Color = ParseColor(ln),
};
var prstDash = ln.Element(A + "prstDash");
if (prstDash != null)
{
stroke.DashStyle = (string)prstDash.Attribute("val") switch
{
"dash" => SmfStrokeDashStyle.Dash,
"dot" => SmfStrokeDashStyle.Dot,
"dashDot" => SmfStrokeDashStyle.DashDot,
"lgDash" => SmfStrokeDashStyle.LongDash,
_ => SmfStrokeDashStyle.Solid,
};
}
return stroke;
}
private static SmfShapeType ParseShapeType(string prst) => prst switch
{
"rect" or "roundRect" => SmfShapeType.Rectangle,
"ellipse" => SmfShapeType.Ellipse,
"triangle" => SmfShapeType.Triangle,
"rtTriangle" => SmfShapeType.RightTriangle,
"diamond" => SmfShapeType.Diamond,
"parallelogram" => SmfShapeType.Parallelogram,
"trapezoid" => SmfShapeType.Trapezoid,
"pentagon" => SmfShapeType.Pentagon,
"hexagon" => SmfShapeType.Hexagon,
"octagon" => SmfShapeType.Octagon,
"star5" => SmfShapeType.Star,
"arrow" or "arrowRight" => SmfShapeType.ArrowRight,
"leftArrow" => SmfShapeType.ArrowLeft,
"upArrow" => SmfShapeType.ArrowUp,
"downArrow" => SmfShapeType.ArrowDown,
"line" or "straightConnector1" => SmfShapeType.Line,
"cloud" => SmfShapeType.Cloud,
"heart" => SmfShapeType.Heart,
"cross" => SmfShapeType.Cross,
"chevron" => SmfShapeType.Chevron,
"pie" => SmfShapeType.Pie,
"blockArc" => SmfShapeType.BlockArc,
_ => SmfShapeType.Rectangle,
};
private static string guessContentType(string fileName)
{
var ext = Path.GetExtension(fileName).ToLower();
return ext switch
{
".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif", ".bmp" => "image/bmp",
".svg" => "image/svg+xml", ".webp" => "image/webp",
".mp4" => "video/mp4", ".mp3" => "audio/mpeg",
_ => "application/octet-stream",
};
}
private string NextId() => $"elem_{_nextId++}";
private static XDocument GetPresentation(Package pptx)
{
var relsUri = PackUriHelper.CreatePartUri(new Uri("/_rels/.rels", UriKind.Relative));
var relsPart = pptx.GetPart(relsUri);
var rels = XDocument.Load(relsPart.GetStream());
var officeRel = rels.Root?.Elements("Relationship")
.FirstOrDefault(r => (string)r.Attribute("Type") ==
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
if (officeRel == null) throw new InvalidDataException("未找到 Office Document 关系");
var presUri = PackUriHelper.CreatePartUri(new Uri((string)officeRel.Attribute("Target"), UriKind.Relative));
var presPart = pptx.GetPart(presUri);
return XDocument.Load(presPart.GetStream());
}
private static Tuple<double, double> GetSlideSize(XDocument presentation)
{
var sldSz = presentation.Root?.Element(P + "sldSz");
if (sldSz == null) return Tuple.Create(12192000.0, 6858000.0);
return Tuple.Create((double)sldSz.Attribute("cx"), (double)sldSz.Attribute("cy"));
}
private static string? GetProperty(XDocument pres, XName name)
{
return pres.Root?.Descendants().FirstOrDefault(d => d.Name == name)?.Value;
}
private static List<(string RelId, string Target, int SlideNum)> GetSlideRelations(Package pptx, XDocument presentation)
{
var relsUri = PackUriHelper.CreatePartUri(new Uri("/ppt/_rels/presentation.xml.rels", UriKind.Relative));
var relsPart = pptx.GetPart(relsUri);
var rels = XDocument.Load(relsPart.GetStream());
var slideType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
var result = new List<(string, string, int)>();
int num = 0;
var sldIdLst = presentation.Root?.Element(P + "sldIdLst");
var order = new List<string>();
if (sldIdLst != null)
foreach (var sldId in sldIdLst.Elements(P + "sldId"))
order.Add((string)sldId.Attribute(R + "id"));
var relDict = rels.Root?.Elements("Relationship")
.Where(r => (string)r.Attribute("Type") == slideType)
.ToDictionary(r => (string)r.Attribute("Id"), r => (string)r.Attribute("Target"));
if (relDict != null)
{
foreach (var rId in order)
if (relDict.TryGetValue(rId, out var target)) { num++; result.Add((rId, target, num)); }
foreach (var (id, target) in relDict)
if (!order.Contains(id)) { num++; result.Add((id, target, num)); }
}
return result;
}
}
}