/*
    This file is part of Msc-generator.
	Copyright (C) 2008-2024 Zoltan Turanyi
	Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

/** @file canvas.cpp The implementation of drawing in Msc generator.
 * @ingroup libcgencommon_files
 *
 * Use http://www.flounder.com/metafileexplorer.htm for debugging EMF output.
 */
#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf


#include <algorithm>
#include <optional>
#define _USE_MATH_DEFINES
#include <cmath>
#include <iostream>
#include <cassert>
#include <cairo-version.h>
#include <cairo-ps.h>
#include <cairo-pdf.h>
#include <cairo-svg.h>
#include <png.h>
#ifdef __GNUC__
#include <netinet/in.h>  //htonl, nothl
#endif
#include "cgen_attribute.h" //for enumencapsulator
#include "utf8utils.h"
#include "canvas.h"
#include "style.h"
#include "cgen_shapes.h"
#include "version.h"
#include "cgencommon.h"
#ifdef CAIRO_HAS_FT_FONT
#include "cairo-ft.h"
#endif

#ifndef M_PI
/** My own definition of PI*/
#define M_PI 3.14159265358979323846
#endif

#ifndef CAIRO_HAS_PNG_FUNCTIONS
#warning Cairo misses PNG functions, this may be fatal.
#endif

#ifndef CAIRO_HAS_PDF_SURFACE
#error Cairo misses PDF functions, resulting exec will not have PDF support
#endif

#ifndef CAIRO_HAS_PS_SURFACE
#error Cairo misses PS functions, resulting exec will not have EPS support
#endif

#ifndef CAIRO_HAS_SVG_SURFACE
#error Cairo needs SVG functions, resulting exec will not have SVG support
#endif

const bool do_not_draw_fonts = getenv("MSCGEN_NOFONT") && getenv("MSCGEN_NOFONT")[0] && getenv("MSCGEN_NOFONT") != std::string_view("0");

/***PNG read-write routines*******************************************************/

namespace pngutil {

struct PNGCRC
{
    /* Table of CRCs of all 8-bit messages. */
    uint32_t crc_table[256];

    /* Make the table for a fast CRC. */
    PNGCRC()
    {
        uint32_t c;
        int n, k;

        for (n = 0; n < 256; n++) {
            c = (uint32_t)n;
            for (k = 0; k < 8; k++) {
                if (c & 1)
                    c = 0xedb88320 ^ (c >> 1);
                else
                    c = c >> 1;
            }
            crc_table[n] = c;
        }
    }

    /* Update a running CRC with the bytes buf[0..len-1]--the CRC
    should be initialized to all 1's, and the transmitted value
    is the 1's complement of the final running CRC (see the
    crc() routine below). */

    uint32_t crc(unsigned char *buf, int len)
    {
        uint32_t c = 0xffffffff;
        int n;

        for (n = 0; n < len; n++) {
            c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
        }
        return c ^ 0xffffffff;
    }
};

PNGCRC CRC;


#define PNG_ITXT_KEY_STRING "Msc-generator source"
#define PNG_ITXT_ALT_TEXT "new-embedding-format"

EmbedChartData ParseKey(std::string_view key)
{
    //Valid key syntax are
    // - Msc-generator source  => assume signalling (no version), chart text is plain text
    // - Msc-generator source,<lang> => use language (no version), chart text is plain text
    // - Msc-generator source,<lang>,vA.B[.C] => lang + version, chart text is plain text
    // - Msc-generator source,new-embedding-format,vA.B[.C] => chart text is embedded alt-text format carrying lang, version, chart text and more
    EmbedChartData ret;
    if (strncmp(key.data(), PNG_ITXT_KEY_STRING, sizeof(PNG_ITXT_KEY_STRING)-1)) { //-1 to not include terminating zero
        //not a key for us.
    } else {
        //Test and parse for lang and version
        auto l = StringFormat::Split(StringWithPos(key, {})); //split by comma
        if (l.size()>=3) {
            int r = sscanf(l[2].name.c_str(), "v%d.%d.%d", &ret.version[0],
                           &ret.version[1], &ret.version[2]);
            if (r==2) ret.version[2] = 0;
            else if (r!=3) ret.version = { -1,-1,-1 }; //unreadable version
        } else
            ret.version = { -1,-1,-1 };
        if (l.size()>=2)
            ret.chart_type = l[1].name;
         else
            ret.chart_type = "signalling";
    }
    return ret;
}

#define XSTR(s) STR(s)
#define STR(s) #s
std::string ComposeKey() {
    return PNG_ITXT_KEY_STRING "," PNG_ITXT_ALT_TEXT ",v" XSTR(LIBMSCGEN_MAJOR) "." XSTR(LIBMSCGEN_MINOR) "." XSTR(LIBMSCGEN_SUPERMINOR);
}
#undef STR
#undef XSTR

std::pair<EPNGResult, std::vector<EmbedChartData>>
ReadPNG(FILE *png_file)
{
    if (png_file==nullptr)
        return { EPNGResult::READ_ERROR,{} };
    png_byte      buf[8];

    /* read and check signature in PNG file */
    const size_t r = fread(buf, 1, 8, png_file);
    if (r != 8)
        return { EPNGResult::READ_ERROR,{} };

    if (!png_check_sig(buf, 8))
        return { EPNGResult::INVALID_PNG,{} };

    /* create png and info structures */
    png_struct *png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                                 NULL, NULL, NULL);
    if (!png_ptr)
        return { EPNGResult::READ_ERROR,{} };

    png_info *info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return { EPNGResult::READ_ERROR,{} };
    }

    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return { EPNGResult::READ_ERROR,{} };
    }

    /* set up the input control for C streams */
    png_init_io(png_ptr, png_file);
    png_set_sig_bytes(png_ptr, 8);  /* we already read the 8 signature bytes */

    /* read the file information */
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr);

    /* Now find if there are any Msc-generator chunks.*/
    std::vector<EmbedChartData> chart_text;
    png_text *text;
    const int num_text_chunks = png_get_text(png_ptr, info_ptr, &text, nullptr);
    for (int u = 0; u<num_text_chunks; u++)
        if (auto k = ParseKey(text[u].key); !k.empty()) {
            chart_text.push_back(std::move(k));
            chart_text.back().chart_text = std::string_view(text[u].text, text[u].itxt_length);
            if (chart_text.back().chart_type==PNG_ITXT_ALT_TEXT) {
                if (auto chart = EmbedChartData::ParseEscapedText(chart_text.back().chart_text); !chart.empty())
                    chart_text.back() = std::move(chart);
                else
                    chart_text.pop_back();
            }
        }

    //Note that libpng does not support removing a text chunk from png_info
    //So we keep the Msc-generator source chunks in the returned png_info.
    //Which means we may append the text again at writing. This results in two
    //text source chunks potentially in one png file. We consider that valid.
    //Hence: remove replicated chunks - keep only that of the largest version
    std::sort(chart_text.begin(), chart_text.end(),
              [](const EmbedChartData &a, EmbedChartData&b)
              { return std::tie(a.chart_type, a.version)< std::tie(b.chart_type, b.version); });
    for (size_t i = 1; i<chart_text.size(); /*nope*/)
        if (chart_text[i-1].chart_type==chart_text[i].chart_type)
            chart_text.erase(chart_text.begin()+i-1);
        else
            i++;
    png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
    return { EPNGResult::OK, std::move(chart_text) };
}

/* Adds a chart text chunk to a PNG file.*/
EPNGResult AddToPNG(FILE *read_file, FILE *write_file, const EmbedChartData& chart)
{
    unsigned char sig[8];
    static const unsigned char can_sig[8] = { 137, 80, 78, 71, 13, 10, 26, 10 };
    if (8!=fread(sig, 1, 8, read_file)) return EPNGResult::READ_ERROR;
    if (!std::equal(sig, sig+8, can_sig)) return EPNGResult::INVALID_PNG;
    if (8!=fwrite(can_sig, 1, 8, write_file)) return EPNGResult::WRITE_ERROR;
    bool did = false; //have we written out lang data?
    while (!feof(read_file)) {
        //LENGTH (4 bytes, big endian), CHUNK TYPE (4 bytes), [DATA], CRC (4 bytes)
        struct
        {
            uint32_t len;
            unsigned char type[4];
        } hdr;
        const size_t r = fread(&hdr.len, 1, 8, read_file);
        //if we have already processed the IEND chunk we can stop
        if (r==0 && did) return EPNGResult::OK;
        if (8!=r) return EPNGResult::READ_ERROR;
        const uint32_t len = ntohl(hdr.len);
        if (len>=0x80000000) return EPNGResult::INVALID_PNG;
        std::vector<unsigned char> buff;
        if (len) {
            buff.resize(len);
            if (len!=fread(buff.data(), 1, len, read_file)) return EPNGResult::READ_ERROR;
        }
        uint32_t crc;
        if (4!=fread(&crc, 1, 4, read_file)) return EPNGResult::READ_ERROR;
        static const unsigned char iTXt[4] = { 105, 84, 88, 116 };
        static const unsigned char IEND[4] = { 73, 69, 78, 68 };
        if (std::equal(IEND, IEND+4, hdr.type)) {
            if (did) return EPNGResult::INVALID_PNG; //Aww, the second time we hit an IEND chunk.
                                                     //If the end chunk, write out our language data
            const std::string key = ComposeKey();
            if (key.length()) {
                std::string escaped = chart.Escape();
                const size_t chunk_len = 4+key.length()+5+escaped.length()+4; //type + key + 3 null terminators +2 bytes for comp method + text +crc
                std::vector<unsigned char> data;
                data.resize(chunk_len);
                char *p = (char*)data.data();
                memcpy(p, iTXt, 4);
                p += 4;
                strcpy(p, key.c_str());
                p += key.length()+1; //also step over terminating zero
                p[0] = 0; //uncompressed text
                p[1] = 0; //compression is set to 0
                p[2] = 0; //terminator for language tag
                p[3] = 0; //terminator for translated key
                p += 4;
                for (auto c : escaped)
                    *p++ = c;
                const uint32_t mycrc = htonl(CRC.crc((unsigned char*)data.data(), uint32_t(chunk_len-4))); //crc type+data, not len, not crc itself
                memcpy(p, &mycrc, 4);
                uint32_t belen = htonl(uint32_t(chunk_len-8)); //chunk len written out excludes len, type and crc
                if (4 != fwrite(&belen, 1, 4, write_file)) return EPNGResult::WRITE_ERROR;
                if (chunk_len != fwrite(data.data(), 1, chunk_len, write_file)) return EPNGResult::WRITE_ERROR;
                did = true;
            } else {
                _ASSERT(0); //No valid key could have been generated from this language
            }
            //Now fall through and write out the end chunk.
        } else if (std::equal(iTXt, iTXt+4, hdr.type)) {
            //if chunk is an iTXt chunk with Msc-generator as a key, drop it.
            //The read data starts with the null terminated key
            auto l = ParseKey((const char*)buff.data());
            if (!l.empty()) //valid Msc-generator key
                continue; //do not write out
        }
        //Else write chunk unmodified
        if (8!=fwrite(&hdr, 1, 8, write_file)) return EPNGResult::WRITE_ERROR;
        if (len && len!=fwrite(buff.data(), 1, len, write_file)) return EPNGResult::WRITE_ERROR;
        if (4!=fwrite(&crc, 1, 4, write_file)) return EPNGResult::WRITE_ERROR;
    }
    if (!did) return EPNGResult::INVALID_PNG; //Aww, we didn't not hit an IEND chunk.
    return EPNGResult::OK;
}

/** Reads a PNG file, extracts iTXt chunks, recognize them, select one and return errors if needed.
 * @param [in] in The png file to read.
 * @param [in] languages The set of languages we recognize
 * @param Error Where we report the error messages
 * @param [in] pos The line number where we shall report the errors.
 * @returns the chart we found. In case of errors, we return empty and add errors to Error.*/
EmbedChartData Select(FILE *in, const LanguageCollection &languages,
                      MscError &Error, const FileLineCol &pos)
{
    if (in==nullptr) {
        Error.FatalError(pos, "Could not find input png file.");
        return {};
    }
    auto [res, charts] = pngutil::ReadPNG(in);
    switch (res) {
        default:
        case pngutil::EPNGResult::INTERNAL_ERROR:
            Error.FatalError(pos, "Out-of memory reading the png input file.");
            break;
        case pngutil::EPNGResult::INVALID_PNG:
            Error.FatalError(pos, "The input PNG file appears invalid.");
            break;
        case pngutil::EPNGResult::WRITE_ERROR:
            _ASSERT(0);
            FALLTHROUGH;
        case pngutil::EPNGResult::READ_ERROR:
            Error.FatalError(pos, "Error reading the input png file.");
            break;
        case pngutil::EPNGResult::OK:
            int num_recognized = 0;
            ptrdiff_t selected = -1;
            for (EmbedChartData &p : charts) {
                if (languages.GetLanguage(p.chart_type)) {
                    num_recognized++;
                    //we select the largest version
                    if (num_recognized==1 || charts[selected].version < p.version)
                        selected = &p-&charts.front();
                }
            }
            if (charts.empty())
                Error.FatalError(pos, "No iTXt chunk with an Msc-generator source text found in png input.");
            else if (num_recognized==0)
                Error.FatalError(pos, "No iTXt chunk with an Msc-generator source text with a language I recognize found in png input.");
            else if (num_recognized>1)
                Error.Warning(pos, "More than one applicable iTXt chunks found. Selecting one.");
            //if we found more than one chunk or the one we found is not OK, list the chunks.
            if (charts.size()>1 || num_recognized==0)
                for (auto &p : charts) {
                    std::string msg = "Found: "+p.chart_type;
                    if (nullptr==languages.GetLanguage(p.chart_type))
                        msg += " (unrecognized)";
                    else if (charts.size() && num_recognized && &p-&charts.front()==selected)
                        msg += " (selected)";
                    if (p.version[0]>0)
                        msg += StrCat(" (v", std::to_string(p.version[0]), '.',
                                      std::to_string(p.version[1]), '.', std::to_string(p.version[2]), ")");
                    Error.Warning(pos, pos, msg);
                }
            if (num_recognized)
                return std::move(charts[selected]);
    }
    return { };
}



} //namespace

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


std::mutex cairo_text_mutex;

/** Possible values for page size*/
template<> const char EnumEncapsulator<EPageSize>::names[][ENUM_STRING_LEN] =
    {"invalid", "none",
    "A0P", "A0L", "A1P", "A1L", "A2P", "A2L",
    "A3P", "A3L", "A4P", "A4L", "A5P", "A5L",
    "A6P", "A6L", "LETTER_P", "LETTER_L",
    "LEGAL_P", "LEGAL_L", "LEDGER", "TABLOID",
    "WIDESCREEN_P", "WIDESCREEN_L", "ON_SCREEN_4_3_P", "ON_SCREEN_4_3_L",
    "ON_SCREEN_16_9_P", "ON_SCREEN_16_9_L", "ON_SCREEN_16_10_P", "ON_SCREEN_16_10_L", ""};

/** Human readable names for page sizes indexed by EPageSize.*/
const char VerbosePageSizeNames[][ENUM_STRING_LEN] =
{"invalid", "none",
"A0 portrait", "A0 landscape", "A1 portrait", "A1 landscape", "A2 portrait", "A2 landscape",
"A3 portrait", "A3 landscape", "A4 portrait", "A4 landscape", "A5 portrait", "A5 landscape",
"A6 portrait", "A6 landscape", "letter portrait", "LETTER_ landscape",
"legal portrait", "legal landscape", "ledger", "tabloid",
"widescreen portrait", "widescreen landscape",
"on-screen show (4:3) portrait", "on-screen show (4:3) landscape",
"on-screen show (16:9) portrait", "on-screen show (16:9) landscape",
"on-screen show (16:10) portrait", "on-screen show (16:10) landscape", ""};

EPageSize ConvertPageSize(std::string_view c)
{
    EPageSize ret = EPageSize::NO_PAGE;
    Convert(c, ret);
    return ret;
}

std::string_view ConvertPageSize(EPageSize ps)
{
    return EnumEncapsulator<EPageSize>::names[static_cast<int>(ps)];
}

std::string_view ConvertPageSizeVerbose(EPageSize ps)
{
    return VerbosePageSizeNames[static_cast<int>(ps)];
}

/** Returns the page size in points (1/72 inch)*/
XY GetPhysicalPageSize(EPageSize ps)
{
    if (ps>=EPageSize::MAX_PAGE || ps<=EPageSize::NO_PAGE) return XY(0,0);
    static const double ISO_size[] = {118.9, 84.1, 59.4, 42.0, 29.7, 21.0, 14.8, 10.5};
    const double mul = PT_PER_CM;
    XY ret;
    switch (ps) {
    case EPageSize::LETTER_P:
    case EPageSize::LETTER_L:
        ret = XY(8.5, 11)*72; break;
    case EPageSize::LEGAL_P:
    case EPageSize::LEGAL_L:
        ret = XY(8.5, 14)*72; break;
    case EPageSize::LEDGER:
    case EPageSize::TABLOID:
        ret = XY(17, 11)*72; break;
    case EPageSize::WIDESCREEN_P:
    case EPageSize::WIDESCREEN_L:
        ret = XY(13.3333, 7.5).SwapXY()*72; break;
    case EPageSize::ON_SCREEN_4_3_P:
    case EPageSize::ON_SCREEN_4_3_L:
        ret = XY(10, 7.5).SwapXY()*72; break;
    case EPageSize::ON_SCREEN_16_9_P:
    case EPageSize::ON_SCREEN_16_9_L:
        ret = XY(10, 5.625).SwapXY()*72; break;
    case EPageSize::ON_SCREEN_16_10_P:
    case EPageSize::ON_SCREEN_16_10_L:
        ret = XY(10, 6.25).SwapXY()*72; break;
    default:
        ret = XY(0, 0); break;
    case EPageSize::A0P:
    case EPageSize::A0L:
    case EPageSize::A1P:
    case EPageSize::A1L:
    case EPageSize::A2P:
    case EPageSize::A2L:
    case EPageSize::A3P:
    case EPageSize::A3L:
    case EPageSize::A4P:
    case EPageSize::A4L:
    case EPageSize::A5P:
    case EPageSize::A5L:
    case EPageSize::A6P:
    case EPageSize::A6L:
        //ISO sizes
        ret.x = mul*ISO_size[unsigned(ps)/2];
        ret.y = mul*ISO_size[unsigned(ps)/2 - 1];
    }
    //handle landscape
    if (unsigned(ps)%2)
        ret.SwapXY();
    return ret;
}

/** Creates an empty canvas, which ignores everything drawn on it.
 * @param [in] dummy_ If true, we create a dummy canvas, where all
 *             operations are successful, have no effect and we use as little
 *             resources as possible. GetContext() will return nullptr.
 *             If false, we actually create a PNG surface that can be used
 *             to query font sizes. Drawing on this will fire a debug
 *             assertion or do nothing for a release build.*/
Canvas::Canvas(Empty e) :
    outFile(nullptr), finalOutFile(nullptr), surface(nullptr), cr(nullptr), candraw(false),
    dummy(e==Canvas::Empty::Dummy), fake_dash_offset(0), outType(PNG), total(0,0,0,0),
    external_surface(false), copyrightTextHeight(0),
    user_scale{1,1}, raw_page_clip(0, 0, 0, 0), h_alignment(0), v_alignment(0)
#ifdef CAIRO_HAS_WIN32_SURFACE
	, stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(false)
#endif
{
    if (!dummy) {
        SetLowLevelParams();
        CreateSurface(XY(0, 0));
        if (error.size()) return;
        CreateNilContext();
        if (error.size()) CloseOutput();
    }
}

/**Creates a canvas to draw one page or all of the chart to a (single-page) file.
  After creation you can start drawing in chart coordinates and the right
  page will show. Call PrepareForHeaderFooter() before drawing the
  automatic heading and copyright text.
  @param [in] ot The type of the file. Cannot be WIN or PRINTER, since
                 these are not to be written to a file. These create a
                 Canvas in error.
  @param [in] tot The total size of the chart in the canvas coordinate space.
  @param [in] ctexth The height of the copyright text.
  @param [in] fn The filename. UTF-8 on both Linux and Windows. Empty for stdout.
  @param [in] scale The output shall be this much bigger than `tot`.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any).
  @param [in] page Specifies which page we the Canvas for, the index of
                   the first page is 1. 0 means we want to treat
                   the whole chart as one page.*/
Canvas::Canvas(EOutputType ot, const Block &tot, double ctexth,
               std::string_view fn, const XY &scale,
               const PBDataVector *pageBreakData, unsigned page) :
    outFile(nullptr), finalOutFile(nullptr), surface(nullptr), cr(nullptr), candraw(false),
    fake_dash_offset(0), outType(ot), total(tot),
    external_surface(false), copyrightTextHeight(ctexth),
    user_scale(scale), raw_page_clip(0,0,0,0), h_alignment(0), v_alignment(0)
#ifdef CAIRO_HAS_WIN32_SURFACE
	, stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(false)
#endif
{
    _ASSERT(ot!=WIN);
    if (ot==WIN || ot==PRINTER) {
        error = "Internal error: WIN/PRINTER DC used when drawing to file.";
        return;
    }

    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, page);
#ifdef CAIRO_HAS_WIN32_SURFACE
    original_device_size.x = (pbData ? pbData->headingLeftingSize.x : 0) + origSize.x;
    original_device_size.y = (pbData ? pbData->headingLeftingSize.y : 0) + origSize.y + copyrightTextHeight;
    original_device_size.x *= user_scale.x;
    original_device_size.y *= user_scale.y;
#endif
    XY size = origSize + (pbData ? pbData->headingLeftingSize : XY(0,0));
    size.y += copyrightTextHeight;
    size.x *= scale.x;
    size.y *= scale.y;
    SetLowLevelParams(size);
    size *= fake_scale;

    fileName = fn;
    if (does_graphics()) {
        if (ot==EMF || ot==WMF || ot==EMFWMF) {
            //For metafiles, there is no need to open a FILE*
            outFile = nullptr;
            //...But we cannot handle stdout, either
            if (fileName.empty()) {
                error = "This file type cannot be sent to stdout and requires a filename.";
                return;
            }
        } else if (fn.size()) {
#ifdef _WIN32
            const std::wstring buff = ConvertFromUTF8_to_UTF16(fileName);
            _wfopen_s(&outFile, buff.c_str(), L"wb");
#else
            outFile = fopen(fileName.c_str(), "wb"); //We assume on Linux fopen eats UTF-8 filenames
#endif
            if (!outFile) {
                error = StrCat("Could not open output file: '", fn, "'.");
                return;
            }
        } else {
            fileName = "<stdout>";
            outFile = stdout;
        }
        CreateSurface(size);
        if (error.size()) return;
        CreateContext(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0));
        if (error.size()) CloseOutput();
        else candraw = true;
    } else {
        if (fn.empty()) {
            error = "This file type cannot be sent to stdout and requires a filename.";
            return;
        }
        _ASSERT(outType==PPT);
        if (!ppt.new_file(fn, 1, GetPhysicalPageSize(DEFAULT_PPT_PAGE_SIZE))) {
            error = ppt.error();
            ppt.del_file();
            return;
        }
        ppt.set_page_size(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0),
                          user_scale, h_alignment, v_alignment, raw_page_clip, copyrightTextHeight);
        CreateSurface(XY(0, 0));
        if (error.size()) return;
        CreateNilContext();
        if (error.size()) CloseOutput();
    }
}


/** Calculates the scaling to use, trying out potentially several suggested scaling
    values suggested by the user.
  A static function that enables calling without an actual canvas.
  @param [in] total The total size of the chart in the canvas coordinate space.
  @param [in] scale A series of scale factors to try. We go through them
                    and see if the pages of the chart fit the fixed page size
                    with the scale factor. We try in the list until we find
                    a scale factor that fits. -1 value means "fit width",
                    -2 value means "fit page" - this one scales to fit the
                    largest page and uses the same factor for all pages and
                    always fits. If none of the values fit, we use the last one.
  @param [in] pageSize The size of the fixed page in points/pixels.
  @param [in] margins The margin size in points/pixels. The order is
                      left,right, up,down.
  @param [in] ctexth The height of the copyright text.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any.)
  @returns the scaling to use. It can be (0,0) if the margins are too large for
           the page size (no meaningful drawing area on paper)*/
XY Canvas::DetermineScaling(const Block &total, const std::vector<XY> &scale,
                            const XY &pageSize, const double margins[4],
                            double ctexth, const PBDataVector *pageBreakData)
{
    XY ret(1, 1);
    if (scale.size()==0) return ret;
    //If no page size is specified, we use first value in 'scale' that is
    //deterministic (non-auto, non-width) or (1,1) if none.
    if (pageSize.x<=0 || pageSize.y<=0) {
        for (auto &xy : scale)
            if (xy.x>0 && xy.y>0) { ret = xy; break; }
        return ret;
    }
    //Determine net page size (physical size - margins)
    Block raw_page_clip = Block(XY(0, 0), pageSize);
    if (margins) {
        raw_page_clip.x.from += margins[0];
        raw_page_clip.x.till -= margins[1];
        raw_page_clip.y.from += margins[2];
        raw_page_clip.y.till -= margins[3];
    }
    if (raw_page_clip.x.from >= raw_page_clip.x.till ||
        raw_page_clip.y.from >= raw_page_clip.y.till) {
        return XY(0,0); //too big margins
    }
    //Determine scaling
    for (unsigned s = 0; s<scale.size(); s++) {
        if (scale[s].x<=0) { //auto or width
            _ASSERT(scale[s].x==-2 || scale[s].x==-1);
            _ASSERT(scale[s].y==scale[s].x);
            //Set scale to what is dictated by width
            ret.x = DBL_MAX;
            for (unsigned p = 1; p<=pageBreakData->size(); p++) {
                auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, p);
                double x_size = origSize.x + (pbData ? pbData->headingLeftingSize.x : 0);
                ret.x = std::min(ret.x, raw_page_clip.x.Spans()/x_size);
            }
            //now ret.x contains the scaling that will fit all page widths into the available page width
            if (scale[s].x==-2) { //auto
                //check if we need to reduce scale for all page length to fit, as well
                for (unsigned p = 1; p<=pageBreakData->size(); p++) {
                    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, p);
                    double y_size = origSize.y + ctexth + (pbData ? pbData->headingLeftingSize.y : 0);
                    ret.x = std::min(ret.x, raw_page_clip.y.Spans()/y_size);
                }
                ret.y = ret.x;
                break; //auto always fits
            }
            ret.y = ret.x; //width is always a proportional scale
        } else
            ret = scale[s];
        //Check if we fit
        bool oversize = false;
        for (unsigned p = 1; p<=pageBreakData->size(); p++) {
            auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, p);
            const double x_size = origSize.x + (pbData ? pbData->headingLeftingSize.x : 0);
            const double y_size = origSize.y + ctexth + (pbData ? pbData->headingLeftingSize.y : 0);
            oversize = raw_page_clip.x.Spans() < x_size*ret.x || raw_page_clip.y.Spans() < y_size*ret.y;
            if (oversize) break;
        }
        if (!oversize) break; //if all pages fit, we stick to this ret
    }
    return ret;
}

/** Creates a canvas to draw the chart to a fixed-page-size multi-page file & starts page No. 1.
  After creation you can start drawing in chart coordinates and the first page
  page will appear. Call PrepareForHeaderFooter() before drawing the
  automatic heading and copyright text. Then call TurnPage and repeat
  for second page and so on.
  @param [in] ot The type of the file. Currently must be PDF or PPT, since
                 this is the only format supporting multiple pages in a single
                 file. Others create a Canvas in error.
  @param [in] tot The total size of the chart in the canvas coordinate space.
  @param [in] fn The filename. UTF-8 on both linux and Windows. Empty for stdout.
  @param [in] scale The scaling to apply.
  @param [in] pageSize The size of the fixed page in points/pixels.
  @param [in] margins The margin size in points/pixels. The order is
                      left,right, up,down.
  @param [in] ha The horizontal alignment -1=left, 0=center, 1=right
  @param [in] va The vertical alignment -1=up, 0=center, 1=down
  @param [in] ctexth The height of the copyright text.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any.)*/
Canvas::Canvas(EOutputType ot, const Block &tot, std::string_view fn, const XY &scale,
               const XY& pageSize, const double margins[4], int ha, int va,
               double ctexth, const PBDataVector* pageBreakData) :
    outFile(nullptr), finalOutFile(nullptr), surface(nullptr), cr(nullptr), candraw(false),
    fake_dash_offset(0), outType(ot), total(tot),
    external_surface(false), copyrightTextHeight(ctexth), user_scale(scale),
    raw_page_clip(0,0,0,0), h_alignment(ha), v_alignment(va)
#ifdef CAIRO_HAS_WIN32_SURFACE
	, stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(false)
#endif
{
    _ASSERT(ot==PDF || ot==PPT);

    SetLowLevelParams();
    _ASSERT(fake_scale==1); //We cannot increase scale with fixed paper sizes...
    if (does_graphics()) {
        _ASSERT(ot==PDF);
        if (fn.size()) {
            fileName = fn;
#ifdef _WIN32
            const std::wstring buff = ConvertFromUTF8_to_UTF16(fileName);
            outFile = _wfopen(buff.c_str(), L"wb");
#else
            outFile = fopen(fileName.c_str(), "wb"); //We assume on Linux fopen eats UTF-8 filenames
#endif
            if (!outFile) {
                error = StrCat("Could not open output file '", fn, "'.");
                return;
            }
        } else {
            fileName = "<stdout>";
            outFile = stdout;
        }
    CreateSurface(pageSize);
    if (error.size()) return;

    } else {
        _ASSERT(ot==PPT);
        if (fn.empty()) {
            error = "This file type cannot be sent to stdout and requires a filename.";
            return;
        }
        _ASSERT(outType==PPT);
        if (!ppt.new_file(fn, pageBreakData ? pageBreakData->size() : 1, pageSize)) {
            error = ppt.error();
            ppt.del_file();
            return;
        }
        CreateSurface(XY(0, 0));
        if (error.size()) return;
    }

    //Determine net page size (physical size - margins)
    raw_page_clip = Block(XY(0,0), pageSize);
    if (margins) {
        raw_page_clip.x.from += margins[0];
        raw_page_clip.x.till -= margins[1];
        raw_page_clip.y.from += margins[2];
        raw_page_clip.y.till -= margins[3];
    }
    if (raw_page_clip.x.from >= raw_page_clip.x.till ||
        raw_page_clip.y.from >= raw_page_clip.y.till) {
        error = "Too big margins - no print area left!";
        fatal = false;
        return;
    }
    //Create context for first page
    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, 1);
#ifdef CAIRO_HAS_WIN32_SURFACE
    original_device_size.x = (pbData ? pbData->headingLeftingSize.x : 0) + origSize.x;
    original_device_size.y = (pbData ? pbData->headingLeftingSize.y : 0) + origSize.y + copyrightTextHeight;
    original_device_size.x *= user_scale.x;
    original_device_size.y *= user_scale.y;
#endif
    if (does_graphics()) {
        CreateContext(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0));
    } else {
        ppt.set_page_size(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0),
                          user_scale, h_alignment, v_alignment, raw_page_clip, copyrightTextHeight);
        CreateNilContext();
    }
    if (error.size()) {
        CloseOutput();
        return;
    }
    candraw = true;
}

/** Prepares a multi-page Canvas to print a new page.
  We flush the previous page and change the translation matrix for the
  new page. After this call you can start drawing in chart coordinates
  and page `next_page` page will appear. Call PrepareForHeaderFooter()
  before drawing the automatic heading and copyright text for `next_page`.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any).
  @param [in] next_page Specifies which page the next page will be.
                        The index of the first page is 1. 0 means we want
                        to treat the whole chart as one page.
                        In principle you can print a page multiple times
                        or in whatever order or print the whole chart as
                        one of the pages - but why would you do so?
  @param error If the height next page does not fit the output page size
               This is where we emit a warning (if not nullptr).
  @returns False if an error (but not warning) has been encountered,
           irrespective of the value of `error` parameter.
*/
bool Canvas::TurnPage(const PBDataVector* pageBreakData, unsigned next_page,
                      MscError *pError) {
    if (does_graphics()) {
        if (cannot_draw()) return true;
        //Complete the old page
        cairo_destroy(cr);
        cairo_surface_show_page(surface);
    } else {
        _ASSERT(outType==PPT);
        ppt.flush_page();
    }
    //Cretate context for next page
    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, next_page);
#ifdef CAIRO_HAS_WIN32_SURFACE
    original_device_size.x = (pbData ? pbData->headingLeftingSize.x : 0) + origSize.x;
    original_device_size.y = (pbData ? pbData->headingLeftingSize.y : 0) + origSize.y + copyrightTextHeight;
    original_device_size.x *= user_scale.x;
    original_device_size.y *= user_scale.y;
#endif
    if (does_graphics()) {
        CreateContext(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0));
        if (error.size()) {
            CloseOutput();
            ErrorAfterCreation(pError, pageBreakData);
            candraw = false;
            return false;
        }
        candraw = true;
    } else {
        _ASSERT(outType==PPT);
        ppt.set_page_size(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0),
                          user_scale, h_alignment, v_alignment, raw_page_clip, copyrightTextHeight);
    }
    //Complain if this page is too big
    XY size = origSize + (pbData ? pbData->headingLeftingSize : XY(0, 0));
    size.y += copyrightTextHeight;
    if (pError && raw_page_clip.y.Spans() < size.y*user_scale.y) {
        string err = "Page #";
        err << next_page << " is ";
        err << ceil(size.y*user_scale.y*100./raw_page_clip.y.Spans()-100);
        pError->Warning(FileLineCol(0, 0), err + "% longer than the paper size and will be cropped.");
    } else if (pError && raw_page_clip.x.Spans() < size.x*user_scale.x) {
        string err = "Page #";
        err << next_page << " is ";
        err << ceil(size.x*user_scale.x*100./raw_page_clip.x.Spans()-100);
        pError->Warning(FileLineCol(0, 0), err + "% wider than the paper size and will be cropped.");
    }
    return true;
}

/** Records the errors/warnings happened during any constructor.
   Adds a warning if the page with is higher than a fixed page width (minus margins)
   Adds a warning if for multi-page output the first page is taller than
   the paper height (minus margins).
  @param error This is where the errors are reported. Can be null, in that case
               we only return true if there is an error.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any).*/
bool Canvas::ErrorAfterCreation(MscError *pError,  const PBDataVector *pageBreakData)
{
    if (pError==nullptr) return cannot_draw(false);
    if (error.size()) {
        if (fatal) pError->FatalError(FileLineCol(0, 0), error, "");
        else pError->Error(FileLineCol(0, 0), error, "");
        return true;
    }
    if (raw_page_clip.x.till==0 || raw_page_clip.y.till==0) return false;
    if (pageBreakData) {
        auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, 1);
        XY size = origSize + (pbData ? pbData->headingLeftingSize : XY(0, 0));
        size.y += copyrightTextHeight;
        if (raw_page_clip.x.Spans() < size.x * user_scale.x) {
            string err = "First page is ";
            err << ceil(size.x * user_scale.x * 100. / raw_page_clip.x.Spans() - 100);
            pError->Warning(FileLineCol(0, 0), err + "% wider than the paper size and will be cropped.");
        }
        if (raw_page_clip.y.Spans() < origSize.y * user_scale.y) {
            string err = "First page is ";
            err << ceil(size.y * user_scale.y * 100. / raw_page_clip.y.Spans() - 100);
            pError->Warning(FileLineCol(0, 0), err + "% longer than the paper size and will be cropped.");
        }
    }
    return false;
}

/**Creates a canvas to redraw one page or all of the chart to a cairo recording surface.
  Chart elements will be scaled only with 'fake_scale', but it is one for all output types
  other than WMF. The selected page or the whole chart is drawn at the origin (0,0).
  Preparation for heading and copyright text is also added.
  How to use:
      After Canvas creation, draw chart in chart coordinates, call
      PrepareForHeaderFooter() and draw automatic heading and copyright text.
  Note that this function is only used when redrawing one page for the GUI in Chart::ReDrawOnePage().

  @param [in] ot The type of the output - Drawing fakes and tweaks will be applied
                 as per this. Does not really influence output target - that
                 is a cairo recording surface here.
  @param surf The cairo recording surface to draw onto (in fact, can be any surface).
  @param [in] tot The total size of the chart in the canvas coordinate space.
  @param [in] ctexth The height of the copyright text.
  @param [in] scale The scaling applied before we draw to the surface.
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any.)
  @param [in] page Specifies which page we the Canvas for, the index of
                   the first page is 1. 0 means we want to treat
                   the whole chart as one page.*/
Canvas::Canvas(EOutputType ot, cairo_surface_t *surf, const Block &tot, double ctexth,
               const XY &scale, const PBDataVector *pageBreakData, unsigned page) :

    outFile(nullptr), finalOutFile(nullptr), surface(surf), cr(nullptr), candraw(false),
    fake_dash_offset(0), outType(ot), total(tot),
    external_surface(true), copyrightTextHeight(ctexth),
    user_scale(scale), raw_page_clip(0,0,0,0), h_alignment(0), v_alignment(0)
#ifdef CAIRO_HAS_WIN32_SURFACE
	, stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(false)
#endif
{
    if (CAIRO_STATUS_SUCCESS != cairo_surface_status(surf)) {
        error = "Internal error: Bad surface to draw on.";
        return; //nodraw state
    }
    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, page);
    SetLowLevelParams(origSize + (pbData ? pbData->headingLeftingSize : XY(0, 0)) + XY(0, ctexth));
    CreateContext(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0));
    if (error.size()) CloseOutput();
    else candraw = true;
    cairo_surface_set_fallback_resolution(surface, fallback_resolution/fake_scale, fallback_resolution/fake_scale);
}

/**Creates a canvas to draw all of the chart to a cairo recording surface.
  All chart elements will be recorded in an area `tot`, not even fake_scale
  applies. Preparation for heading and copyright text is also added.
  How to use:
    After Canvas creation, draw chart in chart coordinates, potentially call
    PrepareForHeaderFooter() and draw automatic heading and copyright text.
  Note that this is used only to draw the chart to a cairo recording surface in
  Chart::DrawToRecordingSurface().

  @param [in] ot The type of the output - Drawing fakes and tweaks will be applied
                 as per this, with the exception of 'fake_scale', which will be (1,1),
                 and 'can_and_shall_clip_total', 'white_background', 'needs_dots_in_corner',
                 which are all set to false, to allow unimpeded drawing.
                 These are taken into account in Canvas constructors, so when the redraw
                 Canvas is opened, we can take them into account.
                 Does not really influence output target - that
                 is a cairo recording surface here.
  @param surf The cairo recording surface to draw onto (in fact, can be any surface).
  @param [in] tot The total size of the chart in the canvas coordinate space.
*/
Canvas::Canvas(EOutputType ot, cairo_surface_t *surf, const Block &tot) :
    outFile(nullptr), finalOutFile(nullptr), surface(surf), cr(nullptr), candraw(false),
    fake_dash_offset(0), outType(ot), total(tot),
    external_surface(true), copyrightTextHeight(0),
    user_scale{1,1}, raw_page_clip(0, 0, 0, 0), h_alignment(0), v_alignment(0)
#ifdef CAIRO_HAS_WIN32_SURFACE
	, stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(false)
#endif
{
    if (CAIRO_STATUS_SUCCESS != cairo_surface_status(surf)) {
        error = "Internal error: Bad surface to draw on.";
        return; //nodraw state
    }
    SetLowLevelParams();
    fake_scale = 1.;
    can_and_shall_clip_total = false;
    white_background = false;
    needs_dots_in_corner = false;
    CreateContext(tot.Spans(), XY(0,0), XY(0, 0));
    if (error.size()) CloseOutput();
    else candraw = true;
    cairo_surface_set_fallback_resolution(surface, fallback_resolution, fallback_resolution);
}


/** A callback to write data to a FILE
  @param closure The FILE* to write to.
  @param [in] data The data to write
  @param [in] length The length of the data
  @returns Either CAIRO_STATUS_WRITE_ERROR or CAIRO_STATUS_SUCCESS.*/
cairo_status_t write_func(void * closure, const unsigned char *data, unsigned length)
{
    if (closure==nullptr) return CAIRO_STATUS_SUCCESS;
    if (length == fwrite(data, 1, length, (FILE*)closure))
        return CAIRO_STATUS_SUCCESS;
    else
        return CAIRO_STATUS_WRITE_ERROR;
}

/** Set the low-level compatibility options according to our output type.
 * @param size The size of the canvas in pixels. Only the page selected,
 * with heading, lefting and copyright text, scaled by user_scale.
 * (This is the amount of drawing space we need.). Only used to set fake_scale
 * for metafile surfaces. If (0,0) we set the fake_scale to 1 (no-op).*/
void Canvas::SetLowLevelParams(const XY& size [[maybe_unused]])
{
    /* Set defaults */
    use_text_path = false;
    use_text_path_rotated = false;
    individual_chars_path = 0;
    fake_gradients = 0;
    fake_dash = false;
    fake_shadows = false;
    fake_spaces = false;
    needs_arrow_fix = false;
    avoid_linewidth_1 = false;
    imprecise_positioning = false;
    needs_strokepath_rclboundsfix = false;
    can_and_shall_clip_total = true;
    avoid_aligned_bk_clip = false;
    avoid_transparency = false;
    white_background = false;
    fake_scale = 1.0;          //normally we map our coordinates 1:1 onto the canvas
    fallback_resolution = 300; //this will be irrelevant for bitmaps - they use the native resolution
    needs_dots_in_corner = false;

    //Customize as per target type
    switch (outType) {
    case Canvas::PPT:
        needs_arrow_fix = true;
        break;
    case Canvas::PNG:
        break;
    case SVG:
        fake_shadows = true; //problem with mesh gradients??
        break;
#ifdef CAIRO_HAS_WIN32_SURFACE
    case WMF:
    case EMFWMF:
        //determine scaling so that no coordinate is larger than 30K - a built-in limit of WMF
        //but scale up, so that coordinates are large, since WMF handles only integer coordinates,
        //thus fonts are bad if we do not scale up.
        //So if we have total calculated already, we select fake_scale as 10, or smaller if this would result in >30K coords.
        //Setting fake_scale higher than 10 seems to result in wrong image fallback positioning, I am not sure why.
        if (size.x>0 && size.y>0)
            fake_scale = std::min(std::min(30000./size.x, 30000./size.y), 10.);
        individual_chars_path = 1;      //do this so that it is easier to convert to WMF
        use_text_path_rotated = true;   //WMF has no support for this
        fake_dash = true;               //WMF has no support for this
        needs_arrow_fix = true;         //WMF does not support complex clipping it seems
        FALLTHROUGH;
    case PRINTER:
        fallback_resolution = 50;
        FALLTHROUGH;
    case EMF:
        avoid_linewidth_1 = true;       //EMF needs wider than 1 horizontal lines to avoid clipping them too much
        needs_dots_in_corner = true;
        imprecise_positioning = true;
        needs_strokepath_rclboundsfix = true;
        fake_gradients = 30;
        fake_shadows = true;
        avoid_aligned_bk_clip = true;
        avoid_transparency = true; //this is to potentially fix Mcufan's bug #5
        break;
    case WIN:
        //(Highest quality) Default image surface settings will do
        //no need to clear bkg, as we always do that when drawing to the client area
#endif
    default:
        break;
    }
}

/** Return position information about a page of the chart
 * All values returned are in chart coordinates.
 * This is static, so as to be possible to get called without an
 * actual canvas.
 * @param [in] total The total size of the chart
 * @param [in] pageBreakData The collection of page break associated data
 *                           generated during parsing. This function only
 *                           uses the y coordinate of the page break and
 *                           the size of the automatically inserted heading
 *                           (if any).
 * @param [in] page Specifies which page we want data for, the index of
 *                  the first page is one. 0 means we want to treat
 *                  the whole chart as one page.
 * @param [out] offset The coordinates of the top of the page.
 * @param [out] size The width & length of the page (not including the automatically
 *                     inserted heading (if any) and the copyright text.
 * @param [out] relevant The relevant PageBreakData struct or nullptr if we could not find it or
 *                       'page' is wrong.*/
Canvas::PagePosition Canvas::GetPagePosition(const Block& total, const PBDataVector* pageBreakData, unsigned page) noexcept
{
    if (page==0 || pageBreakData==nullptr || pageBreakData->empty())
        return {.size = total.Spans(), .offset = total.UpperLeft(), .page_break_data = nullptr};
    if (page>pageBreakData->size())  //too large page num
        return {.size = XY(0,0), .offset = total.LowerRight(), .page_break_data = nullptr};  //nothing drawn
    return {.size = pageBreakData->at(page - 1)->wh,
            .offset = pageBreakData->at(page - 1)->xy,
            .page_break_data = pageBreakData->at(page - 1)};
}


/** Creates a surface of a given size.
 * Uses the `outType` member to determine what type of surface to create.
 * Uses the `fileName` members to open an EMF file, if not empty.
 * Uses the 'outFile' member to direct EPS and SVG output into it.
 * @param [in] size The native size of the surface (can be 0,0), that is
 *                  if the user specifies a scale of 2, this shall be twice the
 *                  chart (page) size.
 * @returns Any error or ERR_OK.*/
void Canvas::CreateSurface(const XY &size)
{
    if (dummy) return;
    const int x = abs(int(size.x));
    const int y = abs(int(size.y));
    switch (outType) {
    default:
        error = "Unsupported/bad output type";
        return;
    case Canvas::PPT:
        //We need an empty surface to be able to calculate font metrics
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0);
        break;
#ifdef CAIRO_HAS_PNG_FUNCTIONS
    case Canvas::PNG:
        surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, x, y);
        break;
#endif
#ifdef CAIRO_HAS_PS_SURFACE
    case Canvas::EPS:
        surface = cairo_ps_surface_create_for_stream(write_func, outFile, x, y);
        cairo_ps_surface_set_eps(surface, true);
        break;
#endif
#ifdef CAIRO_HAS_PDF_SURFACE
    case Canvas::PDF:
        surface = cairo_pdf_surface_create_for_stream(write_func, outFile, x, y);
#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1,10,0)
        //Limit to PDF version 1.4 if possible (appeared in cairo 1.10)
        //since no features of higher are relevant and Texinfo doc compilation
        //produces warnings with 1.5
        if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS)
            cairo_pdf_surface_restrict_to_version(surface, CAIRO_PDF_VERSION_1_4);
#endif
        break;
#endif
#ifdef CAIRO_HAS_SVG_SURFACE
    case Canvas::SVG:
        surface = cairo_svg_surface_create_for_stream(write_func, outFile, x, y);
        break;
#endif
#ifdef CAIRO_HAS_WIN32_SURFACE
    case Canvas::EMF:
    case Canvas::EMFWMF:
        // Create the Enhanced Metafile
        if (fileName.length() > 0) {
            const std::wstring buff = ConvertFromUTF8_to_UTF16(fileName);
            original_hdc = CreateEnhMetaFileW(nullptr, buff.c_str(), nullptr, L"Msc-generator\0\0");
        } else {
            _ASSERT(0);
            error = "Internal error: no filename for EMF or WMF."; //this can only be called with a filename
            return;
        }
        // Did you get a good metafile?
        if( original_hdc == nullptr ) {
            error = "Internal error: could not create metafile.";
            return;
        }
        /* We need a tweak in cairo.
		 ** I modified cairo_win32_printing_surface_create diretcly to use
		 ** an extent bigger than the one reported by GetClipBox(), if the
		 ** DC passed is an EMF DC.
         ** This is to avoid dropping glyphs outside the DC's extent.
         ** I could not make a metafile DC to have larger extent than
         ** the current screen size.
         */
        win32_dc = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
        // Did you get a good metafile?
        if (win32_dc == nullptr) {
            error = "Internal error: could not create metafile #2.";
            return;
        }
        surface = cairo_win32_printing_surface_create(win32_dc);
        break;
	case Canvas::WMF:
        // Create the Metafile on disk
        if (fileName.length() > 0) {
            const std::wstring buff = ConvertFromUTF8_to_UTF16(fileName);
            original_hdc = CreateMetaFileW(buff.c_str());
        } else {
            _ASSERT(0);
            error = "Internal error: no filename for WMF."; //this can only be called with a filename
            return; //this can only be called with a filename
        }
        // Did you get a good metafile?
        if( original_hdc == nullptr ) {
            error = "Internal error: could not create metafile.";
            return;
        }
        // Create the Enhanced Metafile
        win32_dc = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
        // Did you get a good metafile?
        if (win32_dc == nullptr) {
            error = "Internal error: could not create metafile #2.";
            return;
        }
        surface = cairo_win32_printing_surface_create(win32_dc);
#endif
    }
    const cairo_status_t st = cairo_surface_status(surface);
    if (st != CAIRO_STATUS_SUCCESS) {
        CloseOutput();
        //We get CAIRO_STATUS_INVALID_SIZE if the bitmap image is larger
        //than 32767 (for cairo 12.8), which is because they send
        //coordinates to pixman in 16.16 fixpoint format.
        if (st == CAIRO_STATUS_NO_MEMORY || st == CAIRO_STATUS_INVALID_SIZE) {
            error = "Out of memory - too big output image (";
            error << x << "x" << y << ").";
        } else
            error = "Could not create cairo canvas.";
        return;
    }
    cairo_surface_set_fallback_resolution(surface, fallback_resolution/fake_scale, fallback_resolution/fake_scale);
}

/** Creates a cairo context for an already created surface.
 * We not only create a surface, but set the transformation matrix
 * to reflect page alignment, scaling pagination and low-level
 * compatibility options.
 * We use cairo_save twice to create two drawing situations.
 * - The topmost (available on return) will have clipping for the page content
 *   That is drawing the objects of the chart in chart coordinates will
 *   send the content of the page specified by the input parameters of this
 *   function to the output.
 * - The second situation will contain a larger clip area allowing to
 *   draw the automatic heading (if any) at the top of the page and the
 *   copyright notice at the bottom.
 *
 * If `raw_page_clip` contains something, we clip the native surface and
 * translates accorindg to `h_alignment` and `v_alignment`.
 * We also draw the white background and the dots in the corner, if needed.
 * @param [in] origSize The width height of the page in chart coordinates.
 * @param [in] origOffset The top,left corner of the page in chart coordinates.
 * @param [in] headingLeftingSize The height of the automatic header/width automatic left stuff at the
 *                                top of the page (if any).*/
void Canvas::CreateContext(const XY &origSize, const XY &origOffset,
                           const XY &headingLeftingSize)
{
    if (dummy) return;
    cr = cairo_create (surface);
    const cairo_status_t st = cairo_status(cr);
    if (st != CAIRO_STATUS_SUCCESS) {
        CloseOutput();
        error = st == CAIRO_STATUS_NO_MEMORY
            ? "Out-of memory when creating cairo context."
            : "Could not create cairo context.";
        return;
    }
    //Set line cap to butts
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT);
    //Turn font metric hints off. This way scaling the fonts will not change layout.
    cairo_font_options_t *fopt = cairo_font_options_create();
    cairo_get_font_options(cr, fopt);
    cairo_font_options_set_hint_metrics(fopt, CAIRO_HINT_METRICS_OFF);
    cairo_set_font_options(cr, fopt);
    cairo_font_options_destroy(fopt);

    scale_for_shadows = sqrt(user_scale.y*user_scale.x);
    if (total.x.Spans() <= 0 || total.y.Spans()<=0) return; //OK

    if (raw_page_clip.y.till>0) {
        cairo_rectangle(cr, raw_page_clip.x.from, raw_page_clip.y.from, raw_page_clip.x.Spans(), raw_page_clip.y.Spans());
        cairo_clip(cr);
        XY off = raw_page_clip.UpperLeft();
        switch (v_alignment) {
        case +1: //bottom
            off.y += raw_page_clip.y.Spans() - (headingLeftingSize.y+origSize.y+copyrightTextHeight)*user_scale.y; break;
        case 0: //center
            off.y += (raw_page_clip.y.Spans() - (headingLeftingSize.y+origSize.y+copyrightTextHeight)*user_scale.y)/2; break;
        case -1://top
            break;
        default: _ASSERT(0);
        }
        switch (h_alignment) {
        case 1: //right
            off.x += raw_page_clip.x.Spans() - (headingLeftingSize.x+origSize.x)*user_scale.x; break;
        case 0:  //center
            off.x += (raw_page_clip.x.Spans() - (headingLeftingSize.x+origSize.x)*user_scale.x)/2; break;
        case -1: //left
            break;
        default: _ASSERT(0);
        }
        cairo_translate(cr, off.x, off.y);
    }

    cairo_scale(cr, fake_scale*user_scale.x, fake_scale*user_scale.y);
    cairo_save(cr);
    if (can_and_shall_clip_total) {
        //clip first to allow for banner text, but not for WIN contexts
        cairo_rectangle(cr, 0, 0, headingLeftingSize.x+origSize.x,
                                  headingLeftingSize.y+origSize.y+copyrightTextHeight);
        cairo_clip(cr);
    }
    if (white_background) {
        cairo_rectangle(cr, 0, 0, headingLeftingSize.x+origSize.x,
                                  headingLeftingSize.y+origSize.y+copyrightTextHeight);
        cairo_set_source_rgb(cr, 1., 1., 1.);
        cairo_fill(cr);
    } else if (needs_dots_in_corner) {
        //Draw small marks in corners, so EMF an WMF spans correctly
        //(for some reason stroke do not work as intended, so we use fill()s)
        cairo_set_source_rgb(cr, 1, 1, 1);
        cairo_rectangle(cr, 0, 0, 1, 1);
        cairo_fill(cr);
        cairo_rectangle(cr, headingLeftingSize.x+origSize.x-1,
                            headingLeftingSize.y+origSize.y+copyrightTextHeight-1,
                            1,1);
        cairo_fill(cr);
    }
    cairo_translate(cr, headingLeftingSize.x-origOffset.x, headingLeftingSize.y-origOffset.y);
    cairo_save(cr);
    if (can_and_shall_clip_total) {
        //then clip again to exclude banner text, but not for WIN contexts
        cairo_rectangle(cr, origOffset.x, origOffset.y, origSize.x, origSize.y);
        cairo_clip(cr);
    }
}

#ifdef CAIRO_HAS_WIN32_SURFACE

/** Create a Canvas to a Windows Device Context that is either a display/printer DC or a metafile.
  After Canvas creation, draw chart in chart coordinates, call
  PrepareForHeaderFooter() and draw automatic heading and copyright text.

  @param [in] ot The type of the output - WIN, WMF, EMF, EMFWMF and PRINTER.
  @param [in] tot The total size of the chart in the canvas coordinate space.
  @param [in] ctexth The height of the copyright text.
  @param [in] scale Scales the output to be this much bigger
  @param [in] pageBreakData The collection of page break associated data
                            generated during parsing. This function only
                            uses the y coordinate of the page break and
                            the size of the automatically inserted heading
                            (if any.)
  @param [in] page Specifies which page we the Canvas for, the index of
                   the first page is 1. 0 means we want to treat
                   the whole chart as one page.*/
//Use this to display a chart, use other constructors without a DC to save the chart to a file
Canvas::Canvas(EOutputType ot, HDC hdc, const Block &tot, double ctexth,
               const XY &scale, const PBDataVector *pageBreakData, unsigned page) :
    fake_dash_offset(0), outFile(nullptr), finalOutFile(nullptr), surface(nullptr), cr(nullptr), outType(ot),
    total(tot), candraw(false),
    external_surface(false), copyrightTextHeight(ctexth),
    raw_page_clip(0,0,0,0), user_scale(scale), h_alignment(0), v_alignment(0),
    stored_metafile_size(0), win32_dc(nullptr), original_hdc(nullptr), external_hdc(true)
{
    if (ot!=WIN && ot!=WMF && ot!=EMF && ot!=EMFWMF && ot!=PRINTER) {
        error = "Internal error: non-windows target to create a DC.";
        return;
    }

    auto [origSize, origOffset, pbData] = GetPagePosition(total, pageBreakData, page);
    original_device_size.x = (pbData ? pbData->headingLeftingSize.x : 0) + origSize.x;
    original_device_size.y = (pbData ? pbData->headingLeftingSize.y : 0) + origSize.y + copyrightTextHeight;
    original_device_size.x *= user_scale.x;
    original_device_size.y *= user_scale.y;
    SetLowLevelParams(original_device_size);

    switch (ot) {
    case WIN:
        surface = cairo_win32_surface_create(hdc);
        break;
    case EMF:
    case WMF:
    case EMFWMF:
    case PRINTER:
        win32_dc = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
        if (win32_dc == nullptr) return;
        original_hdc = hdc;
        surface = cairo_win32_printing_surface_create(win32_dc);
        break;
    default:
        _ASSERT(0);
    }
    cairo_surface_set_fallback_resolution(surface, fallback_resolution/fake_scale, fallback_resolution/fake_scale);
    CreateContext(origSize, origOffset, pbData ? pbData->headingLeftingSize : XY(0, 0));
    if (error.size()) CloseOutput();
    else candraw = true;
}

/** Metafile record for text output*/
typedef struct tagEXTTO { // lf
    _int16 toY;
    _int16 toX;
    _int16 toSTRLEN;
    _int16 toFWOPTS;
    char   string[1];
} EXTTO;

/** Metafile record for changing font options*/
typedef struct tagLOGFONT16 { // lf
    _int16 lfHeight;
    _int16 lfWidth;
    _int16 lfEscapement;
    _int16 lfOrientation;
    _int16 lfWeight;
    BYTE   lfItalic;
    BYTE   lfUnderline;
    BYTE   lfStrikeOut;
    BYTE   lfCharSet;
    BYTE   lfOutPrecision;
    BYTE   lfClipPrecision;
    BYTE   lfQuality;
    BYTE   lfPitchAndFamily;
    char   lfFaceName[LF_FACESIZE];
} LOGFONT16;

/** Callback to process a WMF record.
  I use this callback to adjust erroneous WMF records
  which are badly converted from EMF.
  @param hDC handle to DC
  @param lpHTable metafile handle table
  @param lpMFR metafile record
  @param nObj count of objects
  @returns 1 if we shall continue processing records*/
int CALLBACK WMFRecordFixProc(HDC hDC,                // handle to DC
                              HANDLETABLE *lpHTable,  // metafile handle table
                              METARECORD *lpMFR,      // metafile record
                              int nObj,               // count of objects
                              LPARAM /*lpClientData*/ // optional data
                             )
{
    BOOL b=true;
    if (lpMFR->rdFunction == META_EXTTEXTOUT) {
        const EXTTO * const pTO = ( EXTTO* )(&lpMFR->rdParm[0]);
        const unsigned len = pTO->toSTRLEN;

        //**Below code is needed for cairo 10.8, but not for 12.2
        //char buff[4096];
        //for (unsigned i=0; i<len; i++)
        //    buff[i] = pTO->string[i] + 29;
        //buff[len]=0;
        //ExtTextOut(hDC, pTO->toX, pTO->toY, 0, nullptr, buff, len, nullptr);
        ExtTextOutA(hDC, pTO->toX, pTO->toY, 0, nullptr, pTO->string, len, nullptr);
    } else
    if (lpMFR->rdFunction == META_CREATEFONTINDIRECT) {
        LOGFONT16* const plogfont = ( LOGFONT16* )(&lpMFR->rdParm[0]);
        plogfont->lfHeight = plogfont->lfHeight / 32;   //When this is negative character height is meant, when positive cell height (cell_h = character_h + lead). Keep sign!
        b=PlayMetaFileRecord(hDC, lpHTable, lpMFR, nObj);
    } else {
        b=PlayMetaFileRecord(hDC, lpHTable, lpMFR, nObj);
    }
    return 1;
}


/** Converts and paints an EMF on a WMF DC.
  @param [in] hemf the Enhanced Metafile
  @param hdc The WMF DC to draw on
  @param [in] r The size of the enhanced metafile
  @param [in] applyTricks Whether we shall apply my conversion tricks (advisable)
  @returns The size of the resulting WMF in bytes.*/
size_t PaintEMFonWMFdc(HENHMETAFILE hemf, HDC hdc, const RECT &r, bool applyTricks)
{
    if (hemf == nullptr || hdc == nullptr) return 0;
    HDC refDC = GetDC(nullptr);
    SetMapMode(refDC, MM_ANISOTROPIC);
    SetWindowExtEx(refDC, r.right, r.bottom, nullptr);
    SetViewportExtEx(refDC, r.right, r.bottom, nullptr);
    UINT size = GetWinMetaFileBits(hemf, 0, nullptr, MM_ANISOTROPIC, refDC);
    std::vector<BYTE> buff;
    buff.resize(size);
    size = GetWinMetaFileBits(hemf, size, buff.data(), MM_ANISOTROPIC, refDC);
    ReleaseDC(nullptr, refDC);
    HMETAFILE hwmf = SetMetaFileBitsEx(size, buff.data());
    if (applyTricks)
        EnumMetaFile(hdc, hwmf, WMFRecordFixProc, 0);
    else
        PlayMetaFile(hdc, hwmf);
    size = GetMetaFileBitsEx(hwmf, 0, nullptr);
    DeleteMetaFile(hwmf);
    return size;
}

/** Callback to process an EMF record.
  I use this to collect fallback image (StetchDIBits records) location*/
int CALLBACK ExtractStretcDIBitsRecords( HDC hDC,
                                         HANDLETABLE *lpHTable,
                                         const ENHMETARECORD *lpEMFR,
                                         int nObj,
                                         LPARAM lpData)
{
    switch (lpEMFR->iType) {
        //operations modifying the transformation matrix
    case EMR_MODIFYWORLDTRANSFORM:
    case EMR_SETWORLDTRANSFORM:
    case EMR_SETVIEWPORTEXTEX:
    case EMR_SETVIEWPORTORGEX:
    case EMR_SETWINDOWEXTEX:
    case EMR_SETWINDOWORGEX:
    case EMR_SAVEDC:
    case EMR_RESTOREDC:
    case EMR_SCALEVIEWPORTEXTEX:
    case EMR_SCALEWINDOWEXTEX:
        PlayEnhMetaFileRecord(hDC, lpHTable, lpEMFR, nObj);
        break;
    case EMR_STRETCHDIBITS:
        Block b;
        const EMRSTRETCHDIBITS * const s = reinterpret_cast<const EMRSTRETCHDIBITS*>(lpEMFR);
        b.x.from = s->xDest;
        b.x.till = b.x.from + s->cxDest;
        b.y.from = s->yDest;
        b.y.till = b.y.from + s->cyDest;

        //Apply the world transform to block b
        //See http://msdn.microsoft.com/en-us/library/windows/desktop/dd145228(v=vs.85).aspx
        XFORM xForm;
        GetWorldTransform(hDC, &xForm);
        //We omit page to device translations now.
        //For those, see http://msdn.microsoft.com/en-us/library/windows/desktop/dd145045(v=vs.85).aspx
        XY points[4] = {
            b.UpperLeft().Transform(xForm.eM11, xForm.eM12, xForm.eM21, xForm.eM22, xForm.eDx, xForm.eDy),
            b.UpperRight().Transform(xForm.eM11, xForm.eM12, xForm.eM21, xForm.eM22, xForm.eDx, xForm.eDy),
            b.LowerRight().Transform(xForm.eM11, xForm.eM12, xForm.eM21, xForm.eM22, xForm.eDx, xForm.eDy),
            b.LowerLeft().Transform(xForm.eM11, xForm.eM12, xForm.eM21, xForm.eM22, xForm.eDx, xForm.eDy)
        };
        reinterpret_cast<Contour*>(lpData)->operator+=(Contour(std::span<XY>(points)));
    }
    return 1;
}

/** Determine the location of fallback images in an enhanced metafile.
  @param [in] hemf The enhanced metafile.
  @param [in] lpRECT The total size of the chart. If set correctly
                     the resulting image locations will be in chart coordinates.*/
Contour Canvas::FallbackImages(HENHMETAFILE hemf, LPRECT lpRECT)
{
    Contour c;
    HDC hDC = CreateEnhMetaFile(nullptr, nullptr, nullptr, nullptr);
    EnumEnhMetaFile(hDC, hemf, ExtractStretcDIBitsRecords, &c, lpRECT);
    DeleteObject(CloseEnhMetaFile(hDC));
    return c;
}


/** Callback to process an EMF record.
  I use this to expand the rect of Strokepath records. */
int CALLBACK ExpandStorkePathRect( HDC hDC,
                                   HANDLETABLE *lpHTable,
                                   const ENHMETARECORD *lpEMFR,
                                   int nObj,
                                   LPARAM lpData)
{
    switch (lpEMFR->iType) {
        //operations modifying the transformation matrix
    default:
        PlayEnhMetaFileRecord(hDC, lpHTable, lpEMFR, nObj);
        break;
    case EMR_STROKEPATH:
        EMRSTROKEPATH s;
        memcpy(&s, lpEMFR, sizeof(s));
        const unsigned amount = (unsigned)lpData;
        s.rclBounds.left   -= amount;
        s.rclBounds.right  += amount;
        s.rclBounds.top    -= amount;
        s.rclBounds.bottom += amount;
        //PlayEnhMetaFileRecord(hDC, lpHTable, lpEMFR, nObj);
        BOOL b =PlayEnhMetaFileRecord(hDC, lpHTable, (ENHMETARECORD*)&s, nObj);
        if (!b) {
            b =PlayEnhMetaFileRecord(hDC, lpHTable, lpEMFR, nObj);
            s.rclBounds.bottom += amount;

        }
    }
    return 1;
}

/** Expand the clip rectangle of StrokePath records.
  @param [in] hemf The enhanced metafile.
  @param [in] lpRECT The total size of the chart.
  @param [in] amount How much to extend.*/
void Canvas::ExpandStrokePathRects(HDC hDC, HENHMETAFILE hemf, LPRECT lpRECT, size_t amount)
{
    EnumEnhMetaFile(hDC, hemf, ExpandStorkePathRect, (LPVOID)amount, lpRECT);
}

/** Close the output and return the resulting emf file.
 * Valid only for EMF and EMFWMF outputs.
 * A complete replacement for CloseOutput().*/
HENHMETAFILE Canvas::CloseAndGetEMF()
{
    _ASSERT(outType == EMF || outType==EMFWMF || outType==PRINTER);
    _ASSERT(original_hdc == nullptr);
    _ASSERT(surface);
    _ASSERT(!external_surface);

    if (error.size())
        return nullptr;
    /* Destroy our context */
    if (cr)
        cairo_destroy (cr);
    cairo_surface_show_page(surface);
    cairo_surface_destroy (surface);
    RECT r;
    SetRect(&r, 0, 0, int(original_device_size.x), int(original_device_size.y));
    HENHMETAFILE hemf = CloseEnhMetaFile(win32_dc);
    stored_fallback_image_places = FallbackImages(hemf, &r);
    stored_metafile_size = GetEnhMetaFileBits(hemf, 0, nullptr);
    stored_fallback_image_places.Scale(1/(user_scale.x/*fake_scale*/)); //TODO: assumes user_scale.x==user_scale.y
    win32_dc = nullptr;
    outFile=nullptr;
    surface=nullptr;
    cr=nullptr;
    fileName.clear();
    candraw = false;
    return hemf;
}

#endif //WIN32

void Canvas::PrepareForHeaderFooter()
{
    if (does_graphics()) {
        if (!cannot_draw())
            cairo_restore(cr);
    } else {
        if (ppt.is_ok())
            ppt.prepare_header_footer();
    }
}

bool Canvas::AddiTXtChunk(EmbedChartData &&chart)
{
    if (outType!=PNG || outFile==nullptr || error.size())
        return false;
    if (cannot_draw()) return true;
    finalOutFile = outFile;
    outFile = tmpfile();
    iTXt_data = std::move(chart);
    return true;
}

void Canvas::CloseOutput()
{
    if (error.size()) {
        if (outFile) fclose(outFile);
        if (finalOutFile) fclose(finalOutFile);
        if (cr) cairo_destroy(cr);
        if (surface) cairo_surface_destroy(surface);
        ppt.del_file();
    } else {
        /* Destroy our context */
        if (cr) cairo_destroy(cr);

        //Flush and destroy surface, but preserve external ones
        if (does_graphics()) {
            if (surface && !external_surface) {
                /* Output the image to the disk file in PNG format. */
                switch (outType) {
                case ISMAP:
                case PPT:
                    _ASSERT(0);
                    break;
#ifdef CAIRO_HAS_PNG_FUNCTIONS
                case PNG:
                    if (outFile) {
                        cairo_surface_write_to_png_stream(surface, write_func, outFile);
                        if (finalOutFile) {
                            _ASSERT(!iTXt_data.empty());
                            fflush(outFile);
                            rewind(outFile);
                            pngutil::AddToPNG(outFile, finalOutFile, iTXt_data);
                            fclose(outFile); //this may actually delete the temp file.
                            //now remove all trace of having added the itxt chunk.
                            outFile = finalOutFile;
                            finalOutFile = nullptr;
                            iTXt_data.clear();
                        } else {
                            _ASSERT(iTXt_data.empty());
                        }
                    }
                    FALLTHROUGH;
#endif
                case EPS:
                case PDF:
                case SVG:
                    cairo_surface_destroy(surface);
                    if (outFile)
                        fclose(outFile);
                    break;
                case EMF:
                case WMF:
                case EMFWMF:
                case PRINTER:
#ifdef CAIRO_HAS_WIN32_SURFACE
                    cairo_surface_show_page(surface);
                    cairo_surface_destroy(surface);
                    RECT r;
                    SetRect(&r, 0, 0, int(original_device_size.x), int(original_device_size.y));
                    if (original_hdc) {
                        //Opened via either
                        //1. with an existing WMF HDC (OutType==WMF)
                        //2. with an existing EMF HDC (outType==EMF or EMFWMF);
                        //3. with a WMF file (OutType==WMF), the DC was created by "this"
                        //4. with a EMF file (OutType==EMF or EMFWMF), the DC was created by "this"
                        //5. with an existing printing DC (OutType==PRINTER)
                        //For 1&3, we need to convert from EMF to WMF, but otherwise can close
                        //"original_hdc" the same way.
                        //For #5, we just copy. (This is a fix, we cannot draw onto a printer DC
                        //with cairo directly, so we use an EMF in-between.)
                        //win32_dc is an EMF DC opened by "this"
                        //"original_dc" contains the original target to draw on
                        HENHMETAFILE hemf = CloseEnhMetaFile(win32_dc);
                        stored_fallback_image_places = FallbackImages(hemf, &r);
                        if (outType==WMF) {
                            stored_metafile_size = PaintEMFonWMFdc(hemf, original_hdc, r, true);
                            //This will leave the file on disk in case of #3
                            if (!external_hdc)
                                DeleteMetaFile(CloseMetaFile(original_hdc));
                        } else { //EMF, EMFWMF or PRINTER
                            //PlayEnhMetaFile(original_hdc, hemf, &r);
                            ExpandStrokePathRects(original_hdc, hemf, &r, 20);
                            stored_metafile_size = GetEnhMetaFileBits(hemf, 0, nullptr);
                            //This will leave the file on disk in case of #4
                            if (!external_hdc)
                                DeleteEnhMetaFile(CloseEnhMetaFile(original_hdc));
                        }
                        DeleteEnhMetaFile(hemf);
                        original_hdc = nullptr;
                        win32_dc = nullptr;
                        //Scale the fallback image places back to chart space (but with page origin at y==0)
                        stored_fallback_image_places.Scale(1/(user_scale.x*fake_scale)); //TODO: assumes user_scale.x==user_scale.y
                    } else
                        _ASSERT(0);
                    break;
                    //Fallthrough if cairo has no WIN32 surface
#endif
                case WIN:
                    cairo_surface_destroy(surface);
                    break;

                }
            }
        } else {
            switch (outType) {
            case PPT:
                ppt.finalize(); //flushes the last page if any
                break;
            default:
                _ASSERT(0);
            }
        }
        _ASSERT(iTXt_data.empty());
    }
    outFile=nullptr;
    finalOutFile = nullptr;
    iTXt_data.clear();
    surface=nullptr;
    cr=nullptr;
    ppt = {};
    fileName.clear();
    candraw = false;
#if defined(CAIRO_HAS_FC_FONT) && defined(HAS_FONTCONFIG)
    if (fc_pattern) {
        FcPatternDestroy(fc_pattern);
        fc_pattern = nullptr;
    }
#endif
}

bool Canvas::Add(GenericShape&& shape) {
    if (does_graphics()) return false;
    if (!ppt.is_ok()) return false;
    for (const auto& v : tr_stack)
        for (const SimpleTransform& t : v)
            shape.transform(t);
    return ppt.add(std::move(shape));
}

cairo_line_join_t Canvas::SetLineJoin(cairo_line_join_t t)
{
    if (!cr) return cairo_line_join_t::CAIRO_LINE_JOIN_MITER;
    cairo_line_join_t tt = cairo_get_line_join(cr);
    cairo_set_line_join(cr, t);
    return tt;
}
cairo_line_cap_t Canvas::SetLineCap(cairo_line_cap_t t)
{
    if (!cr) return cairo_line_cap_t::CAIRO_LINE_CAP_BUTT;
    cairo_line_cap_t tt = cairo_get_line_cap(cr);
    cairo_set_line_cap(cr, t);
    return tt;
}


void Canvas::Clip(const Block &b, const LineAttr &line)
{
    if (cannot_draw()) return;
    _ASSERT(line.IsComplete());
    cairo_save(cr);
    if (line.corner == ECornerType::NOTE && line.IsDoubleOrTriple())
        line.CreateRectangle_ForFill(b).CairoPath(cr, true);  //notefill shrinks by Spacing()
    else
        RectanglePath(b.x.from, b.x.till, b.y.from, b.y.till, line);
    cairo_clip(cr);
}

void Canvas::ClipInverse(const Contour &area)
{
    if (cannot_draw()) return;
    cairo_save(cr);
    Block outer(total);
    outer += area.GetBoundingBox();
    outer.Expand(1);
    cairo_rectangle(cr, outer.x.from, outer.y.from, outer.x.Spans(), outer.y.Spans());
    cairo_new_sub_path(cr);
    area.CairoPath(cr, true);
    cairo_fill_rule_t old = cairo_get_fill_rule(cr);
    cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
    cairo_clip(cr);
    cairo_set_fill_rule(cr, old);
}

/** Applies a 2D affine transformation. 
 * @returns true if the transformation cannot be applied.
 * This happens, when we do PPT output and the transformation contains sheering, that is, it cannot 
 * be expressed as a combination of scale, shift and rotate operations. In that case no transformation 
 * is done, so Untransform() should not be called.*/
bool Canvas::Transform(const TRMatrix& M) {
    if (does_graphics()) {
        if (cannot_draw()) return false;
        cairo_save(cr);
        const cairo_matrix_t m{ .xx = M.a1.x, .yx = M.ab.y, .xy = M.ab.x, .yy = M.a1.y, .x0 = M.a0.x, .y0 = M.a0.y };
        cairo_transform(cr, &m);
        return false;
    } else {
        auto d = M.Decompose();
        if (!d) return true; //contains sheering
        if (d->scale.x <= 0 || d->scale.y <= 0) return true; //contains flip operations (negative scale)
        tr_stack.emplace_back();
        if (!d->rotate.test_equal(XY(1, 0)))
            tr_stack.back().push_back(SimpleTransform::Rotate(atan2(d->rotate.y, d->rotate.x) * (180 / M_PI)));
        if (!d->scale.test_equal(XY(1, 1)))
            tr_stack.back().push_back(SimpleTransform::Scale(XY{fabs(d->scale.x), fabs(d->scale.y)}));
        if (!d->shift.test_equal(XY(0, 0)))
            tr_stack.back().push_back(SimpleTransform::Shift(d->shift));
        return false;
    }
}

/** Rotates the canvas to arbitrary degree around 'center'
 * Any arrow drawn from left to right will show up as rotated clockwise
 * with y coordinate growing downwards*/
void Canvas::Transform_Rotate(const XY &center, double radian)
{
    if (does_graphics()) {
        if (cannot_draw()) return;
        cairo_save(cr);
        cairo_translate(cr, center.x, center.y);
        cairo_rotate(cr, radian);
        cairo_translate(cr, -center.x, -center.y);
    } else {
        tr_stack.emplace_back();
        if (center.x || center.y)
            tr_stack.back().push_back(SimpleTransform::Shift(-center));
        tr_stack.back().push_back(SimpleTransform::Rotate(radian*180/M_PI));
        if (center.x || center.y)
            tr_stack.back().push_back(SimpleTransform::Shift(center));
    }
}

/** Rotates the canvas to arbitrary degree around 'center'
 * Any arrow drawn from left to right will show up as rotated clockwise
 * with y coordinate growing downwards*/
void Canvas::Transform_Rotate(const XY &center, double cos, double sin)
{
    if (does_graphics()) {
        if (cannot_draw()) return;
        cairo_save(cr);
        //shift center to origin
        cairo_translate(cr, center.x, center.y);
        cairo_matrix_t matrix;
        //rotate
        cairo_matrix_init(&matrix,
                          cos, sin,
                          -sin, cos,
                          0, 0);
        cairo_transform(cr, &matrix);
        //shift center to origin
        cairo_translate(cr, -center.x, -center.y);
    } else {
        const double radian = (sin<0 || (sin==0 && cos<0))
            ? 2*M_PI - acos(cos)
            : acos(cos);
        Transform_Rotate(center, radian);
    }
}


/** Rotate the canvas by 90 degrees.
  @param [in] clockwise True if the rotation is clockwise. That is,
                        if after transformation you draw
                        a horizontal line to the right (like this ->) ,
                        then it will appear on the canvas as going up.
  @param [in] s This is an y coordinate for clockwise rotation. Points
                with this y coordinate will have this x coordinate
                after the translation. For counterclockwise swap x and y.
  @param [in] d This is an y coordinate for clockwise rotation. Points
                with this y coordinate will have this x coordinate
                after the translation. For counterclockwise swap x and y.

  I admit I do not get this either.
 */
void Canvas::Transform_Rotate90(double s, double d, bool clockwise)
{
    if (does_graphics()) {
        if (cannot_draw()) return;
        cairo_save(cr);
        cairo_matrix_t matrix;
        matrix.xx = 0;
        matrix.yy = 0;
        if (clockwise) {
            matrix.xy = 1;
            matrix.x0 = 0;
            matrix.yx = -1;
            matrix.y0 = s+d;
        } else {
            matrix.yx = 1;
            matrix.y0 = 0;
            matrix.xy = -1;
            matrix.x0 = s+d;
        }
        cairo_transform(cr, &matrix);
    } else {
        tr_stack.emplace_back();
        tr_stack.back().push_back(SimpleTransform::Rotate(clockwise ? -90 : 90));
        tr_stack.back().push_back(SimpleTransform::Shift(XY(clockwise ? 0 : s+d, clockwise ? s+d : 0)));
    }
}

void Canvas::Transform_SwapXY() {
    cairo_matrix_t matrix;
    matrix.xy = 1;
    matrix.yx = 1;
    matrix.xx = 0;
    matrix.yy = 0;
    matrix.x0 = 0;
    matrix.y0 = 0;
    if (does_graphics()) {
        if (cannot_draw()) return;
        cairo_save(cr);
        cairo_transform(cr, &matrix);
    } else {
        tr_stack.emplace_back();
        tr_stack.back().push_back(SimpleTransform::FlipH());
        tr_stack.back().push_back(SimpleTransform::Rotate(-90));
    }
}

void Canvas::Transform_FlipHorizontal(double y)
{
    cairo_matrix_t matrix;
    matrix.xx = 1;
    matrix.xy = 0;
    matrix.x0 = 0;
    matrix.yy = -1;
    matrix.yx = 0;
    matrix.y0 = 2*y;
    if (does_graphics()) {
        if (cannot_draw()) return;
        cairo_save(cr);
        cairo_transform(cr, &matrix);
    } else {
        tr_stack.emplace_back();
        tr_stack.back().push_back(SimpleTransform::FlipH());
        if (y)
            tr_stack.back().push_back(SimpleTransform::Shift(XY{2*y, 0}));
    }
}

void Canvas::SetColor(ColorType pen)
{
    if (!cr) return;
    if (pen.type!=ColorType::INVALID) {
        _ASSERT(pen.type==ColorType::COMPLETE);
        if (AvoidTransparency())
            pen = pen.FlattenAlpha();
        cairo_set_source_rgba(cr, pen.r/255.0, pen.g/255.0, pen.b/255.0, pen.a/255.0);
    }
}

void Canvas::SetLineAttr(const LineAttr &line)
{
    if (!cr) return;
    if (line.color && line.color->type!=ColorType::INVALID)
        SetColor(*line.color);
    if (line.width)
        cairo_set_line_width(cr, *line.width + (avoid_linewidth_1 ? 0.01 : 0.0));
    if (line.type)
        SetDash(line);
}

void Canvas::SetDash(const LineAttr &line)
{
    if (!cr) return;
    double pattern[LineAttr::MAX_PATTERN_LEN];
    unsigned num = line.DashPattern(std::span<double>(pattern));
    if (fake_dash)
        cairo_set_dash(cr, nullptr, 0, 0);
    else if (line.type && !fake_dash)
        cairo_set_dash(cr, pattern, num, 0);
}

void Canvas::SetFontFace(const std::string& face, const std::string& lang [[maybe_unused]], bool italics, bool bold)
{
    if (!cr) return;
#if defined(CAIRO_HAS_FT_FONT) && defined(CAIRO_HAS_FC_FONT) && defined(HAS_FONTCONFIG)
    if (lang.size()) {
        std::string spattern = face + ":lang="+lang;
        if (italics) spattern.append(":slant=italic");
        if (bold) spattern.append(":weight=bold");
        if (fc_pattern) {
            FcPatternDestroy(fc_pattern);
            fc_pattern = nullptr;
        }
        fc_pattern = FcNameParse((const FcChar8*)spattern.c_str());
        //We need to keep fc_pattern alive as long as we use the font generated here.
        //So therefore fc_pattern is a class memeber, destoryed in CloseOutput (the latest)
        cairo_font_face_t* const font_face = cairo_ft_font_face_create_for_pattern(fc_pattern);
        cairo_set_font_face(cr, font_face);
        cairo_font_face_destroy(font_face);
        return;
    } //else fall back to the toy api
#endif
    cairo_select_font_face(cr, face.c_str(),
        italics ? CAIRO_FONT_SLANT_ITALIC : CAIRO_FONT_SLANT_NORMAL,
        bold ? CAIRO_FONT_WEIGHT_BOLD : CAIRO_FONT_WEIGHT_NORMAL);
}

void Canvas::Text(const XY &xy, const string &s, bool isRotated, const XY &scale)
{
    if (cannot_draw()) return;
    if (scale!=XY(1, 1)) {
        cairo_save(cr);
        cairo_translate(cr, xy.x, xy.y);
        cairo_scale(cr, scale.x, scale.y);
        cairo_move_to(cr, 0, 0);
    } else
        cairo_move_to(cr, xy.x, xy.y);
    //xy is pretty much invalid below
    if (use_text_path || (isRotated && use_text_path_rotated)) {
        thread_safe_cairo_text_path(cr, s.c_str());
        cairo_fill(cr);
    } else if (individual_chars_path) {
        char tmp_string[10] = "";
        for (unsigned i=0; i<s.length(); /*nope*/) {
            unsigned from = i;
            unsigned till = i + UTF8TrailingBytes(s[i]) + 1;
            if (till>s.length())
                break;
            for (/*nope*/; i<till; i++)
                tmp_string[i-from] = s[i];
            tmp_string[till-from] = 0;
            //if unicode && path is on, draw it via cairo_text_path()
            if (individual_chars_path==2 && from+1<till) {
                thread_safe_cairo_text_path(cr, tmp_string);
                cairo_fill(cr);
            } else {
                thread_safe_cairo_show_text(cr, tmp_string);
            }
        }
    } else {
        thread_safe_cairo_show_text(cr, s.c_str());
    }
    if (scale!=XY(1, 1))
        cairo_restore(cr);
}

void Canvas::ArcPath(const XY &c, double r1, double r2, double s_rad, double e_rad, bool reverse)
{
    if (cannot_draw()) return;
    cairo_save(cr);
    cairo_translate(cr, c.x, c.y);
    cairo_scale(cr, r1, r2 ? r2 : r1);
    if (reverse)
        cairo_arc_negative(cr, 0., 0., 1., s_rad, e_rad);
    else
        cairo_arc(cr, 0., 0., 1., s_rad, e_rad);
    cairo_restore (cr);
}

void Canvas::RectanglePath(double sx, double dx, double sy, double dy)
{
    if (cannot_draw()) return;
    cairo_new_path(cr);
    cairo_move_to(cr, sx, sy);
    cairo_line_to(cr, dx, sy);
    cairo_line_to(cr, dx, dy);
    cairo_line_to(cr, sx, dy);
    cairo_close_path(cr);
}

//for ECornerType::NOTE it draws the outer edge
void Canvas::RectanglePath(double sx, double dx, double sy, double dy, const LineAttr &line)
{
    if (cannot_draw()) return;
    cairo_new_path(cr);
    if (line.radius.value_or(0)==0 || line.corner.value_or(ECornerType::NONE)==ECornerType::NONE ) {
        RectanglePath(sx, dx, sy, dy);
        return;
    }
    const double r = std::min(std::min(fabs(sx-dx)/2, fabs(sy-dy)/2), *line.radius);
    switch (line.corner.value_or(ECornerType::NONE)) {
    default:
        RectanglePath(sx, dx, sy, dy);
        return;
    case ECornerType::ROUND:
        ArcPath(XY(sx + r, sy + r), r, r, 2.*M_PI/2., 3.*M_PI/2.);
        ArcPath(XY(dx - r, sy + r), r, r, 3.*M_PI/2., 4.*M_PI/2.);
        ArcPath(XY(dx - r, dy - r), r, r, 0.*M_PI/2., 1.*M_PI/2.);
        ArcPath(XY(sx + r, dy - r), r, r, 1.*M_PI/2., 2.*M_PI/2.);
        cairo_close_path(cr);
        break;
    case ECornerType::BEVEL:
        cairo_move_to(cr, sx+r, sy);
        cairo_line_to(cr, dx-r, sy);
        cairo_line_to(cr, dx, sy+r);
        cairo_line_to(cr, dx, dy-r);
        cairo_line_to(cr, dx-r, dy);
        cairo_line_to(cr, sx+r, dy);
        cairo_line_to(cr, sx, dy-r);
        cairo_line_to(cr, sx, sy+r);
        cairo_close_path(cr);
        break;
    case ECornerType::NOTE:
        cairo_move_to(cr, sx, sy);
        cairo_line_to(cr, dx-r, sy);
        cairo_line_to(cr, dx, sy+r);
        cairo_line_to(cr, dx, dy);
        cairo_line_to(cr, sx, dy);
        cairo_close_path(cr);
        break;
    }
}

////////////////////// Line routines

void Canvas::singleLine(const Block &b, const LineAttr &line)
{
    if (cannot_draw()) return;
    if (line.IsContinuous() || !fake_dash)
        RectanglePath(b.x.from, b.x.till, b.y.from, b.y.till, line);
    else {
        double pattern[LineAttr::MAX_PATTERN_LEN];
        unsigned num = line.DashPattern(std::span<double>(pattern));
        line.CreateRectangle_Midline(b).CairoPathDashed(cr, std::span<double>(pattern, num), true);
    }
    cairo_stroke(cr);
}

void Canvas::singleLine(const Path &p, const LineAttr &line)
{
    if (cannot_draw()) return;
    if (line.IsContinuous() || !fake_dash)
        p.CairoPath(cr, false);
    else {
        double pattern[LineAttr::MAX_PATTERN_LEN];
        unsigned num = line.DashPattern(std::span<double>(pattern));
        p.CairoPathDashed(cr, std::span<double>(pattern, num), false);
    }
    cairo_stroke(cr);
}

void Canvas::singleLine(const Contour&cl, const LineAttr &line)
{
    if (cannot_draw()) return;
    if (line.IsContinuous() || !fake_dash)
        cl.CairoPath(cr, false);
    else {
        double pattern[LineAttr::MAX_PATTERN_LEN];
        unsigned num = line.DashPattern(std::span<double>(pattern));
        cl.CairoPathDashed(cr, std::span<double>(pattern, num), false);
    }
    cairo_stroke(cr);
}

void Canvas::Line(const Edge& edge, const LineAttr &line)
{
    if (cannot_draw()) return;
    _ASSERT(line.IsComplete());
    if (line.type == ELineType::NONE ||
        !line.color ||
        line.color->type==ColorType::INVALID ||
        line.color->a==0) return;
    SetLineAttr(line);
    const double spacing = line.Spacing();
    if (line.IsDoubleOrTriple()) {
        std::list<Edge> edges;
        XY d1, d2;
        edge.CreateExpand(spacing, edges, d1,d2);
        if (edges.size()) {
            cairo_move_to(cr, edges.front().GetStart().x, edges.front().GetStart().y);
            for (const auto &e: edges)
                e.PathTo(cr);
            cairo_stroke(cr);
            edges.clear();
        }
        edge.CreateExpand(-spacing, edges, d1,d2);
        if (edges.size()) {
            cairo_move_to(cr, edges.front().GetStart().x, edges.front().GetStart().y);
            for (const auto &e: edges)
                e.PathTo(cr);
            cairo_stroke(cr);
        }
        if (line.IsDouble())
            return;
        //triple line
        cairo_set_line_width(cr,  line.TripleMiddleWidth());
        //fallthrough
    }
    if (line.IsContinuous()) {
        cairo_move_to(cr, edge.GetStart().x, edge.GetStart().y);
        edge.PathTo(cr);
        cairo_stroke(cr);
        return; //triple line will exit here as only single lines can be dashed
    }
    //Some dashed line
    double pattern[LineAttr::MAX_PATTERN_LEN];
    unsigned num = line.DashPattern(std::span<double>(pattern));
    int pos = 0;
    double offset=0;
    edge.PathDashed(cr, std::span<double>(pattern, num), pos, offset);
    cairo_stroke(cr);
}

void Canvas::Line(const Block &b, const LineAttr &line)
{
    if (cannot_draw()) return;
    _ASSERT(line.IsComplete());
	if (line.type == ELineType::NONE ||
        !line.color ||
        line.color->type==ColorType::INVALID ||
        line.color->a==0) return;
    if (b.IsInvalid()) return;
    SetLineAttr(line);
    const double spacing = line.Spacing();
    const double r = line.SaneRadius(b);
    LineAttr line2(line);
    line2.radius = r;
    if (line.corner!=ECornerType::NOTE || r==0) {
        if (line.IsDoubleOrTriple()) {
            line2.Expand(spacing);
            singleLine(b.CreateExpand(spacing), line2);
            line2.radius = r;
            line2.Expand(-spacing);
            singleLine(b.CreateExpand(-spacing), line2);
        }
        if (line.IsTriple()) {
            cairo_set_line_width(cr,  line.TripleMiddleWidth());
            line2.radius = r;
        }
        if (!line.IsDouble())
            singleLine(b, line2);
    } else {
        //Draw note
        if (line.IsTriple()) {
            //draw inner line
            singleLine(Contour(line2.CreateRectangle_ForFill(b)[0]), line2);
            //draw outer line
            line2.Expand(spacing);
            singleLine(line2.CreateRectangle_Midline(b.CreateExpand(spacing)), line2);
            //prepare for middle line
            line2.radius = r;
            cairo_set_line_width(cr,  line.TripleMiddleWidth());
        }
        if (line.IsDouble()) {
            //draw inner line + triangle
            singleLine(line2.CreateRectangle_ForFill(b), line2);
            //draw outer line
            line2.radius = r;
            line2.Expand(spacing);
            singleLine(b.CreateExpand(spacing), line2);
        } else {
            //for triple and single line
            singleLine(line2.CreateRectangle_Midline(b), line2);
            const Contour tri(b.x.till-r, b.y.from, b.x.till, b.y.from+r, b.x.till-r, b.y.from+r);
            tri.front().Outline().front().SetVisible(false);
            singleLine(tri, line);
        }
    }
}

void Canvas::Line(const Path &p, const LineAttr &line)
{
    if (cannot_draw()) return;
    _ASSERT(line.IsComplete());
    if (line.type == ELineType::NONE ||
        !line.color ||
        line.color->type==ColorType::INVALID ||
        line.color->a==0) return;
    if (p.IsEmpty()) return;
    SetLineAttr(line);
    const double spacing = line.Spacing();
    if (line.IsDoubleOrTriple())
        singleLine(p.DoubleWiden(spacing), line);
    if (line.IsTriple()) cairo_set_line_width(cr, line.TripleMiddleWidth());
    if (!line.IsDouble())
        singleLine(p, line);

}

void Canvas::Line(const Contour &c, const LineAttr &line)
{
    if (cannot_draw()) return;
    _ASSERT(line.IsComplete());
	if (line.type == ELineType::NONE ||
        !line.color ||
        line.color->type==ColorType::INVALID ||
        line.color->a==0) return;
    if (c.IsEmpty()) return;
    SetLineAttr(line);
    const double spacing = line.Spacing();
    if (line.IsDoubleOrTriple()) {
        singleLine(c.CreateExpand(spacing), line);
        singleLine(c.CreateExpand(-spacing), line);
    }
    if (line.IsTriple()) cairo_set_line_width(cr,  line.TripleMiddleWidth());
    if (!line.IsDouble())
        singleLine(c, line);
}


////////////////////// Fill routines


/** Helper to add a color stop to a cairo pattern using our ColorType.*/
inline void _add_color_stop(cairo_pattern_t *pattern, double offset, ColorType color)
{
    cairo_pattern_add_color_stop_rgba(pattern, offset,
                                      color.r/255., color.g/255.,
                                      color.b/255., color.a/255.);
}

/** Assumes angle is between 0 and 360, and calculates the coordinates of a
 *  point on the circumference of 'b' which is at this angle from its center.
 *  Angle here is meant counterclockwise.*/
XY CalculatePointOnEdge(const Block &b, double angle)
{
    angle = fmod(angle+360, 360);
    const XY sp = b.Spans()/2;
    const double cross = sp.length();
    const double rect_angle = asin(b.y.Spans()/2/cross)*180./M_PI;
    XY xyto = XY(cos(angle*M_PI/180.), -sin(angle*M_PI/180.))*cross; //sin is negative, for this is counterclockwise (due to the convention in dot)
    if (360-rect_angle<=angle || angle<=rect_angle)
        xyto *= fabs(sp.x/xyto.x);
    else if (angle <=180-rect_angle) {
        xyto *= fabs(sp.y/xyto.y);
    } else if (angle <= 270-rect_angle) {
        xyto *= fabs(sp.x/xyto.x);
    } else {
        xyto *= fabs(sp.y/xyto.y);
    }
    return xyto+b.Centroid();
}

void Canvas::linearGradient(ColorType from, ColorType to, const Block &b, double angle, EIntGradType type, double frac)
{
    if (cannot_draw()) return;
    cairo_pattern_t *pattern=nullptr;
    if (type == EIntGradType::LINEAR || type == EIntGradType::DEGENERATE) {
        const XY xyto = CalculatePointOnEdge(b, angle);
        const XY xyfr = 2*b.Centroid()-xyto;
        pattern = cairo_pattern_create_linear(xyfr.x, xyfr.y, xyto.x, xyto.y);
        _add_color_stop(pattern, 0, from);
        _add_color_stop(pattern, 1, to);
        if (type == EIntGradType::DEGENERATE) {
            _add_color_stop(pattern, 1-frac, from);
            _add_color_stop(pattern, 1-frac, to);
        } else if (frac!=0.5) {
            //create an equal mix of the two colors
            from.r = (from.r+to.r)/2;
            from.g = (from.g+to.g)/2;
            from.b = (from.b+to.b)/2;
            from.a = (from.a+to.a)/2;
            _add_color_stop(pattern, 1-frac, from);
        }
    } else if (type == EIntGradType::BUTTON) {
        pattern = cairo_pattern_create_linear(b.x.from, b.y.from, b.x.from, b.y.till);
        _add_color_stop(pattern, 0, to.Lighter(0.8));
        _add_color_stop(pattern, 0.43, to);
        _add_color_stop(pattern, 0.5, to.Darker(0.1));
        _add_color_stop(pattern, 1, to);
    } else if (type == EIntGradType::NONE) {
        pattern = cairo_pattern_create_rgba(from.r/255., from.g/255., from.b/255., from.a/255.);
    } else {
        _ASSERT(0);
    }
    cairo_set_source(cr, pattern);
    cairo_pattern_destroy(pattern);
}

/** Assumes angle is between 0 and 360. Button is not applicable here */
void Canvas::fakeLinearGrad(ColorType from, ColorType to, const Block &bb,
                            double angle, unsigned steps, double frac)
{
    if (cannot_draw()) return;
    if (steps==0) steps = 1;
    const double steps_per_pass = (frac==0.5) ? steps : std::max(steps/2, 1U); //we have two passes, only half as many steps for one
    const XY xyto = (CalculatePointOnEdge(bb, angle)-bb.Centroid()).Rotate(cos(angle*M_PI/180.), sin(angle*M_PI/180.));

    double r = from.r/255.0; const double dr = (double(to.r)-double(from.r))/steps/255.0;
    double g = from.g/255.0; const double dg = (double(to.g)-double(from.g))/steps/255.0;
    double b = from.b/255.0; const double db = (double(to.b)-double(from.b))/steps/255.0;
    double a = from.a/255.0; const double da = (double(to.a)-double(from.a))/steps/255.0;

    Clip(bb); //this saves a cairo context
    cairo_translate(cr, bb.x.MidPoint(), bb.y.MidPoint());
    cairo_rotate(cr, -(angle+180)*M_PI/180.); ///extra 180 is because we have screwed below and this way is easiest to fix

    double current = -xyto.x;
    const double ultimate_till = xyto.x;
    double till = frac==0.5 ? ultimate_till : (frac*2-1)*ultimate_till;
    //if (till-current < steps) steps = till-current; //Not finer than 1.
    const double max = bb.Spans().length();

    //starting block
    cairo_rectangle(cr, -max, -max, current+max, 2*max);
    cairo_set_source_rgba(cr, from.r, from.g, from.b, from.a);
    cairo_fill(cr);
    //gradient
    while (current<ultimate_till) {
        const double advance = (till-current)/steps_per_pass;
        while (current < till) {
            cairo_rectangle(cr, current, -max, advance, 2*max);
            cairo_set_source_rgba(cr, r, g, b, a);
            cairo_fill(cr);
            current += advance;
            r += dr; g += dg; b += db; a += da;
        }
        till = ultimate_till;
    }
    //ending block
    cairo_rectangle(cr, current, -max, max-current, 2*max);
    cairo_set_source_rgba(cr, to.r, to.g, to.b, to.a);
    cairo_fill(cr);
    UnClip();
}

void Canvas::radialGradient(ColorType from, ColorType to, const XY &s, double outer_radius, double inner_radius)
{
    if (cannot_draw()) return;
    cairo_pattern_t *pattern;
    pattern = cairo_pattern_create_radial(s.x, s.y, inner_radius, s.x, s.y, outer_radius);
    _add_color_stop(pattern, 0, from);
    _add_color_stop(pattern, 1, to);
    cairo_set_source(cr, pattern);
}

//from is from the outside, caller must apply clipping!!!
void Canvas::fakeRadialGrad(ColorType from, ColorType to, const XY &s, double outer_radius, double inner_radius,
                 unsigned steps, bool rectangle, double rad_from, double rad_to)
{
    if (cannot_draw()) return;
    //Normalize radiuses
    if (outer_radius < inner_radius)
        std::swap(inner_radius, outer_radius);
    if (outer_radius==0) return;
    if (outer_radius == inner_radius)
        outer_radius++;

    //Initial full-color background
    if (rectangle) {
        cairo_rectangle(cr, s.x-outer_radius, s.y-outer_radius, s.x+outer_radius, s.y+outer_radius);
        cairo_set_source_rgba(cr, from.r/255., from.g/255., from.b/255., from.a/255.);
        cairo_fill(cr);
    }
    if (steps==0) steps = 1;
    //if (outer_radius-inner_radius < steps) steps = outer_radius-inner_radius; //Not finer than 1.
    double advance = double(outer_radius-inner_radius)/steps;
    double current = outer_radius;

    double r = from.r/255.0; const double dr = (double(to.r)-double(from.r))/steps/255.0;
    double g = from.g/255.0; const double dg = (double(to.g)-double(from.g))/steps/255.0;
    double b = from.b/255.0; const double db = (double(to.b)-double(from.b))/steps/255.0;
    double a = from.a/255.0; const double da = (double(to.a)-double(from.a))/steps/255.0;

    while (current > inner_radius) {
        cairo_move_to(cr, s.x, s.y);
        cairo_arc (cr, s.x, s.y, current, rad_from, rad_to);
        cairo_set_source_rgba(cr, r, g, b, a);
        cairo_fill(cr);
        current -= advance;
        r+=dr; g+=dg; b+=db; a+=da;
    }
}

void Canvas::Fill(const Block &b, const FillAttr &fill)
{
    if (cannot_draw()) return;
    _ASSERT(fill.IsComplete());
    if (fill.color->type==ColorType::INVALID ||
        fill.color->a==0) return;
	const double outer_ring = std::min(b.x.Spans(), b.y.Spans());
	//If gradients are to be drawn, we fake them only if no alpha is present
	//If we draw somewhat transparent, we will fall back to images anyway,
	//so let us not fake gradients either -> that goes to the else branch
    if (fill.HasRealGradient() && fake_gradients && fill.color->a==255) {
        Clip(b);
        ColorType color = *fill.color;
        ColorType color2 = fill.color2.value_or(fill.color->Lighter(0.8));
        if (AvoidTransparency()) {
            color.FlattenAlpha();
            color2.FlattenAlpha();
        }
        if (fill.gradient==EIntGradType::RADIAL && *fill.gradient_angle>=0) //out
            fakeRadialGrad(color, color2, b.Centroid(), outer_ring, 0, fake_gradients, true);
        else if (fill.gradient==EIntGradType::RADIAL && *fill.gradient_angle<0) //in
            fakeRadialGrad(color2, color, b.Centroid(), outer_ring, 0, fake_gradients, true);
        else if (fill.gradient==EIntGradType::BUTTON) {
            const double step = (b.y.till-b.y.from)/100;
            Block b2(b);
            b2.y.till = b.y.till - step*66;
            fakeLinearGrad(color.Lighter(0.66), color.Lighter(0.35), b2, 270, unsigned(fake_gradients*0.34), 0.5);
            b2.y.till = b.y.till;
            b2.y.from = b.y.from + step*34;
            fakeLinearGrad(color.Lighter(0.16), color, b2, 270, unsigned(fake_gradients*0.66), 0.5);
        } else if (fill.gradient==EIntGradType::LINEAR) {
            fakeLinearGrad(color, color2, b, *fill.gradient_angle, fake_gradients, *fill.frac);
        } else {
            _ASSERT(0);
        }
        UnClip();
    } else {
        ColorType to = *fill.color, from;
        if (!fill.color2) {
            if (fill.gradient==EIntGradType::RADIAL)
                from = to.Lighter(0.6);
            else if (fill.gradient==EIntGradType::LINEAR || fill.gradient==EIntGradType::DEGENERATE)
                from = to.Lighter(0.8);
            else
                from = to; //to is not used for BUTTON and NONE
        } else
            from = *fill.color2;
        if (AvoidTransparency()) {
            from.FlattenAlpha();
            to.FlattenAlpha();
        }
        if (fill.gradient==EIntGradType::RADIAL && *fill.gradient_angle>=0) //out
            radialGradient(from, to, b.Centroid(), outer_ring, 0);
        else if (fill.gradient==EIntGradType::RADIAL && *fill.gradient_angle<0) //in
            radialGradient(to, from, b.Centroid(), outer_ring, 0);
        else //linear, degenerate or button
            linearGradient(from, to, b, *fill.gradient_angle, *fill.gradient, *fill.frac);
        RectanglePath(b.x.from, b.x.till, b.y.from, b.y.till);
        cairo_fill(cr);
    }
}

////////////////////// Shadow routines

//Angle degree is 0 then shadow is offset +off in both y and x dir
//angle grows clockwise (y grows downward) and means the rotation of the object.
//From that we need to calculate the offset of the shadow assuming that the space is already rotated.
void Canvas::Shadow(const Contour &area, const ShadowAttr &shadow, double angle_radian)
{
    if (cannot_draw()) return;
    _ASSERT(shadow.IsComplete());
    if (shadow.offset.value_or(0)==0 || area.IsEmpty()) return;
    const bool clip = false;
    //Clip out the actual shape we are the shadow of
    if (clip) {
        area.CairoPath(cr, true);
        cairo_new_sub_path(cr);
        cairo_rectangle(cr, area.GetBoundingBox().x.from-1, area.GetBoundingBox().y.from-1,
                            area.GetBoundingBox().x.Spans()+2+*shadow.offset, area.GetBoundingBox().y.Spans()+2+*shadow.offset);
        cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
        cairo_clip(cr);
    }
    const Contour &substract = area;//.CreateExpand(-0.5);
    Contour outer(area);
    XY off(*shadow.offset, *shadow.offset);
    off.Rotate(cos(-angle_radian), sin(-angle_radian));
    outer.Shift(off);
    ColorType color = *shadow.color;
    if (*shadow.blur>0) {
        std::pair<cairo_pattern_t *, Contour> mesh = { nullptr, {} };
        if (!fake_shadows && outer.size()==1 && !outer[0].HasHoles())
            mesh = outer[0].Outline().CairoMeshGradient(*shadow.blur,
                                                        color.r/255., color.g/255., color.b/255., color.a/255.,
                                                        color.r/255., color.g/255., color.b/255., 0 /* fully transparent at the edge*/,
                                                        EXPAND_ROUND);
        if (mesh.first) {
            cairo_pattern_status(mesh.first);
            cairo_set_source(cr, mesh.first);
            //take the original contour out from the fill
            (total - area).CairoPath(cr, true);
            cairo_fill(cr);
            cairo_pattern_destroy(mesh.first);
            outer = std::move(mesh.second); //prepare to fill the inner part
        } else {
            const Contour original(outer);
            const unsigned steps = unsigned(std::min(*shadow.blur, *shadow.offset)*scale_for_shadows + 0.5);
            const double transp_step = double(color.a)/(steps+1);
            double alpha = 0;
            for (unsigned i = 0; i<steps; i++) {
                const double ex = double(i+1)/-scale_for_shadows;
                Contour inner = original.CreateExpand(ex);
                if (inner.IsEmpty())
                    break;

                alpha += transp_step;
                color.a = (unsigned char)alpha;
                if (avoid_transparency)
                    SetColor(ColorType(color).FlattenAlpha());
                else
                    SetColor(color);
                //since clip is not working so well, we substract the original area
                ((outer-inner)-substract).Fill(cr);
                std::swap(inner, outer);
            }
        }
    }
    if (*shadow.blur <= *shadow.offset) {
        if (avoid_transparency)  //we still use a blurred shadow color
            SetColor(ColorType(*shadow.color).FlattenAlpha());
        else
            SetColor(*shadow.color);
    }
    (outer-substract).Fill(cr);
    if (clip)
        cairo_restore(cr);
}

/* Set clip, if the rectangle of which this is the shadow of is not opaque */
void Canvas::Shadow(const Block &b, const LineAttr &line, const ShadowAttr &shadow, double angle_radian)
{
    if (cannot_draw()) return;
    _ASSERT(shadow.IsComplete() && line.radius);
    if (shadow.IsNone()) return;
    //For now just call the other Shadow Routine
    Contour c = line.CreateRectangle_OuterEdge(b);
    c.Expand(-*line.width);
    Shadow(std::move(c), shadow, angle_radian);
    return;
}

void Canvas::Entity(const Block & outer_edge, const Label &label,
                    const SimpleStyle& style, const ShapeCollection& shapes,
                    int shape, bool align_top)
{
    if (shape>=0) {
        if (does_graphics()) {
            if (cannot_draw()) return;
            shapes.Draw(*this, shape, outer_edge, style.line, style.fill, style.shadow);
        }
        const XY twh = label.getTextWidthHeight();
        const Block b = shapes[shape].GetLabelPos(outer_edge);
        if (b.IsInvalid()) {
            const double x = outer_edge.x.MidPoint();
            if (does_graphics())
                label.Draw(*this, shapes, x-twh.x/2, x+twh.x/2, outer_edge.y.till);
            else {
                AddMore(GSMscShape(shapes, shape, outer_edge, style.line, style.fill, style.shadow));
                Add(GSText(label, shapes, x-twh.x/2, x+twh.x/2, outer_edge.y.till));
            }
        } else {
            if (does_graphics()) {
                const double r = std::min(1., std::min(b.x.Spans()/twh.x, b.y.Spans()/twh.y));
                cairo_save(cr);
                cairo_scale(cr, r, r);
                const double yoff = (b.y.Spans() - twh.y*r)/2;
                label.Draw(*this, shapes, b.x.from/r, b.x.till/r,
                    (b.y.from + yoff)/r);
                cairo_restore(cr);
            } else {
                AddMore(GSMscShape(shapes, shape, outer_edge, style.line, style.fill, style.shadow, label, true));
            }
        }
        return;
    }

    const double lw = style.line.LineWidth();
    Block b(outer_edge);

    if (does_graphics()) {
        if (cannot_draw()) return;
        Block b2(b); //save b
        LineAttr line2 = style.line;   //style.line.radius corresponds to midpoint of line
        *line2.radius = std::min(std::min(outer_edge.y.Spans()/2 - lw, outer_edge.x.Spans()/2 - lw),
                                 *line2.radius);
        if (*line2.radius>0)
            *line2.radius += lw-*line2.width/2.;  //expand to outer edge
        b.Expand(-*line2.width/2.);
        Shadow(b, style.line, style.shadow);
        if (style.fill.color && style.fill.color->type!=ColorType::INVALID) {
            _ASSERT(style.fill.color->type==ColorType::COMPLETE);
            b.Expand(-lw+*style.line.width);
            *line2.radius += -lw+*style.line.width; //only decreases radius
            Fill(b, style.line, style.fill);
        }
        b2.Expand(-lw/2);
        line2 = style.line;
        *line2.radius -= lw/2;
        Line(b2, style.line);

        //Draw text
        label.Draw(*this, shapes, b2.x.from, b2.x.till, b2.y.from + lw/2);
    } else {
        Add(GSBox(b, style.line, style.fill, style.shadow, label, align_top ? ETextAlign::Min : ETextAlign::Mid));
    }
}


inline std::array<GS, 3>
GSMscShape(const ShapeCollection& shapes, int shape, const Block &outer_edge,
           const LineAttr& l, const FillAttr& f, const ShadowAttr& s,
           const Label &lab, bool scale_font) {
    std::array<GS, 3> ret;
    unsigned num = 0;
    const Shape *p = shapes.GetShape(shape);
    if (p) {
        const Path Fill_fill = p->GetSection(0, outer_edge);
        const Path Fill_line = p->GetSection(1, outer_edge);
        const Path Line_line = p->GetSection(2, outer_edge);
        const Block text_pos = p->GetLabelPos(outer_edge);
        bool need = true; //if we need to emit label and shadow yet
        const XY twh = lab.getTextWidthHeight();
        const double scale = scale_font && !lab.IsEmpty() && twh.x>0 && twh.y>0
             ? std::min({1., text_pos.x.Spans()/twh.x, text_pos.y.Spans()/twh.y})
             : 1.;
        const Label *label = &lab;
        Label label_storage;
        if (scale!=1.) {
            label_storage = lab;
            label_storage.SetScale(scale);
            label = &label_storage;
        }
        if (Fill_fill==Line_line && Fill_fill.size()) {
            //background and line is the same
            //TODO: detect if Fill_fill and Line_line is the same path, just cycled.
            ret[num++] = GSShape(Contour(Fill_fill), l, f, s, text_pos, *label);
            need = false;
        } else {
            if (Fill_fill.size()) {
                ret[num++] = GSShape(Contour(Fill_fill), LineAttr::None(), f, s, text_pos, *label);
                need = false;
            }
            if (Line_line.size())
                ret[num++] = GSPath(Line_line, l);
        }
        if (Fill_line.size()) {
            if (need)
                ret[num++] = GSShape(Contour(Line_line), LineAttr::None(), FillAttr::Solid(*l.color), s, text_pos, *label);
            else
                ret[num++] = GSShape(Contour(Line_line), LineAttr::None(), FillAttr::Solid(*l.color));
        }
    } else
        //This is a box
        ret[num++] = GSBox(outer_edge, l, f, s, lab);
    return ret;
}

/* END OF FILE */
