#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#undef _CRT_SECURE_NO_WARNINGS
#include <unordered_map>
#include <unordered_set>
#include <filesystem>
#include <cstdlib>
#include "ppt.h"
#include "ppt_def.h"
#include "cgen_shapes.h"
#include "commandline.h"
#include "tinyxml2.h"
#include "error.h"
#include "utf8utils.h"
#ifndef WIN32
#include <unistd.h>
#endif

/** A note on points: When exporting to PPT, one chart pixel is emitted as one PPT point (1/72 of an inch).
 * PPT pixels (1/96 of an inch) are never used.*/

/***Chart source embedding*******************************************************/

//We do escaping
//We could use XML escaping for <>&"' but since we need to correctly encode whitespace anyway,
//we use a single system of escapes with '~'
//~:~~, \n:~n, \t:~t, N consecutive space (between 0-9):~N, <:~l, >:~g, ":~q, ':~a, &:~@
//Note that ~0 simply means nothing (zero spaces), it can be used to break up a character
//sequence in the input that for some reason is not accepted by PowerPoint
//(Version 2208 Build 16.0.15601.20540) 64-bit) (but is accepted by Word).
//Currently known such character input strings are: "cid".
//Supply the field name and the value. We always end in "~|"
static void escape_embedded_alt_text(std::string& ret, std::string_view t, std::string_view v) {
    _ASSERT(std::ranges::all_of(t, [](char c) {return isalnum(c)|| c=='_'; }));
    using namespace std::string_view_literals;
    ret.reserve(ret.size()+v.size()*5/4+t.size()+1);
    ret.append(t).push_back('=');
    unsigned spaces = 0;
    for (; v.size(); v.remove_prefix(1))
        if (const char c = v.front(); c==' ')
            switch (++spaces) {
            case 1: ret += ' '; continue;
            case 2: ret.back() = '~'; ret.push_back('2'); continue;
            case 9: spaces = 0; FALLTHROUGH; //end this run
            default: ++ret.back(); continue; //increase digit after '~'
            }
        else {
            spaces = 0;
            if (c == '~')
                ret += "~~"sv;
            else if (c == '\n')
                ret += "~n"sv;
            else if (c == '\t')
                ret += "~t"sv;
            else if (c == '<')
                ret += "~l";
            else if (c == '>')
                ret += "~g";
            else if (c == '"')
                ret += "~q";
            else if (c == '&')
                ret += "~@";
            else if (c == '\'')
                ret += "~a";
            else if (v.starts_with("cid"))
                ret += "c~0"; //break up "cid" - MS PPT does not accept it
            else
                ret += c;
        }
    ret += "~|";
}

//~| terminates processing (we swallow it)
static std::pair<std::string_view, std::string> descape_embedded_alt_text_field(std::string_view& v) {
    using namespace std::string_view_literals;
    std::pair<std::string_view, std::string> ret;
    if (const size_t eq = v.find('='); eq==std::string::npos) return ret;
    else if (const size_t ti = v.find('~'); ti<eq) return ret; //tilde in field name
    else {
        ret.first = v.substr(0, eq);
        v.remove_prefix(eq+1);
    }
    while (v.size())
        if (v.front()!='~' || v.size()==1) {
            ret.second += v.front();
            v.remove_prefix(1);
        } else {
            switch (v[1]) {
            case '|': v.remove_prefix(2); return ret;
            case '~': ret.second += '~'; break;
            case 'n': ret.second += '\n'; break;
            case 't': ret.second += '\t'; break;
            case 'l': ret.second += '<'; break;
            case 'g': ret.second += '>'; break;
            case 'q': ret.second += '"'; break;
            case 'a': ret.second += '\''; break;
            case '@': ret.second += '&'; break;
            case '0': break;
            case '1': ret.second += ' '; break;
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9': ret.second.append(v[1]-'0', ' '); break;
            default: ret.second += '~'; ret.second += v[1]; _ASSERT(0); //use the verbatim on unrecognized escape
            }
            v.remove_prefix(2);
        }
    return ret;
}

EmbedChartData EmbedChartData::ParseEscapedText(std::string_view v) {
    EmbedChartData ret;
    if (!v.starts_with(ALT_TEXT_PREAMBLE)) return ret;
    v.remove_prefix(ALT_TEXT_PREAMBLE.size());
    std::unordered_map<std::string_view, std::string> fields;
    while (v.size()) {
        std::pair field = descape_embedded_alt_text_field(v);
        if (field.first.empty()) goto fail;
        fields.insert(std::move(field)); //repeated fields are dropped
    }
    if (auto i = fields.find("lang"); i==fields.end()) goto fail;
    else ret.chart_type = std::move(i->second);
    if (auto i = fields.find("text"); i==fields.end()) goto fail;
    else ret.chart_text = std::move(i->second);
    std::ranges::fill(ret.version, 0);
    if (auto i = fields.find("version"); i!=fields.end())
        sscanf(i->second.c_str(), "%d.%d.%d", &ret.version[0], &ret.version[1], &ret.version[2]);
    ret.chart_size = { 0,0 };
    if (auto i = fields.find("size"); i!=fields.end())
        sscanf(i->second.c_str(), "%lfx%lf", &ret.chart_size.x, &ret.chart_size.y);
    if (auto i = fields.find("gui_state"); i!=fields.end()) {
        if (i->second.length()%2) goto fail; //odd length string
        ret.gui_state.reserve(i->second.length()/2);
        for (size_t u = 0; u<i->second.length(); u += 2)
            if ('a'<=i->second[u] && i->second[u]<'a'+16 && 'a'<=i->second[u+1] && i->second[u+1]<'a'+16)
                ret.gui_state.push_back((char)(unsigned char)((i->second[u]-'a')*16 + i->second[u+1]-'a'));
            else
                goto fail;
    }
    if (auto i = fields.find("page"); i!=fields.end())
        if (from_chars(i->second, ret.page))
            goto fail;
    return ret;
fail:
    ret.clear();
    return ret;
}

std::string EmbedChartData::Escape() const {
    std::string ret{ ALT_TEXT_PREAMBLE };
    char buff[100] = "";
    snprintf(buff, sizeof buff, "%d.%d.%d", std::max(0, version[0]), std::max(0, version[1]), std::max(0, version[2]));
    escape_embedded_alt_text(ret, "version", buff);
    escape_embedded_alt_text(ret, "lang", chart_type);
    snprintf(buff, sizeof buff, "%dx%d", int(chart_size.x), int(chart_size.y));
    escape_embedded_alt_text(ret, "size", buff);
    escape_embedded_alt_text(ret, "text", chart_text);
    if (gui_state.length()) {
        ret += "gui_state=";
        ret.reserve(ret.size()+gui_state.size()*2+2);
        for (char c : gui_state) {
            ret.push_back('a'+((unsigned char)c)/16);
            ret.push_back('a'+((unsigned char)c)%16);
        }
        ret += "~|";
    }
    if (page)
        ret.append("page=").append(std::to_string(page)).append("~|");
    return ret;
}

std::string my_tmpnam() {
#if WIN32
    WCHAR szTempFileName[MAX_PATH];
    WCHAR lpTempPathBuffer[MAX_PATH];
    // Gets the temp path env string (no guarantee it's a valid path).
    if (const DWORD dwRetVal = GetTempPathW(MAX_PATH, lpTempPathBuffer);
             dwRetVal > MAX_PATH || (dwRetVal == 0))
        throw std::runtime_error("Cannot obtain temp dir name.");
    // Generates a temporary file name.
    if (!GetTempFileNameW(lpTempPathBuffer, // directory for tmp files
                          L"mscgen",        // temp file name prefix
                          0,                // create unique name - checks if unique and creates zero-length file
                          szTempFileName))  // buffer for name
        throw std::runtime_error("Cannot obtain temp file name.");
    return ConvertFromUTF16_to_UTF8(szTempFileName);
#else
    namespace fs = std::filesystem;
    std::string ret = fs::temp_directory_path() / "msc-gen.XXXXXX";
    if (auto fd = mkstemp(ret.data()); fd == -1)
        throw std::runtime_error("Cannot create temp file: " + my_strerror(fd));
    else {
        close(fd);
        return ret;
    }
#endif
}

/************************************************************************************/

static constexpr const char xml_content_types_preamble[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">)"
    R"(<Default Extension="jpeg" ContentType="image/jpeg"/>)"
    R"(<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>)"
    R"(<Default Extension="xml" ContentType="application/xml"/>)"
    R"(<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>)"
    R"(<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>)";
    //R"(<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>)"

static constexpr const char xml_content_types_postscript[] =
    R"(<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>)"
    R"(<Override PartName="/ppt/viewProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"/>)"
    R"(<Override PartName="/ppt/tableStyles.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout2.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout3.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout4.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout5.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout6.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout7.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout8.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout9.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout10.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/ppt/slideLayouts/slideLayout11.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>)"
    R"(<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>)"
    R"(<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>)"
    R"(<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>)"
R"(</Types>)";

static constexpr const char xml_app_template[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">)"
	//R"(<TotalTime>0</TotalTime>)"
	//R"(<Words>0</Words>)"
	R"(<Application>Msc-generator</Application>)"
	R"(<PresentationFormat>%s</PresentationFormat>)" //Widescreen
	//R"(<Paragraphs>0</Paragraphs>)"
	R"(<Slides>%d</Slides>)" //slide no.
	//R"(<Notes>0</Notes>)"
	//R"(<HiddenSlides>0</HiddenSlides>)"
	//R"(<MMClips>0</MMClips>)"
	//R"(<ScaleCrop>false</ScaleCrop>)"
	//R"(<HeadingPairs>)"
	//	R"(<vt:vector size="6" baseType="variant">)"
	//		R"(<vt:variant>)"
	//			R"(<vt:lpstr>Fonts Used</vt:lpstr>)"
	//		R"(</vt:variant>)"
	//		R"(<vt:variant>)"
	//			R"(<vt:i4>3</vt:i4>)"
	//		R"(</vt:variant>)"
	//		R"(<vt:variant>)"
	//			R"(<vt:lpstr>Theme</vt:lpstr>)"
	//		R"(</vt:variant>)"
	//		R"(<vt:variant>)"
	//			R"(<vt:i4>1</vt:i4>)"
	//		R"(</vt:variant>)"
	//		R"(<vt:variant>)"
	//			R"(<vt:lpstr>Slide Titles</vt:lpstr>)"
	//		R"(</vt:variant>)"
	//		R"(<vt:variant>)"
	//			R"(<vt:i4>2</vt:i4>)"
	//		R"(</vt:variant>)"
	//	R"(</vt:vector>)"
	//R"(</HeadingPairs>)"
	//R"(<TitlesOfParts>)"
	//	R"(<vt:vector size="6" baseType="lpstr">)"
	//		R"(<vt:lpstr>Arial</vt:lpstr>)"
	//		R"(<vt:lpstr>Calibri</vt:lpstr>)"
	//		R"(<vt:lpstr>Calibri Light</vt:lpstr>)"
	//		R"(<vt:lpstr>Office Theme</vt:lpstr>)"
	//		R"(<vt:lpstr>PowerPoint Presentation</vt:lpstr>)"
	//		R"(<vt:lpstr>PowerPoint Presentation</vt:lpstr>)"
	//	R"(</vt:vector>)"
	//R"(</TitlesOfParts>)"
	//R"(<Company></Company>)"
	//R"(<LinksUpToDate>false</LinksUpToDate>)"
	//R"(<SharedDoc>false</SharedDoc>)"
	//R"(<HyperlinksChanged>false</HyperlinksChanged>)"
	//R"(<AppVersion>%d.%d.%d</AppVersion>)" //Better not add current version for reproducible production of pptxes across versions.
R"(</Properties>)";


static constexpr const char xml_presentation_preamble[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:presentation )"
	R"(xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" )"
	R"(xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" )"
	R"(xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" saveSubsetFonts="1">)"
	R"(<p:sldMasterIdLst>)"
		R"(<p:sldMasterId id="2147483648" r:id="rId1"/>)"
	R"(</p:sldMasterIdLst>)"
	R"(<p:sldIdLst>)";
		//R"(<p:sldId id="256" r:id="rId6"/>)"
static constexpr const char xml_presentation_postscript_template[] =
	R"(</p:sldIdLst>)"
	R"(<p:sldSz cx="%d" cy="%d"%s/>)" ///X and Y slide size in EMUs, optional ' type="xxx"'
	R"(<p:notesSz cx="6858000" cy="9144000"/>)"
	R"(<p:defaultTextStyle>)"
		R"(<a:defPPr>)"
			R"(<a:defRPr lang="en-US"/>)"
		R"(</a:defPPr>)"
		R"(<a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl1pPr>)"
		R"(<a:lvl2pPr marL="457200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl2pPr>)"
		R"(<a:lvl3pPr marL="914400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl3pPr>)"
		R"(<a:lvl4pPr marL="1371600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl4pPr>)"
		R"(<a:lvl5pPr marL="1828800" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl5pPr>)"
		R"(<a:lvl6pPr marL="2286000" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl6pPr>)"
		R"(<a:lvl7pPr marL="2743200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl7pPr>)"
		R"(<a:lvl8pPr marL="3200400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl8pPr>)"
		R"(<a:lvl9pPr marL="3657600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">)"
			R"(<a:defRPr sz="1800" kern="1200">)"
				R"(<a:solidFill>)"
					R"(<a:schemeClr val="tx1"/>)"
				R"(</a:solidFill>)"
				R"(<a:latin typeface="+mn-lt"/>)"
				R"(<a:ea typeface="+mn-ea"/>)"
				R"(<a:cs typeface="+mn-cs"/>)"
			R"(</a:defRPr>)"
		R"(</a:lvl9pPr>)"
	R"(</p:defaultTextStyle>)"
R"(</p:presentation>)";


static constexpr const char xml_presentation_rels_preamble[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">)"
	R"(<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" Target="presProps.xml"/>)";
	//R"(<Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>)"
static constexpr const char xml_presentation_rels_postscript[] =
	R"(<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>)"
	R"(<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles" Target="tableStyles.xml"/>)"
	R"(<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>)"
	R"(<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps" Target="viewProps.xml"/>)"
R"(</Relationships>)";

static constexpr const char xml_slide_preamble[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">)"
    "<p:cSld>"
        "<p:spTree>"
            "<p:nvGrpSpPr>"
                R"(<p:cNvPr id="1" name=""/>)"
                "<p:cNvGrpSpPr/>"
    "<p:nvPr/>"
            "</p:nvGrpSpPr>"
            "<p:grpSpPr/>";
static constexpr const char xml_slide_postscript[] =
"</p:spTree></p:cSld></p:sld>";

static constexpr const char xml_slide_rels_preamble[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">)"
R"(<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>)";
static constexpr const char xml_slide_rels_postscript[] =
R"(</Relationships>)";

//returns compound type and dash type
std::pair<std::string_view, std::string_view> linetype_str(ELineType lt) {
    switch (lt) {
    default:
    case ELineType::INVALID: _ASSERT(0); [[fallthrough]];
    case ELineType::NONE: return {};
    case ELineType::DOUBLE: return {"dbl", "solid"};
    case ELineType::TRIPLE: return {"tri", "solid"};
    case ELineType::TRIPLE_THICK: return {"tri", "solid"};
    case ELineType::SOLID: return {"", "solid"};
    case ELineType::DOTTED: return {"", "dot"};
    case ELineType::DASHED: return {"", "dash"};
    case ELineType::LONG_DASHED: return {"", "lgDash"};
    case ELineType::DASH_DOT: return {"", "dashDot"};
    }
}

std::string_view arrowtype_str(ESingleArrowType at) {
    switch (at) {
    case ESingleArrowType::NONE:
    case ESingleArrowType::TICK:
    case ESingleArrowType::JUMPOVER:
        return {};// "none" in the standard but easier to test & we don't emit it anyway
    case ESingleArrowType::INV:
    case ESingleArrowType::VEE:
    case ESingleArrowType::TEE:
    case ESingleArrowType::LINE:
    case ESingleArrowType::CROW:
    case ESingleArrowType::CURVE:
    case ESingleArrowType::ICURVE:
        return "arrow";
    case ESingleArrowType::DIAMOND:
    case ESingleArrowType::NSDIAMOND:
    case ESingleArrowType::BOX:
    case ESingleArrowType::NSBOX:
        return "diamond";
    case ESingleArrowType::DOT:
    case ESingleArrowType::NSDOT:
        return "oval";
    case ESingleArrowType::SHARP:
        return "stealth";
    case ESingleArrowType::NORMAL:
    case ESingleArrowType::STRIPES:
    case ESingleArrowType::TRIANGLE_STRIPES:
    default:
        return "triangle";
    }
}

template<typename T>
auto emu(T&& x) { return GenericShape::emu(std::forward<T>(x)); }

unsigned px2pt(double px) { return unsigned(px * 3 / 4); }

//tag should be head or tail
std::string arrow_attrs(LineAttr const& l, ArrowHead const* a, std::string_view tag) {
    if (!a) return "";
    std::string_view type = arrowtype_str(a->front().type);
    if (type.empty()) return "";
    const auto who = a->front().WidthHeightOffset(l, l);
    if (who.half_height==0) return "";
    const auto size_str = [&l](double pixel) noexcept->std::string_view {
        const double lw = std::max(2., l.LineWidth()); //PPT uses the total width of a compound line
        //Small arrowheads are linewidth*2, Normal=l2*3, Big are lw*5, but only above lw of 2. Below that we have what we have for lw=2
        if (pixel/lw<2.5) return "sm"; //small
        if (pixel/lw<4.0) return "med";//mediun
        return "lg";                   //large
    };
    return StrCat("<a:", tag, "End type=\"", type, "\" w=\"", size_str(who.half_height*2), "\" len=\"", size_str(who.before_x_extent+who.after_x_extent), "\"/>");

}

std::string line_attrs(LineAttr const& l, ArrowHead const* a1, ArrowHead const* a2) {
    _ASSERT(l.type);
    _ASSERT(l.width);
    auto const [cmpd, type] = linetype_str(*l.type);
    return "<a:ln w=\"" + std::to_string(emu(l.LineWidth()))
        + (cmpd.empty()? "" : "\" cmpd=\"" + std::string(cmpd))
        + "\">" //cap is good as default
        + (type.empty() ? "<a:noFill/>" : "<a:prstDash val=\"" + std::string(type) + "\"/>")
        + arrow_attrs(l, a1, "head") + arrow_attrs(l, a2, "tail") + "</a:ln>";
}

auto color(ColorType c) {
    std::string s(6, 0);
    snprintf(s.data(), s.size() + 1, "%02X%02X%02X", c.r, c.g, c.b); // In OOXML, lowercase is OK but M$ PPT only reads uppercase
    return c.IsFullyOpaque()
    	? "<a:srgbClr val=\"" + s + "\"/>"
    	: "<a:srgbClr val=\"" + s + "\">"
          "<a:alpha val=\"" + std::to_string(unsigned(1e5 * c.a / 255)) + "\"/>"
          "</a:srgbClr>";
}
auto solid_fill(ColorType c) { return "<a:solidFill>" + color(c) + "</a:solidFill>"; };
auto fill_attrs(const FillAttr& f, bool force_white=false) {
    _ASSERT(0<=f.frac && f.frac<=1);
    if (f.IsFullyTransparent()) return force_white ? solid_fill(ColorType::white()) : "";
    const auto gs = [](double p, ColorType c) {
        _ASSERT(0<=p && p<=1);
        return "<a:gs pos=\""+std::to_string(unsigned(1e5*p)) +"\">" + color(c) + "</a:gs>";
    };
    if (f.gradient == EIntGradType::NONE || f.color==f.color2) return solid_fill(*f.color);
    ColorType c1 = *f.color;
    ColorType c2 = f.color2.value_or(c1.Lighter(0.8));
    if (f.gradient == EIntGradType::RADIAL) {
        if (f.gradient_angle<0) std::swap(c1, c2); //in
        return R"(<a:gradFill flip="none" rotWithShape="1">)"
               R"(<a:gsLst>)"
             + gs(0., c2) + gs(1., c1)
             + R"(</a:gsLst>)"
               R"(<a:path path="circle">)"
               R"(<a:fillToRect l="50000" t="50000" r="50000" b="50000"/>)"
               R"(</a:path>)"
               R"(<a:tileRect/>)"
               R"(</a:gradFill>)";
    }
    _ASSERT(0<=*f.gradient_angle && *f.gradient_angle<360);
    double angle = *f.gradient_angle;
    std::string gslist;
    switch (*f.gradient) {
    case EIntGradType::NONE:
    case EIntGradType::RADIAL:
    case EIntGradType::INVALID:
        _ASSERT(0);
        return solid_fill(*f.color);
    case EIntGradType::DEGENERATE:
        gslist = gs(0., c1) + gs(*f.frac, c1) + gs(*f.frac, c2) + gs(1., c2);
        break;
    case EIntGradType::LINEAR:
        if (f.frac!=0.5)
            gslist = gs(1-*f.frac, f.color->MixWith(c2));
        gslist = gs(0., c1) + gslist + gs(1., c2);
        break;
    case EIntGradType::BUTTON:
        gslist = gs(0, c1.Lighter(0.8))
               + gs(0.43, c1)
               + gs(0.5, c1.Darker(0.1))
               + gs(1, c1);
        angle = 90;
    }
    angle = fmod(360+180-angle, 360);
    return "<a:gradFill><a:gsLst>" + gslist + "</a:gsLst>"
        "<a:lin ang=\"" + std::to_string(unsigned(60000*angle)) + "\" scaled=\"1\"/>"
        "</a:gradFill>";
}
auto preset_geom(std::string_view type, std::string_view adjustments = {}) {
    if (adjustments.empty())
        return "<a:prstGeom prst=\"" + std::string(type) + "\"/>";
    return StrCat("<a:prstGeom prst=\"", type, "\">",
                  "<a:avLst>", adjustments, "</a:avLst>" "</a:prstGeom>");
}

std::pair<std::string, Block> path_list(const Path &P, bool closed) {
    Block rect(false);
    std::string path;
    for (auto& i: P)
        if (i.IsDot()) continue;
        else if (i.IsStraight())
            (rect += i.GetStart()) += i.GetEnd();
        else
            rect += i.GetBezierHullBlock(); //We must have all endpoints and control points whithin 'rect'
    if (rect.IsInvalid()) return {"", rect};
    const auto wh = emu(rect.Spans());
    auto const& ul = rect.UpperLeft();
    for (std::span<const Edge> &p : P.Split(true)) {
        bool first = true;
        for (auto& i: p)
            if (!i.IsDot()) {
                if (first) {
                    auto const& xy = emu(i.GetStart() - ul);
                    path += "<a:moveTo><a:pt x=\"" + std::to_string(xy.x) + "\" y=\"" + std::to_string(xy.y) + "\"/></a:moveTo>";
                    first=false;
                }
                auto const& end = emu(i.GetEnd() - ul);
                if (i.IsStraight())
                    path += "<a:lnTo><a:pt x=\"" + std::to_string(end.x) + "\" y=\"" + std::to_string(end.y) + "\"/></a:lnTo>";
                else {
                    auto const& c1 = emu(i.GetC1() - ul), &c2 = emu(i.GetC2() - ul);
                    path += "<a:cubicBezTo>"
                                "<a:pt x=\"" + std::to_string(c1.x) + "\" y=\"" + std::to_string(c1.y) + "\"/>"
                                "<a:pt x=\"" + std::to_string(c2.x) + "\" y=\"" + std::to_string(c2.y) + "\"/>"
                                "<a:pt x=\"" + std::to_string(end.x) + "\" y=\"" + std::to_string(end.y) + "\"/>"
                            "</a:cubicBezTo>";
                }
            }
        if (closed && p.front().GetStart()==p.back().GetEnd()) path += "<a:close/>";
    }
    path = StrCat("<a:pathLst><a:path w=\"", std::to_string(wh.x), "\" h=\"", std::to_string(wh.y), "\">", path, "</a:path></a:pathLst>");
    return {std::move(path), rect};
}

std::string label_font_attrs(const StringFormat& sf, double scale_font = 1, unsigned hyperlink_id = 0) {
    const double size = sf.fontType != EFontType::MSC_FONT_SMALL ? *sf.normalFontSize : *sf.smallFontSize; //superscript & subscript automatically reduce font size
    const unsigned scaled_size = unsigned(size * scale_font * 100); // Thanks, M$, that all else is in EMUs but this one is in (hundreths of) _points_.
    std::string lang = sf.lang->size()!=2 ? "en-en" : *sf.lang+"-"+*sf.lang; //if not a two letter ISO code, we use english
    auto ret = R"(<a:rPr dirty="0" sz=")" + std::to_string(scaled_size) + R"(" err="1" lang=")" + lang + '"';
    if (sf.italics.value_or(ETriState::no) == ETriState::yes) ret += R"( i="1")";
    if (sf.bold.value_or(ETriState::no) == ETriState::yes) ret += R"( b="1")";
    if (sf.underline.value_or(ETriState::no) == ETriState::yes) ret += R"( u="sng")";
    if (sf.fontType == EFontType::MSC_FONT_SUBSCRIPT) ret += R"( baseline="-40000")";
    if (sf.fontType == EFontType::MSC_FONT_SUPERSCRIPT) ret += R"( baseline="30000")";
    ret += '>';
    if (do_not_draw_fonts) ret += solid_fill(ColorType(0,0,0,0));
    else if (sf.color && sf.color != ColorType::black()) ret += solid_fill(*sf.color); // MS: Color must come before typeface
    if (sf.bgcolor && !sf.bgcolor->IsFullyTransparent()) ret += "<a:highlight>" + color(*sf.bgcolor) + "</a:highlight>";
    if (sf.face && sf.face->size()) ret += "<a:latin typeface=\"" + *sf.face + "\"/>";  // TODO non-latin?
    else ret += R"(<a:latin typeface="Arial"/>)";
    if (hyperlink_id) ret.append(R"(<a:hlinkClick r:id="rId)").append(std::to_string(hyperlink_id)).append(
        R"(">)" //here comes a MS specific extension to reflect the color of the formatting
		    R"(<a:extLst>)"
                R"(<a:ext uri="{A12FA001-AC4F-418D-AE19-62706E023703}">)"
                    R"(<ahyp:hlinkClr xmlns:ahyp="http://schemas.microsoft.com/office/drawing/2018/hyperlinkcolor" val="tx"/>)"
                R"(</a:ext>)"
            R"(</a:extLst>)"
        R"(</a:hlinkClick>)");
    ret += "</a:rPr>";
    return ret;
}

std::string label_para_attrs(const StringFormat& sf) {
    std::string ret = "<a:pPr ";
    switch (sf.ident.value_or(EIdentType::MSC_IDENT_CENTER)) {
    default:
    case EIdentType::MSC_IDENT_INVALID:
    case EIdentType::MSC_IDENT_LEFT: break;
    case EIdentType::MSC_IDENT_CENTER: ret += R"(algn="ctr" )"; break;
    case EIdentType::MSC_IDENT_RIGHT: ret += R"(algn="r" )"; break;
    }
    ret += R"(marL="0" marR="0" latinLnBrk="0"/>)"; //latinLnBrk="0" pevents moving a whole word to the next line
    return ret;
}

static std::string xml_escape(std::string_view v) {
    std::string ret;
    for (auto c: v)
        if (c == '<')
            ret += "&lt;";
        else if (c == '>')
            ret += "&gt;";
        else if (c == '"')
            ret += "&quot;";
        else if (c == '&')
            ret += "&amp;";
        else if (c == '\'')
            ret += "&apos;";
        else
            ret += c;
    return ret;
}

std::string PptFile::MakeLabel(const Label& L, std::string_view asterisk_replacement, double scale_font) {
    std::string ret;
    if (L.IsEmpty()) return ret;
    StringFormat sf; //running style
    unsigned url_id=0; //the rels id of the current link we are in. 0 if none
    bool r_started = false;  //'ret' has a <a:r> tag opened
    bool format_changed = true; //the format in 'sf' has changed since we last emitted label_font_attrs()
    const auto check_start_r = [&ret, &sf, &r_started, &format_changed, &url_id] (double scale_font) {
        if (!format_changed && r_started) return;
        if (r_started)
            ret += "</a:t></a:r>";
        ret += "<a:r>";
        ret += label_font_attrs(sf, scale_font, url_id);
        ret += "<a:t>";
        r_started = true;
        format_changed = false;
    };
    for (const auto& l : L) {
        std::string_view text = l.GetLine();
        sf = l.GetStartFormat();
        ret.append("<a:p>").append(label_para_attrs(sf));
        const double line_scale_font = scale_font* sqrt(l.GetScale().x * l.GetScale().y);
        std::string replaceto;
        while (text.length() && text[0]) {
            size_t length;
            switch (sf.ProcessEscape(text, length, false, true, &replaceto)) {
            case StringFormat::NON_FORMATTING:
                check_start_r(line_scale_font);
                if (length)
                    ret.append(xml_escape(replaceto));
                break;
            case StringFormat::NON_ESCAPE:
                check_start_r(line_scale_font);
                ret.append(xml_escape(text.substr(0, length)));
                break;
            case StringFormat::ASTERISK:
                check_start_r(line_scale_font);
                ret.append(xml_escape(asterisk_replacement));
                break;
            case StringFormat::LINE_BREAK:
            case StringFormat::SOFT_LINE_BREAK:
            case StringFormat::LINK_ESCAPE: //we must have it translated already
                _ASSERT(0);
                break;
            case StringFormat::LINK2_ESCAPE:
                _ASSERT(length>=4);
                _ASSERT((length>4) == (url_id==0));
                url_id = GetLinkId(std::string_view(text).substr(3, length-4));
                format_changed = true;
                break;
            default: //all other escapes
                format_changed = true;
                break;
            }
            text.remove_prefix(length);
        }
        ret += "</a:t></a:r></a:p>";
        r_started = false;
    }
    return ret;
}

//Returns the relation ID to use for a given URL
//If url is empty, we return zero
//start ids from 2, the slide layout relation is id=1
unsigned PptFile::GetLinkId(std::string_view url) {
    if (url.empty()) return 0;
    auto i = std::ranges::find(links, url);
    if (i==links.end()) {
        links.push_back(std::string(url));
        const unsigned id = (unsigned)links.size()+1;
        rel_content += R"(<Relationship Id="rId)";
        rel_content += std::to_string(id);
        rel_content += R"(" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target=")";
        rel_content += url;
        rel_content += R"(" TargetMode="External"/>)";
        return id;
    }
    return i-links.begin()+2;
}


std::string PptFile::MakeElement(const Block& rect, const LineAttr& line, ArrowHead const* a_start, ArrowHead const* a_end,
                                 std::string outer, std::string header, std::string header2, std::string props, std::string extra, std::string transforms) {
    _ASSERT(line.color);
    _ASSERT(transforms.empty() || transforms[0] == ' ');
    const auto xywh = emu(rect);
    return  '<' + outer + ">"
        "<" + header + ">"
        R"(<p:cNvPr id=")" + std::to_string(++shape_id) + R"(" name=""/>)"
        "<" + header2 + "/>"
        "<p:nvPr/>"
        "</" + header + ">"
        "<p:spPr>"
        "<a:xfrm" + std::move(transforms) + ">"
        R"(<a:off x=")" + std::to_string(xywh.x) + R"(" y=")" + std::to_string(xywh.y) + R"("/>)"
        R"(<a:ext cx=")" + std::to_string(xywh.w) + R"(" cy=")" + std::to_string(xywh.h) + R"("/>)"
        "</a:xfrm>"
        + props
        + line_attrs(line, a_start, a_end) +
        "</p:spPr>"
        "<p:style>"
        R"(<a:lnRef idx="1">)"
        + color(*line.color) +
        "</a:lnRef>"
        R"(<a:fillRef idx="0"/>)"
        R"(<a:effectRef idx="0"/>)"
        R"(<a:fontRef idx="none"/>)"
        "</p:style>"
        + extra
        + "</" + outer + '>';
};

void PptFile::reset() {
    content = err = rel_content = {};
    file_name.clear();
    transform.clear();
    page = 1;
    clip = Block(false);
}

bool PptFile::new_file(std::string_view fname, int pageno [[maybe_unused]], const XY& page_size_in_points_) {
    if (file_name.size()) {
        err = {};
        return false;
    }
    reset();
    file_name.assign(fname);
    page_size_in_points = page_size_in_points_;
    std::ofstream out(file_name.data(), std::ios::binary | std::ios::out | std::ios::trunc);
    if (out.fail()) {
        err = "Could not open '"+ std::string(fname)+"' for writing.";
        file_name.clear();
    } else {
        out.write(reinterpret_cast<char*>(default_ppt), sizeof default_ppt);
        if (out.fail())
            err = "Could not write to '"+ std::string(fname)+"'.";
        out.close();
        if (err.empty() && out.fail())
            err = "Could not write to '"+ std::string(fname)+"'.";
        if (err.empty()) {
            content = xml_slide_preamble;
            rel_content = xml_slide_rels_preamble;
        } else
            del_file();
    }
    return is_ok();
}

bool PptFile::set_page_size(const XY &origSize, const XY &origOffset,
                            const XY &headingLeftingSize, const XY &user_scale,
                            int h_alignment, int v_alignment, const Block& raw_page_clip,
                            double copyrightTextHeight) {
    if (origSize.x<1 || origSize.y<1) return false;
    if (!is_ok()) return false;
    transform.clear();
    transform.push_back(SimpleTransform::Scale(user_scale));
    double margins[4] = { 0,0,0,0 };
    if (raw_page_clip.GetArea()>0) {
        margins[0] = std::max(0., raw_page_clip.x.from);
        margins[1] = std::max(0., page_size_in_points.x - raw_page_clip.x.till);
        margins[2] = std::max(0., raw_page_clip.y.from);
        margins[3] = std::max(0., page_size_in_points.y - raw_page_clip.y.till);
    }
    const XY fullSize = { headingLeftingSize.x+origSize.x, headingLeftingSize.y+origSize.y+copyrightTextHeight };
    XY off(0,0);
    switch (v_alignment) {
    case +1: //bottom
        off.y =  page_size_in_points.y - margins[3] - fullSize.y*user_scale.y; break;
    case 0: //center
        off.y = (page_size_in_points.y - margins[3] - margins[2] - fullSize.y*user_scale.y)/2; break;
    case -1://top
        off.y = margins[2];
        break;
    default: _ASSERT(0);
    }
    off.y -= origOffset.y*user_scale.y;
    switch (h_alignment) {
    case 1: //right
        off.x =  page_size_in_points.x - margins[1] - fullSize.x*user_scale.x; break;
    case 0:  //center
        off.x = (page_size_in_points.x - margins[0] - margins[1] - fullSize.x*user_scale.x)/2; break;
    case -1: //left
        off.x = margins[0];
        break;
    default: _ASSERT(0);
    }
    off.x -= origOffset.x*user_scale.x;
    transform.push_back(SimpleTransform::Shift(off));
    //transform the page clip back to chart space
    Block chart_space_raw_page_clip = raw_page_clip.CreateShifted(-off);
    chart_space_raw_page_clip.x.from /= user_scale.x;
    chart_space_raw_page_clip.x.till /= user_scale.x;
    chart_space_raw_page_clip.y.from /= user_scale.y;
    chart_space_raw_page_clip.y.till /= user_scale.y;
    //create clip rectangles for chart and header/footer/lefting drawing
    clip = Block(origOffset, origOffset+origSize) * chart_space_raw_page_clip;
    clip_hdr = Block(origOffset-headingLeftingSize, origOffset+origSize);
    clip_hdr.y.till += copyrightTextHeight;
    clip_hdr = clip_hdr * chart_space_raw_page_clip;
    return true;
}

void PptFile::del_file() {
    if (file_name.size()) {
        std::error_code ec;
        std::filesystem::remove(file_name, ec);
    }
    reset();
}

bool PptFile::flush_page(bool force_empty) {
    if (file_name.empty()) {
        reset();
        return false;
    }
    if (err.empty() && (force_empty || content.size())) {
        content += xml_slide_postscript;
        rel_content += xml_slide_rels_postscript;

        std::vector<FileInZip> files = {
            {StrCat("ppt/slides/slide", std::to_string(page), ".xml"), std::move(content)},
            {StrCat("ppt/slides/_rels/slide", std::to_string(page), ".xml.rels"), std::move(rel_content)},
        };
        err = AddToZip(ZipArchive::FromFile(file_name), files);
        if (err.size()) {
            del_file();
        } else {
            content = xml_slide_preamble;
            rel_content = xml_slide_rels_preamble;
            transform.clear();
            ++page;
        }
    }
    return is_ok();
}

bool PptFile::finalize() {
    if (file_name.empty()) {
        reset();
        return false;
    }
    if (err.size()) return false;
    flush_page(false);  //flush anything added to the last page, but do not flush an empty page

    std::vector<FileInZip> files;
    {
        std::string content_type = xml_content_types_preamble;
        for (unsigned u = 1; u<page; u++)
            content_type.append(R"(<Override PartName="/ppt/slides/slide)")
                        .append(std::to_string(u))
                        .append(R"(.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>)");
        content_type += xml_content_types_postscript;
        files.emplace_back("[Content_Types].xml", std::move(content_type));
    }
    {
        std::string app(strlen(xml_app_template)+1000, '\0');
        const int len = snprintf(app.data(), app.size(), xml_app_template, "Widescreen", page-1);
        _ASSERT(len>0);
        app.resize(len);
        files.emplace_back("docProps/app.xml", std::move(app));
    }
    {
        std::string presentation = xml_presentation_preamble;
        for (unsigned u = 1; u<page; u++)
            presentation.append(R"(<p:sldId id=")")
                        .append(std::to_string(255+u))
                        .append(R"(" r:id="rId)")
                        .append(std::to_string(u+5))
                        .append(R"("/>)");
        std::string post(strlen(xml_presentation_postscript_template)+256, '\0');
        auto slide_wh = emu(page_size_in_points);
        //add "type" to slide size, see bottom of http://officeopenxml.com/prPresentation.php
        std::string slide_type;
        const auto comp = [this](EPageSize p) { return GetPhysicalPageSize(p)==page_size_in_points || GetPhysicalPageSize(p).SwapXY()==page_size_in_points; };
             if (comp(EPageSize::A3P)) slide_type = R"( type="A3")";
        else if (comp(EPageSize::A4P)) slide_type = R"( type="A4")";
        else if (comp(EPageSize::LEDGER)) slide_type = R"( type="ledger")";
        else if (comp(EPageSize::LETTER_P)) slide_type = R"( type="letter")";
        else if (comp(EPageSize::ON_SCREEN_16_10_L)) slide_type = R"( type="screen16x10")";
        else if (comp(EPageSize::ON_SCREEN_16_9_L)) slide_type = R"( type="screen16x9")";
        else if (comp(EPageSize::ON_SCREEN_4_3_L)) slide_type = R"( type="screen4x3")";
        const int len = snprintf(post.data(), post.size(), xml_presentation_postscript_template, slide_wh.x, slide_wh.y, slide_type.c_str());
        _ASSERT(len>1);
        post.resize(len);
        presentation += post;
        files.emplace_back("ppt/presentation.xml", std::move(presentation));
    }
    {
        std::string presentation_rels = xml_presentation_rels_preamble;
        for (unsigned u = 1; u<page; u++)
            presentation_rels.append(R"(<Relationship Id="rId)")
                             .append(std::to_string(u+5))
                             .append(R"(" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide)")
                             .append(std::to_string(u))
                             .append(R"(.xml"/>)");
        presentation_rels += xml_presentation_rels_postscript;
        files.emplace_back("ppt/_rels/presentation.xml.rels", std::move(presentation_rels));
    }
    err = AddToZip(ZipArchive::FromFile(file_name), files);
    if (err.size()) del_file();
    else reset();
    return is_ok();
}

bool PptFile::add(GenericShape&& shape) {
    _ASSERT(file_name.size());
    if (!is_ok()) return false;
    if (shape.empty()) return true;
    if (!clip.IsInvalid() && shape.clip(clip)) return true;
    for (const SimpleTransform &T : transform)
        shape.transform(T);
    if (auto* p = dynamic_cast<GSPath const*>(&shape)) {
        if (p->path.size()==1 && p->path.front().IsStraight()) {
            const XY& s = p->path.front().GetStart();
            const Block bb(s, p->path.front().GetEnd());
            content += MakeElement(bb, p->line, p->aStart, p->aEnd,
                                   "p:cxnSp", "p:nvCxnSpPr", "p:cNvCxnSpPr", preset_geom("line"), {},  //Lines are expressed as a line from the upper left of the bounding box
                                   s==bb.UpperLeft() ? "" : s==bb.UpperRight() ? " flipH=\"1\"" :      //to the lower right, with some flip transformations applied.
                                   s==bb.LowerLeft() ? " flipV=\"1\"" : " flipV=\"1\" flipH=\"1\"");
        } else {
            auto [geom, rect] = path_list(p->path, false);
            content += MakeElement(rect, p->line, p->aStart, p->aEnd, "p:sp", "p:nvSpPr", "p:cNvSpPr",
                                   "<a:custGeom>" + std::move(geom) + "</a:custGeom>" "<a:noFill/>");
        }
    } else if (auto* const b = dynamic_cast<GSBox*>(&shape)) { //GSBox and GSShape
        std::string lines = MakeLabel(b->label, "", b->scale_font);
        auto shadow = [&sh=b->shadow] {
            return sh.offset && *sh.offset
                   ? "<a:outerShdw dist=\"" + std::to_string(emu(*sh.offset)) + "\" dir=\"2700000\" algn=\"tl\""
                       + (sh.blur && *sh.blur ? " blurRad=\"" + std::to_string(emu(*sh.blur)) + '"' : "")
                       + (sh.color ? ">" + color(*sh.color) + "</a:outerShdw>" : "/>")
                   : "";
        };
        auto fill = [b]{
            return fill_attrs(b->fill, b->shadow.offset && *b->shadow.offset); // need a solid fill when adding a shadow otherwise only line has shadow
        };
        auto effects = [](std::string&& x) { return x.empty() ? x : x.insert(0, "<a:effectLst>").append("</a:effectLst>"); };
        /** The OOXML specification allows a shape to have a label and can set
          * the bounding box for that label via the a:rect attr. PowerPoint supports it,
          * whereas LibreOffice does not. If the below flag is true, we emit shape
          * labels as a separate textbox. */
        constexpr bool libreoffice_workaround = true;
        Block bb;
        std::string geom;
        std::optional<std::pair<Block, std::string>> label; //The bb and text of a label if 'libreoffice_workaround'
        for (auto* s = dynamic_cast<GSShape const*>(&shape); ; s=nullptr) { //sometimes we want to add a separate shape and label
            if (s) {
                std::tie(geom, bb) = path_list(s->shape, true);
                if (lines.size() && !b->rect.IsInvalid()) {
                    //expand a bit to avoid accidential line breaking
                    b->rect.Expand(1);
                    if (libreoffice_workaround) {
                        label.emplace(b->rect, std::move(lines));
                        lines.clear();
                    } else {
                        const auto text = emu(b->rect.CreateShifted(-bb.UpperLeft()));
                        geom = StrCat("<a:rect l=\"", std::to_string(text.x), "\" t=\"", std::to_string(text.y), "\" "
                                        "r=\"", std::to_string(text.x + text.w), "\" b=\"", std::to_string(text.y + text.h), "\"/>",  // MS: 'a:rect' must come before pathlist
                                        geom);
                    }
                }
                geom = "<a:custGeom>" + std::move(geom) + "</a:custGeom>";
            } else {
                bb = b->rect;
                const auto val = [&] {
                    //The formula "val X" gives the size of the adjustment in fractions of 100.000, which is the max adjustment possible
                    //We interpet it such that the max value is min(width,height). Also, number must be integer (probably non-zero, too).
                    //And I get this from https://stackoverflow.com/questions/44030139/set-radius-of-rounded-rectangle-in-powerpoint-document-with-apache-poi (which has an error)
                    return std::to_string(unsigned(1e5*std::min(1., std::max(0., *b->line.radius)/std::min(bb.x.Spans(), bb.y.Spans()))));

                };
                if (*b->line.radius<=0) goto box;
                switch (*b->line.corner) {
                case ECornerType::NONE:
                case ECornerType::INVALID:
                default:
                box:
                    geom = preset_geom("rect");
                    break;
                case ECornerType::ROUND:
                    geom = preset_geom("roundRect", StrCat(R"(<a:gd name="adj" fmla="val )", val(), R"("/>)"));
                    break;
                case ECornerType::NOTE:
                    geom = preset_geom("foldedCorner", StrCat(R"(<a:gd name="adj" fmla="val )", val(), R"("/>)"));
                    break;
                case ECornerType::BEVEL:
                    geom = preset_geom("snip2SameRect", StrCat(R"(<a:gd name="adj1" fmla="val )", val(), R"("/>)" R"(<a:gd name="adj2" fmla="val )", val(), R"("/>)"));
                    break;
                }
                //for text-only shapes, expand the bounding box, to avoid accidental text wrapping
                if (b->line.IsFullyTransparent() && b->fill.IsFullyTransparent() && b->shadow.IsNone() && !b->label.IsEmpty())
                    bb.Expand(1);
            }
            const std::string_view valign = b->valign==ETextAlign::Min ? "anchor=\"t\" " : b->valign==ETextAlign::Mid ? "anchor=\"ctr\" " : "anchor=\"b\" ";
            content += MakeElement(bb, b->line, nullptr, nullptr, "p:sp", "p:nvSpPr", "p:cNvSpPr", std::move(geom) + fill() + effects(shadow()),
                                   lines.empty() ? "" : StrCat("<p:txBody>"
                                                               "<a:bodyPr ", valign, R"(lIns="0" tIns="0" rIns="0" bIns="0"/>)",
                                                               lines,
                                                               "</p:txBody>"),
                                   (b->degree ? " rot=\"" + std::to_string(int64_t(b->degree * 60'000)) + '"' : "")
                                   + (!lines.empty() && b->scale_font < 0 ? " flipH=\"1\"" : "")
            );
            if (label) {
                b->rect = label->first;
                lines = std::move(label->second);
                b->line = LineAttr::None();
                b->fill = FillAttr::None();
                b->shadow = ShadowAttr::None();
                b->label = {};
                label.reset();
            } else
                break;
        }
    } //else GenericShape is a No-op
    _ASSERT(is_ok());
    return true;
}

/******************************************************************/

static tinyxml2::XMLElement* get_path(tinyxml2::XMLDocument& doc, std::initializer_list<const char*> path) {
    _ASSERT(path.size());
    auto i = path.begin();
    tinyxml2::XMLElement* ret = doc.FirstChildElement(*i);
    while (++i!=path.end() && ret)
        ret = ret->FirstChildElement(*i);
    return ret;
}

static tinyxml2::XMLElement* get_path(tinyxml2::XMLElement *e, std::initializer_list<const char*> path) {
    _ASSERT(path.size());
    for (auto i = path.begin(); i!=path.end(); ++i)
        e = e->FirstChildElement(*i);
    return e;
}


std::string PptFile::parse_pages(ZipArchive archive, std::vector<Slide>& slides, std::pair<int, int>*slide_size) {
    using namespace std::string_view_literals;
    slides.clear();
    std::vector<FileInZip> files = { {"ppt/presentation.xml", ""}, {"ppt/_rels/presentation.xml.rels", ""}};
    std::string err = ExtractFromZip(archive, files);
    if (err.size()) return err;
    if (files.size()<2) return "Invalid PPT "+archive.qfname_or_clipboard()+"': No 'ppt/presentation.xml' or 'ppt/_rels/presentation.xml.rels'.";
    tinyxml2::XMLDocument presentation;
    presentation.Parse(files.front().content.c_str(), files.front().content.size());
    if (presentation.Error()) return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': "+tinyxml2::XMLDocument::ErrorIDToName(presentation.ErrorID());
    auto pSlideLst = get_path(presentation, { "p:presentation", "p:sldIdLst" });
    if (!pSlideLst) return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': Did not find <p:sldIdLst>";
    auto pSlide = pSlideLst->FirstChildElement("p:sldId");
    std::vector<std::pair<unsigned, std::string>> ids; //The list of slides in order, identified by their id, rId
    while (pSlide) {
        auto prID = pSlide->FindAttribute("r:id");
        if (!prID) return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': Did not find <r:id> attr of <p:sldId>";
        auto pID = pSlide->FindAttribute("id");
        if (!pID) return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': Did not find <id> attr of <p:sldId>";
        int i = pID->IntValue();
        if (i==0) return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': id attr of <p:sldId> is not a positive number.";
        ids.emplace_back(i, prID->Value());
        pSlide = pSlide->NextSiblingElement("p:sldId");
    }
    if (slide_size) {
        auto pSize = get_path(presentation, { "p:presentation", "p:sldSz" });
        if (!pSize)
            return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': No <p:sldSz> element.";
        slide_size->first = pSize->IntAttribute("cx");
        slide_size->second = pSize->IntAttribute("cy");
        if (slide_size->first==0 || slide_size->second==0)
            return "Error in "+archive.qfname_or_clipboard()+":/ppt/presentation.xml': No 'cx' or 'cy' attr on the <p:sldSz> element.";
    }
    //Now find filenames for each rID
    presentation.Parse(files.back().content.c_str(), files.back().content.size());
    if (presentation.Error()) return "Error in "+archive.qfname_or_clipboard()+":/ppt/_rels/presentation.xml.rels': "+tinyxml2::XMLDocument::ErrorIDToName(presentation.ErrorID());
    auto pRel = get_path(presentation, { "Relationships", "Relationship" });
    if (!pRel) return "Error in "+archive.qfname_or_clipboard()+":/ppt/_rels/presentation.xml.rels': Did not find <Relationship>";
    std::unordered_map<std::string, const char*> slide_ids;
    while (pRel) {
        if (auto pType = pRel->FindAttribute("Type"))
            if (pType->Value()=="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"sv)
                if (auto pTarget = pRel->FindAttribute("Target"))
                    if (auto pId = pRel->FindAttribute("Id"))
                        slide_ids[pId->Value()] = pTarget->Value();
        pRel = pRel->NextSiblingElement("Relationship");
    }
    for (const auto &id : ids)
        if (auto i = slide_ids.find(id.second); i == slide_ids.end())
            return "Error in "+archive.qfname_or_clipboard()+"': No slide file for '"+id.second+"'.";
        else
            slides.emplace_back(id.first, id.second, std::move(i->second));
    return "";
}

std::string PptFile::append_page(ZipArchive archive, unsigned* page) {
    std::vector<Slide> slides;
    if (std::string err = parse_pages(archive, slides); err.size()) return err;
    std::vector<FileInZip> files = { {"ppt/presentation.xml", ""}, {"ppt/_rels/presentation.xml.rels", ""}, {"[Content_Types].xml", ""} };
    std::string err = ExtractFromZip(archive, files);
    if (err.size()) return err;
    if (files.size()<3) return "Invalid PPT "+archive.qfname_or_clipboard()+"': No 'ppt/presentation.xml', 'ppt/_rels/presentation.xml.rels' or '[Content_Types].xml'.";
    std::array<tinyxml2::XMLDocument,3> docs;
    for (unsigned u = 0; u<docs.size(); u++) {
        docs[u].Parse(files[u].content.c_str());
        if (docs[u].Error()) return "Error in "+archive.qfname_or_clipboard()+":"+files[u].inner_filename+"': "+tinyxml2::XMLDocument::ErrorIDToName(docs[u].ErrorID());
    }
    std::array elems = {
        get_path(docs[0], {"p:presentation", "p:sldIdLst"}),
        get_path(docs[1], { "Relationships" }),
        get_path(docs[2], { "Types" }),
    };
    if (auto i = std::ranges::find(elems, nullptr); i!=elems.end())
        return "Error in "+archive.qfname_or_clipboard()+":"+files[i-elems.begin()].inner_filename+"': Did not find relevant child elements.";

    //Determine new slide id, rID and filename
    const unsigned id = std::ranges::max_element(slides, {}, &Slide::id)->id+1;
    unsigned rId = 0; //count how many relationships we have
    for (tinyxml2::XMLElement* child = elems[1]->FirstChildElement(); child; child = child->NextSiblingElement())
        rId++;
    std::string rId_str, fn_str;
    do {
        rId++;
        rId_str = "rId"+std::to_string(rId);
    } while (std::ranges::find(slides, rId_str, &Slide::rId) != slides.end());
    unsigned fn = slides.size();
    do {
        fn++;
        fn_str = "slides/slide"+std::to_string(fn)+".xml";
    } while (std::ranges::find(slides, fn_str, &Slide::filename) != slides.end());
    //Add page to presentation.xml
    elems[0] = elems[0]->InsertNewChildElement("p:sldId");
    elems[0]->SetAttribute("id", id);
    elems[0]->SetAttribute("r:id", rId_str.c_str());

    //Add page to presentation.xml.rels
    elems[1] = elems[1]->InsertNewChildElement("Relationship");
    elems[1]->SetAttribute("Id", rId_str.c_str());
    elems[1]->SetAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide");
    elems[1]->SetAttribute("Target", fn_str.c_str());

    //Add page to content
    elems[2] = elems[2]->InsertNewChildElement("Override");
    elems[2]->SetAttribute("PartName", ("/ppt/"+fn_str).c_str());
    elems[2]->SetAttribute("ContentType", "application/vnd.openxmlformats-officedocument.presentationml.slide+xml");

    for (unsigned u = 0; u<docs.size(); u++) {
        tinyxml2::XMLPrinter printer(nullptr, true);
        docs[u].Print(&printer);
        files[u].content.assign(printer.CStr(), printer.CStrSize());
        if (files[u].content.ends_with('\0')) files[u].content.pop_back(); //TinyXML includes the terminating zero in CStrSize()
    }

    //Now add an empty slide
    files.emplace_back("ppt/"+fn_str, StrCat(xml_slide_preamble, xml_slide_postscript));
    files.emplace_back("ppt/slides/_rels/"+fn_str.substr(7)+".rels", StrCat(xml_slide_rels_preamble, xml_slide_rels_postscript));

    if (page) *page = slides.size()+1;
    return AddToZip(archive, files);
}

static std::optional<PptFile::ChartInPPT> ExtractChart(tinyxml2::XMLElement* pic, tinyxml2::XMLElement* Relationships, bool clipboard) {
    constexpr unsigned MAX = std::numeric_limits<unsigned>::max();
    tinyxml2::XMLElement* prop = get_path(pic, clipboard ? std::initializer_list{ "a:nvPicPr", "a:cNvPr" } : std::initializer_list{ "p:nvPicPr", "p:cNvPr" });
    if (!prop) return {};
    PptFile::ChartInPPT s;
    s.slide = s.chart = 0;
    s.id = prop->UnsignedAttribute("id", MAX);
    if (s.id==MAX) return {};
    const char* const descr = prop->Attribute("descr");
    if (!descr) return {};
    if (EmbedChartData chart = EmbedChartData::ParseEscapedText(descr); !chart.empty())
        s.data = std::move(chart);
    else return {};

    prop = get_path(pic, { clipboard ? "a:spPr" : "p:spPr", "a:xfrm" });
    if (!prop) return {};
    tinyxml2::XMLElement* off = prop->FirstChildElement();
    if (!off) return {};
    if (unsigned x = off->UnsignedAttribute("x", MAX); x==MAX) return {};
    else s.x = x;
    if (unsigned y = off->UnsignedAttribute("y", MAX); y==MAX) return {};
    else s.y = y;
    tinyxml2::XMLElement* ext = prop->FirstChildElement("a:ext");
    if (!ext) return {};
    if (unsigned x = ext->UnsignedAttribute("cx", MAX); x==MAX) return {};
    else s.w = x;
    if (unsigned y = ext->UnsignedAttribute("cy", MAX); y==MAX) return {};
    else s.h = y;

    tinyxml2::XMLElement* blipfill = pic->FirstChildElement(clipboard ? "a:blipFill" : "p:blipFill");
    if (!blipfill) return {};
    if (tinyxml2::XMLElement* srcRect = blipfill->FirstChildElement("a:srcRect")) {
        s.crop_l = srcRect->IntAttribute("l", 0);
        s.crop_r = srcRect->IntAttribute("r", 0);
        s.crop_t = srcRect->IntAttribute("t", 0);
        s.crop_b = srcRect->IntAttribute("b", 0);
    } else
        s.crop_l = s.crop_r = s.crop_t = s.crop_b = 0;

    prop = blipfill->FirstChildElement("a:blip");
    if (!prop) return {};
    const char* const rId = prop->Attribute("r:embed");
    if (!rId) return {};
    s.rId = rId;
    //Now find this PNG image in the rels file
    for (tinyxml2::XMLElement* r = Relationships->FirstChildElement("Relationship"); r; r = r->NextSiblingElement("Relationship"))
        if (r->Attribute("Id", rId)) //Maybe check Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" ?
            if (const char* const fn = r->Attribute("Target")) {
                s.internal_name = fn;
                return { std::move(s) };
            }
    return {};
}

/** Updates an XML chart element (p:pic) from one chart to another.
 * @param [in] current Info extracted about the chart currently in 'pic'
 * @param [in] c Info about the new chart to update to.
 * @param pic The <p:pic> XML element holding the current chart
 * @param [in] alloc_new_image If set, we use this rId and inner filename instead.
 * @param [in] slide_size The size of the slide in points. If omitted, we are updating a chart in cliboard content
 *             - we do not check if the chart exceeds slide size
 *             - we use the a:XXX XML elements and not p:XML elements.*/
std::string UpdateChart(const PptFile::ChartInPPT& current, const PptFile::ChartToPPT& c, tinyxml2::XMLElement* pic,
                        std::optional<std::pair<std::string, std::string>> alloc_new_image = {},
                        std::optional<std::pair<int, int>> slide_size = {}) {
    const bool is_clipboard = !slide_size;
    std::string png_inner_fn;
    if (alloc_new_image) {
        png_inner_fn = alloc_new_image->second;
        tinyxml2::XMLElement* blip = get_path(pic, { is_clipboard ? "a:blipFill" : "p:blipFill", "a:blip" });
        if (!blip) return {};
        blip->SetAttribute("r:embed", alloc_new_image->first.c_str());
    } else {
        png_inner_fn = current.internal_name;
        if (png_inner_fn.starts_with(".."))
            png_inner_fn.erase(0, 3);
    }
    const XY current_visible = current.crop(current.data.chart_size);
    const XY c_visible = current.crop(c.data.chart_size);
    double zoom = sqrt((current.w/ current_visible.x) * current.h/ current_visible.y); //avg of the existing zoom in X and Y dir
    if (slide_size)
        zoom = std::min({ zoom, slide_size->first/c_visible.x, slide_size->second/c_visible.y }); //maximize zoom to fit
    GenericShape::int_xy cs = { int(c_visible.x*zoom), int(c_visible.y*zoom) };
    //Keep the chart center the same
    GenericShape::int_xy cp = {
        std::max(0, current.x - cs.x/2 + (int)current.w/2),
        std::max(0, current.y - cs.y/2 + (int)current.h/2) };
    //avoid the chart to extend beyond the slide
    if (slide_size) {
        if (slide_size->first<cp.x+cs.x) cp.x = 0;
        if (slide_size->first<cp.y+cs.y) cp.y = 0;
    }

    //Now replace the fields need replacing: name, descr, bounding box. Id, rId, srcRect remains
    tinyxml2::XMLElement* prop = get_path(pic, is_clipboard ? std::initializer_list{ "a:nvPicPr", "a:cNvPr" } : std::initializer_list{ "p:nvPicPr", "p:cNvPr" });
    if (!prop) return {};
    prop->SetAttribute("name", ("Msc-generator "+c.data.chart_type).c_str());
    prop->SetAttribute("descr", c.data.Escape().c_str());
    if (is_clipboard) {//for the clipboard, we delete extensions
        if (tinyxml2::XMLElement* exts = prop->FirstChildElement("a:extLst"))
            prop->DeleteChild(exts);
        prop->SetAttribute("id", "7");
    }

    prop = get_path(pic, { is_clipboard ? "a:spPr" : "p:spPr" , "a:xfrm" });
    if (!prop) return {};
    tinyxml2::XMLElement* pic_off = prop->FirstChildElement("a:off");
    if (!pic_off) return {};
    tinyxml2::XMLElement* pic_ext = prop->FirstChildElement("a:ext");
    if (!pic_ext) return {};

    if (false && is_clipboard) {
        //For clipboard, if we only copy a single image (size & width equals to that of the image)
        //set the Group Shape extent and position to the same values to facilitate insertion at the
        //right location. We dead code this as it does not really help...
        tinyxml2::XMLElement* grpSpPr = pic->PreviousSiblingElement("a:grpSpPr");
        if (!grpSpPr) grpSpPr = pic->NextSiblingElement("a:grpSpPr");
        if (grpSpPr)
            if (tinyxml2::XMLElement* xfrm = grpSpPr->FirstChildElement("a:xfrm")) {
                tinyxml2::XMLElement* off = xfrm->FirstChildElement("a:off");
                if (!off) return {};
                tinyxml2::XMLElement* ext = xfrm->FirstChildElement("a:ext");
                if (!ext) return {};
                tinyxml2::XMLElement* choff = xfrm->FirstChildElement("a:chOff");
                if (!choff) return {};
                tinyxml2::XMLElement* chext = xfrm->FirstChildElement("a:chExt");
                if (!chext) return {};

                if (unsigned cx = chext->UnsignedAttribute("cx"), cy = chext->UnsignedAttribute("cy")
                        ; cx ==pic_ext->UnsignedAttribute("cx")
                        && cy==pic_ext->UnsignedAttribute("cy")
                        && cx==ext->UnsignedAttribute("cx")
                        && cy==ext->UnsignedAttribute("cy")) {
                    if (abs(int(cx)-int(cs.x))<5) cs.x = cx;
                    else {
                        ext->SetAttribute("cx", cs.x);
                        chext->SetAttribute("cx", cs.x);
                    }
                    if (abs(int(cy)-int(cs.y))<5) cs.y = cy;
                    else {
                        ext->SetAttribute("cy", cs.y);
                        chext->SetAttribute("cy", cs.y);
                    }
                }
                if (unsigned x = choff->UnsignedAttribute("x"), y = choff->UnsignedAttribute("y")
                        ; x ==pic_off->UnsignedAttribute("x") && y==pic_off->UnsignedAttribute("y")) {
                    //if values are close, use the original
                    if (abs(int(x)-int(cp.x))<5) cp.x = x;
                    else choff->SetAttribute("x", cp.x);
                    if (abs(int(y)-int(cp.y))<5) cp.y = y;
                    else choff->SetAttribute("y", cp.y);
                    //Group Shape offset is usually zero, we keep it that way
                }
            }
    }
    pic_off->SetAttribute("x", cp.x);
    pic_off->SetAttribute("y", cp.y);
    pic_ext->SetAttribute("cx", cs.x);
    pic_ext->SetAttribute("cy", cs.y);

    return png_inner_fn;
}

static std::string ExtractTitle(tinyxml2::XMLElement* spTree) {
    //Search for the title placeholder on the path (under spTree): <p:sp><p:nvSpPr><p:nvPr><p:ph>
    //if ph has an attribute 'type' that is either 'title' or 'ctrTitle', we extract the text.
    std::string ret;
    for (tinyxml2::XMLElement* sp = spTree->FirstChildElement("p:sp"); sp; sp = sp->NextSiblingElement("p:sp")) {
        tinyxml2::XMLElement* const ph = get_path(sp, { "p:nvSpPr", "p:nvPr", "p:ph" });
        if (!ph) continue;
        const char* const type = ph->Attribute("type");
        if (!type) continue;
        using namespace std::string_view_literals;
        if (type!="title"sv && type!="ctrTitle"sv) continue;

        //cycle through the a:r elements (for all fragments) of the first paragraph
        for (tinyxml2::XMLElement* r = get_path(sp, { "p:txBody", "a:p", "a:r" }); r; r = r->NextSiblingElement("a:r"))
            if (tinyxml2::XMLElement* t = r->FirstChildElement("a:t"))
                if (const char* const text = t->GetText())
                    ret += text;
        if (ret.size()) break;
    }
    return ret;
}

std::string Print(const tinyxml2::XMLDocument& doc) {
    tinyxml2::XMLPrinter printer(nullptr, true);
    doc.Print(&printer);
    std::string ret(printer.CStr(), printer.CStrSize());
    if (ret.ends_with('\0')) ret.pop_back(); //TinyXML includes the terminating zero in CStrSize()
    return ret;
}

/** Adds the PNG file type tp [Content Types].xml
 * @param file_content the content of the [Content Types].xml file. We update it if needed
 * @returns An error message on a problem, an empty string if success and file_content
 *   remained unchanged (PNG type is already added), an empty optional if PNG type added.*/
std::optional<std::string> AddPNGFileTypeTo(std::string& file_content) {
    tinyxml2::XMLDocument content_types;
    content_types.Parse(file_content.c_str());
    if (content_types.Error())
        return StrCat("': ", tinyxml2::XMLDocument::ErrorIDToName(content_types.ErrorID()));
    tinyxml2::XMLElement* types = get_path(content_types, { "Types" });
    if (!types)
        return "': Cant find <types>.";

    bool found_png_type = false;
    for (tinyxml2::XMLElement* def = types->FirstChildElement("Default"); def && !found_png_type; def = def->NextSiblingElement("Default"))
        if (def->Attribute("Extension", "png"))
            return ""; //OK, no change to [Content Types].xml
    tinyxml2::XMLElement* png = types->InsertNewChildElement("Default");
    png->SetAttribute("Extension", "png");
    png->SetAttribute("ContentType", "image/png");
    file_content = Print(content_types);
    return {};
}

//returns new rId and inner filename (not from the root of the zip, but starting with 'media/').
//Also adds the new ref to the rels file ('Relations')
//On error we return an empty string and the error (and make no change to 'Relations')
std::pair<std::string, std::string> alloc_new_image(ZipArchive archive, tinyxml2::XMLElement *Relations) {
    //Generate a new unused rID
    unsigned rId = 0; //count how many relationships we have
    for (tinyxml2::XMLElement* child = Relations->FirstChildElement(); child; child = child->NextSiblingElement())
        rId++;
    std::string rId_str;
    while (true) {
        rId++;
        rId_str = "rId"+std::to_string(rId);
        bool found = false;
        for (tinyxml2::XMLElement* child = Relations->FirstChildElement(); child && !found; child = child->NextSiblingElement()) {
            const char* id = child->Attribute("Id");
            found = id && id==rId_str;
        }
        if (!found) break;
    };
    //Generate a new inner filename for the PNG
    std::vector<FileInZip> dir;
    if (std::string err = GetFilesInZip(archive, dir); err.size())
        return { "", "Error in "+archive.qfname_or_clipboard()+"': I cant read the directory." };
    std::erase_if(dir, [](const FileInZip& f) {
        //drop first dir, it can be 'ppt' or 'clipboard'
        const size_t dash = f.inner_filename.find('/');
        if (dash==std::string::npos) return true;
        return !std::string_view(f.inner_filename).substr(dash).starts_with("/media/image");
    });
    unsigned num = dir.size();
    std::string png_inner_fn;
    do {
        num++;
        png_inner_fn = "media/image"+std::to_string(num)+".png";
    } while (std::ranges::find(dir, png_inner_fn, &FileInZip::inner_filename)!=dir.end());

    //Add the new relation <Relationship Id = "rId2" Type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target = "../media/image12.png"/>
    Relations = Relations->InsertNewChildElement("Relationship");
    Relations->SetAttribute("Id", rId_str.c_str());
    Relations->SetAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image");
    Relations->SetAttribute("Target", ("../"+png_inner_fn).c_str());

    return { std::move(rId_str), std::move(png_inner_fn) };
}


std::string PptFile::add_images(ZipArchive archive, const std::vector<ChartToPPT>& charts_,
                                OpenNamedFileFunction OpenNamedFile) {
    if (charts_.empty()) return "";
    std::vector<const ChartToPPT*> charts;
    charts.reserve(charts_.size());
    for (const ChartToPPT& c:charts_) {
        if (c.slide<=0)
            if (std::string err = append_page(archive, &c.slide); err.size())
                return err;
        charts.push_back(&c);
    }
    //Sort _reverse_ by chart/slide ID. This is to have a larger rId for later Z-order charts within a page
    std::ranges::sort(charts, [](const ChartToPPT* a, const ChartToPPT* b) { return std::tie(a->slide, a->chart)>std::tie(b->slide, b->chart); });
    std::vector<Slide> slides;
    std::pair<int, int> slide_size;
    if (std::string err = parse_pages(archive, slides, &slide_size); err.size())
        return err;
    if (unsigned max_page = std::ranges::max_element(charts_, {}, &ChartToPPT::slide)->slide; slides.size()<max_page)
        return "Bad slide number "+std::to_string(max_page)+", "+archive.qfname_or_clipboard()+"' has only "+std::to_string(slides.size())+"' slides.";
    std::vector<FileInZip> files_in_zip;
    std::unordered_map<unsigned, size_t> slides_index; //maps PPT slides to an even index in files_in_zip.
    //If there are multiple charts on a slide, make sure we read the files of the slide only once
    for (const ChartToPPT* c : charts)
        if (!slides_index.contains(c->slide)) {
            slides_index[c->slide] = files_in_zip.size();
            files_in_zip.emplace_back("ppt/"+slides[c->slide-1].filename, "");
            files_in_zip.emplace_back("ppt/slides/_rels"+slides[c->slide-1].filename.substr(6)+".rels", "");
        }
    files_in_zip.emplace_back("[Content_Types].xml", "");
    std::string err = ExtractFromZip(archive, files_in_zip);
    if (files_in_zip.size()<slides_index.size()*2+1)
        return "Error in "+archive.qfname_or_clipboard()+"': I don't find a slide, its rels file or '[Content_Types].xml'.";

    //Start by adding PNG type to content types, if needed
    if (auto oErr = AddPNGFileTypeTo(files_in_zip.back().content); oErr && oErr->length())
        return "Error in "+archive.qfname_or_clipboard()+":"+files_in_zip.back().inner_filename+*oErr;
    else if (oErr.has_value())
        files_in_zip.pop_back(); //[Content Types].xml not changed, do not write out it again.

    for (const ChartToPPT* c :charts) {
        FileInZip& file_slide = files_in_zip[slides_index[c->slide]];
        FileInZip& file_rels = files_in_zip[slides_index[c->slide]+1];
        tinyxml2::XMLDocument slide, rels;
        slide.Parse(file_slide.content.c_str());
        if (slide.Error())
            return "Error in "+archive.qfname_or_clipboard()+":"+file_slide.inner_filename+"': "+tinyxml2::XMLDocument::ErrorIDToName(slide.ErrorID());
        tinyxml2::XMLElement* spTree = get_path(slide, { "p:sld", "p:cSld", "p:spTree" });
        if (!spTree)
            return "Error in "+archive.qfname_or_clipboard()+":"+file_slide.inner_filename+"': Cant find p:sld/p:cSld/p:spTree.";

        rels.Parse(file_rels.content.c_str());
        if (rels.Error())
            return "Error in "+archive.qfname_or_clipboard()+":"+file_rels.inner_filename+"': "+tinyxml2::XMLDocument::ErrorIDToName(rels.ErrorID());
        tinyxml2::XMLElement* rel = get_path(rels, { "Relationships" });
        if (!rel)
            return "Error in "+archive.qfname_or_clipboard()+":"+file_rels.inner_filename+"': Cant find <Relationships>.";
        bool rels_modified = false;

        std::string png_inner_fn;
        if (c->chart==0) {
            //Determine, how many charts we already have and update 'c.chart'
            for (tinyxml2::XMLElement* pic = spTree->FirstChildElement("p:pic"); pic; pic = pic->NextSiblingElement("p:pic"))
                if (ExtractChart(pic, rel, false))
                    c->chart++;
            c->chart++;

            //Determine pic location & size - centering
            GenericShape::int_xy cs = GenericShape::emu(c->data.chart_size);
            const double zoom = std::min({ 1., double(slide_size.first)/cs.x, double(slide_size.second)/cs.y });
            cs.x = int(cs.x*zoom);
            cs.y = int(cs.y*zoom);
            GenericShape::int_xy cp = {
                std::max(0, (slide_size.first-cs.x)/2),
                std::max(0, (slide_size.second-cs.y)/2) };

            std::string rId_str;
            std::tie(rId_str, png_inner_fn) = alloc_new_image(archive, rel);
            if (rId_str.empty()) return png_inner_fn; //this holds an error in this case
            rels_modified = true;

            //Generate a new id for the pic
            //Find all "<p:cNvPr id=\""
            std::string_view sld = file_slide.content;
            int id = 1;
            while (sld.size()) {
                const size_t pos = sld.find("<p:cNvPr id=\"");
                if (pos == std::string_view::npos) break;
                sld.remove_prefix(pos+13);
                int loc_id = 0;
                while (sld.size() && '0'<=sld.front() && sld.front()<='9') {
                    loc_id = loc_id*10+sld.front()-'0';
                    sld.remove_prefix(1);
                }
                if (loc_id>id) id = loc_id;
            }
            id++;

            std::string name = "Msc-generator "+c->data.chart_type;

            constexpr static const char pic_template[] = //ID, name, descr, rID, X, Y, W, H
                R"(<p:pic>)"
                    R"(<p:nvPicPr>)"
                        R"(<p:cNvPr id="%u" name="%s" descr="%s"/>)"
                        R"(<p:cNvPicPr>)"
                            R"(<a:picLocks noChangeAspect="1"/>)"
                        R"(</p:cNvPicPr>)"
                        R"(<p:nvPr/>)"
                    R"(</p:nvPicPr>)"
                    R"(<p:blipFill>)"
                        R"(<a:blip r:embed="%s"/>)"
                        R"(<a:stretch>)"
                           R"(<a:fillRect/>)"
                        R"(</a:stretch>)"
                    R"(</p:blipFill>)"
                    R"(<p:spPr>)"
                        R"(<a:xfrm>)"
                            R"(<a:off x="%u" y="%u"/>)"
                            R"(<a:ext cx="%u" cy="%u"/>)"
                        R"(</a:xfrm>)"
                        R"(<a:prstGeom prst="rect">)"
                            R"(<a:avLst/>)"
                        R"(</a:prstGeom>)"
                    R"(</p:spPr>)"
                R"(</p:pic>)";
            std::string alt_text = c->data.Escape();
            std::string buff(strlen(pic_template)+alt_text.length()+name.length()+rId_str.length()+255, '\0');
            if (const int len = snprintf(buff.data(), buff.size(), pic_template, id,
                                         name.c_str(), alt_text.c_str(), rId_str.c_str(),
                                         cp.x, cp.y, cs.x, cs.y)
                ; len<1)
                return "Internal error.";
            else
                buff.resize(len);

            tinyxml2::XMLDocument pic;
            pic.Parse(buff.c_str());
            tinyxml2::XMLElement* pic_elem = pic.FirstChildElement("p:pic");
            _ASSERT(pic_elem);
            spTree->InsertEndChild(pic_elem->DeepClone(&slide));
        } else {
            unsigned ch = c->chart;
            std::optional<ChartInPPT> oChart;
            tinyxml2::XMLElement* pic;
            std::unordered_set<std::string> rIds; //The rId of the other charts
            for (pic = spTree->FirstChildElement("p:pic"); pic; pic = pic->NextSiblingElement("p:pic")) {
                oChart = ExtractChart(pic, rel, false);
                if (!oChart) continue;
                if (--ch) rIds.insert(oChart->rId);
                else break;
            }
            if (pic==nullptr)
                return "Page "+std::to_string(c->slide)+" of "+archive.qfname_or_clipboard()+"' contains only "+std::to_string(c->chart-ch)+
                " charts and not "+std::to_string(c->chart);
            for (auto pic2 = pic->NextSiblingElement("p:pic"); pic2; pic2 = pic2->NextSiblingElement("p:pic"))
                if (auto oChart2 = ExtractChart(pic2, rel, false))
                    rIds.insert(oChart2->rId);
            const bool shared_rId = rIds.contains(oChart->rId);
            if (shared_rId) {
                //Two charts share the same png (due to copy pasting the chart within the slide in PPT)
                std::pair rId_fn = alloc_new_image(archive, rel);
                if (rId_fn.first.empty()) return std::move(rId_fn.second);
                rels_modified = true;
                png_inner_fn = UpdateChart(*oChart, *c, pic, rId_fn, slide_size);
            } else
                png_inner_fn = UpdateChart(*oChart, *c, pic, {}, slide_size);
        }
        //Serialize the slide and the rels file
        file_slide.content = Print(slide);
        if (rels_modified) file_rels.content = Print(rels);
        //Add/update the image
        FILE* fin = OpenNamedFile(c->png_fn.c_str(), false, true);
        if (!fin)
            return "Cannot read temporary file '"+c->png_fn+"'.";
        files_in_zip.emplace_back("ppt/"+png_inner_fn, ReadFile(fin)); //Warning: invalidates file_slide and file_rels due to emplace
        fclose(fin);
    }

    return AddToZip(archive, files_in_zip);
}

std::string PptFile::parse_charts(ZipArchive archive, std::vector<ChartInPPT>& charts, std::vector<std::string>* pages) {
    std::vector<Slide> slides;
    std::pair<int, int> slide_size;
    if (std::string err = parse_pages(archive, slides, &slide_size); err.size())
        return err;
    std::vector<FileInZip> files;
    for (const Slide& c : slides)
        files.emplace_back("ppt/"+c.filename, "");
    for (const Slide& c : slides)
        files.emplace_back("ppt/slides/_rels"+c.filename.substr(6)+".rels", "");
    if (std::string err = ExtractFromZip(archive, files); err.size())
        return err;
    if (slides.size()*2 != files.size())
        return "Some files missing from "+archive.qfname_or_clipboard()+"'.";
    for (unsigned page = 0; page<slides.size(); page++) {
        //Parse slide and rels file
        tinyxml2::XMLDocument slide, rels;
        slide.Parse(files[page].content.c_str());
        if (slide.Error())
            return "Error in "+archive.qfname_or_clipboard()+" page "+std::to_string(page+1)+":"+files[page].inner_filename+"': "+tinyxml2::XMLDocument::ErrorIDToName(slide.ErrorID());
        rels.Parse(files[slides.size()+page].content.c_str());
        if (rels.Error())
            return "Error in "+archive.qfname_or_clipboard()+" page "+std::to_string(page+1)+":"+files[slides.size()+page].inner_filename+"': "+tinyxml2::XMLDocument::ErrorIDToName(rels.ErrorID());

        //Get to spTree in slide and to Relations in rels
        tinyxml2::XMLElement* spTree = get_path(slide, { "p:sld", "p:cSld", "p:spTree" });
        if (!spTree)
            return "Error in "+archive.qfname_or_clipboard()+" page "+std::to_string(page+1)+":"+files[page].inner_filename+"': Cant find p:sld/p:cSld/p:spTree.";
        tinyxml2::XMLElement* Relationships = rels.FirstChildElement("Relationships");
        if (!Relationships)
            return "Error in "+archive.qfname_or_clipboard()+" page "+std::to_string(page+1)+":"+files[slides.size()+page].inner_filename+"': Cant find Relationships.";

        unsigned count = 0;
        //Cylce through all the "p:pic" elements and check if they match
        for (tinyxml2::XMLElement* pic = spTree->FirstChildElement("p:pic"); pic; pic = pic->NextSiblingElement("p:pic"))
            if (auto oChart = ExtractChart(pic, Relationships, false)) {
                oChart->slide = page+1;
                oChart->chart = ++count;
                charts.push_back(std::move(*oChart));
            }

        if (pages)
            pages->push_back(ExtractTitle(spTree));
    }
    return "";
}

/********************************************************************/

std::tuple<std::string, tinyxml2::XMLElement*, tinyxml2::XMLElement*, std::vector<FileInZip>>
ParseClipboard(std::string& zipped, tinyxml2::XMLDocument &xml, tinyxml2::XMLDocument &rels) { //we promise not to modify zipped
    std::tuple<std::string, tinyxml2::XMLElement*, tinyxml2::XMLElement*, std::vector<FileInZip>> ret = { "", nullptr, nullptr, {{"clipboard/drawings/drawing1.xml", ""}, {"clipboard/drawings/_rels/drawing1.xml.rels", ""}} };
    std::string& error = std::get<0>(ret);
    //TODO: ensure we have no other drawingX.xml file.
    error = ExtractFromZip(ZipArchive::FromMem(zipped), std::get<3>(ret));
    if (error.size()) return ret;
    if (std::get<3>(ret).size()!=2) error = "No drawings1.xml.";
    if (error.size()) return ret;

    xml.Parse(std::get<3>(ret)[0].content.c_str());
    if (xml.Error())
        error = StrCat("Error in clipboard content: ", tinyxml2::XMLDocument::ErrorIDToName(xml.ErrorID()));
    if (error.size()) return ret;
    std::get<1>(ret) = get_path(xml, { "a:graphic", "a:graphicData", "lc:lockedCanvas" });
    if (!std::get<1>(ret))
        error = "Error in clipboard content : Cant find p:sld/p:cSld/p:spTree.";
    if (error.size()) return ret;

    rels.Parse(std::get<3>(ret)[1].content.c_str());
    if (rels.Error())
        error = StrCat("Error in clipboard content: ", tinyxml2::XMLDocument::ErrorIDToName(rels.ErrorID()));
    if (error.size()) return ret;
    std::get<2>(ret) = rels.FirstChildElement("Relationships");
    if (!std::get<2>(ret))
        error = "Error in clipboard content : Cant find Relationships.";
    return ret;
}

PptClipboard::PptClipboard(std::string&& zipped_content_) : zipped_content(std::move(zipped_content_)) {
    tinyxml2::XMLDocument xml, rels;
    auto [err, spTree, Relationships, files] = ParseClipboard(zipped_content, xml, rels);
    if (err.empty()) {
        unsigned count = 0;
        for (tinyxml2::XMLElement* pic = spTree->FirstChildElement("a:pic"); pic; pic = pic->NextSiblingElement("a:pic"))
            if (auto oChart = ExtractChart(pic, Relationships, true)) {
                oChart->chart = ++count;
                charts.push_back(std::move(*oChart));
            }
    }
}


std::string PptClipboard::update(const std::vector<PptFile::ChartToPPT>& C, OpenNamedFileFunction OpenNamedFile) {
    if (charts.empty()) return "No charts to update.";
    tinyxml2::XMLDocument xml, rels;
    auto [err, lockedCanvas, Relationships, files] = ParseClipboard(zipped_content, xml, rels);
    if (err.size()) return std::move(err);
    for (const PptFile::ChartToPPT& c : C) {
        unsigned ch = c.chart;
        std::optional<PptFile::ChartInPPT> oChart;
        tinyxml2::XMLElement* pic;
        std::unordered_set<std::string> rIds; //The rId of the other charts
        for (pic = lockedCanvas->FirstChildElement("a:pic"); pic; pic = pic->NextSiblingElement("a:pic")) {
            oChart = ExtractChart(pic, Relationships, true);
            if (oChart && --ch==0) break;
            rIds.insert(oChart->rId);
        }
        if (pic==nullptr)
            return "Internal error 97dyugouqxvo";
        for (auto pic2 = pic->NextSiblingElement("a:pic"); pic2; pic2 = pic2->NextSiblingElement("a:pic"))
            if (auto oChart2 = ExtractChart(pic2, Relationships, true))
                rIds.insert(oChart2->rId);

        const bool shared_rId = rIds.contains(oChart->rId);
        std::string png_inner_fn = shared_rId
            ? UpdateChart(*oChart, c, pic, alloc_new_image(ZipArchive::FromMem(zipped_content), Relationships))
            : UpdateChart(*oChart, c, pic);
        //Serialize the slide
        files.front().content = Print(xml);

        FILE* fin = OpenNamedFile(c.png_fn.c_str(), false, true);
        if (!fin)
            return "Cannot read temporary file '"+c.png_fn+"'.";
        files.emplace_back("clipboard/"+png_inner_fn, ReadFile(fin));
        fclose(fin);
    }
    return AddToZip(ZipArchive::FromMem(zipped_content), files);
}

// use with X, Y, X, Y, chart type, chart data, X, Y
constexpr const char xml_clipboard_template[] =
R"(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>)"
R"(<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">)"
	R"(<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">)"
		R"(<lc:lockedCanvas xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">)"
			R"(<a:nvGrpSpPr>)"
				R"(<a:cNvPr id="0" name=""/>)"
				R"(<a:cNvGrpSpPr/>)"
			R"(</a:nvGrpSpPr>)"
			R"(<a:grpSpPr>)"
				R"(<a:xfrm>)"
					R"(<a:off x="0" y="0"/>)"
					R"(<a:ext cx="%u" cy="%u"/>)"
					R"(<a:chOff x="0" y="0"/>)"
					R"(<a:chExt cx="%u" cy="%u"/>)"
				R"(</a:xfrm>)"
			R"(</a:grpSpPr>)"
			R"(<a:pic>)"
				R"(<a:nvPicPr>)"
					R"(<a:cNvPr id="6" name="Msc-generator %s" descr="%s"/>)"
						R"(<a:cNvPicPr>)"
							R"(<a:picLocks noChangeAspect="1"/>)"
						R"(</a:cNvPicPr>)"
					R"(</a:nvPicPr>)"
					R"(<a:blipFill>)"
						R"(<a:blip xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="rId1"/>)"
							R"(<a:stretch>)"
								R"(<a:fillRect/>)"
							R"(</a:stretch>)"
						R"(</a:blipFill>)"
						R"(<a:spPr>)"
							R"(<a:xfrm>)"
								R"(<a:off x="0" y="0"/>)"
								R"(<a:ext cx="%u" cy="%u"/>)"
							R"(</a:xfrm>)"
							R"(<a:prstGeom prst="rect">)"
								R"(<a:avLst/>)"
							R"(</a:prstGeom>)"
						R"(</a:spPr>)"
					R"(</a:pic>)"
				R"(</lc:lockedCanvas>)"
			R"(</a:graphicData>)"
		R"(</a:graphic>)";

std::string PptClipboard::create(EmbedChartData&& data, std::string&& png_content) {
    std::string descr = data.Escape();
    std::string drawing1xml(strlen(xml_clipboard_template)+descr.length()+255, '\0');
    const XY slide_size = GetPhysicalPageSize(EPageSize::ON_SCREEN_4_3_L);
    const double zoom = data.chart_size.x>0 && data.chart_size.y>0
        ? std::min({ 1., slide_size.x/data.chart_size.x, slide_size.y/data.chart_size.y })
        : 1.;
    const auto [X,Y]= emu(data.chart_size*zoom);
    const int len = snprintf(drawing1xml.data(), drawing1xml.size(), xml_clipboard_template,
                             X, Y, X, Y, data.chart_type.c_str(), descr.c_str(), X, Y);
    if (len<1) return {};
    drawing1xml.resize(len);

    std::vector<FileInZip> files = {
        {"clipboard/drawings/drawing1.xml", std::move(drawing1xml)},
        {"clipboard/media/image1.png", std::move(png_content)}
    };
    std::string zip{ reinterpret_cast<const char*>(default_clipboard_ppt), sizeof(default_clipboard_ppt) };
    if (AddToZip(ZipArchive::FromMem(zip), files).size()) zip = {};
    return zip;
}
