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 static readonly XNamespace Rel = "http://schemas.openxmlformats.org/package/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); var presPartUri = PackUriHelper.CreatePartUri(new Uri("/ppt/presentation.xml", UriKind.Relative)); foreach (var (relId, target, slideNum) in slideRels) { var resolvedUri = PackUriHelper.ResolvePartUri(presPartUri, new Uri(target, UriKind.Relative)); var slidePart = pptx.GetPart(resolvedUri); 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(Rel + "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(Rel + "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(Rel + "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 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(); if (sldIdLst != null) foreach (var sldId in sldIdLst.Elements(P + "sldId")) order.Add((string)sldId.Attribute(R + "id")); var relDict = rels.Root?.Elements(Rel + "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; } } }