ETC

[C#] Power Point - OpenXml 사용해서 Elements clone, copy 하는 방법

Pearl_mini 2024. 9. 13. 10:30
728x90

 

 

 

 

 

PowerPoint 정말 지겹다아.....

기존에 실행되던 닷넷 프로그램이 있었는데 내용은 기존 ppt 파일에 있는 object들을 신규 ppt 파일에 모두 copy해오는 로직이 들어있었다.

근데 뭔가 이상하게 가끔씩 object가 몇 개씩 누락되는 오류가 발생..!!

사용자들이 일일히 빠진거를 발견하면 기존 파일을 다시 다운받고 열어서 복사해와야하는 작업이 추가되어버리니,, 

수정을 해야했다!

 

처음 현상을 파악해보기 위해서 실행파일을 실행해보니 문제가 100% 재현되지가 않았다.

그러다가 발생한 에러 메시지를 발견!!

 

Shapes (unknown member) : Invalid request. Clipboard is empty or contains data which may not be pasted here.

 

위와 같은 메시지가 떴다.

Clipboard에 뭔가 붙여넣을 수 없는 데이터가 있다?

이런 메시지 같아서

프로그램실행직전에 뭔가 텍스트를 복사하고 실행하니까 에러메시지가 떴다!!

 

재현이 되길래 소스를 수정했다. (미리 결과는 실패이니 따라하지 마셔요...)

 

처음 실행하기 직전에 클립보드를 Clear하라는 내용을 추가.

 

[STAThread]
static void Main(string[] args)
{
    File.AppendAllText(@"c:\temp\SetShapesToShapeGroupXAIS.log", "start\r\n");

    // 클립보드 초기화
    Clipboard.Clear();

}

 

위와같이 Clipboard.Clear(); 를 추가하고 STAThread를 명시해주었다.

그래야 프로그램이 STA 모드에서 실행되며, OLE 호출을 포함한 클립보드 작업이 정상적으로 작동하게 된다고 한다.

 

이렇게 해서 수정을 했는데, 결국 결과는 실패!!

원인 모를 이유로 한번씩 계속 에러가 나면서 copy할 object들이 누락되는 문제가 생겼고,

그래서 Copy, Paste를 쓰지않고 다른 방법을 찾게되었다.

 

그러다가 발견한 내용이 NuGet 패키지에 있는 OpenXml이다. (요것은 성공했으니 따라해보시길!)

요즘 Microsoft의 Excel이라던지 PPT는 다 Xml로 구성되어있어서 이 Xml을 가지고 내용을 바꿀 수 있다고 한다.

 

우선 기존ppt와 신규ppt의 경로를 가지고와서 파일을 open하고 필요한 항목들을 정의한다.

 

PresentationDocument srcPresent = PresentationDocument.Open("C:\temp\sourcefilename.pptx", false);
PresentationDocument dstPresent = PresentationDocument.Open("C:\temp\destfilename.pptx", false);


PresentationPart srcPresentPart = srcPresent.PresentationPart;
PresentationPart dstPresentPart = dstPresent.PresentationPart;

List<SlideId> srcSlideIds = srcPresentPart.Presentation.SlideIdList.Elements<SlideId>().ToList();
List<SlideId> dstSlideIds = dstPresentPart.Presentation.SlideIdList.Elements<SlideId>().ToList();

 

그리고나서 slide의 장수를 파악해서 슬라이드 장수만큼 저장을 한다.

 

실제 Object를 Copy하는 부분은 Clone함수이다.

 

int slideCount = Math.Min(this.srcSlideIds.Count, this.dstSlideIds.Count);
for (int i = 0; i < slideCount; i++)
{
    SlidePart srcSlidePart = (SlidePart)srcPresentPart.GetPartById(srcSlideIds[i].RelationshipId);
    SlidePart dstSlidePart = (SlidePart)dstPresentPart.GetPartById(dstSlideIds[i].RelationshipId);

	OpenXmlCompositeElement srcElement = srcSlidePart.Slide.CommonSlideData.ShapeTree;
	OpenXmlCompositeElement dstElement = dstSlidePart.Slide.CommonSlideData.ShapeTree;
    
    Clone(srcElement,dstElement);
    
    dstSlidePart.Slide.Save();
}

public void Clone(OpenXmlCompositeElement srcElement, OpenXmlCompositeElement dstElement) {
	foreach (var element in srcElement.ChildElements)
    {
        string objectType = element.GetType().Name;
        
        switch (objectType)
        {
            case "NonVisualGroupShapeProperties":
            case "GroupShapeProperties":
                string parentType = element.Parent.GetType().Name;
                if (parentType == "GroupShape")
                {
                    dstElement.Append(element.CloneNode(true));
                }
                break;
            case "GroupShape":
                GroupShape dstGroupShape = (GroupShape)element.CloneNode(false);
                Clone((GroupShape)element, dstGroupShape);
                dstElement.Append(dstGroupShape);
                break;
            case "Picture":
                Picture newPicture = (Picture)element.CloneNode(true);
                RemoveBlipExtensionList(newPicture);
                RegistPictureStorage(newPicture);

                dstElement.Append(newPicture);
                break;
            case "Shape":
                Shape newShape = (Shape)element.CloneNode(true);
                RemoveCustomerDataList(newShape); /// 일반적인 복사의 경우 오류발생 -> CustomerDataList 속성을 제외하고 복사하도록 변경
                dstElement.Append(newShape);
                break;
            case "ConnectionShape":
            case "GraphicFrame":
                dstElement.Append(element.CloneNode(true));
                break;
            case "Line": // 이거 아직 처리 케이스가 없음
            default:
                Console.WriteLine($"Skip Type: {element.GetType().Name}");
                break;
        }
    }
}

 

대부분은 OpenXml.CloneNode(true)의 형태로 추가하면 되는데, 

Object의 타입별로 다른 처리가 필요한 경우가 생긴다.

연결선은 ConnectionShape, Picture, Group Shape 등등 Shape의 종류별로 저장하는 처리 방식이 달라서 애를 많이 먹었다..

 

그리고 파일을 Clone하면 깨져버리는 오류가 타입별로 종종 생겨서 그부분을 처리하는 함수들을 만들어서 처리했다.

 

private void RegistPictureStorage(Picture picture)
{
    ImagePart srcImgPart = (ImagePart)srcSlidePart.GetPartById(picture.BlipFill.Blip.Embed.Value);
    ImagePart dstImgPart = dstSlidePart.AddImagePart(srcImgPart.ContentType);
    using (Stream srcStream = srcImgPart.GetStream())
    using (Stream dstStream = dstImgPart.GetStream())
    {
        srcStream.CopyTo(dstStream);
    }

    picture.BlipFill.Blip.Embed.Value = dstSlidePart.GetIdOfPart(dstImgPart);
}

private void RemoveCustomerDataList(Shape shape)
{
    CustomerDataList custDataList = shape.NonVisualShapeProperties?.Descendants<CustomerDataList>().FirstOrDefault();
    custDataList?.Remove();
}

private void RemoveBlipExtensionList(Picture picture)
{
    BlipExtensionList extList = picture.BlipFill?.Blip?.Descendants<BlipExtensionList>().FirstOrDefault();
    extList?.Remove();
}

 

 

이렇게 처리하면 거의 대부분의 PPT 객체들을 복사해올 수 있다!!

 

타입별로 에러 때려맞으면서 정리된 소스... 여기까지 찾아 들어오신 분들께도 도움되셨길 바랍니다.

 

 

 

728x90

'ETC' 카테고리의 다른 글

프린터 세팅 옮겨오는 방법!  (0) 2024.07.20
Server computer에 IIS server 설치하기  (0) 2024.01.30