From 92d83971cf577d3dcf74a8eee2e98e39a8fec63e Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Tue, 30 Aug 2016 20:40:02 +0200
Subject: [PATCH] OO-1593: word export of hotspot

---
 .../olat/core/util/openxml/DocReference.java  |   8 +-
 .../util/openxml/HTMLToOpenXMLHandler.java    |   9 +
 .../core/util/openxml/OpenXMLDocument.java    | 493 +++++++++++++++++-
 .../util/openxml/OpenXMLDocumentWriter.java   |  14 +-
 .../core/util/openxml/OpenXMLGraphic.java     |  63 +++
 .../olat/core/util/openxml/OpenXMLSize.java   |  63 +++
 .../olat/core/util/openxml/OpenXMLUtils.java  |  16 +-
 .../manager/openxml/QTI21WordExport.java      |  42 +-
 8 files changed, 677 insertions(+), 31 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/openxml/OpenXMLGraphic.java
 create mode 100644 src/main/java/org/olat/core/util/openxml/OpenXMLSize.java

diff --git a/src/main/java/org/olat/core/util/openxml/DocReference.java b/src/main/java/org/olat/core/util/openxml/DocReference.java
index d9688651a7b..96a6af2c1bf 100644
--- a/src/main/java/org/olat/core/util/openxml/DocReference.java
+++ b/src/main/java/org/olat/core/util/openxml/DocReference.java
@@ -21,8 +21,6 @@ package org.olat.core.util.openxml;
 
 import java.io.File;
 
-import org.olat.core.commons.services.image.Size;
-
 /**
  * 
  * Initial date: 04.09.2013<br>
@@ -34,9 +32,9 @@ public class DocReference {
 	private final String id;
 	private final String filename;
 	private final File file;
-	private final Size emuSize;
+	private final OpenXMLSize emuSize;
 	
-	public DocReference(String id, String filename, Size emuSize, File file) {
+	public DocReference(String id, String filename, OpenXMLSize emuSize, File file) {
 		this.id = id;
 		this.file = file;
 		this.emuSize = emuSize;
@@ -55,7 +53,7 @@ public class DocReference {
 		return file;
 	}
 
-	public Size getEmuSize() {
+	public OpenXMLSize getEmuSize() {
 		return emuSize;
 	}
 }
diff --git a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
index 8841283514e..916109b575e 100644
--- a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
+++ b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
@@ -334,6 +334,15 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		}
 	}
 	
+	protected void startGraphic(File backgroundImage, List<OpenXMLGraphic> elements) {
+		Element paragrapheEl = getCurrentParagraph(true);
+		Element graphicEl = factory.createGraphicEl(backgroundImage, elements);
+		Element runEl = factory.createRunEl();
+		runEl.appendChild(graphicEl);
+		paragrapheEl.appendChild(runEl);
+		closeParagraph();
+	}
+	
 	protected void startTable() {
 		closeParagraph();
 		currentTable = new Table();
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
index 4bb7244b524..08020e0342b 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
@@ -77,6 +77,8 @@ public class OpenXMLDocument {
 	
 	private static final OLog log = Tracing.createLoggerFor(OpenXMLDocument.class);
 	
+	private final int DPI = 72;
+	
 	private final Document document;
 	private final Element rootElement;
 	private final Element bodyElement;
@@ -537,6 +539,10 @@ public class OpenXMLDocument {
 		return paragraphEl;
 	}
 	
+	public Element createRunEl() {
+		return createRunEl(null, null);
+	}
+	
 	public Element createRunEl(Collection<? extends Node> textEls) {
 		return createRunEl(textEls, null);
 	}
@@ -1037,21 +1043,10 @@ public class OpenXMLDocument {
  * @return
  */
 	public Element createImageEl(File image) {
-		String id;
-		Size emuSize;
-		String filename;
-		if(fileToImagesMap.containsKey(image)) {
-			DocReference ref = fileToImagesMap.get(image);
-			id = ref.getId();
-			emuSize = ref.getEmuSize();
-			filename = ref.getFilename();
-		} else {
-			id = generateId();
-			Size size = ImageUtils.getImageSize(image);
-			emuSize = OpenXMLUtils.convertPixelToEMUs(size, 72, 15.9/* cm */);
-			filename = getUniqueFilename(image);
-			fileToImagesMap.put(image, new DocReference(id, filename, emuSize, image));
-		}
+		DocReference ref = registerImage(image);
+		String id = ref.getId();
+		OpenXMLSize emuSize = ref.getEmuSize();
+		String filename = ref.getFilename();
 
 		Element drawingEl = document.createElement("w:drawing");
 		Element inlineEl = (Element)drawingEl.appendChild(document.createElement("wp:inline"));
@@ -1062,8 +1057,8 @@ public class OpenXMLDocument {
 		//wp14:anchorId="152D4A51" wp14:editId="0588CC29"
 
 		Element extentEl = (Element)inlineEl.appendChild(document.createElement("wp:extent"));
-		extentEl.setAttribute("cx", Integer.toString(emuSize.getWidth()));
-		extentEl.setAttribute("cy", Integer.toString(emuSize.getHeight()));
+		extentEl.setAttribute("cx", Integer.toString(emuSize.getWidthEmu()));
+		extentEl.setAttribute("cy", Integer.toString(emuSize.getHeightEmu()));
 		Element effectExtentEl = (Element)inlineEl.appendChild(document.createElement("wp:effectExtent"));
 		effectExtentEl.setAttribute("l", "0");
 		effectExtentEl.setAttribute("t", "0");
@@ -1083,6 +1078,8 @@ public class OpenXMLDocument {
 		graphicEl.setAttribute("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main");
 		Element graphicDataEl = (Element)graphicEl.appendChild(document.createElement("a:graphicData"));
 		graphicDataEl.setAttribute("uri", "http://schemas.openxmlformats.org/drawingml/2006/picture");
+		
+		//pic
 		Element picEl = (Element)graphicDataEl.appendChild(document.createElement("pic:pic"));
 		picEl.setAttribute("xmlns:pic", "http://schemas.openxmlformats.org/drawingml/2006/picture");
 		
@@ -1124,8 +1121,8 @@ public class OpenXMLDocument {
 		xfrmOffEl.setAttribute("x", "0");
 		xfrmOffEl.setAttribute("y", "0");
 		Element xfrmExtEl = (Element)xfrmEl.appendChild(document.createElement("a:ext"));
-		xfrmExtEl.setAttribute("cx", Integer.toString(emuSize.getWidth()));
-		xfrmExtEl.setAttribute("cy", Integer.toString(emuSize.getHeight()));
+		xfrmExtEl.setAttribute("cx", Integer.toString(emuSize.getWidthEmu()));
+		xfrmExtEl.setAttribute("cy", Integer.toString(emuSize.getHeightEmu()));
 		//pic -> spPr -> prstGeom
 		Element prstGeomEl = (Element)spPrEl.appendChild(document.createElement("a:prstGeom"));
 		prstGeomEl.setAttribute("prst","rect");
@@ -1134,11 +1131,465 @@ public class OpenXMLDocument {
 		spPrEl.appendChild(document.createElement("a:noFill"));
 		Node lnEl = spPrEl.appendChild(document.createElement("a:ln"));
 		lnEl.appendChild(document.createElement("a:noFill"));
+		
+		return drawingEl;
+	}
+	
+	private DocReference registerImage(File image) {
+		DocReference ref;
+		if(fileToImagesMap.containsKey(image)) {
+			ref = fileToImagesMap.get(image);
+		} else {
+			String id = generateId();
+			Size size = ImageUtils.getImageSize(image);
+			OpenXMLSize emuSize = OpenXMLUtils.convertPixelToEMUs(size, DPI, 15.9/* cm */);
+			String filename = getUniqueFilename(image);
+			ref = new DocReference(id, filename, emuSize, image);
+			fileToImagesMap.put(image, ref);
+		}
+		return ref;
+	}
+	
+	private void appendPicture(Element parentEl, DocReference ref) {
+		String id = ref.getId();
+		String filename = ref.getFilename();
+		OpenXMLSize emuSize = ref.getEmuSize();
+		
+		//pic
+		Element picEl = (Element)parentEl.appendChild(document.createElement("pic:pic"));
+		picEl.setAttribute("xmlns:pic", "http://schemas.openxmlformats.org/drawingml/2006/picture");
+		
+		//picture information
+		Node nvPicPrEl = picEl.appendChild(document.createElement("pic:nvPicPr"));
+		Element cNvPrEl = (Element)nvPicPrEl.appendChild(document.createElement("pic:cNvPr"));
+		cNvPrEl.setAttribute("id", "0");
+		cNvPrEl.setAttribute("name", filename);
+		Node cNvPicPrEl = nvPicPrEl.appendChild(document.createElement("pic:cNvPicPr"));
+		Element picLocksEl = (Element)cNvPicPrEl.appendChild(document.createElement("a:picLocks"));
+		picLocksEl.setAttribute("noChangeAspect", "1");
+		picLocksEl.setAttribute("noChangeArrowheads", "1");
 
+		//picture blip
+		Node blipFillEl = picEl.appendChild(document.createElement("pic:blipFill"));
+		Element blipEl = (Element)blipFillEl.appendChild(document.createElement("a:blip"));
+		blipEl.setAttribute("r:embed", id);
 		
+		//extLst
+		Node extLstEl = blipEl.appendChild(document.createElement("a:extLst"));
+		Element extEl = (Element)extLstEl.appendChild(document.createElement("a:ext"));
+		extEl.setAttribute("uri", "{" + UUID.randomUUID().toString() + "}");
+		Element useLocalDpiEl = (Element)extEl.appendChild(document.createElement("a14:useLocalDpi"));
+		useLocalDpiEl.setAttribute("xmlns:a14", "http://schemas.microsoft.com/office/drawing/2010/main");
+		useLocalDpiEl.setAttribute("val", "0");
+
+		//srcRect
+		blipFillEl.appendChild(document.createElement("a:srcRect"));
+		//fill
+		Node strechEl = blipFillEl.appendChild(document.createElement("a:stretch"));
+		strechEl.appendChild(document.createElement("a:fillRect"));
+
+		//pic -> spPr
+		Element spPrEl = (Element)picEl.appendChild(document.createElement("pic:spPr"));
+		spPrEl.setAttribute("bwMode", "auto");
+		//pic -> spPr -> xfrm
+		Node xfrmEl = spPrEl.appendChild(document.createElement("a:xfrm"));
+		Element xfrmOffEl = (Element)xfrmEl.appendChild(document.createElement("a:off"));
+		xfrmOffEl.setAttribute("x", "0");
+		xfrmOffEl.setAttribute("y", "0");
+		Element xfrmExtEl = (Element)xfrmEl.appendChild(document.createElement("a:ext"));
+		xfrmExtEl.setAttribute("cx", Integer.toString(emuSize.getWidthEmu()));
+		xfrmExtEl.setAttribute("cy", Integer.toString(emuSize.getHeightEmu()));
+		//pic -> spPr -> prstGeom
+		Element prstGeomEl = (Element)spPrEl.appendChild(document.createElement("a:prstGeom"));
+		prstGeomEl.setAttribute("prst","rect");
+		prstGeomEl.appendChild(document.createElement("a:avLst"));
+
+		spPrEl.appendChild(document.createElement("a:noFill"));
+		Node lnEl = spPrEl.appendChild(document.createElement("a:ln"));
+		lnEl.appendChild(document.createElement("a:noFill"));
+	}
+	
+	
+/*
+<w:drawing>
+	<wp:anchor distT="0" distB="0" distL="114300" distR="114300"
+		simplePos="0" relativeHeight="251663360" behindDoc="0" locked="0"
+		layoutInCell="1" allowOverlap="1" wp14:anchorId="0DC40B5E"
+		wp14:editId="2CD7359E">
+		<wp:simplePos x="0" y="0" />
+		<wp:positionH relativeFrom="column">
+			<wp:posOffset>0</wp:posOffset>
+		</wp:positionH>
+		<wp:positionV relativeFrom="paragraph">
+			<wp:posOffset>179070</wp:posOffset>
+		</wp:positionV>
+		<wp:extent cx="5756910" cy="2282190" />
+		<wp:effectExtent l="0" t="0" r="8890" b="3810" />
+		<wp:wrapThrough wrapText="bothSides">
+			<wp:wrapPolygon edited="0">
+				<wp:start x="0" y="0" />
+				<wp:lineTo x="0" y="21396" />
+				<wp:lineTo x="21538" y="21396" />
+				<wp:lineTo x="21538" y="0" />
+				<wp:lineTo x="0" y="0" />
+			</wp:wrapPolygon>
+		</wp:wrapThrough>
+		<wp:docPr id="5" name="Gruppierung 5" />
+		<wp:cNvGraphicFramePr />
+		<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
+			<a:graphicData
+				uri="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup">
+				<wpg:wgp>
+					<wpg:cNvGrpSpPr />
+					<wpg:grpSpPr>
+						<a:xfrm>
+							<a:off x="0" y="0" />
+							<a:ext cx="5756910" cy="2282190" />
+							<a:chOff x="0" y="0" />
+							<a:chExt cx="5756910" cy="2282190" />
+						</a:xfrm>
+					</wpg:grpSpPr>
+				</wpg:wgp>
+			</a:graphicData>
+		</a:graphic>
+	</wp:anchor>
+</w:drawing>
+*/
+	public Element createGraphicEl(File backgroundImage, List<OpenXMLGraphic> elements) {
+		DocReference backgroundImageRef = registerImage(backgroundImage);
+		OpenXMLSize emuSize = backgroundImageRef.getEmuSize();
+
+		Element drawingEl = document.createElement("w:drawing");
+		
+		//anchor
+		Element anchorEl = (Element)drawingEl.appendChild(document.createElement("wp:anchor"));
+		anchorEl.setAttribute("distT", "0");
+		anchorEl.setAttribute("distB", "0");
+		anchorEl.setAttribute("distL", "0");
+		anchorEl.setAttribute("distR", "0");
+		anchorEl.setAttribute("simplePos", "0");
+		anchorEl.setAttribute("relativeHeight", "251663360");//TODO
+		anchorEl.setAttribute("behindDoc", "0");
+		anchorEl.setAttribute("locked", "0");
+		anchorEl.setAttribute("layoutInCell", "1");
+		anchorEl.setAttribute("allowOverlap", "1");
+		anchorEl.setAttribute("locked", "0");
+		
+		//simple pos
+		Element simplePosEl = (Element)anchorEl.appendChild(document.createElement("wp:simplePos"));
+		simplePosEl.setAttribute("x", "0");
+		simplePosEl.setAttribute("y", "0");
+		
+		/*<wp:positionH relativeFrom="column">
+			<wp:posOffset>0</wp:posOffset>
+		</wp:positionH>*/
+		Element positionHEl = (Element)anchorEl.appendChild(document.createElement("wp:positionH"));
+		positionHEl.setAttribute("relativeFrom", "column");
+		Element positionHPosOffsetEl = (Element)positionHEl.appendChild(document.createElement("wp:posOffset"));
+		positionHPosOffsetEl.appendChild(document.createTextNode("0"));
+
+		/*<wp:positionV relativeFrom="paragraph">
+			<wp:posOffset>179070</wp:posOffset>
+		</wp:positionV>*/
+		Element positionVEl = (Element)anchorEl.appendChild(document.createElement("wp:positionV"));
+		positionVEl.setAttribute("relativeFrom", "paragraph");
+		Element positionVposOffsetEl = (Element)positionVEl.appendChild(document.createElement("wp:posOffset"));
+		positionVposOffsetEl.appendChild(document.createTextNode("179070"));
+		
+		String width = Integer.toString(emuSize.getWidthEmu());//"5756910";
+		String height = Integer.toString(emuSize.getHeightEmu());// "2282190";
+		
+		//extent
+		Element extentEl = (Element)anchorEl.appendChild(document.createElement("wp:extent"));
+		extentEl.setAttribute("cx", width);
+		extentEl.setAttribute("cy", height);
+		//effectExtent
+		Element effectExtentEl = (Element)anchorEl.appendChild(document.createElement("wp:effectExtent"));
+		effectExtentEl.setAttribute("l", "0");
+		effectExtentEl.setAttribute("t", "0");
+		effectExtentEl.setAttribute("r", "8890");
+		effectExtentEl.setAttribute("b", "3810");
+		
+		/*<wp:wrapThrough wrapText="bothSides">
+			<wp:wrapPolygon edited="0">
+				<wp:start x="0" y="0" />
+				<wp:lineTo x="0" y="21396" />
+				<wp:lineTo x="21538" y="21396" />
+				<wp:lineTo x="21538" y="0" />
+				<wp:lineTo x="0" y="0" />
+			</wp:wrapPolygon>
+		</wp:wrapThrough>*/
+		Element wrapThroughEl = (Element)anchorEl.appendChild(document.createElement("wp:wrapThrough"));
+		wrapThroughEl.setAttribute("wrapText", "bothSides");
+		Element wrapPolygonEl = (Element)wrapThroughEl.appendChild(document.createElement("wp:wrapPolygon"));
+		wrapPolygonEl.setAttribute("edited", "0");
+		Element wrapPolygonStartEl = (Element)wrapPolygonEl.appendChild(document.createElement("wp:start"));
+		wrapPolygonStartEl.setAttribute("x", "0");
+		wrapPolygonStartEl.setAttribute("y", "0");
+		appendLineTo(wrapPolygonEl, "0", "21396");//TODO
+		appendLineTo(wrapPolygonEl, "21538", "21396");
+		appendLineTo(wrapPolygonEl, "21538", "0");
+		appendLineTo(wrapPolygonEl, "0", "0");
+		
+		//<wp:docPr id="5" name="Gruppierung 5" />
+		Element docPrEl = (Element)anchorEl.appendChild(document.createElement("wp:docPr"));
+		String groupId = generateSimpleId();
+		docPrEl.setAttribute("id", groupId);
+		docPrEl.setAttribute("name", "Gruppierung " + groupId);
+		//<wp:cNvGraphicFramePr />
+		anchorEl.appendChild(document.createElement("wp:cNvGraphicFramePr"));
+		
+		//<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
+		Element graphicEl = (Element)anchorEl.appendChild(document.createElement("a:graphic"));
+		graphicEl.setAttribute("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main");
+		//<a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup">
+		Element graphicDataEl = (Element)graphicEl.appendChild(document.createElement("a:graphicData"));
+		graphicDataEl.setAttribute("uri", "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup");
+		
+		//groups
+		Element wpgEl = (Element)graphicDataEl.appendChild(document.createElement("wpg:wgp"));
+		//<wpg:cNvGrpSpPr />
+		wpgEl.appendChild(document.createElement("wpg:cNvGrpSpPr"));
+		
+		Element wpgGrpSpPrEl = (Element)wpgEl.appendChild(document.createElement("wpg:grpSpPr"));
+		appendAXfrm_ch(wpgGrpSpPrEl, width, height);
+		
+		// list of elements <wpg:grpSp>
+		Element grpSpEl = (Element)wpgEl.appendChild(document.createElement("wpg:grpSp"));
+		
+		Element cNvPrEl = (Element)grpSpEl.appendChild(document.createElement("wpg:cNvPr"));
+		String subGroupId = generateSimpleId();
+		cNvPrEl.setAttribute("id", subGroupId);
+		cNvPrEl.setAttribute("name", "Gruppierung " + subGroupId);
+		grpSpEl.appendChild(document.createElement("wpg:cNvGrpSpPr"));
+		Element grpSpPrEl = (Element)grpSpEl.appendChild(document.createElement("wpg:grpSpPr"));
+		appendAXfrm_ch(grpSpPrEl, width, height);
+
+		appendPicture(grpSpEl, backgroundImageRef);
+		
+		for(OpenXMLGraphic element:elements) {
+			appendGraphicElementEl(grpSpEl, backgroundImageRef.getEmuSize(), element);
+		}
+
 		return drawingEl;
 	}
 	
+	/*
+	<wps:wsp>
+		<wps:cNvPr id="2" name="Rechteck 2" />
+		<wps:cNvSpPr />
+		<wps:spPr>
+			<a:xfrm>
+				<a:off x="2028190" y="581660" />
+				<a:ext cx="822960" cy="822960" />
+			</a:xfrm>
+			<a:prstGeom prst="rect">
+				<a:avLst />
+			</a:prstGeom>
+			<a:noFill />
+			<a:ln w="38100" />
+			<a:effectLst />
+			<a:extLst>
+				<a:ext uri="{FAA26D3D-D897-4be2-8F04-BA451C77F1D7}">
+					<ma14:placeholderFlag
+						xmlns:ma14="http://schemas.microsoft.com/office/mac/drawingml/2011/main" />
+				</a:ext>
+				<a:ext uri="{C572A759-6A51-4108-AA02-DFA0A04FC94B}">
+					<ma14:wrappingTextBoxFlag
+						xmlns:ma14="http://schemas.microsoft.com/office/mac/drawingml/2011/main" />
+				</a:ext>
+			</a:extLst>
+		</wps:spPr>
+		<wps:style>
+			<a:lnRef idx="1">
+				<a:schemeClr val="accent1" />
+			</a:lnRef>
+			<a:fillRef idx="3">
+				<a:schemeClr val="accent1" />
+			</a:fillRef>
+			<a:effectRef idx="2">
+				<a:schemeClr val="accent1" />
+			</a:effectRef>
+			<a:fontRef idx="minor">
+				<a:schemeClr val="lt1" />
+			</a:fontRef>
+		</wps:style>
+		<wps:bodyPr />
+	</wps:wsp>
+	*/
+	private void appendGraphicElementEl(Element parentEl, OpenXMLSize size, OpenXMLGraphic element) {
+		Element wspEl = (Element)parentEl.appendChild(document.createElement("wps:wsp"));
+		
+		String formId = generateSimpleId();
+		//<wps:cNvPr id="2" name="Rechteck 2" />
+		Element cNvPrEl = (Element)wspEl.appendChild(document.createElement("wps:cNvPr"));
+		cNvPrEl.setAttribute("id", formId);
+		cNvPrEl.setAttribute("name", "Form " + formId);
+		//<wps:cNvSpPr />
+		wspEl.appendChild(document.createElement("wps:cNvSpPr"));
+		
+		Element spPrEl = (Element)wspEl.appendChild(document.createElement("wps:spPr"));
+		if(element.type() == OpenXMLGraphic.Type.rectangle) {
+			appendGraphicRectangle(spPrEl, size, element);
+		} else if(element.type() == OpenXMLGraphic.Type.circle) {
+			appendGraphicEllipse(spPrEl, size, element);
+		}
+	
+		appendGraphicSolidFill(spPrEl, element.getStyle());
+		Element lnEl = (Element)spPrEl.appendChild(document.createElement("a:ln"));
+		lnEl.setAttribute("w", "38100");
+		//spPrEl.appendChild(document.createElement("a:noFill"));
+		spPrEl.appendChild(document.createElement("a:effectLst"));
+
+		//styles
+		Element styleEl = (Element)wspEl.appendChild(document.createElement("wps:style"));
+		appendAStyle(styleEl, "a:lnRef", "1", element.getStyle().name());
+		appendAStyle(styleEl, "a:fillRef", "3", element.getStyle().name());
+		appendAStyle(styleEl, "a:effectRef", "2", element.getStyle().name());
+		appendAStyle(styleEl, "a:fontRef", "minor", "lt1");
+
+		wspEl.appendChild(document.createElement("wps:bodyPr"));
+	}
+	
+	
+	/*
+	<a:solidFill>
+		<a:schemeClr val="accent3">
+			<a:lumMod val="60000" />
+			<a:lumOff val="40000" />
+			<a:alpha val="63000" />
+		</a:schemeClr>
+	</a:solidFill>
+	*/
+	private void appendGraphicSolidFill(Element parentEl, OpenXMLGraphic.Style style) {
+		Element solidFillEl = (Element)parentEl.appendChild(document.createElement("a:solidFill"));
+		Element schemeClrEl = (Element)solidFillEl.appendChild(document.createElement("a:schemeClr"));
+		schemeClrEl.setAttribute("val", style.name());
+		
+		/*Element lumModEl = (Element)solidFillEl.appendChild(document.createElement("a:lumMod"));
+		lumModEl.setAttribute("val", "60000");
+		Element lumOffEl = (Element)solidFillEl.appendChild(document.createElement("a:lumOff"));
+		lumOffEl.setAttribute("val", "60000");*/
+		Element alphaEl = (Element)solidFillEl.appendChild(document.createElement("a:alpha"));
+		alphaEl.setAttribute("val", "50000");
+	}
+	
+	private void appendGraphicRectangle(Element spPrEl, OpenXMLSize size, OpenXMLGraphic element) {
+		/*
+		<a:xfrm>
+			<a:off x="2028190" y="581660" />
+			<a:ext cx="822960" cy="822960" />
+		</a:xfrm>
+		*/
+		Element aXfrmEl = (Element)spPrEl.appendChild(document.createElement("a:xfrm"));
+		Element aOffEl = (Element)aXfrmEl.appendChild(document.createElement("a:off"));
+		List<Integer> coords = element.getCoords();
+		int leftx = coords.get(0);
+		int topy = coords.get(1);
+		int leftxEmu = OpenXMLUtils.convertPixelToEMUs(leftx, DPI, size.getResizeRatio());
+		int topyEmu = OpenXMLUtils.convertPixelToEMUs(topy, DPI, size.getResizeRatio());	
+		aOffEl.setAttribute("x", Integer.toString(leftxEmu));
+		aOffEl.setAttribute("y", Integer.toString(topyEmu));
+		
+		Element aExtEl = (Element)aXfrmEl.appendChild(document.createElement("a:ext"));
+		int rightx = coords.get(2);
+		int bottomy = coords.get(3);
+		int width = rightx -leftx;
+		int cx = OpenXMLUtils.convertPixelToEMUs(width, DPI, size.getResizeRatio());	
+		int height = bottomy - topy;
+		int cy = OpenXMLUtils.convertPixelToEMUs(height, DPI, size.getResizeRatio());	
+		aExtEl.setAttribute("cx", Integer.toString(cx));
+		aExtEl.setAttribute("cy", Integer.toString(cy));
+		/*
+		<a:prstGeom prst="rect">
+			<a:avLst />
+		</a:prstGeom>
+		*/
+		Element prstGeomEl = (Element)spPrEl.appendChild(document.createElement("a:prstGeom"));
+		prstGeomEl.setAttribute("prst", "rect");
+		prstGeomEl.appendChild(document.createElement("a:avLst"));
+	}
+	
+	private void appendGraphicEllipse(Element spPrEl, OpenXMLSize size, OpenXMLGraphic element) {
+		/*
+		<a:xfrm>
+			<a:off x="2028190" y="581660" />
+			<a:ext cx="822960" cy="822960" />
+		</a:xfrm>
+		*/
+		Element aXfrmEl = (Element)spPrEl.appendChild(document.createElement("a:xfrm"));
+		Element aOffEl = (Element)aXfrmEl.appendChild(document.createElement("a:off"));
+		List<Integer> coords = element.getCoords();
+		int centerx = coords.get(0);
+		int centery = coords.get(1);	
+		int radius = coords.get(2);
+		
+		int topx = centerx - radius;
+		int lefty = centery - radius;
+		int topxEmu = OpenXMLUtils.convertPixelToEMUs(topx, DPI, size.getResizeRatio());
+		int leftyEmu = OpenXMLUtils.convertPixelToEMUs(lefty, DPI, size.getResizeRatio());
+		
+		aOffEl.setAttribute("x", Integer.toString(topxEmu));
+		aOffEl.setAttribute("y", Integer.toString(leftyEmu));
+	
+		Element aExtEl = (Element)aXfrmEl.appendChild(document.createElement("a:ext"));
+		
+		int width = (radius * 2);
+		int widthEmu = OpenXMLUtils.convertPixelToEMUs(width, DPI, size.getResizeRatio());
+		aExtEl.setAttribute("cx", Integer.toString(widthEmu));
+		aExtEl.setAttribute("cy", Integer.toString(widthEmu));
+		/*
+		<a:prstGeom prst="rect">
+			<a:avLst />
+		</a:prstGeom>
+		*/
+		Element prstGeomEl = (Element)spPrEl.appendChild(document.createElement("a:prstGeom"));
+		prstGeomEl.setAttribute("prst", "ellipse");
+		prstGeomEl.appendChild(document.createElement("a:avLst"));
+	}
+	
+	private void appendAStyle(Element styleEl, String name, String idx, String schemeClrVal) {
+		Element aStyleEl = (Element)styleEl.appendChild(document.createElement(name));
+		aStyleEl.setAttribute("idx", idx);
+		Element schemeClrEl = (Element)aStyleEl.appendChild(document.createElement("a:schemeClr"));
+		schemeClrEl.setAttribute("val", schemeClrVal);
+	}
+	
+	// <wp:lineTo x="0" y="21396" />
+	private void appendLineTo(Element parentEl, String x, String y) {
+		Element lineToEl = (Element)parentEl.appendChild(document.createElement("wp:lineTo"));
+		lineToEl.setAttribute("x", x);
+		lineToEl.setAttribute("y", y);
+	}
+	
+	/*
+	<a:xfrm>
+		<a:off x="0" y="0" />
+		<a:ext cx="5756910" cy="2282190" />
+		<a:chOff x="0" y="0" />
+		<a:chExt cx="5756910" cy="2282190" />
+	</a:xfrm>
+	*/
+	private void appendAXfrm_ch(Element parentEl, String cx, String cy) {
+		Element aXfrmEl = (Element)parentEl.appendChild(document.createElement("a:xfrm"));
+		//<a:off x="0" y="0" />
+		Element aOffEl = (Element)aXfrmEl.appendChild(document.createElement("a:off"));
+		aOffEl.setAttribute("x", "0");
+		aOffEl.setAttribute("y", "0");
+		//<a:ext cx="5756910" cy="2282190" />
+		Element aExtEl = (Element)aXfrmEl.appendChild(document.createElement("a:ext"));
+		aExtEl.setAttribute("cx", cx);
+		aExtEl.setAttribute("cy", cy);
+		//<a:chOff x="0" y="0" />
+		Element chOffEl = (Element)aXfrmEl.appendChild(document.createElement("a:chOff"));
+		chOffEl.setAttribute("x", "0");
+		chOffEl.setAttribute("y", "0");
+		//<a:chExt cx="5756910" cy="2282190" />
+		Element chExtEl = (Element)aXfrmEl.appendChild(document.createElement("a:chExt"));
+		chExtEl.setAttribute("cx", cx);
+		chExtEl.setAttribute("cy", cy);
+	}
+	
 	private String getUniqueFilename(File image) {
 		String filename = image.getName().toLowerCase();
 		int extensionIndex = filename.lastIndexOf('.');
@@ -1167,6 +1618,10 @@ public class OpenXMLDocument {
 	private String generateId() {
 		return "rId" + (++currentId);
 	}
+	
+	private String generateSimpleId() {
+		return Integer.toString(++currentId);
+	}
 
 	private final Element createRootElement(Document doc) {
 		Element docEl = (Element)doc.appendChild(doc.createElement("w:document"));
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
index 49e1a5efc66..324b102ade4 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
@@ -102,6 +102,16 @@ public class OpenXMLDocumentWriter {
 		//word/media
 		appendMedias(out, document);
 		
+		/* word/theme/theme1.xml
+		out.putNextEntry(new ZipEntry("word/theme/theme1.xml"));
+		try(InputStream in = OpenXMLDocumentWriter.class.getResourceAsStream("_resources/theme1.xml")) {
+			IOUtils.copy(in, out);
+		} catch (IOException e) {
+			log.error("", e);
+		}
+		out.closeEntry();
+		*/
+		
 		//word/numbering
 		ZipEntry numberingDocument = new ZipEntry("word/numbering.xml");
 		out.putNextEntry(numberingDocument);
@@ -321,8 +331,8 @@ public class OpenXMLDocumentWriter {
 			Document doc = OpenXMLUtils.createDocument();
 			Element propertiesEl = (Element)doc.appendChild(doc.createElement("properties:Properties"));
 			propertiesEl.setAttribute("xmlns:properties", SCHEMA_EXT_PROPERTIES);
-			addExtProperty("Application", "OpenOLAT", propertiesEl, doc);
-			addExtProperty("AppVersion", "9.1.0", propertiesEl, doc);
+			addExtProperty("Application", "Microsoft Macintosh Word", propertiesEl, doc);
+			addExtProperty("AppVersion", "14.0000", propertiesEl, doc);
 			OpenXMLUtils.writeTo(doc, out, false);
 		} catch (DOMException e) {
 			log.error("", e);
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLGraphic.java b/src/main/java/org/olat/core/util/openxml/OpenXMLGraphic.java
new file mode 100644
index 00000000000..14d1d6f68ce
--- /dev/null
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLGraphic.java
@@ -0,0 +1,63 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.openxml;
+
+import java.util.List;
+
+/**
+ * 
+ * Initial date: 30.08.2016<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpenXMLGraphic {
+	
+	private final Style style;
+	private final Type type;
+	private final List<Integer> coords;
+	
+	public OpenXMLGraphic(Type type, Style style, List<Integer> coords) {
+		this.type = type;
+		this.style = style;
+		this.coords = coords;
+	}
+	
+	public Type type() {
+		return type;
+	}
+	
+	public Style getStyle() {
+		return style;
+	}
+	
+	public List<Integer> getCoords() {
+		return coords;
+	}
+	
+	public enum Type {
+		circle,
+		rectangle
+	}
+	
+	public enum Style {
+		accent1,
+		accent3;
+	}
+}
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLSize.java b/src/main/java/org/olat/core/util/openxml/OpenXMLSize.java
new file mode 100644
index 00000000000..e0ff3061232
--- /dev/null
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLSize.java
@@ -0,0 +1,63 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util.openxml;
+
+/**
+ * 
+ * Initial date: 30.08.2016<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpenXMLSize {
+	
+	private final int widthPx;
+	private final int heightPx;
+	private final int widthEmu;
+	private final int heightEmu;
+	private final double resizeRatio;
+	
+	public OpenXMLSize(int widthPx, int heightPx, int widthEmu, int heightEmu, double resizeRatio) {
+		this.widthPx = widthPx;
+		this.heightPx = heightPx;
+		this.widthEmu = widthEmu;
+		this.heightEmu = heightEmu;
+		this.resizeRatio = resizeRatio;
+	}
+
+	public int getWidthPx() {
+		return widthPx;
+	}
+
+	public int getHeightPx() {
+		return heightPx;
+	}
+
+	public int getWidthEmu() {
+		return widthEmu;
+	}
+
+	public int getHeightEmu() {
+		return heightEmu;
+	}
+
+	public double getResizeRatio() {
+		return resizeRatio;
+	}
+}
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
index 75b96833c4f..87e2edbbf0f 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
@@ -66,7 +66,13 @@ public class OpenXMLUtils {
 	public static final double emusPerInch = 914400.0d;
 	public static final double emusPerCm = 360000.0d;
 	
-	public static final Size convertPixelToEMUs2(Size img, int dpi) {
+
+	public static final int convertPixelToEMUs(int pixel, int dpi, double resizeRatio) {
+		double rezDpi = dpi * 1.0d;
+		return (int)(((pixel / rezDpi) * emusPerInch) / resizeRatio);
+	}
+	
+	public static final OpenXMLSize convertPixelToEMUs2(Size img, int dpi) {
 		int widthPx = img.getWidth();
 		int heightPx = img.getHeight();
 		double horzRezDpi = dpi * 1.0d;
@@ -75,10 +81,10 @@ public class OpenXMLUtils {
 		double widthEmus = (widthPx / horzRezDpi) * emusPerInch;
 		double heightEmus = (heightPx / vertRezDpi) * emusPerInch;
 		
-		return new Size((int)widthEmus, (int)heightEmus, 0, 0, true);
+		return new OpenXMLSize(widthPx, heightPx, (int)widthEmus, (int)heightEmus, 1.0d);
 	}
 	
-	public static final Size convertPixelToEMUs(Size img, int dpi, double maxWidthCm) {
+	public static final OpenXMLSize convertPixelToEMUs(Size img, int dpi, double maxWidthCm) {
 		int widthPx = img.getWidth();
 		int heightPx = img.getHeight();
 		double horzRezDpi = dpi * 1.0d;
@@ -88,13 +94,15 @@ public class OpenXMLUtils {
 		double heightEmus = (heightPx / vertRezDpi) * emusPerInch;
 
 		double maxWidthEmus = maxWidthCm * emusPerCm;
+		double resizeRatio = 1.0d;
 		if (widthEmus > maxWidthEmus) {
+			resizeRatio = maxWidthEmus / widthEmus;
 			double ratio = heightEmus / widthEmus;
 			widthEmus = maxWidthEmus;
 			heightEmus = widthEmus * ratio;
 		}
 
-		return new Size((int)widthEmus, (int)heightEmus, 0, 0, true);
+		return new OpenXMLSize(widthPx, heightPx, (int)widthEmus, (int)heightEmus, resizeRatio);
 	}
 	
 	public static int getSpanAttribute(String name, Attributes attrs) {
diff --git a/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java
index 98836350853..cf7b8ce13b7 100644
--- a/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java
+++ b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java
@@ -19,11 +19,14 @@
  */
 package org.olat.ims.qti21.manager.openxml;
 
+import static org.olat.ims.qti21.model.xml.QtiNodesExtractor.extractIdentifiersFromCorrectResponse;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.CountDownLatch;
@@ -47,6 +50,7 @@ import org.olat.core.util.openxml.OpenXMLDocument;
 import org.olat.core.util.openxml.OpenXMLDocument.Style;
 import org.olat.core.util.openxml.OpenXMLDocument.Unit;
 import org.olat.core.util.openxml.OpenXMLDocumentWriter;
+import org.olat.core.util.openxml.OpenXMLGraphic;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.course.assessment.AssessmentHelper;
 import org.olat.ims.qti.editor.beecom.objects.Item;
@@ -70,7 +74,9 @@ import org.xml.sax.Attributes;
 import uk.ac.ed.ph.jqtiplus.node.content.basic.Block;
 import uk.ac.ed.ph.jqtiplus.node.content.variable.RubricBlock;
 import uk.ac.ed.ph.jqtiplus.node.content.xhtml.object.Object;
+import uk.ac.ed.ph.jqtiplus.node.expression.operator.Shape;
 import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
+import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicAssociateInteraction;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicOrderInteraction;
@@ -81,6 +87,7 @@ import uk.ac.ed.ph.jqtiplus.node.item.interaction.PositionObjectInteraction;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.SelectPointInteraction;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleAssociableChoice;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleMatchSet;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.graphic.HotspotChoice;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
@@ -509,7 +516,40 @@ public class QTI21WordExport implements MediaResource {
 			Interaction interaction = getInteractionByResponseIdentifier(attributes);
 			if(interaction instanceof HotspotInteraction) {
 				HotspotInteraction hotspotInteraction = (HotspotInteraction)interaction;
-				setObject(hotspotInteraction.getObject());
+				
+				Object object = hotspotInteraction.getObject();
+				if(object != null && StringHelper.containsNonWhitespace(object.getData())) {
+					File backgroundImg = new File(itemFile.getParentFile(), object.getData());
+					
+					List<Identifier> correctAnswers = new ArrayList<>();
+					ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier());
+					if(responseDeclaration != null) {
+						CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
+						if(correctResponse != null) {
+							extractIdentifiersFromCorrectResponse(correctResponse, correctAnswers);
+						}
+					}
+					
+					List<OpenXMLGraphic> elements = new ArrayList<>();
+					List<HotspotChoice> choices = hotspotInteraction.getHotspotChoices();
+					for(HotspotChoice choice:choices) {
+						OpenXMLGraphic.Style style = OpenXMLGraphic.Style.accent1;
+						if(withResponses) {
+							boolean correct = correctAnswers.contains(choice.getIdentifier());
+							if(correct) {
+								style = OpenXMLGraphic.Style.accent3;
+							}
+						}
+						
+						Shape shape = choice.getShape();
+						if(shape == Shape.CIRCLE || shape == Shape.ELLIPSE) {
+							elements.add(new OpenXMLGraphic(OpenXMLGraphic.Type.circle, style, choice.getCoords()));
+						} else if(shape == Shape.RECT) {
+							elements.add(new OpenXMLGraphic(OpenXMLGraphic.Type.rectangle, style, choice.getCoords()));
+						}
+					}
+					startGraphic(backgroundImg, elements);
+				}
 			}
 		}
 		
-- 
GitLab