//========================================================================
//
// TextOutputDev.cc
//
// Copyright 1997-2003 Glyph & Cog, LLC
//
//========================================================================

//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2005-2007 Kristian Høgsberg <krh@redhat.com>
// Copyright (C) 2005 Nickolay V. Shmyrev <nshmyrev@yandex.ru>
// Copyright (C) 2006-2008, 2011-2013 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2006, 2007, 2013 Ed Catmur <ed@catmur.co.uk>
// Copyright (C) 2006 Jeff Muizelaar <jeff@infidigm.net>
// Copyright (C) 2007, 2008, 2012, 2017 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2008 Koji Otani <sho@bbr.jp>
// Copyright (C) 2008, 2010-2012, 2014-2022, 2024-2026 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2008 Pino Toscano <pino@kde.org>
// Copyright (C) 2008, 2010 Hib Eris <hib@hiberis.nl>
// Copyright (C) 2009 Ross Moore <ross@maths.mq.edu.au>
// Copyright (C) 2009 Kovid Goyal <kovid@kovidgoyal.net>
// Copyright (C) 2010 Brian Ewins <brian.ewins@gmail.com>
// Copyright (C) 2010, 2021 Marek Kasik <mkasik@redhat.com>
// Copyright (C) 2010, 2020 Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp>
// Copyright (C) 2011 Sam Liao <phyomh@gmail.com>
// Copyright (C) 2012 Horst Prote <prote@fmi.uni-stuttgart.de>
// Copyright (C) 2012, 2013-2018 Jason Crain <jason@aquaticape.us>
// Copyright (C) 2012 Peter Breitenlohner <peb@mppmu.mpg.de>
// Copyright (C) 2013 José Aliste <jaliste@src.gnome.org>
// Copyright (C) 2013 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2013 Ed Catmur <ed@catmur.co.uk>
// Copyright (C) 2016 Khaled Hosny <khaledhosny@eglug.org>
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, <info@kdab.com>. Work sponsored by the LiMux project of the city of Munich
// Copyright (C) 2018 Sanchit Anand <sanxchit@gmail.com>
// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2018-2022, 2024, 2025 Nelson Benítez León <nbenitezl@gmail.com>
// Copyright (C) 2019 Christian Persch <chpe@src.gnome.org>
// Copyright (C) 2019, 2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2019 Dan Shea <dan.shea@logical-innovations.com>
// Copyright (C) 2021 Peter Williams <peter@newton.cx>
// Copyright (C) 2024 Adam Sampson <ats@offog.org>
// Copyright (C) 2024-2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2024-2026 Stefan Brüns <stefan.bruens@rwth-aachen.de>
// Copyright (C) 2025 Hagen Möbius <hagen.moebius@googlemail.com>
// Copyright (C) 2026 Aditya Tiwari <suntiwari3495@gmail.com>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <config.h>

#include <cstdio>
#include <cstdlib>
#include <cstddef>
#include <cmath>
#include <cfloat>
#include <algorithm>
#include <functional>
#if defined(_WIN32) || defined(__CYGWIN__)
#    include <fcntl.h> // for O_BINARY
#    include <io.h> // for _setmode
#endif
#include "goo/gfile.h"
#include "goo/gmem.h"
#include "goo/GooString.h"
#include "Error.h"
#include "GlobalParams.h"
#include "UnicodeMap.h"
#include "UnicodeTypeTable.h"
#include "Link.h"
#include "TextOutputDev.h"
#include "Page.h"
#include "Annot.h"
#include "UTF.h"

//------------------------------------------------------------------------
// parameters
//------------------------------------------------------------------------

// Each bucket in a text pool includes baselines within a range of
// this many points.
constexpr int textPoolStep = 4;

// Inter-character space width which will cause addChar to start a new
// word.
constexpr double minWordBreakSpace = 0.1;

// Negative inter-character space width, i.e., overlap, which will
// cause addChar to start a new word.
constexpr double minDupBreakOverlap = 0.2;

// Max distance between baselines of two lines within a block, as a
// fraction of the font size.
constexpr double maxLineSpacingDelta = 1.5;

// Max difference in primary font sizes on two lines in the same
// block.  Delta1 is used when examining new lines above and below the
// current block; delta2 is used when examining text that overlaps the
// current block; delta3 is used when examining text to the left and
// right of the current block.
constexpr double maxBlockFontSizeDelta1 = 0.05;
constexpr double maxBlockFontSizeDelta2 = 0.6;
constexpr double maxBlockFontSizeDelta3 = 0.2;

// Max difference in font sizes inside a word.
constexpr double maxWordFontSizeDelta = 0.05;

// Maximum distance between baselines of two words on the same line,
// e.g., distance between subscript or superscript and the primary
// baseline, as a fraction of the font size.
constexpr double maxIntraLineDelta = 0.5;

// Maximum inter-word spacing, as a fraction of the font size.
constexpr double maxWordSpacing = 1.5;

// Maximum horizontal spacing which will allow a word to be pulled
// into a block, as a fraction of the font size.
// This default value can be tweaked via API.
double TextOutputDev::minColSpacing1_default = 0.7;

// Minimum spacing between columns, as a fraction of the font size.
constexpr double minColSpacing2 = 1.0;

// Maximum vertical spacing between blocks within a flow, as a
// multiple of the font size.
constexpr double maxBlockSpacing = 2.5;

// Minimum spacing between characters within a word, as a fraction of
// the font size.
constexpr double minCharSpacing = -0.5;

// Maximum spacing between characters within a word, as a fraction of
// the font size, when there is no obvious extra-wide character
// spacing.
constexpr double maxCharSpacing = 0.03;

// When extra-wide character spacing is detected, the inter-character
// space threshold is set to the minimum inter-character space
// multiplied by this constant.
constexpr double maxWideCharSpacingMul = 1.3;

// Upper limit on spacing between characters in a word.
constexpr double maxWideCharSpacing = 0.4;

// Max difference in primary,secondary coordinates (as a fraction of
// the font size) allowed for duplicated text (fake boldface, drop
// shadows) which is to be discarded.
constexpr double dupMaxPriDelta = 0.1;
constexpr double dupMaxSecDelta = 0.2;

// Max width of underlines (in points).
constexpr int maxUnderlineWidth = 3;

// Min distance between baseline and underline (in points).
//~ this should be font-size-dependent
constexpr int minUnderlineGap = -2;

// Max distance between baseline and underline (in points).
//~ this should be font-size-dependent
constexpr int maxUnderlineGap = 4;

// Max horizontal distance between edge of word and start of underline
// (in points).
//~ this should be font-size-dependent
constexpr int underlineSlack = 1;

// Max distance between edge of text and edge of link border
constexpr int hyperlinkSlack = 2;

// Max distance between characters when combining a base character and
// combining character
constexpr double combMaxMidDelta = 0.3;
constexpr double combMaxBaseDelta = 0.4;

// Text is considered diagonal if abs(tan(angle)) > diagonalThreshold.
// (Or 1/tan(angle) for 90/270 degrees.)
constexpr double diagonalThreshold = 0.1;

// How opaque a selection on a glyphless font should be. Since the font is
// glyphless and overlaid over text in image form, this must enable users
// to read the underlying image. Issue #157
constexpr double glyphlessSelectionOpacity = 0.4;

// Returns whether x is between a and b or equal to a or b.
// a and b don't need to be sorted.
#define XBetweenAB(x, a, b) (!(((x) > (a) && (x) > (b)) || ((x) < (a) && (x) < (b))) ? true : false)

namespace {

inline bool isAscii7(Unicode uchar)
{
    return uchar < 128;
}

}

static int reorderText(const Unicode *text, int len, const UnicodeMap *uMap, bool primaryLR, GooString *s, Unicode *u)
{
    char lre[8], rle[8], popdf[8], buf[8];
    int lreLen = 0, rleLen = 0, popdfLen = 0, n;
    int nCols, i, j, k;

    nCols = 0;

    if (s) {
        lreLen = uMap->mapUnicode(0x202a, lre, sizeof(lre));
        rleLen = uMap->mapUnicode(0x202b, rle, sizeof(rle));
        popdfLen = uMap->mapUnicode(0x202c, popdf, sizeof(popdf));
    }

    if (primaryLR) {
        i = 0;
        while (i < len) {
            // output a left-to-right section
            for (j = i; j < len && !unicodeTypeR(text[j]); ++j) {
                ;
            }
            for (k = i; k < j; ++k) {
                if (s) {
                    n = uMap->mapUnicode(text[k], buf, sizeof(buf));
                    s->append(buf, n);
                }
                if (u) {
                    u[nCols] = text[k];
                }
                ++nCols;
            }
            i = j;
            // output a right-to-left section
            for (j = i; j < len && !(unicodeTypeL(text[j]) || unicodeTypeNum(text[j])); ++j) {
                ;
            }
            if (j > i) {
                if (s) {
                    s->append(rle, rleLen);
                }
                for (k = j - 1; k >= i; --k) {
                    if (s) {
                        n = uMap->mapUnicode(text[k], buf, sizeof(buf));
                        s->append(buf, n);
                    }
                    if (u) {
                        u[nCols] = text[k];
                    }
                    ++nCols;
                }
                if (s) {
                    s->append(popdf, popdfLen);
                }
                i = j;
            }
        }
    } else {
        // Note: This code treats numeric characters (European and
        // Arabic/Indic) as left-to-right, which isn't strictly correct
        // (incurs extra LRE/POPDF pairs), but does produce correct
        // visual formatting.
        if (s) {
            s->append(rle, rleLen);
        }
        i = len - 1;
        while (i >= 0) {
            // output a right-to-left section
            for (j = i; j >= 0 && !(unicodeTypeL(text[j]) || unicodeTypeNum(text[j])); --j) {
                ;
            }
            for (k = i; k > j; --k) {
                if (s) {
                    n = uMap->mapUnicode(text[k], buf, sizeof(buf));
                    s->append(buf, n);
                }
                if (u) {
                    u[nCols] = text[k];
                }
                ++nCols;
            }
            i = j;
            // output a left-to-right section
            for (j = i; j >= 0 && !unicodeTypeR(text[j]); --j) {
                ;
            }
            if (j < i) {
                if (s) {
                    s->append(lre, lreLen);
                }
                for (k = j + 1; k <= i; ++k) {
                    if (s) {
                        n = uMap->mapUnicode(text[k], buf, sizeof(buf));
                        s->append(buf, n);
                    }
                    if (u) {
                        u[nCols] = text[k];
                    }
                    ++nCols;
                }
                if (s) {
                    s->append(popdf, popdfLen);
                }
                i = j;
            }
        }
        if (s) {
            s->append(popdf, popdfLen);
        }
    }

    return nCols;
}

//------------------------------------------------------------------------
// TextUnderline
//------------------------------------------------------------------------

class TextUnderline
{
public:
    TextUnderline(double x0A, double y0A, double x1A, double y1A)
    {
        x0 = x0A;
        y0 = y0A;
        x1 = x1A;
        y1 = y1A;
        horiz = y0 == y1;
    }
    ~TextUnderline() = default;

    double x0, y0, x1, y1;
    bool horiz;
};

//------------------------------------------------------------------------
// TextLink
//------------------------------------------------------------------------

class TextLink
{
public:
    TextLink(int xMinA, int yMinA, int xMaxA, int yMaxA, AnnotLink *linkA)
    {
        xMin = xMinA;
        yMin = yMinA;
        xMax = xMaxA;
        yMax = yMaxA;
        link = linkA;
    }
    ~TextLink() = default;

    int xMin, yMin, xMax, yMax;
    AnnotLink *link;
};

//------------------------------------------------------------------------
// TextFontInfo
//------------------------------------------------------------------------

TextFontInfo::TextFontInfo(const GfxState *state)
{
    gfxFont = state->getFont();
    if (gfxFont) {
        const std::optional<std::string> &gfxFontName = gfxFont->getName();
        if (gfxFontName) {
            fontName = new GooString(*gfxFontName);
        } else {
            fontName = nullptr;
        }
    } else {
        fontName = nullptr;
    }
    flags = gfxFont ? gfxFont->getFlags() : 0;
}

TextFontInfo::~TextFontInfo()
{
    delete fontName;
}

bool TextFontInfo::matches(const GfxState *state) const
{
    return state->getFont() == gfxFont;
}

bool TextFontInfo::matches(const TextFontInfo *fontInfo) const
{
    return gfxFont == fontInfo->gfxFont;
}

bool TextFontInfo::matches(const Ref *ref) const
{
    return gfxFont && (*(gfxFont->getID()) == *ref);
}

double TextFontInfo::getAscent() const
{
    return gfxFont ? gfxFont->getAscent() : 0.95;
}

double TextFontInfo::getDescent() const
{
    return gfxFont ? gfxFont->getDescent() : -0.35;
}

GfxFont::WritingMode TextFontInfo::getWMode() const
{
    return gfxFont ? gfxFont->getWMode() : GfxFont::WritingMode::Horizontal;
}

//------------------------------------------------------------------------
// TextWord
//------------------------------------------------------------------------

TextWord::TextWord(const GfxState *state, int rotA, double fontSizeA)
{
    rot = rotA;
    fontSize = fontSizeA;
    spaceAfter = false;
    next = nullptr;
    invisible = state->getRender() == 3;

    GfxRGB rgb;

    if ((state->getRender() & 3) == 1) {
        state->getStrokeRGB(&rgb);
    } else {
        state->getFillRGB(&rgb);
    }
    colorR = colToDbl(rgb.r);
    colorG = colToDbl(rgb.g);
    colorB = colToDbl(rgb.b);

    underlined = false;
    link = nullptr;
}

TextWord::~TextWord() = default;

void TextWord::addChar(TextFontInfo *fontA, double x, double y, double dx, double dy, int charPosA, int charLen, CharCode c, Unicode u, const Matrix &textMatA)
{
    chars.push_back(CharInfo { .text = u, .charcode = c, .charPos = charPosA, .edge = 0.0, .font = fontA, .textMat = textMatA });
    charPosEnd = charPosA + charLen;

    if (len() == 1) {
        setInitialBounds(fontA, x, y);
    }

    if (wMode == GfxFont::WritingMode::Vertical) { // vertical writing mode
        // NB: the rotation value has been incremented by 1 (in
        // TextPage::beginWord()) for vertical writing mode
        switch (rot) {
        case 0:
            chars.back().edge = x - fontSize;
            xMax = edgeEnd = x;
            break;
        case 1:
            chars.back().edge = y - fontSize;
            yMax = edgeEnd = y;
            break;
        case 2:
            chars.back().edge = x + fontSize;
            xMin = edgeEnd = x;
            break;
        case 3:
            chars.back().edge = y + fontSize;
            yMin = edgeEnd = y;
            break;
        }
    } else { // horizontal writing mode
        switch (rot) {
        case 0:
            chars.back().edge = x;
            xMax = edgeEnd = x + dx;
            break;
        case 1:
            chars.back().edge = y;
            yMax = edgeEnd = y + dy;
            break;
        case 2:
            chars.back().edge = x;
            xMin = edgeEnd = x + dx;
            break;
        case 3:
            chars.back().edge = y;
            yMin = edgeEnd = y + dy;
            break;
        }
    }
}

void TextWord::setInitialBounds(TextFontInfo *fontA, double x, double y)
{
    double ascent = fontA->getAscent() * fontSize;
    double descent = fontA->getDescent() * fontSize;
    wMode = fontA->getWMode();

    if (wMode == GfxFont::WritingMode::Vertical) { // vertical writing mode
        // NB: the rotation value has been incremented by 1 (in
        // TextPage::beginWord()) for vertical writing mode
        switch (rot) {
        case 0:
            xMin = x - fontSize;
            yMin = y - fontSize;
            yMax = y;
            base = y;
            break;
        case 1:
            xMin = x;
            yMin = y - fontSize;
            xMax = x + fontSize;
            base = x;
            break;
        case 2:
            yMin = y;
            xMax = x + fontSize;
            yMax = y + fontSize;
            base = y;
            break;
        case 3:
            xMin = x - fontSize;
            xMax = x;
            yMax = y + fontSize;
            base = x;
            break;
        }
    } else { // horizontal writing mode
        switch (rot) {
        case 0:
            xMin = x;
            yMin = y - ascent;
            yMax = y - descent;
            if (yMin == yMax) {
                // this is a sanity check for a case that shouldn't happen -- but
                // if it does happen, we want to avoid dividing by zero later
                yMin = y;
                yMax = y + 1;
            }
            base = y;
            break;
        case 1:
            xMin = x + descent;
            yMin = y;
            xMax = x + ascent;
            if (xMin == xMax) {
                // this is a sanity check for a case that shouldn't happen -- but
                // if it does happen, we want to avoid dividing by zero later
                xMin = x;
                xMax = x + 1;
            }
            base = x;
            break;
        case 2:
            yMin = y + descent;
            xMax = x;
            yMax = y + ascent;
            if (yMin == yMax) {
                // this is a sanity check for a case that shouldn't happen -- but
                // if it does happen, we want to avoid dividing by zero later
                yMin = y;
                yMax = y + 1;
            }
            base = y;
            break;
        case 3:
            xMin = x - ascent;
            xMax = x - descent;
            yMax = y;
            if (xMin == xMax) {
                // this is a sanity check for a case that shouldn't happen -- but
                // if it does happen, we want to avoid dividing by zero later
                xMin = x;
                xMax = x + 1;
            }
            base = x;
            break;
        }
    }
}

struct CombiningTable
{
    Unicode base;
    Unicode comb;
};

static const struct CombiningTable combiningTable[] = {
    { .base = 0x0060, .comb = 0x0300 }, // grave
    { .base = 0x00a8, .comb = 0x0308 }, // dieresis
    { .base = 0x00af, .comb = 0x0304 }, // macron
    { .base = 0x00b4, .comb = 0x0301 }, // acute
    { .base = 0x00b8, .comb = 0x0327 }, // cedilla
    { .base = 0x02c6, .comb = 0x0302 }, // circumflex
    { .base = 0x02c7, .comb = 0x030c }, // caron
    { .base = 0x02d8, .comb = 0x0306 }, // breve
    { .base = 0x02d9, .comb = 0x0307 }, // dotaccent
    { .base = 0x02da, .comb = 0x030a }, // ring
    { .base = 0x02dc, .comb = 0x0303 }, // tilde
    { .base = 0x02dd, .comb = 0x030b } // hungarumlaut (double acute accent)
};

// returning combining versions of characters
static Unicode getCombiningChar(Unicode u)
{
    for (const CombiningTable &combining : combiningTable) {
        if (u == combining.base) {
            return combining.comb;
        }
    }
    return 0;
}

bool TextWord::addCombining(TextFontInfo *fontA, double fontSizeA, double x, double y, double dx, double dy, int charPosA, int charLen, CharCode c, Unicode u, const Matrix &textMatA)
{
    if (chars.empty() || wMode != GfxFont::WritingMode::Horizontal || fontA->getWMode() != GfxFont::WritingMode::Horizontal) {
        return false;
    }

    Unicode cCurrent = getCombiningChar(u);
    if (cCurrent != 0 && unicodeTypeAlphaNum(chars.back().text)) {
        // Current is a combining character, previous is base character
        double maxScaledMidDelta = fabs(edgeEnd - chars.back().edge) * combMaxMidDelta;
        double charMid, charBase, maxScaledBaseDelta;

        // Test if characters overlap
        if (rot == 0 || rot == 2) {
            charMid = x + (dx / 2);
            charBase = y;
            maxScaledBaseDelta = (yMax - yMin) * combMaxBaseDelta;
        } else {
            charMid = y + (dy / 2);
            charBase = x;
            maxScaledBaseDelta = (xMax - xMin) * combMaxBaseDelta;
        }

        double edgeMid = (chars.back().edge + edgeEnd) / 2;
        if (fabs(charMid - edgeMid) >= maxScaledMidDelta || fabs(charBase - base) >= maxScaledBaseDelta) {
            return false;
        }

        // Add character, but don't adjust edge / bounding box because
        // combining character's positioning could be odd.
        chars.emplace_back(CharInfo { .text = cCurrent, .charcode = c, .charPos = charPosA, .edge = edgeMid, .font = fontA, .textMat = textMatA });
        charPosEnd = charPosA + charLen;

        return true;
    }

    Unicode cPrev = getCombiningChar(chars.back().text);
    if (cPrev != 0 && unicodeTypeAlphaNum(u)) {
        // Previous is a combining character, current is base character
        double maxScaledBaseDelta = (fontA->getAscent() - fontA->getDescent()) * fontSizeA * combMaxBaseDelta;
        double charMid, charBase, maxScaledMidDelta;

        // Test if characters overlap
        if (rot == 0 || rot == 2) {
            charMid = x + (dx / 2);
            charBase = y;
            maxScaledMidDelta = fabs(dx * combMaxMidDelta);
        } else {
            charMid = y + (dy / 2);
            charBase = x;
            maxScaledMidDelta = fabs(dy * combMaxMidDelta);
        }

        double edgeMid = (chars.back().edge + edgeEnd) / 2;
        if (fabs(charMid - edgeMid) >= maxScaledMidDelta || fabs(charBase - base) >= maxScaledBaseDelta) {
            return false;
        }

        fontSize = fontSizeA;
        // move combining character to after base character
        chars.emplace_back(CharInfo { .text = cPrev, .charcode = chars.back().charcode, .charPos = charPosA, .edge = edgeMid, .font = chars.back().font, .textMat = chars.back().textMat });

        auto &lastChar = chars[chars.size() - 2];

        charPosEnd = charPosA + charLen;
        lastChar.text = u;
        lastChar.charcode = c;
        lastChar.font = fontA;
        lastChar.textMat = textMatA;

        if (len() == 2) {
            setInitialBounds(fontA, x, y);
        }

        // Updated edges / bounding box because we changed the base
        // character.
        if (wMode == GfxFont::WritingMode::Vertical) {
            // FIXME unreachable, wMode == 0
            switch (rot) {
            case 0:
                lastChar.edge = x - fontSize;
                xMax = edgeEnd = x;
                break;
            case 1:
                lastChar.edge = y - fontSize;
                yMax = edgeEnd = y;
                break;
            case 2:
                lastChar.edge = x + fontSize;
                xMin = edgeEnd = x;
                break;
            case 3:
                lastChar.edge = y + fontSize;
                yMin = edgeEnd = y;
                break;
            }
        } else {
            switch (rot) {
            case 0:
                lastChar.edge = x;
                xMax = edgeEnd = x + dx;
                break;
            case 1:
                lastChar.edge = y;
                yMax = edgeEnd = y + dy;
                break;
            case 2:
                lastChar.edge = x;
                xMin = edgeEnd = x + dx;
                break;
            case 3:
                lastChar.edge = y;
                yMin = edgeEnd = y + dy;
                break;
            }
        }

        chars.back().edge = (edgeEnd + lastChar.edge) / 2;
        return true;
    }
    return false;
}

void TextWord::merge(TextWord *word)
{
    if (word->xMin < xMin) {
        xMin = word->xMin;
    }
    if (word->yMin < yMin) {
        yMin = word->yMin;
    }
    if (word->xMax > xMax) {
        xMax = word->xMax;
    }
    if (word->yMax > yMax) {
        yMax = word->yMax;
    }
    chars.insert(chars.end(), word->chars.begin(), word->chars.end());
    edgeEnd = word->edgeEnd;
    charPosEnd = word->charPosEnd;
}

inline int TextWord::primaryCmp(const TextWord *word) const
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (rot) {
    case 0:
        cmp = xMin - word->xMin;
        break;
    case 1:
        cmp = yMin - word->yMin;
        break;
    case 2:
        cmp = word->xMax - xMax;
        break;
    case 3:
        cmp = word->yMax - yMax;
        break;
    }
    return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}

double TextWord::primaryDelta(const TextWord *word) const
{
    double delta;

    delta = 0; // make gcc happy
    switch (rot) {
    case 0:
        delta = word->xMin - xMax;
        break;
    case 1:
        delta = word->yMin - yMax;
        break;
    case 2:
        delta = xMin - word->xMax;
        break;
    case 3:
        delta = yMin - word->yMax;
        break;
    }
    return delta;
}

bool TextWord::cmpYX(const TextWord *const word1, const TextWord *const word2)
{
    double cmp;

    cmp = word1->yMin - word2->yMin;
    if (cmp == 0) {
        cmp = word1->xMin - word2->xMin;
    }
    return cmp < 0;
}

std::unique_ptr<std::string> TextWord::getText() const
{
    const UnicodeMap *uMap;
    char buf[8];

    auto s = std::make_unique<std::string>();
    if (!(uMap = globalParams->getTextEncoding())) {
        return s;
    }
    for (size_t i = 0; i < len(); ++i) {
        auto n = uMap->mapUnicode(chars[i].text, buf, sizeof(buf));
        s->append(buf, n);
    }
    return s;
}

void TextWord::getCharBBox(int charIdx, double *xMinA, double *yMinA, double *xMaxA, double *yMaxA) const
{
    if (charIdx < 0) {
        return;
    }
    size_t uCharIdx = charIdx;
    if (uCharIdx >= len()) {
        return;
    }
    auto startingEdge = chars[uCharIdx].edge;
    auto endingEdge = (uCharIdx + 1 == len()) ? edgeEnd : chars[charIdx + 1].edge;
    switch (rot) {
    case 0:
        *xMinA = startingEdge;
        *xMaxA = endingEdge;
        *yMinA = yMin;
        *yMaxA = yMax;
        break;
    case 1:
        *xMinA = xMin;
        *xMaxA = xMax;
        *yMinA = startingEdge;
        *yMaxA = endingEdge;
        break;
    case 2:
        *xMinA = endingEdge;
        *xMaxA = startingEdge;
        *yMinA = yMin;
        *yMaxA = yMax;
        break;
    case 3:
        *xMinA = xMin;
        *xMaxA = xMax;
        *yMinA = endingEdge;
        *yMaxA = startingEdge;
        break;
    }
}

//------------------------------------------------------------------------
// TextPool
//------------------------------------------------------------------------

TextPool::TextPool()
{
    minBaseIdx = 0;
    maxBaseIdx = -1;
}

TextPool::~TextPool()
{
    for (auto &wordList : pool) {
        delete wordList.head;
    }
    pool.resize(0);
}

int TextPool::getBaseIdx(double base) const
{
    const double baseIdxDouble = base / textPoolStep;
    if (std::isnan(baseIdxDouble) || baseIdxDouble < minBaseIdx) {
        return minBaseIdx;
    }
    if (baseIdxDouble > maxBaseIdx) {
        return maxBaseIdx;
    }
    return static_cast<int>(baseIdxDouble);
}

void TextPool::sort()
{
    const auto SplitWordList = [](TextWord *list) -> TextWord * {
        auto *slow = list;
        auto *fast = list->next;

        // fast is advanced twice as often as slow, so when
        // fast reaches the end slow points to the midpoint.
        while (fast) {
            fast = fast->next;
            if (fast) {
                slow = slow->next;
                fast = fast->next;
            }
        }
        auto *back = slow->next;
        slow->next = nullptr;
        return back;
    };

    const auto PosCmp = [](const TextWord &a, const TextWord &b) -> bool {
        if (a.rot == 0) {
            return ((a.xMin + a.xMax) <= (b.xMin + b.xMax));
        }
        if (a.rot == 1) {
            return ((a.yMin + a.yMax) <= (b.yMin + b.yMax));
        }
        if (a.rot == 2) {
            return ((b.xMin + b.xMax) <= (a.xMin + a.xMax));
        }
        return ((b.yMin + b.yMax) <= (a.yMin + a.yMax));
    };

    const auto SortedMerge = [PosCmp](TextWord *a, TextWord *b) {
        if (!a) {
            return b;
        }
        if (!b) {
            return a;
        }

        TextWord *head = nullptr;
        TextWord *cursor = nullptr;

        if (PosCmp(*a, *b)) {
            head = cursor = a;
            a = a->next;
        } else {
            head = cursor = b;
            b = b->next;
        }

        while (a && b) {
            if (PosCmp(*a, *b)) {
                cursor->next = a;
                cursor = cursor->next;
                a = a->next;
            } else {
                cursor->next = b;
                cursor = cursor->next;
                b = b->next;
            }
        }

        if (a) {
            cursor->next = a;
        } else {
            cursor->next = b;
        }

        return head;
    };

    const std::function<void(TextWord **)> SortWordList = [&](TextWord **list) -> void {
        if ((*list == nullptr) || ((*list)->next == nullptr)) {
            return;
        }

        TextWord *second = SplitWordList(*list);

        SortWordList(list);
        SortWordList(&second);

        *list = SortedMerge(*list, second);
    };

    for (WordList &list : pool) {
        if (!list.head) {
            continue;
        }
        SortWordList(&list.head);
    }
}

void TextPool::addWord(TextWord *word)
{
    // expand the array if needed
    int wordBaseIdx = static_cast<int>(word->base / textPoolStep);

    if (unlikely(wordBaseIdx <= INT_MIN + 128 || wordBaseIdx >= INT_MAX - 128)) {
        error(errSyntaxWarning, -1, "wordBaseIdx out of range");
        delete word;
        return;
    }

    if (pool.empty()) {
        minBaseIdx = wordBaseIdx - 128;
        maxBaseIdx = wordBaseIdx + 128;
        pool.resize(257);
    }
    while (wordBaseIdx < minBaseIdx) {
        pool.insert(pool.begin(), 128, {});
        minBaseIdx -= 128;
    }
    while (wordBaseIdx > maxBaseIdx) {
        pool.insert(pool.end(), 128, {});
        maxBaseIdx += 128;
    }

    // append the new word
    auto &wordList = pool[wordBaseIdx - minBaseIdx];
    if (wordList.tail) {
        wordList.tail->next = word;
    } else {
        wordList.head = word;
    }
    wordList.tail = word;
}

//------------------------------------------------------------------------
// TextLine
//------------------------------------------------------------------------

TextLine::TextLine(TextBlock *blkA, int rotA, double baseA)
{
    blk = blkA;
    rot = rotA;
    base = baseA;
    words = lastWord = nullptr;
    text = nullptr;
    edge = nullptr;
    col = nullptr;
    len = 0;
    convertedLen = 0;
    next = nullptr;
    xMin = yMin = 0;
    xMax = yMax = -1;
    normalized = nullptr;
    normalized_len = 0;
    normalized_idx = nullptr;
    ascii_translation = nullptr;
    ascii_len = 0;
    ascii_idx = nullptr;
}

TextLine::~TextLine()
{
    TextWord *word;

    while (words) {
        word = words;
        words = words->next;
        delete word;
    }
    gfree(text);
    gfree(edge);
    gfree(col);
    if (normalized) {
        gfree(normalized);
        gfree(normalized_idx);
    }
    if (ascii_translation) {
        gfree(ascii_translation);
        gfree(ascii_idx);
    }
}

void TextLine::addWord(TextWord *word)
{
    if (lastWord) {
        lastWord->next = word;
    } else {
        words = word;
    }
    lastWord = word;

    if (xMin > xMax) {
        xMin = word->xMin;
        xMax = word->xMax;
        yMin = word->yMin;
        yMax = word->yMax;
    } else {
        if (word->xMin < xMin) {
            xMin = word->xMin;
        }
        if (word->xMax > xMax) {
            xMax = word->xMax;
        }
        if (word->yMin < yMin) {
            yMin = word->yMin;
        }
        if (word->yMax > yMax) {
            yMax = word->yMax;
        }
    }
}

double TextLine::primaryDelta(const TextLine *line) const
{
    double delta;

    delta = 0; // make gcc happy
    switch (rot) {
    case 0:
        delta = line->xMin - xMax;
        break;
    case 1:
        delta = line->yMin - yMax;
        break;
    case 2:
        delta = xMin - line->xMax;
        break;
    case 3:
        delta = yMin - line->yMax;
        break;
    }
    return delta;
}

int TextLine::primaryCmp(const TextLine *line) const
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (rot) {
    case 0:
        cmp = xMin - line->xMin;
        break;
    case 1:
        cmp = yMin - line->yMin;
        break;
    case 2:
        cmp = line->xMax - xMax;
        break;
    case 3:
        cmp = line->yMax - yMax;
        break;
    }
    return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}

int TextLine::secondaryCmp(const TextLine *line) const
{
    double cmp;

    cmp = (rot == 0 || rot == 3) ? base - line->base : line->base - base;
    return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}

int TextLine::cmpYX(const TextLine *line) const
{
    int cmp;

    if ((cmp = secondaryCmp(line))) {
        return cmp;
    }
    return primaryCmp(line);
}

bool TextLine::cmpXY(const TextLine *const line1, const TextLine *const line2)
{
    int cmp;

    if ((cmp = line1->primaryCmp(line2))) {
        return cmp < 0;
    }
    return line1->secondaryCmp(line2) < 0;
}

void TextLine::coalesce(const UnicodeMap *uMap)
{
    double space, delta, minSpace;
    bool isUnicode;
    char buf[8];

    if (words->next) {

        // compute the inter-word space threshold
        if (words->len() > 1 || words->next->len() > 1) {
            minSpace = 0;
        } else {
            minSpace = words->primaryDelta(words->next);
            for (auto word0 = words->next, word1 = word0->next; word1 && minSpace > 0; word0 = word1, word1 = word0->next) {
                if (word1->len() > 1) {
                    minSpace = 0;
                }
                delta = word0->primaryDelta(word1);
                if (delta < minSpace) {
                    minSpace = delta;
                }
            }
        }
        if (minSpace <= 0) {
            space = maxCharSpacing * words->fontSize;
        } else {
            space = maxWideCharSpacingMul * minSpace;
            if (space > maxWideCharSpacing * words->fontSize) {
                space = maxWideCharSpacing * words->fontSize;
            }
        }

        // merge words
        auto *word0 = words;
        auto *word1 = words->next;
        while (word1) {
            if (word0->primaryDelta(word1) >= space) {
                word0->spaceAfter = true;
                word0 = word1;
                word1 = word1->next;
            } else if (word0->chars.back().font == word1->chars.front().font //
                       && word0->underlined == word1->underlined //
                       && fabs(word0->fontSize - word1->fontSize) < maxWordFontSizeDelta * words->fontSize //
                       && word1->chars.front().charPos == word0->charPosEnd) {
                word0->merge(word1);
                word0->next = word1->next;
                delete word1;
                word1 = word0->next;
            } else {
                word0 = word1;
                word1 = word1->next;
            }
        }
    }

    // build the line text
    isUnicode = uMap ? uMap->isUnicode() : false;
    len = 0;
    for (auto *word1 = words; word1; word1 = word1->next) {
        len += word1->len();
        if (word1->spaceAfter) {
            ++len;
        }
    }
    text = static_cast<Unicode *>(gmallocn(len, sizeof(Unicode)));
    edge = static_cast<double *>(gmallocn(len + 1, sizeof(double)));
    size_t i = 0;
    for (auto *word1 = words; word1; word1 = word1->next) {
        for (size_t j = 0; j < word1->len(); ++j) {
            text[i] = word1->chars[j].text;
            edge[i] = word1->chars[j].edge;
            ++i;
        }
        edge[i] = word1->edgeEnd;
        if (word1->spaceAfter) {
            text[i] = static_cast<Unicode>(0x0020);
            ++i;
        }
    }

    // compute convertedLen and set up the col array
    col = static_cast<int *>(gmallocn(len + 1, sizeof(int)));
    convertedLen = 0;
    for (int ci = 0; ci < len; ++ci) {
        col[ci] = convertedLen;
        if (isUnicode) {
            ++convertedLen;
        } else if (uMap) {
            convertedLen += uMap->mapUnicode(text[ci], buf, sizeof(buf));
        }
    }
    col[len] = convertedLen;
}

//------------------------------------------------------------------------
// TextLineFrag
//------------------------------------------------------------------------

class TextLineFrag
{
public:
    TextLine *line; // the line object
    int start, len; // offset and length of this fragment
                    //   (in Unicode chars)
    double xMin, xMax; // bounding box coordinates
    double yMin, yMax;
    double base; // baseline virtual coordinate
    int col; // first column

    void init(TextLine *lineA, int startA, int lenA);
    void computeCoords(bool oneRot);

    static bool cmpYXPrimaryRot(const TextLineFrag &frag1, const TextLineFrag &frag2);
    static bool cmpYXLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2);
    static bool cmpXYLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2);
    static bool cmpXYColumnPrimaryRot(const TextLineFrag &frag1, const TextLineFrag &frag2);
    static bool cmpXYColumnLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2);
};

void TextLineFrag::init(TextLine *lineA, int startA, int lenA)
{
    line = lineA;
    start = startA;
    len = lenA;
    col = line->col[start];
}

void TextLineFrag::computeCoords(bool oneRot)
{
    TextBlock *blk;
    double d0, d1, d2, d3, d4;

    if (oneRot) {

        switch (line->rot) {
        case 0:
            xMin = line->edge[start];
            xMax = line->edge[start + len];
            yMin = line->yMin;
            yMax = line->yMax;
            break;
        case 1:
            xMin = line->xMin;
            xMax = line->xMax;
            yMin = line->edge[start];
            yMax = line->edge[start + len];
            break;
        case 2:
            xMin = line->edge[start + len];
            xMax = line->edge[start];
            yMin = line->yMin;
            yMax = line->yMax;
            break;
        case 3:
            xMin = line->xMin;
            xMax = line->xMax;
            yMin = line->edge[start + len];
            yMax = line->edge[start];
            break;
        }
        base = line->base;

    } else {

        if (line->rot == 0 && line->blk->page->primaryRot == 0) {

            xMin = line->edge[start];
            xMax = line->edge[start + len];
            yMin = line->yMin;
            yMax = line->yMax;
            base = line->base;

        } else {

            blk = line->blk;
            d0 = line->edge[start];
            d1 = line->edge[start + len];
            d2 = d3 = d4 = 0; // make gcc happy

            switch (line->rot) {
            case 0:
                d2 = line->yMin;
                d3 = line->yMax;
                d4 = line->base;
                d0 = (d0 - blk->xMin) / (blk->xMax - blk->xMin);
                d1 = (d1 - blk->xMin) / (blk->xMax - blk->xMin);
                d2 = (d2 - blk->yMin) / (blk->yMax - blk->yMin);
                d3 = (d3 - blk->yMin) / (blk->yMax - blk->yMin);
                d4 = (d4 - blk->yMin) / (blk->yMax - blk->yMin);
                break;
            case 1:
                d2 = line->xMax;
                d3 = line->xMin;
                d4 = line->base;
                d0 = (d0 - blk->yMin) / (blk->yMax - blk->yMin);
                d1 = (d1 - blk->yMin) / (blk->yMax - blk->yMin);
                d2 = (blk->xMax - d2) / (blk->xMax - blk->xMin);
                d3 = (blk->xMax - d3) / (blk->xMax - blk->xMin);
                d4 = (blk->xMax - d4) / (blk->xMax - blk->xMin);
                break;
            case 2:
                d2 = line->yMax;
                d3 = line->yMin;
                d4 = line->base;
                d0 = (blk->xMax - d0) / (blk->xMax - blk->xMin);
                d1 = (blk->xMax - d1) / (blk->xMax - blk->xMin);
                d2 = (blk->yMax - d2) / (blk->yMax - blk->yMin);
                d3 = (blk->yMax - d3) / (blk->yMax - blk->yMin);
                d4 = (blk->yMax - d4) / (blk->yMax - blk->yMin);
                break;
            case 3:
                d2 = line->xMin;
                d3 = line->xMax;
                d4 = line->base;
                d0 = (blk->yMax - d0) / (blk->yMax - blk->yMin);
                d1 = (blk->yMax - d1) / (blk->yMax - blk->yMin);
                d2 = (d2 - blk->xMin) / (blk->xMax - blk->xMin);
                d3 = (d3 - blk->xMin) / (blk->xMax - blk->xMin);
                d4 = (d4 - blk->xMin) / (blk->xMax - blk->xMin);
                break;
            }

            switch (line->blk->page->primaryRot) {
            case 0:
                xMin = blk->xMin + d0 * (blk->xMax - blk->xMin);
                xMax = blk->xMin + d1 * (blk->xMax - blk->xMin);
                yMin = blk->yMin + d2 * (blk->yMax - blk->yMin);
                yMax = blk->yMin + d3 * (blk->yMax - blk->yMin);
                base = blk->yMin + d4 * (blk->yMax - blk->yMin);
                break;
            case 1:
                xMin = blk->xMax - d3 * (blk->xMax - blk->xMin);
                xMax = blk->xMax - d2 * (blk->xMax - blk->xMin);
                yMin = blk->yMin + d0 * (blk->yMax - blk->yMin);
                yMax = blk->yMin + d1 * (blk->yMax - blk->yMin);
                base = blk->xMax - d4 * (blk->xMax - blk->xMin);
                break;
            case 2:
                xMin = blk->xMax - d1 * (blk->xMax - blk->xMin);
                xMax = blk->xMax - d0 * (blk->xMax - blk->xMin);
                yMin = blk->yMax - d3 * (blk->yMax - blk->yMin);
                yMax = blk->yMax - d2 * (blk->yMax - blk->yMin);
                base = blk->yMax - d4 * (blk->yMax - blk->yMin);
                break;
            case 3:
                xMin = blk->xMin + d2 * (blk->xMax - blk->xMin);
                xMax = blk->xMin + d3 * (blk->xMax - blk->xMin);
                yMin = blk->yMax - d1 * (blk->yMax - blk->yMin);
                yMax = blk->yMax - d0 * (blk->yMax - blk->yMin);
                base = blk->xMin + d4 * (blk->xMax - blk->xMin);
                break;
            }
        }
    }
}

bool TextLineFrag::cmpYXPrimaryRot(const TextLineFrag &frag1, const TextLineFrag &frag2)
{
    double cmp = 0; // make gcc happy

    switch (frag1.line->blk->page->primaryRot) {
    case 0:
        if ((cmp = frag1.yMin - frag2.yMin) == 0) {
            cmp = frag1.xMin - frag2.xMin;
        }
        break;
    case 1:
        if ((cmp = frag2.xMax - frag1.xMax) == 0) {
            cmp = frag1.yMin - frag2.yMin;
        }
        break;
    case 2:
        if ((cmp = frag2.yMin - frag1.yMin) == 0) {
            cmp = frag2.xMax - frag1.xMax;
        }
        break;
    case 3:
        if ((cmp = frag1.xMax - frag2.xMax) == 0) {
            cmp = frag2.yMax - frag1.yMax;
        }
        break;
    }
    return cmp < 0;
}

bool TextLineFrag::cmpYXLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2)
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (frag1.line->rot) {
    case 0:
        if ((cmp = frag1.yMin - frag2.yMin) == 0) {
            cmp = frag1.xMin - frag2.xMin;
        }
        break;
    case 1:
        if ((cmp = frag2.xMax - frag1.xMax) == 0) {
            cmp = frag1.yMin - frag2.yMin;
        }
        break;
    case 2:
        if ((cmp = frag2.yMin - frag1.yMin) == 0) {
            cmp = frag2.xMax - frag1.xMax;
        }
        break;
    case 3:
        if ((cmp = frag1.xMax - frag2.xMax) == 0) {
            cmp = frag2.yMax - frag1.yMax;
        }
        break;
    }
    return cmp < 0;
}

bool TextLineFrag::cmpXYLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2)
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (frag1.line->rot) {
    case 0:
        if ((cmp = frag1.xMin - frag2.xMin) == 0) {
            cmp = frag1.yMin - frag2.yMin;
        }
        break;
    case 1:
        if ((cmp = frag1.yMin - frag2.yMin) == 0) {
            cmp = frag2.xMax - frag1.xMax;
        }
        break;
    case 2:
        if ((cmp = frag2.xMax - frag1.xMax) == 0) {
            cmp = frag2.yMin - frag1.yMin;
        }
        break;
    case 3:
        if ((cmp = frag2.yMax - frag1.yMax) == 0) {
            cmp = frag1.xMax - frag2.xMax;
        }
        break;
    }
    return cmp < 0;
}

bool TextLineFrag::cmpXYColumnPrimaryRot(const TextLineFrag &frag1, const TextLineFrag &frag2)
{
    double cmp;

    // if columns overlap, compare y values
    if (frag1.col < frag2.col + (frag2.line->col[frag2.start + frag2.len] - frag2.line->col[frag2.start]) && frag2.col < frag1.col + (frag1.line->col[frag1.start + frag1.len] - frag1.line->col[frag1.start])) {
        cmp = 0; // make gcc happy
        switch (frag1.line->blk->page->primaryRot) {
        case 0:
            cmp = frag1.yMin - frag2.yMin;
            break;
        case 1:
            cmp = frag2.xMax - frag1.xMax;
            break;
        case 2:
            cmp = frag2.yMin - frag1.yMin;
            break;
        case 3:
            cmp = frag1.xMax - frag2.xMax;
            break;
        }
        return cmp < 0;
    }

    // otherwise, compare starting column
    return frag1.col - frag2.col < 0;
}

bool TextLineFrag::cmpXYColumnLineRot(const TextLineFrag &frag1, const TextLineFrag &frag2)
{
    double cmp;

    // if columns overlap, compare y values
    if (frag1.col < frag2.col + (frag2.line->col[frag2.start + frag2.len] - frag2.line->col[frag2.start]) && frag2.col < frag1.col + (frag1.line->col[frag1.start + frag1.len] - frag1.line->col[frag1.start])) {
        cmp = 0; // make gcc happy
        switch (frag1.line->rot) {
        case 0:
            cmp = frag1.yMin - frag2.yMin;
            break;
        case 1:
            cmp = frag2.xMax - frag1.xMax;
            break;
        case 2:
            cmp = frag2.yMin - frag1.yMin;
            break;
        case 3:
            cmp = frag1.xMax - frag2.xMax;
            break;
        }
        return cmp < 0;
    }

    // otherwise, compare starting column
    return frag1.col - frag2.col < 0;
}

//------------------------------------------------------------------------
// TextBlock
//------------------------------------------------------------------------

TextBlock::TextBlock(TextPage *pageA, int rotA)
{
    page = pageA;
    rot = rotA;
    xMin = yMin = 0;
    xMax = yMax = -1;
    priMin = 0;
    priMax = page->pageWidth;
    pool = new TextPool();
    lines = nullptr;
    curLine = nullptr;
    next = nullptr;
    stackNext = nullptr;
    tableId = -1;
    tableEnd = false;
}

TextBlock::~TextBlock()
{
    TextLine *line;

    delete pool;
    while (lines) {
        line = lines;
        lines = lines->next;
        delete line;
    }
}

void TextBlock::addWord(TextWord *word)
{
    pool->addWord(word);
    if (xMin > xMax) {
        xMin = word->xMin;
        xMax = word->xMax;
        yMin = word->yMin;
        yMax = word->yMax;
    } else {
        if (word->xMin < xMin) {
            xMin = word->xMin;
        }
        if (word->xMax > xMax) {
            xMax = word->xMax;
        }
        if (word->yMin < yMin) {
            yMin = word->yMin;
        }
        if (word->yMax > yMax) {
            yMax = word->yMax;
        }
    }
}

void TextBlock::coalesce(const UnicodeMap *uMap, double fixedPitch)
{
    // Sort words topologically before coalescing
    pool->sort();

    // discard duplicated text (fake boldface, drop shadows)
    for (int idx0 = pool->minBaseIdx; idx0 <= pool->maxBaseIdx; ++idx0) {
        // Get the first LHS word from the pool
        TextWord *word0 = pool->getPool(idx0);

        while (word0) {
            double priDelta = dupMaxPriDelta * word0->fontSize;
            double secDelta = dupMaxSecDelta * word0->fontSize;

            // Helper to determine if the second word "matches" the first one
            // Finds identical words with almost identical positions, but also
            // partially overlapping words.
            auto keepSecond = [&priDelta, &secDelta](const TextWord &w0, const TextWord &w1) -> std::pair<size_t, size_t> {
                auto equalText = [](const TextWord &iw0, const TextWord &iw1, size_t len, size_t offset = 0) -> bool { //
                    return std::equal(iw0.chars.begin() + offset, iw0.chars.begin() + offset + len, iw1.chars.begin(), iw1.chars.begin() + len, //
                                      [](auto c1, auto c2) { return c1.text == c2.text; });
                };
                auto matchEdges = [priDelta](const TextWord &iw0, const TextWord &iw1, size_t len, size_t offset = 0) -> bool { //
                    return fabs(iw0.chars[offset].edge - iw1.chars[0].edge) < priDelta //
                            && fabs(iw0.chars[offset + len - 1].edge - iw1.chars[len - 1].edge) < priDelta;
                };
                auto matchBaseTop = [secDelta](const TextWord &iw0, const TextWord &iw1) -> bool { //
                    if ((iw0.rot == 0) || (iw0.rot == 2)) {
                        return (fabs(iw0.yMin - iw1.yMin) < secDelta && fabs(iw0.yMax - iw1.yMax) < secDelta);
                    }
                    return (fabs(iw0.xMin - iw1.xMin) < secDelta && fabs(iw0.xMax - iw1.xMax) < secDelta);

                };

                // Check if bounding boxes overlap, if not, keep both words
                if (w0.xMin > w1.xMax || w0.xMax < w1.xMin || w0.yMin > w1.yMax || w0.yMax < w1.yMin) {
                    return { 0, w1.len() };
                }

                if (w0.len() == w1.len() && equalText(w0, w1, w0.len())) {
                    // Identical words
                    if (matchEdges(w0, w1, w0.len()) && matchBaseTop(w0, w1)) {
                        // Discard w1
                        return { 0, 0 };
                    }
                    return { 0, w1.len() };
                }
                if (w0.len() < w1.len() && equalText(w0, w1, w0.len())) {
                    // w0 is the prefix of w1 (e.g. if the bold word ends with a non-bold colon)
                    if (matchEdges(w0, w1, w0.len()) && matchBaseTop(w0, w1)) {
                        return { w0.len(), w1.len() };
                    }
                } else if (w0.len() > w1.len() && equalText(w0, w1, w1.len())) {
                    // w1 is the prefix of w0 (e.g. if the bold word ends with a non-bold colon)
                    if (matchEdges(w0, w1, w1.len()) && matchBaseTop(w0, w1)) {
                        return { 0, 0 };
                    }
                }

                // Check if the tail of w0 is the head of w1, (e.g. a "quoted" bold word)
                // e.g.: '"quoted' + 'quoted"' -> '"quoted" + '"'
                for (size_t offset = 1; offset < w0.len(); offset++) {
                    auto len = std::min(w0.len() - offset, w1.len());
                    if (len == 0) {
                        break;
                    }
                    if (equalText(w0, w1, len, offset) && matchEdges(w0, w1, len, offset)) {
                        return { len, w1.len() };
                    }
                }

                return { 0, w1.len() };
            };

            int maxBaseIdx = pool->getBaseIdx(word0->base + secDelta);

            for (int idx1 = idx0; idx1 <= maxBaseIdx; idx1++) {
                TextWord *prevWord;
                /* In case the RHS word is from the same pool as the LHS word,
                 * start the inner loop with the word following the LHS word.
                 * Otherwise, start with the second word from the subsequent pools
                 * - the first word is compared at the end.
                 */
                if (idx0 == idx1) {
                    prevWord = word0;
                } else {
                    prevWord = pool->getPool(idx1);
                    if (!prevWord) {
                        continue;
                    }
                }
                TextWord *word1 = prevWord->next;

                while (word1) {
                    if (auto keep = keepSecond(*word0, *word1); keep.first == keep.second) {
                        prevWord->next = word1->next;
                        delete word1;
                        word1 = prevWord->next;
                    } else if (keep.first != 0) {
                        // Discard first part of second word
                        word1->chars.erase(word1->chars.begin(), word1->chars.begin() + keep.first);
                        if (word1->rot == 0) {
                            word1->xMin = word0->xMax;
                        } else if (word1->rot == 2) {
                            word1->xMax = word0->xMin;
                        } else if (word1->rot == 1) {
                            word1->yMin = word0->yMax;
                        } else {
                            word1->yMax = word0->yMin;
                        }

                        prevWord = word1;
                        word1 = word1->next;
                    } else {
                        prevWord = word1;
                        word1 = word1->next;
                    }
                }

                // Check the first word from each subsequent pool
                if (idx0 != idx1) {
                    word1 = pool->getPool(idx1);
                }
                if (!word1) {
                    continue;
                }
                if (auto keep = keepSecond(*word0, *word1); keep.first == keep.second) {
                    pool->setPool(idx1, word1->next);
                    delete word1;
                } else if (keep.first != 0) {
                    word1->chars.erase(word1->chars.begin(), word1->chars.begin() + keep.first);
                    word1->xMin = word0->xMax;
                }
            }

            word0 = word0->next;
        }
    }

    TextWord *word0, *word1;
    TextWord *bestWord0, *bestWord1, *lastWord;
    TextLine *line, *line0, *line1;
    TextLine **lineArray;
    int poolMinBaseIdx, startBaseIdx, minBaseIdx, maxBaseIdx;
    int baseIdx, bestWordBaseIdx;
    double minBase, maxBase;
    double fontSize, wordSpacing, delta;
    bool overlap;
    int col1, col2;
    int i, j, k;

    // build the lines
    curLine = nullptr;
    poolMinBaseIdx = pool->minBaseIdx;
    charCount = 0;
    nLines = 0;
    while (true) {

        // find the first non-empty line in the pool
        for (; poolMinBaseIdx <= pool->maxBaseIdx && !pool->getPool(poolMinBaseIdx); ++poolMinBaseIdx) {
            ;
        }
        if (poolMinBaseIdx > pool->maxBaseIdx) {
            break;
        }

        // look for the left-most word in the first four lines of the
        // pool -- this avoids starting with a superscript word
        startBaseIdx = poolMinBaseIdx;
        for (baseIdx = poolMinBaseIdx + 1; baseIdx < poolMinBaseIdx + 4 && baseIdx <= pool->maxBaseIdx; ++baseIdx) {
            if (!pool->getPool(baseIdx)) {
                continue;
            }
            if (pool->getPool(baseIdx)->primaryCmp(pool->getPool(startBaseIdx)) < 0) {
                startBaseIdx = baseIdx;
            }
        }

        // create a new line
        word0 = pool->getPool(startBaseIdx);
        pool->setPool(startBaseIdx, word0->next);
        word0->next = nullptr;
        line = new TextLine(this, word0->rot, word0->base);
        line->addWord(word0);
        lastWord = word0;

        // compute the search range
        fontSize = word0->fontSize;
        minBase = word0->base - maxIntraLineDelta * fontSize;
        maxBase = word0->base + maxIntraLineDelta * fontSize;
        minBaseIdx = pool->getBaseIdx(minBase);
        maxBaseIdx = pool->getBaseIdx(maxBase);
        wordSpacing = fixedPitch ? fixedPitch : maxWordSpacing * fontSize;

        // find the rest of the words in this line
        while (true) {

            // find the left-most word whose baseline is in the range for
            // this line
            bestWordBaseIdx = 0;
            bestWord0 = bestWord1 = nullptr;
            overlap = false;
            for (baseIdx = minBaseIdx; !overlap && baseIdx <= maxBaseIdx; ++baseIdx) {
                for (word0 = nullptr, word1 = pool->getPool(baseIdx); word1; word0 = word1, word1 = word1->next) {
                    if (word1->base >= minBase && word1->base <= maxBase) {
                        delta = lastWord->primaryDelta(word1);
                        if (delta < minCharSpacing * fontSize) {
                            overlap = true;
                            break;
                        }
                        if (delta < wordSpacing && (!bestWord1 || word1->primaryCmp(bestWord1) < 0)) {
                            bestWordBaseIdx = baseIdx;
                            bestWord0 = word0;
                            bestWord1 = word1;
                        }
                        break;
                    }
                }
            }
            if (overlap || !bestWord1) {
                break;
            }

            // remove it from the pool, and add it to the line
            if (bestWord0) {
                bestWord0->next = bestWord1->next;
            } else {
                pool->setPool(bestWordBaseIdx, bestWord1->next);
            }
            bestWord1->next = nullptr;
            line->addWord(bestWord1);
            lastWord = bestWord1;
        }

        // add the line
        if (curLine && line->cmpYX(curLine) > 0) {
            line0 = curLine;
            line1 = curLine->next;
        } else {
            line0 = nullptr;
            line1 = lines;
        }
        for (; line1 && line->cmpYX(line1) > 0; line0 = line1, line1 = line1->next) {
            ;
        }
        if (line0) {
            line0->next = line;
        } else {
            lines = line;
        }
        line->next = line1;
        curLine = line;
        line->coalesce(uMap);
        charCount += line->len;
        ++nLines;
    }

    // sort lines into xy order for column assignment
    lineArray = static_cast<TextLine **>(gmallocn(nLines, sizeof(TextLine *)));
    for (line = lines, i = 0; line; line = line->next, ++i) {
        lineArray[i] = line;
    }
    std::sort(lineArray, lineArray + nLines, &TextLine::cmpXY);

    // column assignment
    nColumns = 0;
    if (fixedPitch) {
        for (i = 0; i < nLines; ++i) {
            line0 = lineArray[i];
            col1 = 0; // make gcc happy
            switch (rot) {
            case 0:
                col1 = static_cast<int>((line0->xMin - xMin) / fixedPitch + 0.5);
                break;
            case 1:
                col1 = static_cast<int>((line0->yMin - yMin) / fixedPitch + 0.5);
                break;
            case 2:
                col1 = static_cast<int>((xMax - line0->xMax) / fixedPitch + 0.5);
                break;
            case 3:
                col1 = static_cast<int>((yMax - line0->yMax) / fixedPitch + 0.5);
                break;
            }
            for (k = 0; k <= line0->len; ++k) {
                line0->col[k] += col1;
            }
            if (line0->col[line0->len] > nColumns) {
                nColumns = line0->col[line0->len];
            }
        }
    } else {
        for (i = 0; i < nLines; ++i) {
            line0 = lineArray[i];
            col1 = 0;
            for (j = 0; j < i; ++j) {
                line1 = lineArray[j];
                if (line1->primaryDelta(line0) >= 0) {
                    col2 = line1->col[line1->len] + 1;
                } else {
                    k = 0; // make gcc happy
                    switch (rot) {
                    case 0:
                        for (k = 0; k < line1->len && line0->xMin >= 0.5 * (line1->edge[k] + line1->edge[k + 1]); ++k) {
                            ;
                        }
                        break;
                    case 1:
                        for (k = 0; k < line1->len && line0->yMin >= 0.5 * (line1->edge[k] + line1->edge[k + 1]); ++k) {
                            ;
                        }
                        break;
                    case 2:
                        for (k = 0; k < line1->len && line0->xMax <= 0.5 * (line1->edge[k] + line1->edge[k + 1]); ++k) {
                            ;
                        }
                        break;
                    case 3:
                        for (k = 0; k < line1->len && line0->yMax <= 0.5 * (line1->edge[k] + line1->edge[k + 1]); ++k) {
                            ;
                        }
                        break;
                    }
                    col2 = line1->col[k];
                }
                if (col2 > col1) {
                    col1 = col2;
                }
            }
            for (k = 0; k <= line0->len; ++k) {
                line0->col[k] += col1;
            }
            if (line0->col[line0->len] > nColumns) {
                nColumns = line0->col[line0->len];
            }
        }
    }
    gfree(static_cast<void *>(lineArray));
}

void TextBlock::updatePriMinMax(const TextBlock *blk)
{
    double newPriMin, newPriMax;
    bool gotPriMin, gotPriMax;

    gotPriMin = gotPriMax = false;
    newPriMin = newPriMax = 0; // make gcc happy
    switch (page->primaryRot) {
    case 0:
    case 2:
        if (blk->yMin < yMax && blk->yMax > yMin) {
            if (blk->xMin < xMin) {
                newPriMin = blk->xMax;
                gotPriMin = true;
            }
            if (blk->xMax > xMax) {
                newPriMax = blk->xMin;
                gotPriMax = true;
            }
        }
        break;
    case 1:
    case 3:
        if (blk->xMin < xMax && blk->xMax > xMin) {
            if (blk->yMin < yMin) {
                newPriMin = blk->yMax;
                gotPriMin = true;
            }
            if (blk->yMax > yMax) {
                newPriMax = blk->yMin;
                gotPriMax = true;
            }
        }
        break;
    }
    if (gotPriMin) {
        if (newPriMin > xMin) {
            newPriMin = xMin;
        }
        if (newPriMin > priMin) {
            priMin = newPriMin;
        }
    }
    if (gotPriMax) {
        if (newPriMax < xMax) {
            newPriMax = xMax;
        }
        if (newPriMax < priMax) {
            priMax = newPriMax;
        }
    }
}

bool TextBlock::cmpXYPrimaryRot(const TextBlock *const blk1, const TextBlock *const blk2)
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (blk1->page->primaryRot) {
    case 0:
        if ((cmp = blk1->xMin - blk2->xMin) == 0) {
            cmp = blk1->yMin - blk2->yMin;
        }
        break;
    case 1:
        if ((cmp = blk1->yMin - blk2->yMin) == 0) {
            cmp = blk2->xMax - blk1->xMax;
        }
        break;
    case 2:
        if ((cmp = blk2->xMax - blk1->xMax) == 0) {
            cmp = blk2->yMin - blk1->yMin;
        }
        break;
    case 3:
        if ((cmp = blk2->yMax - blk1->yMax) == 0) {
            cmp = blk1->xMax - blk2->xMax;
        }
        break;
    }
    return cmp < 0;
}

int TextBlock::primaryCmp(const TextBlock *blk) const
{
    double cmp;

    cmp = 0; // make gcc happy
    switch (rot) {
    case 0:
        cmp = xMin - blk->xMin;
        break;
    case 1:
        cmp = yMin - blk->yMin;
        break;
    case 2:
        cmp = blk->xMax - xMax;
        break;
    case 3:
        cmp = blk->yMax - yMax;
        break;
    }
    return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
}

double TextBlock::secondaryDelta(const TextBlock *blk) const
{
    double delta;

    delta = 0; // make gcc happy
    switch (rot) {
    case 0:
        delta = blk->yMin - yMax;
        break;
    case 1:
        delta = xMin - blk->xMax;
        break;
    case 2:
        delta = yMin - blk->yMax;
        break;
    case 3:
        delta = blk->xMin - xMax;
        break;
    }
    return delta;
}

bool TextBlock::isBelow(const TextBlock *blk) const
{
    bool below;

    below = false; // make gcc happy
    switch (page->primaryRot) {
    case 0:
        below = xMin >= blk->priMin && xMax <= blk->priMax && yMin > blk->yMin;
        break;
    case 1:
        below = yMin >= blk->priMin && yMax <= blk->priMax && xMax < blk->xMax;
        break;
    case 2:
        below = xMin >= blk->priMin && xMax <= blk->priMax && yMax < blk->yMax;
        break;
    case 3:
        below = yMin >= blk->priMin && yMax <= blk->priMax && xMin > blk->xMin;
        break;
    }

    return below;
}

bool TextBlock::isBeforeByRule1(const TextBlock *blk1)
{
    bool before = false;
    bool overlap = false;

    switch (this->page->primaryRot) {
    case 0:
    case 2:
        overlap = ((this->ExMin <= blk1->ExMin) && (blk1->ExMin <= this->ExMax)) || ((blk1->ExMin <= this->ExMin) && (this->ExMin <= blk1->ExMax));
        break;
    case 1:
    case 3:
        overlap = ((this->EyMin <= blk1->EyMin) && (blk1->EyMin <= this->EyMax)) || ((blk1->EyMin <= this->EyMin) && (this->EyMin <= blk1->EyMax));
        break;
    }
    switch (this->page->primaryRot) {
    case 0:
        before = overlap && this->EyMin < blk1->EyMin;
        break;
    case 1:
        before = overlap && this->ExMax > blk1->ExMax;
        break;
    case 2:
        before = overlap && this->EyMax > blk1->EyMax;
        break;
    case 3:
        before = overlap && this->ExMin < blk1->ExMin;
        break;
    }
    return before;
}

bool TextBlock::isBeforeByRule2(const TextBlock *blk1)
{
    double cmp = 0;
    int rotLR = rot;

    if (!page->primaryLR) {
        rotLR = (rotLR + 2) % 4;
    }

    switch (rotLR) {
    case 0:
        cmp = ExMax - blk1->ExMin;
        break;
    case 1:
        cmp = EyMin - blk1->EyMax;
        break;
    case 2:
        cmp = blk1->ExMax - ExMin;
        break;
    case 3:
        cmp = blk1->EyMin - EyMax;
        break;
    }
    return cmp <= 0;
}

// Sort into reading order by performing a topological sort using the rules
// given in "High Performance Document Layout Analysis", T.M. Breuel, 2003.
// See http://pubs.iupr.org/#2003-breuel-sdiut
// Topological sort is done by depth first search, see
// http://en.wikipedia.org/wiki/Topological_sorting
int TextBlock::visitDepthFirst(TextBlock *blkList, int pos1, TextBlock **sorted, int sortPos, bool *visited, TextBlock **cache, int cacheSize)
{
    int pos2;
    TextBlock *blk1, *blk2, *blk3;
    bool before;

    if (visited[pos1]) {
        return sortPos;
    }

    blk1 = this;

#if 0 // for debugging
  printf("visited: %d %.2f..%.2f %.2f..%.2f\n",
	 sortPos, blk1->ExMin, blk1->ExMax, blk1->EyMin, blk1->EyMax);
#endif
    visited[pos1] = true;
    pos2 = -1;
    for (blk2 = blkList; blk2; blk2 = blk2->next) {
        pos2++;
        if (visited[pos2]) {
            // skip visited nodes
            continue;
        }
        before = false;

        // is blk2 before blk1? (for table entries)
        if (blk1->tableId >= 0 && blk1->tableId == blk2->tableId) {
            if (page->primaryLR) {
                if (blk2->xMax <= blk1->xMin && blk2->yMin <= blk1->yMax && blk2->yMax >= blk1->yMin) {
                    before = true;
                }
            } else {
                if (blk2->xMin >= blk1->xMax && blk2->yMin <= blk1->yMax && blk2->yMax >= blk1->yMin) {
                    before = true;
                }
            }

            if (blk2->yMax <= blk1->yMin) {
                before = true;
            }
        } else {
            if (blk2->isBeforeByRule1(blk1)) {
                // Rule (1) blk1 and blk2 overlap, and blk2 is above blk1.
                before = true;
#if 0 // for debugging
        printf("rule1: %.2f..%.2f %.2f..%.2f %.2f..%.2f %.2f..%.2f\n",
	       blk2->ExMin, blk2->ExMax, blk2->EyMin, blk2->EyMax,
	       blk1->ExMin, blk1->ExMax, blk1->EyMin, blk1->EyMax);
#endif
            } else if (blk2->isBeforeByRule2(blk1)) {
                // Rule (2) blk2 left of blk1, and no intervening blk3
                //          such that blk1 is before blk3 by rule 1,
                //          and blk3 is before blk2 by rule 1.
                before = true;
                for (int i = 0; i < cacheSize && cache[i]; ++i) {
                    if (blk1->isBeforeByRule1(cache[i]) && cache[i]->isBeforeByRule1(blk2)) {
                        before = false;
                        std::rotate(cache, cache + i, cache + i + 1);
                        break;
                    }
                }

                if (before) {
                    for (blk3 = blkList; blk3; blk3 = blk3->next) {
                        if (blk3 == blk2 || blk3 == blk1) {
                            continue;
                        }
                        if (blk1->isBeforeByRule1(blk3) && blk3->isBeforeByRule1(blk2)) {
                            before = false;
                            std::copy_backward(cache, cache + cacheSize - 1, cache + cacheSize);
                            cache[0] = blk3;
                            break;
                        }
                    }
                }
#if 0 // for debugging
        if (before) {
	  printf("rule2: %.2f..%.2f %.2f..%.2f %.2f..%.2f %.2f..%.2f\n",
	         blk1->ExMin, blk1->ExMax, blk1->EyMin, blk1->EyMax,
	         blk2->ExMin, blk2->ExMax, blk2->EyMin, blk2->EyMax);
        }
#endif
            }
        }
        if (before) {
            // blk2 is before blk1, so it needs to be visited
            // before we can add blk1 to the sorted list.
            sortPos = blk2->visitDepthFirst(blkList, pos2, sorted, sortPos, visited, cache, cacheSize);
        }
    }
#if 0 // for debugging
  printf("sorted: %d %.2f..%.2f %.2f..%.2f\n",
	 sortPos, blk1->ExMin, blk1->ExMax, blk1->EyMin, blk1->EyMax);
#endif
    sorted[sortPos++] = blk1;
    return sortPos;
}

int TextBlock::visitDepthFirst(TextBlock *blkList, int pos1, TextBlock **sorted, int sortPos, bool *visited)
{
    const int blockCacheSize = 4;
    TextBlock *blockCache[blockCacheSize];
    std::fill(blockCache, blockCache + blockCacheSize, nullptr);
    return visitDepthFirst(blkList, pos1, sorted, sortPos, visited, blockCache, blockCacheSize);
}

//------------------------------------------------------------------------
// TextFlow
//------------------------------------------------------------------------

TextFlow::TextFlow(TextPage *pageA, TextBlock *blk)
{
    page = pageA;
    xMin = blk->xMin;
    xMax = blk->xMax;
    yMin = blk->yMin;
    yMax = blk->yMax;
    priMin = blk->priMin;
    priMax = blk->priMax;
    blocks = lastBlk = blk;
    next = nullptr;
}

TextFlow::~TextFlow()
{
    TextBlock *blk;

    while (blocks) {
        blk = blocks;
        blocks = blocks->next;
        delete blk;
    }
}

void TextFlow::addBlock(TextBlock *blk)
{
    if (lastBlk) {
        lastBlk->next = blk;
    } else {
        blocks = blk;
    }
    lastBlk = blk;
    if (blk->xMin < xMin) {
        xMin = blk->xMin;
    }
    if (blk->xMax > xMax) {
        xMax = blk->xMax;
    }
    if (blk->yMin < yMin) {
        yMin = blk->yMin;
    }
    if (blk->yMax > yMax) {
        yMax = blk->yMax;
    }
}

bool TextFlow::blockFits(const TextBlock *blk) const
{
    bool fits;

    // lower blocks must use smaller fonts
    if (blk->lines->words->fontSize > lastBlk->lines->words->fontSize) {
        return false;
    }

    fits = false; // make gcc happy
    switch (page->primaryRot) {
    case 0:
        fits = blk->xMin >= priMin && blk->xMax <= priMax;
        break;
    case 1:
        fits = blk->yMin >= priMin && blk->yMax <= priMax;
        break;
    case 2:
        fits = blk->xMin >= priMin && blk->xMax <= priMax;
        break;
    case 3:
        fits = blk->yMin >= priMin && blk->yMax <= priMax;
        break;
    }
    return fits;
}

//------------------------------------------------------------------------
// TextWordList
//------------------------------------------------------------------------

TextWordList::TextWordList(const TextPage *text, bool physLayout)
{
    TextFlow *flow;
    TextBlock *blk;
    TextLine *line;
    TextWord *word;

    if (text->rawOrder) {
        for (word = text->rawWords; word; word = word->next) {
            words.push_back(word);
        }

    } else if (physLayout) {
        // this is inefficient, but it's also the least useful of these
        // three cases
        int nWords = 0;
        for (flow = text->flows; flow; flow = flow->next) {
            for (blk = flow->blocks; blk; blk = blk->next) {
                for (line = blk->lines; line; line = line->next) {
                    for (word = line->words; word; word = word->next) {
                        ++nWords;
                    }
                }
            }
        }
        words.reserve(nWords);
        for (flow = text->flows; flow; flow = flow->next) {
            for (blk = flow->blocks; blk; blk = blk->next) {
                for (line = blk->lines; line; line = line->next) {
                    for (word = line->words; word; word = word->next) {
                        words.push_back(word);
                    }
                }
            }
        }
        std::ranges::sort(words, &TextWord::cmpYX);

    } else {
        for (flow = text->flows; flow; flow = flow->next) {
            for (blk = flow->blocks; blk; blk = blk->next) {
                for (line = blk->lines; line; line = line->next) {
                    for (word = line->words; word; word = word->next) {
                        words.push_back(word);
                    }
                }
            }
        }
    }
}

TextWordList::~TextWordList() = default;

//------------------------------------------------------------------------
// TextPage
//------------------------------------------------------------------------

TextPage::TextPage(bool rawOrderA, bool discardDiagA)
{
    int rot;

    rawOrder = rawOrderA;
    discardDiag = discardDiagA;
    curWord = nullptr;
    charPos = 0;
    curFont = nullptr;
    curFontSize = 0;
    nest = 0;
    nTinyChars = 0;
    lastCharOverlap = false;
    if (!rawOrder) {
        for (rot = 0; rot < 4; ++rot) {
            pools[rot] = std::make_unique<TextPool>();
        }
    }
    flows = nullptr;
    blocks = nullptr;
    rawWords = nullptr;
    rawLastWord = nullptr;
    lastFindXMin = lastFindYMin = 0;
    haveLastFind = false;
    mergeCombining = true;
    diagonal = false;
}

TextPage::~TextPage()
{
    clear();
}

void TextPage::startPage(const GfxState *state)
{
    clear();
    if (state) {
        pageWidth = state->getPageWidth();
        pageHeight = state->getPageHeight();
    } else {
        pageWidth = pageHeight = 0;
    }
}

void TextPage::endPage()
{
    if (curWord) {
        endWord();
    }
    for (std::unique_ptr<TextPool> &pool : pools) {
        if (pool) {
            pool->sort();
        }
    }
}

void TextPage::clear()
{
    int rot;
    TextFlow *flow;
    TextWord *word;

    if (curWord) {
        delete curWord;
        curWord = nullptr;
    }
    if (rawOrder) {
        while (rawWords) {
            word = rawWords;
            rawWords = rawWords->next;
            delete word;
        }
    } else {
        for (rot = 0; rot < 4; ++rot) {
            pools[rot] = std::make_unique<TextPool>();
        }
        while (flows) {
            flow = flows;
            flows = flows->next;
            delete flow;
        }
        gfree(static_cast<void *>(blocks));
    }
    fonts.clear();
    underlines.clear();
    links.clear();

    diagonal = false;
    curWord = nullptr;
    charPos = 0;
    curFont = nullptr;
    curFontSize = 0;
    nest = 0;
    nTinyChars = 0;
    flows = nullptr;
    blocks = nullptr;
    rawWords = nullptr;
    rawLastWord = nullptr;
}

void TextPage::updateFont(const GfxState *state)
{
    const char *name;
    int code, mCode, letterCode, anyCode;
    double w;

    // get the font info object
    curFont = nullptr;
    for (const std::unique_ptr<TextFontInfo> &f : fonts) {
        if (f->matches(state)) {
            curFont = f.get();
            break;
        }
    }
    if (!curFont) {
        fonts.emplace_back(std::make_unique<TextFontInfo>(state));
        curFont = fonts.back().get();
    }

    // adjust the font size
    GfxFont *const gfxFont = state->getFont().get();
    curFontSize = state->getTransformedFontSize();
    if (gfxFont && gfxFont->getType() == fontType3) {
        // This is a hack which makes it possible to deal with some Type 3
        // fonts.  The problem is that it's impossible to know what the
        // base coordinate system used in the font is without actually
        // rendering the font.  This code tries to guess by looking at the
        // width of the character 'm' (which breaks if the font is a
        // subset that doesn't contain 'm').
        mCode = letterCode = anyCode = -1;
        for (code = 0; code < 256; ++code) {
            name = (static_cast<Gfx8BitFont *>(gfxFont))->getCharName(code);
            int nameLen = name ? strlen(name) : 0;
            bool nameOneChar = nameLen == 1 || (nameLen > 1 && name[1] == '\0');
            if (nameOneChar && name[0] == 'm') {
                mCode = code;
            }
            if (letterCode < 0 && nameOneChar && ((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z'))) {
                letterCode = code;
            }
            if (anyCode < 0 && name && (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(code) > 0) {
                anyCode = code;
            }
        }
        if (mCode >= 0 && (w = (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(mCode)) > 0) {
            // 0.6 is a generic average 'm' width -- yes, this is a hack
            curFontSize *= w / 0.6;
        } else if (letterCode >= 0 && (w = (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(letterCode)) > 0) {
            // even more of a hack: 0.5 is a generic letter width
            curFontSize *= w / 0.5;
        } else if (anyCode >= 0 && (w = (static_cast<Gfx8BitFont *>(gfxFont))->getWidth(anyCode)) > 0) {
            // better than nothing: 0.5 is a generic character width
            curFontSize *= w / 0.5;
        }
        const std::array<double, 6> &fm = gfxFont->getFontMatrix();
        if (fm[0] != 0) {
            curFontSize *= fabs(fm[3] / fm[0]);
        }
    }
}

void TextPage::beginWord(const GfxState *state)
{
    double m[4], m2[4];
    int rot;

    // This check is needed because Type 3 characters can contain
    // text-drawing operations (when TextPage is being used via
    // {X,Win}SplashOutputDev rather than TextOutputDev).
    if (curWord) {
        ++nest;
        return;
    }

    // compute the rotation
    state->getFontTransMat(&m[0], &m[1], &m[2], &m[3]);
    const std::shared_ptr<GfxFont> &gfxFont = state->getFont();
    if (gfxFont && gfxFont->getType() == fontType3) {
        const std::array<double, 6> &fontm = state->getFont()->getFontMatrix();
        m2[0] = fontm[0] * m[0] + fontm[1] * m[2];
        m2[1] = fontm[0] * m[1] + fontm[1] * m[3];
        m2[2] = fontm[2] * m[0] + fontm[3] * m[2];
        m2[3] = fontm[2] * m[1] + fontm[3] * m[3];
        m[0] = m2[0];
        m[1] = m2[1];
        m[2] = m2[2];
        m[3] = m2[3];
    }
    if (fabs(m[0] * m[3]) > fabs(m[1] * m[2])) {
        rot = (m[0] > 0 || m[3] < 0) ? 0 : 2;
    } else {
        rot = (m[2] > 0) ? 1 : 3;
    }
    if (fabs(m[0]) >= fabs(m[1])) {
        diagonal = fabs(m[1]) > diagonalThreshold * fabs(m[0]);
    } else {
        diagonal = fabs(m[0]) > diagonalThreshold * fabs(m[1]);
    }

    // for vertical writing mode, the lines are effectively rotated 90
    // degrees
    if (gfxFont && gfxFont->getWMode() == GfxFont::WritingMode::Vertical) {
        rot = (rot + 1) & 3;
    }

    curWord = new TextWord(state, rot, curFontSize);
}

void TextPage::addChar(const GfxState *state, double x, double y, double dx, double dy, CharCode c, int nBytes, const Unicode *u, int uLen)
{
    double x1, y1, w1, h1, dx2, dy2, base, sp, delta;
    bool overlap;
    int i;
    Matrix mat;

    // subtract char and word spacing from the dx,dy values
    sp = state->getCharSpace();
    if (c == static_cast<CharCode>(0x20)) {
        sp += state->getWordSpace();
    }
    state->textTransformDelta(sp * state->getHorizScaling(), 0, &dx2, &dy2);
    dx -= dx2;
    dy -= dy2;
    state->transformDelta(dx, dy, &w1, &h1);

    // throw away chars that aren't inside the page bounds
    // (and also do a sanity check on the character size)
    state->transform(x, y, &x1, &y1);
    if (x1 + w1 < 0 || x1 > pageWidth || y1 + h1 < 0 || y1 > pageHeight || std::isnan(x1) || std::isnan(y1) || std::isnan(w1) || std::isnan(h1)) {
        charPos += nBytes;
        return;
    }

    // check the tiny chars limit
    if (fabs(w1) < 3 && fabs(h1) < 3) {
        if (++nTinyChars > 50000) {
            charPos += nBytes;
            return;
        }
    }

    // break words at space character
    if (uLen == 1 && UnicodeIsWhitespace(u[0])) {
        charPos += nBytes;
        endWord();
        return;
    }
    if (uLen == 1 && u[0] == static_cast<Unicode>(0x0)) {
        // ignore null characters
        charPos += nBytes;
        return;
    }

    state->getFontTransMat(mat.m.data(), &mat.m[1], &mat.m[2], &mat.m[3]);
    mat.m[0] *= state->getHorizScaling();
    mat.m[1] *= state->getHorizScaling();
    mat.m[4] = x1;
    mat.m[5] = y1;

    if (mergeCombining && curWord && uLen == 1 && curWord->addCombining(curFont, curFontSize, x1, y1, w1, h1, charPos, nBytes, c, u[0], mat)) {
        charPos += nBytes;
        return;
    }

    // start a new word if:
    // (1) this character doesn't fall in the right place relative to
    //     the end of the previous word (this places upper and lower
    //     constraints on the position deltas along both the primary
    //     and secondary axes), or
    // (2) this character overlaps the previous one (duplicated text), or
    // (3) the previous character was an overlap (we want each duplicated
    //     character to be in a word by itself at this stage),
    // (4) the font size has changed
    // (5) the WMode changed
    if (curWord && curWord->len() > 0) {
        base = sp = delta = 0; // make gcc happy
        switch (curWord->rot) {
        case 0:
            base = y1;
            sp = x1 - curWord->xMax;
            delta = x1 - curWord->chars.back().edge;
            break;
        case 1:
            base = x1;
            sp = y1 - curWord->yMax;
            delta = y1 - curWord->chars.back().edge;
            break;
        case 2:
            base = y1;
            sp = curWord->xMin - x1;
            delta = curWord->chars.back().edge - x1;
            break;
        case 3:
            base = x1;
            sp = curWord->yMin - y1;
            delta = curWord->chars.back().edge - y1;
            break;
        }
        overlap = fabs(delta) < dupMaxPriDelta * curWord->fontSize && fabs(base - curWord->base) < dupMaxSecDelta * curWord->fontSize;
        const GfxFont::WritingMode wMode = curFont->getWMode();
        if (overlap || lastCharOverlap || sp < -minDupBreakOverlap * curWord->fontSize || sp > minWordBreakSpace * curWord->fontSize || fabs(base - curWord->base) > 0.5 || curFontSize != curWord->fontSize || wMode != curWord->wMode) {
            endWord();
        }
        lastCharOverlap = overlap;
    } else {
        lastCharOverlap = false;
    }

    if (uLen != 0) {
        // start a new word if needed
        if (!curWord) {
            beginWord(state);
        }

        // throw away diagonal chars
        if (discardDiag && diagonal) {
            charPos += nBytes;
            return;
        }

        // page rotation and/or transform matrices can cause text to be
        // drawn in reverse order -- in this case, swap the begin/end
        // coordinates and break text into individual chars
        if ((curWord->rot == 0 && w1 < 0) || (curWord->rot == 1 && h1 < 0) || (curWord->rot == 2 && w1 > 0) || (curWord->rot == 3 && h1 > 0)) {
            endWord();
            beginWord(state);

            // throw away diagonal chars
            if (discardDiag && diagonal) {
                charPos += nBytes;
                return;
            }

            x1 += w1;
            y1 += h1;
            w1 = -w1;
            h1 = -h1;
        }

        // add the characters to the current word
        w1 /= uLen;
        h1 /= uLen;
        for (i = 0; i < uLen; ++i) {
            curWord->addChar(curFont, x1 + i * w1, y1 + i * h1, w1, h1, charPos, nBytes, c, u[i], mat);
        }
    }
    charPos += nBytes;
}

void TextPage::incCharCount(int nChars)
{
    charPos += nChars;
}

void TextPage::endWord()
{
    // This check is needed because Type 3 characters can contain
    // text-drawing operations (when TextPage is being used via
    // {X,Win}SplashOutputDev rather than TextOutputDev).
    if (nest > 0) {
        --nest;
        return;
    }

    if (curWord) {
        addWord(curWord);
        curWord = nullptr;
    }
}

void TextPage::addWord(TextWord *word)
{
    // throw away zero-length words -- they don't have valid xMin/xMax
    // values, and they're useless anyway
    if (word->len() == 0) {
        delete word;
        return;
    }

    if (rawOrder) {
        if (rawLastWord) {
            rawLastWord->next = word;
        } else {
            rawWords = word;
        }
        rawLastWord = word;
    } else {
        pools[word->rot]->addWord(word);
    }
}

void TextPage::addUnderline(double x0, double y0, double x1, double y1)
{
    underlines.emplace_back(std::make_unique<TextUnderline>(x0, y0, x1, y1));
}

void TextPage::addLink(int xMin, int yMin, int xMax, int yMax, AnnotLink *link)
{
    links.emplace_back(std::make_unique<TextLink>(xMin, yMin, xMax, yMax, link));
}

void TextPage::coalesce(bool physLayout, double fixedPitch, bool doHTML)
{
    coalesce(physLayout, fixedPitch, doHTML, TextOutputDev::minColSpacing1_default);
}

void TextPage::coalesce(bool physLayout, double fixedPitch, bool doHTML, double minColSpacing1)
{
    TextWord *word0, *word1, *word2;
    TextLine *line;
    TextBlock *blkList, *blk, *lastBlk, *blk0, *blk1, *blk2;
    TextFlow *flow, *lastFlow;
    int rot, poolMinBaseIdx, baseIdx, startBaseIdx, endBaseIdx;
    double minBase, maxBase, newMinBase, newMaxBase;
    double fontSize, colSpace1, colSpace2, lineSpace, intraLineSpace, blkSpace;
    bool found;
    int count[4];
    int lrCount;
    int col1, col2;
    int j, n;

    if (rawOrder) {
        primaryRot = 0;
        primaryLR = true;
        return;
    }

    const UnicodeMap *uMap = globalParams->getTextEncoding();
    blkList = nullptr;
    lastBlk = nullptr;
    nBlocks = 0;
    primaryRot = 0;

#if 0 // for debugging
  printf("*** initial words ***\n");
  for (rot = 0; rot < 4; ++rot) {
    pool = pools[rot];
    for (baseIdx = pool->minBaseIdx; baseIdx <= pool->maxBaseIdx; ++baseIdx) {
      for (word0 = pool->getPool(baseIdx); word0; word0 = word0->next) {
	printf("    word: x=%.2f..%.2f y=%.2f..%.2f base=%.2f fontSize=%.2f rot=%d link=%p '",
	       word0->xMin, word0->xMax, word0->yMin, word0->yMax,
	       word0->base, word0->fontSize, rot*90, word0->link);
	for (i = 0; i < word0->len; ++i) {
	  fputc(word0->text[i] & 0xff, stdout);
	}
	printf("'\n");
      }
    }
  }
  printf("\n");
#endif

#if 0 //~ for debugging
  for (i = 0; i < underlines->getLength(); ++i) {
    underline = (TextUnderline *)underlines->get(i);
    printf("underline: x=%g..%g y=%g..%g horiz=%d\n",
	   underline->x0, underline->x1, underline->y0, underline->y1,
	   underline->horiz);
  }
#endif

    if (doHTML) {

        //----- handle underlining
        for (const std::unique_ptr<TextUnderline> &underline : underlines) {
            if (underline->horiz) {
                // rot = 0
                if (pools[0]->minBaseIdx <= pools[0]->maxBaseIdx) {
                    startBaseIdx = pools[0]->getBaseIdx(underline->y0 + minUnderlineGap);
                    endBaseIdx = pools[0]->getBaseIdx(underline->y0 + maxUnderlineGap);
                    for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                        for (word0 = pools[0]->getPool(j); word0; word0 = word0->next) {
                            //~ need to check the y value against the word baseline
                            if (underline->x0 < word0->xMin + underlineSlack && word0->xMax - underlineSlack < underline->x1) {
                                word0->underlined = true;
                            }
                        }
                    }
                }

                // rot = 2
                if (pools[2]->minBaseIdx <= pools[2]->maxBaseIdx) {
                    startBaseIdx = pools[2]->getBaseIdx(underline->y0 - maxUnderlineGap);
                    endBaseIdx = pools[2]->getBaseIdx(underline->y0 - minUnderlineGap);
                    for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                        for (word0 = pools[2]->getPool(j); word0; word0 = word0->next) {
                            if (underline->x0 < word0->xMin + underlineSlack && word0->xMax - underlineSlack < underline->x1) {
                                word0->underlined = true;
                            }
                        }
                    }
                }
            } else {
                // rot = 1
                if (pools[1]->minBaseIdx <= pools[1]->maxBaseIdx) {
                    startBaseIdx = pools[1]->getBaseIdx(underline->x0 - maxUnderlineGap);
                    endBaseIdx = pools[1]->getBaseIdx(underline->x0 - minUnderlineGap);
                    for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                        for (word0 = pools[1]->getPool(j); word0; word0 = word0->next) {
                            if (underline->y0 < word0->yMin + underlineSlack && word0->yMax - underlineSlack < underline->y1) {
                                word0->underlined = true;
                            }
                        }
                    }
                }

                // rot = 3
                if (pools[3]->minBaseIdx <= pools[3]->maxBaseIdx) {
                    startBaseIdx = pools[3]->getBaseIdx(underline->x0 + minUnderlineGap);
                    endBaseIdx = pools[3]->getBaseIdx(underline->x0 + maxUnderlineGap);
                    for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                        for (word0 = pools[3]->getPool(j); word0; word0 = word0->next) {
                            if (underline->y0 < word0->yMin + underlineSlack && word0->yMax - underlineSlack < underline->y1) {
                                word0->underlined = true;
                            }
                        }
                    }
                }
            }
        }

        //----- handle links
        for (const std::unique_ptr<TextLink> &link : links) {
            // rot = 0
            if (pools[0]->minBaseIdx <= pools[0]->maxBaseIdx) {
                startBaseIdx = pools[0]->getBaseIdx(link->yMin);
                endBaseIdx = pools[0]->getBaseIdx(link->yMax);
                for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                    for (word0 = pools[0]->getPool(j); word0; word0 = word0->next) {
                        if (link->xMin < word0->xMin + hyperlinkSlack && word0->xMax - hyperlinkSlack < link->xMax && link->yMin < word0->yMin + hyperlinkSlack && word0->yMax - hyperlinkSlack < link->yMax) {
                            word0->link = link->link;
                        }
                    }
                }
            }

            // rot = 2
            if (pools[2]->minBaseIdx <= pools[2]->maxBaseIdx) {
                startBaseIdx = pools[2]->getBaseIdx(link->yMin);
                endBaseIdx = pools[2]->getBaseIdx(link->yMax);
                for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                    for (word0 = pools[2]->getPool(j); word0; word0 = word0->next) {
                        if (link->xMin < word0->xMin + hyperlinkSlack && word0->xMax - hyperlinkSlack < link->xMax && link->yMin < word0->yMin + hyperlinkSlack && word0->yMax - hyperlinkSlack < link->yMax) {
                            word0->link = link->link;
                        }
                    }
                }
            }

            // rot = 1
            if (pools[1]->minBaseIdx <= pools[1]->maxBaseIdx) {
                startBaseIdx = pools[1]->getBaseIdx(link->xMin);
                endBaseIdx = pools[1]->getBaseIdx(link->xMax);
                for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                    for (word0 = pools[1]->getPool(j); word0; word0 = word0->next) {
                        if (link->yMin < word0->yMin + hyperlinkSlack && word0->yMax - hyperlinkSlack < link->yMax && link->xMin < word0->xMin + hyperlinkSlack && word0->xMax - hyperlinkSlack < link->xMax) {
                            word0->link = link->link;
                        }
                    }
                }
            }

            // rot = 3
            if (pools[3]->minBaseIdx <= pools[3]->maxBaseIdx) {
                startBaseIdx = pools[3]->getBaseIdx(link->xMin);
                endBaseIdx = pools[3]->getBaseIdx(link->xMax);
                for (j = startBaseIdx; j <= endBaseIdx; ++j) {
                    for (word0 = pools[3]->getPool(j); word0; word0 = word0->next) {
                        if (link->yMin < word0->yMin + hyperlinkSlack && word0->yMax - hyperlinkSlack < link->yMax && link->xMin < word0->xMin + hyperlinkSlack && word0->xMax - hyperlinkSlack < link->xMax) {
                            word0->link = link->link;
                        }
                    }
                }
            }
        }
    }

    //----- assemble the blocks

    //~ add an outer loop for writing mode (vertical text)

    // build blocks for each rotation value
    for (rot = 0; rot < 4; ++rot) {
        std::unique_ptr<TextPool> &pool = pools[rot];
        poolMinBaseIdx = pool->minBaseIdx;
        count[rot] = 0;

        // add blocks until no more words are left
        while (true) {

            // find the first non-empty line in the pool
            for (; poolMinBaseIdx <= pool->maxBaseIdx && !pool->getPool(poolMinBaseIdx); ++poolMinBaseIdx) {
                ;
            }
            if (poolMinBaseIdx > pool->maxBaseIdx) {
                break;
            }

            // look for the left-most word in the first four lines of the
            // pool -- this avoids starting with a superscript word
            startBaseIdx = poolMinBaseIdx;
            for (baseIdx = poolMinBaseIdx + 1; baseIdx < poolMinBaseIdx + 4 && baseIdx <= pool->maxBaseIdx; ++baseIdx) {
                if (!pool->getPool(baseIdx)) {
                    continue;
                }
                if (pool->getPool(baseIdx)->primaryCmp(pool->getPool(startBaseIdx)) < 0) {
                    startBaseIdx = baseIdx;
                }
            }

            // create a new block
            word0 = pool->getPool(startBaseIdx);
            pool->setPool(startBaseIdx, word0->next);
            word0->next = nullptr;
            blk = new TextBlock(this, rot);
            blk->addWord(word0);

            fontSize = word0->fontSize;
            minBase = maxBase = word0->base;
            colSpace1 = minColSpacing1 * fontSize;
            colSpace2 = minColSpacing2 * fontSize;
            lineSpace = maxLineSpacingDelta * fontSize;
            intraLineSpace = maxIntraLineDelta * fontSize;

            // add words to the block
            do {
                found = false;

                // look for words on the line above the current top edge of
                // the block
                newMinBase = minBase;
                for (baseIdx = pool->getBaseIdx(minBase); baseIdx >= pool->getBaseIdx(minBase - lineSpace); --baseIdx) {
                    word0 = nullptr;
                    word1 = pool->getPool(baseIdx);
                    while (word1) {
                        if (word1->base < minBase && word1->base >= minBase - lineSpace && ((rot == 0 || rot == 2) ? (word1->xMin < blk->xMax && word1->xMax > blk->xMin) : (word1->yMin < blk->yMax && word1->yMax > blk->yMin))
                            && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta1 * fontSize) {
                            word2 = word1;
                            if (word0) {
                                word0->next = word1->next;
                            } else {
                                pool->setPool(baseIdx, word1->next);
                            }
                            word1 = word1->next;
                            word2->next = nullptr;
                            blk->addWord(word2);
                            found = true;
                            newMinBase = word2->base;
                        } else {
                            word0 = word1;
                            word1 = word1->next;
                        }
                    }
                }
                minBase = newMinBase;

                // look for words on the line below the current bottom edge of
                // the block
                newMaxBase = maxBase;
                for (baseIdx = pool->getBaseIdx(maxBase); baseIdx <= pool->getBaseIdx(maxBase + lineSpace); ++baseIdx) {
                    word0 = nullptr;
                    word1 = pool->getPool(baseIdx);
                    while (word1) {
                        if (word1->base > maxBase && word1->base <= maxBase + lineSpace && ((rot == 0 || rot == 2) ? (word1->xMin < blk->xMax && word1->xMax > blk->xMin) : (word1->yMin < blk->yMax && word1->yMax > blk->yMin))
                            && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta1 * fontSize) {
                            word2 = word1;
                            if (word0) {
                                word0->next = word1->next;
                            } else {
                                pool->setPool(baseIdx, word1->next);
                            }
                            word1 = word1->next;
                            word2->next = nullptr;
                            blk->addWord(word2);
                            found = true;
                            newMaxBase = word2->base;
                        } else {
                            word0 = word1;
                            word1 = word1->next;
                        }
                    }
                }
                maxBase = newMaxBase;

                // look for words that are on lines already in the block, and
                // that overlap the block horizontally
                for (baseIdx = pool->getBaseIdx(minBase - intraLineSpace); baseIdx <= pool->getBaseIdx(maxBase + intraLineSpace); ++baseIdx) {
                    word0 = nullptr;
                    word1 = pool->getPool(baseIdx);
                    while (word1) {
                        if (word1->base >= minBase - intraLineSpace && word1->base <= maxBase + intraLineSpace
                            && ((rot == 0 || rot == 2) ? (word1->xMin < blk->xMax + colSpace1 && word1->xMax > blk->xMin - colSpace1) : (word1->yMin < blk->yMax + colSpace1 && word1->yMax > blk->yMin - colSpace1))
                            && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta2 * fontSize) {
                            word2 = word1;
                            if (word0) {
                                word0->next = word1->next;
                            } else {
                                pool->setPool(baseIdx, word1->next);
                            }
                            word1 = word1->next;
                            word2->next = nullptr;
                            blk->addWord(word2);
                            found = true;
                        } else {
                            word0 = word1;
                            word1 = word1->next;
                        }
                    }
                }

                // only check for outlying words (the next two chunks of code)
                // if we didn't find anything else
                if (found) {
                    continue;
                }

                // scan down the left side of the block, looking for words
                // that are near (but not overlapping) the block; if there are
                // three or fewer, add them to the block
                n = 0;
                for (baseIdx = pool->getBaseIdx(minBase - intraLineSpace); baseIdx <= pool->getBaseIdx(maxBase + intraLineSpace); ++baseIdx) {
                    word1 = pool->getPool(baseIdx);
                    while (word1) {
                        if (word1->base >= minBase - intraLineSpace && word1->base <= maxBase + intraLineSpace
                            && ((rot == 0 || rot == 2) ? (word1->xMax <= blk->xMin && word1->xMax > blk->xMin - colSpace2) : (word1->yMax <= blk->yMin && word1->yMax > blk->yMin - colSpace2))
                            && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta3 * fontSize) {
                            ++n;
                            break;
                        }
                        word1 = word1->next;
                    }
                }
                if (n > 0 && n <= 3) {
                    for (baseIdx = pool->getBaseIdx(minBase - intraLineSpace); baseIdx <= pool->getBaseIdx(maxBase + intraLineSpace); ++baseIdx) {
                        word0 = nullptr;
                        word1 = pool->getPool(baseIdx);
                        while (word1) {
                            if (word1->base >= minBase - intraLineSpace && word1->base <= maxBase + intraLineSpace
                                && ((rot == 0 || rot == 2) ? (word1->xMax <= blk->xMin && word1->xMax > blk->xMin - colSpace2) : (word1->yMax <= blk->yMin && word1->yMax > blk->yMin - colSpace2))
                                && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta3 * fontSize) {
                                word2 = word1;
                                if (word0) {
                                    word0->next = word1->next;
                                } else {
                                    pool->setPool(baseIdx, word1->next);
                                }
                                word1 = word1->next;
                                word2->next = nullptr;
                                blk->addWord(word2);
                                if (word2->base < minBase) {
                                    minBase = word2->base;
                                } else if (word2->base > maxBase) {
                                    maxBase = word2->base;
                                }
                                found = true;
                                break;
                            }
                            word0 = word1;
                            word1 = word1->next;
                        }
                    }
                }

                // scan down the right side of the block, looking for words
                // that are near (but not overlapping) the block; if there are
                // three or fewer, add them to the block
                n = 0;
                for (baseIdx = pool->getBaseIdx(minBase - intraLineSpace); baseIdx <= pool->getBaseIdx(maxBase + intraLineSpace); ++baseIdx) {
                    word1 = pool->getPool(baseIdx);
                    while (word1) {
                        if (word1->base >= minBase - intraLineSpace && word1->base <= maxBase + intraLineSpace
                            && ((rot == 0 || rot == 2) ? (word1->xMin >= blk->xMax && word1->xMin < blk->xMax + colSpace2) : (word1->yMin >= blk->yMax && word1->yMin < blk->yMax + colSpace2))
                            && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta3 * fontSize) {
                            ++n;
                            break;
                        }
                        word1 = word1->next;
                    }
                }
                if (n > 0 && n <= 3) {
                    for (baseIdx = pool->getBaseIdx(minBase - intraLineSpace); baseIdx <= pool->getBaseIdx(maxBase + intraLineSpace); ++baseIdx) {
                        word0 = nullptr;
                        word1 = pool->getPool(baseIdx);
                        while (word1) {
                            if (word1->base >= minBase - intraLineSpace && word1->base <= maxBase + intraLineSpace
                                && ((rot == 0 || rot == 2) ? (word1->xMin >= blk->xMax && word1->xMin < blk->xMax + colSpace2) : (word1->yMin >= blk->yMax && word1->yMin < blk->yMax + colSpace2))
                                && fabs(word1->fontSize - fontSize) < maxBlockFontSizeDelta3 * fontSize) {
                                word2 = word1;
                                if (word0) {
                                    word0->next = word1->next;
                                } else {
                                    pool->setPool(baseIdx, word1->next);
                                }
                                word1 = word1->next;
                                word2->next = nullptr;
                                blk->addWord(word2);
                                if (word2->base < minBase) {
                                    minBase = word2->base;
                                } else if (word2->base > maxBase) {
                                    maxBase = word2->base;
                                }
                                found = true;
                                break;
                            }
                            word0 = word1;
                            word1 = word1->next;
                        }
                    }
                }

            } while (found);

            //~ need to compute the primary writing mode (horiz/vert) in
            //~ addition to primary rotation

            // coalesce the block, and add it to the list
            blk->coalesce(uMap, fixedPitch);
            if (lastBlk) {
                lastBlk->next = blk;
            } else {
                blkList = blk;
            }
            lastBlk = blk;
            count[rot] += blk->charCount;
            ++nBlocks;
        }

        if (count[rot] > count[primaryRot]) {
            primaryRot = rot;
        }
    }

#if 0 // for debugging
  printf("*** rotation ***\n");
  for (rot = 0; rot < 4; ++rot) {
    printf("  %d: %6d\n", rot, count[rot]);
  }
  printf("  primary rot = %d\n", primaryRot);
  printf("\n");
#endif

#if 0 // for debugging
  printf("*** blocks ***\n");
  for (blk = blkList; blk; blk = blk->next) {
    printf("block: rot=%d x=%.2f..%.2f y=%.2f..%.2f\n",
	   blk->rot, blk->xMin, blk->xMax, blk->yMin, blk->yMax);
    for (line = blk->lines; line; line = line->next) {
      printf("  line: x=%.2f..%.2f y=%.2f..%.2f base=%.2f\n",
	     line->xMin, line->xMax, line->yMin, line->yMax, line->base);
      for (word0 = line->words; word0; word0 = word0->next) {
	printf("    word: x=%.2f..%.2f y=%.2f..%.2f base=%.2f fontSize=%.2f space=%d: '",
	       word0->xMin, word0->xMax, word0->yMin, word0->yMax,
	       word0->base, word0->fontSize, word0->spaceAfter);
	for (i = 0; i < word0->len; ++i) {
	  fputc(word0->text[i] & 0xff, stdout);
	}
	printf("'\n");
      }
    }
  }
  printf("\n");
#endif

    // determine the primary direction
    lrCount = 0;
    for (blk = blkList; blk; blk = blk->next) {
        for (line = blk->lines; line; line = line->next) {
            for (word0 = line->words; word0; word0 = word0->next) {
                for (size_t i = 0; i < word0->len(); ++i) {
                    if (unicodeTypeL(word0->chars[i].text)) {
                        ++lrCount;
                    } else if (unicodeTypeR(word0->chars[i].text)) {
                        --lrCount;
                    }
                }
            }
        }
    }
    primaryLR = lrCount >= 0;

#if 0 // for debugging
  printf("*** direction ***\n");
  printf("lrCount = %d\n", lrCount);
  printf("primaryLR = %d\n", primaryLR);
#endif

    //----- column assignment

    // sort blocks into xy order for column assignment
    if (blocks) {
        gfree(static_cast<void *>(blocks));
    }
    if (physLayout && fixedPitch) {

        blocks = static_cast<TextBlock **>(gmallocn(nBlocks, sizeof(TextBlock *)));
        int i;
        for (blk = blkList, i = 0; blk; blk = blk->next, ++i) {
            blocks[i] = blk;
            col1 = 0; // make gcc happy
            switch (primaryRot) {
            case 0:
                col1 = static_cast<int>(blk->xMin / fixedPitch + 0.5);
                break;
            case 1:
                col1 = static_cast<int>(blk->yMin / fixedPitch + 0.5);
                break;
            case 2:
                col1 = static_cast<int>((pageWidth - blk->xMax) / fixedPitch + 0.5);
                break;
            case 3:
                col1 = static_cast<int>((pageHeight - blk->yMax) / fixedPitch + 0.5);
                break;
            }
            blk->col = col1;
            for (line = blk->lines; line; line = line->next) {
                for (j = 0; j <= line->len; ++j) {
                    line->col[j] += col1;
                }
            }
        }

    } else {

        // sort blocks into xy order for column assignment
        blocks = static_cast<TextBlock **>(gmallocn(nBlocks, sizeof(TextBlock *)));
        int i;
        for (blk = blkList, i = 0; blk; blk = blk->next, ++i) {
            blocks[i] = blk;
        }
        std::sort(blocks, blocks + nBlocks, &TextBlock::cmpXYPrimaryRot);

        // column assignment
        for (i = 0; i < nBlocks; ++i) {
            blk0 = blocks[i];
            col1 = 0;
            for (j = 0; j < i; ++j) {
                blk1 = blocks[j];
                col2 = 0; // make gcc happy
                switch (primaryRot) {
                case 0:
                    if (blk0->xMin > blk1->xMax) {
                        col2 = blk1->col + blk1->nColumns + 3;
                    } else if (blk1->xMax == blk1->xMin) {
                        col2 = blk1->col;
                    } else {
                        col2 = blk1->col + static_cast<int>(((blk0->xMin - blk1->xMin) / (blk1->xMax - blk1->xMin)) * blk1->nColumns);
                    }
                    break;
                case 1:
                    if (blk0->yMin > blk1->yMax) {
                        col2 = blk1->col + blk1->nColumns + 3;
                    } else if (blk1->yMax == blk1->yMin) {
                        col2 = blk1->col;
                    } else {
                        col2 = blk1->col + static_cast<int>(((blk0->yMin - blk1->yMin) / (blk1->yMax - blk1->yMin)) * blk1->nColumns);
                    }
                    break;
                case 2:
                    if (blk0->xMax < blk1->xMin) {
                        col2 = blk1->col + blk1->nColumns + 3;
                    } else if (blk1->xMin == blk1->xMax) {
                        col2 = blk1->col;
                    } else {
                        col2 = blk1->col + static_cast<int>(((blk0->xMax - blk1->xMax) / (blk1->xMin - blk1->xMax)) * blk1->nColumns);
                    }
                    break;
                case 3:
                    if (blk0->yMax < blk1->yMin) {
                        col2 = blk1->col + blk1->nColumns + 3;
                    } else if (blk1->yMin == blk1->yMax) {
                        col2 = blk1->col;
                    } else {
                        col2 = blk1->col + static_cast<int>(((blk0->yMax - blk1->yMax) / (blk1->yMin - blk1->yMax)) * blk1->nColumns);
                    }
                    break;
                }
                if (col2 > col1) {
                    col1 = col2;
                }
            }
            blk0->col = col1;
            for (line = blk0->lines; line; line = line->next) {
                for (j = 0; j <= line->len; ++j) {
                    line->col[j] += col1;
                }
            }
        }
    }

#if 0 // for debugging
  printf("*** blocks, after column assignment ***\n");
  for (blk = blkList; blk; blk = blk->next) {
    printf("block: rot=%d x=%.2f..%.2f y=%.2f..%.2f col=%d nCols=%d\n",
	   blk->rot, blk->xMin, blk->xMax, blk->yMin, blk->yMax, blk->col,
	   blk->nColumns);
    for (line = blk->lines; line; line = line->next) {
      printf("  line: col[0]=%d\n", line->col[0]);
      for (word0 = line->words; word0; word0 = word0->next) {
	printf("    word: x=%.2f..%.2f y=%.2f..%.2f base=%.2f fontSize=%.2f space=%d: '",
	       word0->xMin, word0->xMax, word0->yMin, word0->yMax,
	       word0->base, word0->fontSize, word0->spaceAfter);
	for (i = 0; i < word0->len; ++i) {
	  fputc(word0->text[i] & 0xff, stdout);
	}
	printf("'\n");
      }
    }
  }
  printf("\n");
#endif

    //----- reading order sort

    // compute space on left and right sides of each block
    for (int i = 0; i < nBlocks; ++i) {
        blk0 = blocks[i];
        for (j = 0; j < nBlocks; ++j) {
            blk1 = blocks[j];
            if (blk1 != blk0) {
                blk0->updatePriMinMax(blk1);
            }
        }
    }

#if 0 // for debugging
  printf("PAGE\n");
#endif

    int sortPos = 0;
    bool *visited = static_cast<bool *>(gmallocn(nBlocks, sizeof(bool)));
    for (int i = 0; i < nBlocks; i++) {
        visited[i] = false;
    }

    double bxMin0, byMin0, bxMin1, byMin1;
    int numTables = 0;
    int tableId = -1;
    int correspondenceX, correspondenceY;
    double xCentre1, yCentre1, xCentre2, yCentre2;
    double xCentre3, yCentre3, xCentre4, yCentre4;
    double deltaX, deltaY;
    TextBlock *fblk2 = nullptr, *fblk3 = nullptr, *fblk4 = nullptr;

    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        blk1->ExMin = blk1->xMin;
        blk1->ExMax = blk1->xMax;
        blk1->EyMin = blk1->yMin;
        blk1->EyMax = blk1->yMax;

        bxMin0 = DBL_MAX;
        byMin0 = DBL_MAX;
        bxMin1 = DBL_MAX;
        byMin1 = DBL_MAX;

        fblk2 = nullptr;
        fblk3 = nullptr;
        fblk4 = nullptr;

        /*  find fblk2, fblk3 and fblk4 so that
         *  fblk2 is on the right of blk1 and overlap with blk1 in y axis
         *  fblk3 is under blk1 and overlap with blk1 in x axis
         *  fblk4 is under blk1 and on the right of blk1
         *  and they are closest to blk1
         */
        for (blk2 = blkList; blk2; blk2 = blk2->next) {
            if (blk2 != blk1) {
                if (blk2->yMin <= blk1->yMax && blk2->yMax >= blk1->yMin && blk2->xMin > blk1->xMax && blk2->xMin < bxMin0) {
                    bxMin0 = blk2->xMin;
                    fblk2 = blk2;
                } else if (blk2->xMin <= blk1->xMax && blk2->xMax >= blk1->xMin && blk2->yMin > blk1->yMax && blk2->yMin < byMin0) {
                    byMin0 = blk2->yMin;
                    fblk3 = blk2;
                } else if (blk2->xMin > blk1->xMax && blk2->xMin < bxMin1 && blk2->yMin > blk1->yMax && blk2->yMin < byMin1) {
                    bxMin1 = blk2->xMin;
                    byMin1 = blk2->yMin;
                    fblk4 = blk2;
                }
            }
        }

        /*  fblk4 can not overlap with fblk3 in x and with fblk2 in y
         *  fblk2 can not overlap with fblk3 in x and y
         *  fblk4 has to overlap with fblk3 in y and with fblk2 in x
         */
        if (fblk2 != nullptr && fblk3 != nullptr && fblk4 != nullptr) {
            if (((fblk3->xMin <= fblk4->xMax && fblk3->xMax >= fblk4->xMin) || (fblk2->yMin <= fblk4->yMax && fblk2->yMax >= fblk4->yMin) || (fblk2->xMin <= fblk3->xMax && fblk2->xMax >= fblk3->xMin)
                 || (fblk2->yMin <= fblk3->yMax && fblk2->yMax >= fblk3->yMin))
                || fblk4->xMin > fblk2->xMax || fblk4->xMax < fblk2->xMin || fblk4->yMin > fblk3->yMax || fblk4->yMax < fblk3->yMin) {
                fblk2 = nullptr;
                fblk3 = nullptr;
                fblk4 = nullptr;
            }
        }

        // if we found any then look whether they form a table
        if (fblk2 != nullptr && fblk3 != nullptr && fblk4 != nullptr) {
            tableId = -1;
            correspondenceX = 0;
            correspondenceY = 0;
            deltaX = 0.0;
            deltaY = 0.0;

            if (blk1->lines && blk1->lines->words) {
                deltaX = blk1->lines->words->getFontSize();
            }
            if (fblk2->lines && fblk2->lines->words) {
                deltaX = deltaX < fblk2->lines->words->getFontSize() ? deltaX : fblk2->lines->words->getFontSize();
            }
            if (fblk3->lines && fblk3->lines->words) {
                deltaX = deltaX < fblk3->lines->words->getFontSize() ? deltaX : fblk3->lines->words->getFontSize();
            }
            if (fblk4->lines && fblk4->lines->words) {
                deltaX = deltaX < fblk4->lines->words->getFontSize() ? deltaX : fblk4->lines->words->getFontSize();
            }

            deltaY = deltaX;

            deltaX *= minColSpacing1;
            deltaY *= maxIntraLineDelta;

            xCentre1 = (blk1->xMax + blk1->xMin) / 2.0;
            yCentre1 = (blk1->yMax + blk1->yMin) / 2.0;
            xCentre2 = (fblk2->xMax + fblk2->xMin) / 2.0;
            yCentre2 = (fblk2->yMax + fblk2->yMin) / 2.0;
            xCentre3 = (fblk3->xMax + fblk3->xMin) / 2.0;
            yCentre3 = (fblk3->yMax + fblk3->yMin) / 2.0;
            xCentre4 = (fblk4->xMax + fblk4->xMin) / 2.0;
            yCentre4 = (fblk4->yMax + fblk4->yMin) / 2.0;

            // are blocks centrally aligned in x ?
            if (fabs(xCentre1 - xCentre3) <= deltaX && fabs(xCentre2 - xCentre4) <= deltaX) {
                correspondenceX++;
            }

            // are blocks centrally aligned in y ?
            if (fabs(yCentre1 - yCentre2) <= deltaY && fabs(yCentre3 - yCentre4) <= deltaY) {
                correspondenceY++;
            }

            // are blocks aligned to the left ?
            if (fabs(blk1->xMin - fblk3->xMin) <= deltaX && fabs(fblk2->xMin - fblk4->xMin) <= deltaX) {
                correspondenceX++;
            }

            // are blocks aligned to the right ?
            if (fabs(blk1->xMax - fblk3->xMax) <= deltaX && fabs(fblk2->xMax - fblk4->xMax) <= deltaX) {
                correspondenceX++;
            }

            // are blocks aligned to the top ?
            if (fabs(blk1->yMin - fblk2->yMin) <= deltaY && fabs(fblk3->yMin - fblk4->yMin) <= deltaY) {
                correspondenceY++;
            }

            // are blocks aligned to the bottom ?
            if (fabs(blk1->yMax - fblk2->yMax) <= deltaY && fabs(fblk3->yMax - fblk4->yMax) <= deltaY) {
                correspondenceY++;
            }

            // are blocks aligned in x and y ?
            if (correspondenceX > 0 && correspondenceY > 0) {

                // find maximal tableId
                tableId = tableId < fblk4->tableId ? fblk4->tableId : tableId;
                tableId = tableId < fblk3->tableId ? fblk3->tableId : tableId;
                tableId = tableId < fblk2->tableId ? fblk2->tableId : tableId;
                tableId = tableId < blk1->tableId ? blk1->tableId : tableId;

                // if the tableId is -1, then we found new table
                if (tableId < 0) {
                    tableId = numTables;
                    numTables++;
                }

                blk1->tableId = tableId;
                fblk2->tableId = tableId;
                fblk3->tableId = tableId;
                fblk4->tableId = tableId;
            }
        }
    }

    /*  set extended bounding boxes of all table entries
     *  so that they contain whole table
     *  (we need to process whole table size when comparing it
     *   with regular text blocks)
     */
    auto *envelopes = new PDFRectangle[numTables];
    auto **ending_blocks = new TextBlock *[numTables];

    for (int i = 0; i < numTables; i++) {
        envelopes[i].x1 = DBL_MAX;
        envelopes[i].x2 = DBL_MIN;
        envelopes[i].y1 = DBL_MAX;
        envelopes[i].y2 = DBL_MIN;
        ending_blocks[i] = nullptr;
    }

    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        if (blk1->tableId >= 0) {
            if (blk1->ExMin < envelopes[blk1->tableId].x1) {
                envelopes[blk1->tableId].x1 = blk1->ExMin;
                if (!blk1->page->primaryLR) {
                    ending_blocks[blk1->tableId] = blk1;
                }
            }

            if (blk1->ExMax > envelopes[blk1->tableId].x2) {
                envelopes[blk1->tableId].x2 = blk1->ExMax;
                if (blk1->page->primaryLR) {
                    ending_blocks[blk1->tableId] = blk1;
                }
            }

            envelopes[blk1->tableId].y1 = blk1->EyMin < envelopes[blk1->tableId].y1 ? blk1->EyMin : envelopes[blk1->tableId].y1;
            envelopes[blk1->tableId].y2 = blk1->EyMax > envelopes[blk1->tableId].y2 ? blk1->EyMax : envelopes[blk1->tableId].y2;
        }
    }

    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        if (blk1->tableId >= 0 && ending_blocks[blk1->tableId] && blk1->xMin <= ending_blocks[blk1->tableId]->xMax && blk1->xMax >= ending_blocks[blk1->tableId]->xMin) {
            blk1->tableEnd = true;
        }
    }

    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        if (blk1->tableId >= 0) {
            blk1->ExMin = envelopes[blk1->tableId].x1;
            blk1->ExMax = envelopes[blk1->tableId].x2;
            blk1->EyMin = envelopes[blk1->tableId].y1;
            blk1->EyMax = envelopes[blk1->tableId].y2;
        }
    }
    delete[] envelopes;
    delete[] ending_blocks;

    /*  set extended bounding boxes of all other blocks
     *  so that they extend in x without hitting neighbours
     */
    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        if (!(blk1->tableId >= 0)) {
            double xMax = DBL_MAX;
            double xMin = DBL_MIN;

            for (blk2 = blkList; blk2; blk2 = blk2->next) {
                if (blk2 == blk1) {
                    continue;
                }

                if (blk1->yMin <= blk2->yMax && blk1->yMax >= blk2->yMin) {
                    if (blk2->xMin < xMax && blk2->xMin > blk1->xMax) {
                        xMax = blk2->xMin;
                    }

                    if (blk2->xMax > xMin && blk2->xMax < blk1->xMin) {
                        xMin = blk2->xMax;
                    }
                }
            }

            for (blk2 = blkList; blk2; blk2 = blk2->next) {
                if (blk2 == blk1) {
                    continue;
                }

                if (blk2->xMax > blk1->ExMax && blk2->xMax <= xMax && blk2->yMin >= blk1->yMax) {
                    blk1->ExMax = blk2->xMax;
                }

                if (blk2->xMin < blk1->ExMin && blk2->xMin >= xMin && blk2->yMin >= blk1->yMax) {
                    blk1->ExMin = blk2->xMin;
                }
            }
        }
    }

    int i = -1;
    for (blk1 = blkList; blk1; blk1 = blk1->next) {
        i++;
        sortPos = blk1->visitDepthFirst(blkList, i, blocks, sortPos, visited);
    }
    if (visited) {
        gfree(visited);
    }

#if 0 // for debugging
  printf("*** blocks, after ro sort ***\n");
  for (i = 0; i < nBlocks; ++i) {
    blk = blocks[i];
    printf("block: rot=%d x=%.2f..%.2f y=%.2f..%.2f space=%.2f..%.2f\n",
	   blk->rot, blk->xMin, blk->xMax, blk->yMin, blk->yMax,
	   blk->priMin, blk->priMax);
    for (line = blk->lines; line; line = line->next) {
      printf("  line:\n");
      for (word0 = line->words; word0; word0 = word0->next) {
	printf("    word: x=%.2f..%.2f y=%.2f..%.2f base=%.2f fontSize=%.2f space=%d: '",
	       word0->xMin, word0->xMax, word0->yMin, word0->yMax,
	       word0->base, word0->fontSize, word0->spaceAfter);
	for (j = 0; j < word0->len; ++j) {
	  fputc(word0->text[j] & 0xff, stdout);
	}
	printf("'\n");
      }
    }
  }
  printf("\n");
  fflush(stdout);
#endif

    // build the flows
    //~ this needs to be adjusted for writing mode (vertical text)
    //~ this also needs to account for right-to-left column ordering
    while (flows) {
        flow = flows;
        flows = flows->next;
        delete flow;
    }
    flow = nullptr;
    flows = lastFlow = nullptr;
    // assume blocks are already in reading order,
    // and construct flows accordingly.
    for (i = 0; i < nBlocks; i++) {
        blk = blocks[i];
        blk->next = nullptr;
        if (flow) {
            blk1 = blocks[i - 1];
            blkSpace = maxBlockSpacing * blk1->lines->words->fontSize;
            if (blk1->secondaryDelta(blk) <= blkSpace && blk->isBelow(blk1) && flow->blockFits(blk)) {
                flow->addBlock(blk);
                continue;
            }
        }
        flow = new TextFlow(this, blk);
        if (lastFlow) {
            lastFlow->next = flow;
        } else {
            flows = flow;
        }
        lastFlow = flow;
    }

#if 0 // for debugging
  printf("*** flows ***\n");
  for (flow = flows; flow; flow = flow->next) {
    printf("flow: x=%.2f..%.2f y=%.2f..%.2f pri:%.2f..%.2f\n",
	   flow->xMin, flow->xMax, flow->yMin, flow->yMax,
	   flow->priMin, flow->priMax);
    for (blk = flow->blocks; blk; blk = blk->next) {
      printf("  block: rot=%d x=%.2f..%.2f y=%.2f..%.2f pri=%.2f..%.2f\n",
	     blk->rot, blk->ExMin, blk->ExMax, blk->EyMin, blk->EyMax,
	     blk->priMin, blk->priMax);
      for (line = blk->lines; line; line = line->next) {
	printf("    line:\n");
	for (word0 = line->words; word0; word0 = word0->next) {
	  printf("      word: x=%.2f..%.2f y=%.2f..%.2f base=%.2f fontSize=%.2f space=%d: '",
		 word0->xMin, word0->xMax, word0->yMin, word0->yMax,
		 word0->base, word0->fontSize, word0->spaceAfter);
	  for (i = 0; i < word0->len; ++i) {
	    fputc(word0->text[i] & 0xff, stdout);
	  }
	  printf("'\n");
	}
      }
    }
  }
  printf("\n");
#endif
}

void TextPage::adjustRotation(TextLine *line, int start, int end, double *xMin, double *xMax, double *yMin, double *yMax)
{
    switch (line->rot) {
    case 0:
        *xMin = line->edge[start];
        *xMax = line->edge[end];
        *yMin = line->yMin;
        *yMax = line->yMax;
        break;
    case 1:
        *xMin = line->xMin;
        *xMax = line->xMax;
        *yMin = line->edge[start];
        *yMax = line->edge[end];
        break;
    case 2:
        *xMin = line->edge[end];
        *xMax = line->edge[start];
        *yMin = line->yMin;
        *yMax = line->yMax;
        break;
    case 3:
        *xMin = line->xMin;
        *xMax = line->xMax;
        *yMin = line->edge[end];
        *yMax = line->edge[start];
        break;
    }
}

bool TextPage::findText(const Unicode *s, int len, bool startAtTop, bool stopAtBottom, bool startAtLast, bool stopAtLast, bool caseSensitive, bool backward, bool wholeWord, double *xMin, double *yMin, double *xMax, double *yMax)
{
    return findText(s, len, startAtTop, stopAtBottom, startAtLast, stopAtLast, caseSensitive, false, false, backward, wholeWord, xMin, yMin, xMax, yMax, nullptr, nullptr);
}

bool TextPage::findText(const Unicode *s, int len, bool startAtTop, bool stopAtBottom, bool startAtLast, bool stopAtLast, bool caseSensitive, bool ignoreDiacritics, bool backward, bool wholeWord, double *xMin, double *yMin, double *xMax,
                        double *yMax)
{
    return findText(s, len, startAtTop, stopAtBottom, startAtLast, stopAtLast, caseSensitive, ignoreDiacritics, false, backward, wholeWord, xMin, yMin, xMax, yMax, nullptr, nullptr);
}

bool TextPage::findText(const Unicode *s, int len, bool startAtTop, bool stopAtBottom, bool startAtLast, bool stopAtLast, bool caseSensitive, bool ignoreDiacritics, bool matchAcrossLines, bool backward, bool wholeWord, double *xMin,
                        double *yMin, double *xMax, double *yMax, PDFRectangle *continueMatch, bool *ignoredHyphen)
{
    TextBlock *blk;
    TextLine *line;
    Unicode *s2, *txt, *reordered;
    Unicode *p;
    TextLine *nextline;
    Unicode *nextline_txt;
    int nextline_len;
    bool nextlineAfterHyphen = false;
    int txtSize, m, i, j, k;
    double xStart, yStart, xStop, yStop;
    double xMin0, yMin0, xMax0, yMax0;
    double xMin1, yMin1, xMax1, yMax1;
    double xMin2, yMin2, xMax2, yMax2;
    bool found;

    if (len == 0) {
        return false;
    }

    if (rawOrder) {
        return false;
    }

    if (matchAcrossLines && backward) {
        // matchAcrossLines is unimplemented for backward search
        matchAcrossLines = false;
    }

    // handle right-to-left text
    reordered = static_cast<Unicode *>(gmallocn(len, sizeof(Unicode)));
    reorderText(s, len, nullptr, primaryLR, nullptr, reordered);

    // normalize the search string
    s2 = unicodeNormalizeNFKC(reordered, len, &len, nullptr);

    // if search string is not pure ascii then don't
    // use ignoreDiacritics (as they won't match)
    if (!caseSensitive) {
        // convert the search string to uppercase
        for (i = 0; i < len; ++i) {
            s2[i] = unicodeToUpper(s2[i]);
            if (ignoreDiacritics && !isAscii7(s2[i])) {
                ignoreDiacritics = false;
            }
        }
    } else if (ignoreDiacritics) {
        for (i = 0; i < len; ++i) {
            if (!isAscii7(s2[i])) {
                ignoreDiacritics = false;
                break;
            }
        }
    }

    txt = nullptr;
    txtSize = 0;

    xStart = yStart = xStop = yStop = 0;
    if (startAtLast && haveLastFind) {
        xStart = lastFindXMin;
        yStart = lastFindYMin;
    } else if (!startAtTop) {
        xStart = *xMin;
        yStart = *yMin;
    }
    if (stopAtLast && haveLastFind) {
        xStop = lastFindXMin;
        yStop = lastFindYMin;
    } else if (!stopAtBottom) {
        xStop = *xMax;
        yStop = *yMax;
    }

    found = false;
    xMin0 = xMax0 = yMin0 = yMax0 = 0; // make gcc happy
    xMin1 = xMax1 = yMin1 = yMax1 = 0; // make gcc happy

    for (i = backward ? nBlocks - 1 : 0; backward ? i >= 0 : i < nBlocks; i += backward ? -1 : 1) {
        blk = blocks[i];

        // check: is the block above the top limit?
        // (this only works if the page's primary rotation is zero --
        // otherwise the blocks won't be sorted in the useful order)
        if (!startAtTop && primaryRot == 0 && (backward ? blk->yMin > yStart : blk->yMax < yStart)) {
            continue;
        }

        // check: is the block below the bottom limit?
        // (this only works if the page's primary rotation is zero --
        // otherwise the blocks won't be sorted in the useful order)
        if (!stopAtBottom && primaryRot == 0 && (backward ? blk->yMax < yStop : blk->yMin > yStop)) {
            break;
        }

        for (line = blk->lines; line; line = line->next) {

            // check: is the line above the top limit?
            // (this only works if the page's primary rotation is zero --
            // otherwise the lines won't be sorted in the useful order)
            if (!startAtTop && primaryRot == 0 && (backward ? line->yMin > yStart : line->yMin < yStart)) {
                continue;
            }

            // check: is the line below the bottom limit?
            // (this only works if the page's primary rotation is zero --
            // otherwise the lines won't be sorted in the useful order)
            if (!stopAtBottom && primaryRot == 0 && (backward ? line->yMin < yStop : line->yMin > yStop)) {
                continue;
            }

            if (!line->normalized) {
                line->normalized = unicodeNormalizeNFKC(line->text, line->len, &line->normalized_len, &line->normalized_idx, true);
            }

            nextline = nullptr;
            nextline_txt = nullptr;
            nextline_len = 0;
            if (line->next) {
                nextline = line->next;
            } else {
                // set nextline to first line of next block
                int ind = i + (backward ? -1 : 1);
                if ((backward && ind >= 0) || (!backward && ind < nBlocks)) {
                    nextline = blocks[ind]->lines;
                }
            }

            if (matchAcrossLines && nextline && !nextline->normalized) {
                nextline->normalized = unicodeNormalizeNFKC(nextline->text, nextline->len, &nextline->normalized_len, &nextline->normalized_idx, true);
            }

            // convert the line to uppercase
            m = line->normalized_len;

            if (ignoreDiacritics) {
                if (!line->ascii_translation) {
                    unicodeToAscii7(std::span(line->normalized, line->normalized_len), &line->ascii_translation, &line->ascii_len, line->normalized_idx, &line->ascii_idx);
                }
                if (line->ascii_len) {
                    m = line->ascii_len;
                } else {
                    ignoreDiacritics = false;
                }

                if (matchAcrossLines && nextline && !nextline->ascii_translation) {
                    unicodeToAscii7(std::span(nextline->normalized, nextline->normalized_len), &nextline->ascii_translation, &nextline->ascii_len, nextline->normalized_idx, &nextline->ascii_idx);
                }
            }
            if (!caseSensitive) {
                if (m > txtSize) {
                    txt = static_cast<Unicode *>(greallocn(txt, m, sizeof(Unicode)));
                    txtSize = m;
                }
                for (k = 0; k < m; ++k) {
                    if (ignoreDiacritics) {
                        txt[k] = unicodeToUpper(line->ascii_translation[k]);
                    } else {
                        txt[k] = unicodeToUpper(line->normalized[k]);
                    }
                }
                if (matchAcrossLines && nextline) {
                    nextline_len = ignoreDiacritics ? nextline->ascii_len : nextline->normalized_len;
                    nextline_txt = static_cast<Unicode *>(gmallocn(nextline_len, sizeof(Unicode)));
                    for (k = 0; k < nextline_len; ++k) {
                        nextline_txt[k] = ignoreDiacritics ? unicodeToUpper(nextline->ascii_translation[k]) : unicodeToUpper(nextline->normalized[k]);
                    }
                }
            } else {
                if (ignoreDiacritics) {
                    txt = line->ascii_translation;
                } else {
                    txt = line->normalized;
                }

                if (matchAcrossLines && nextline) {
                    nextline_len = ignoreDiacritics ? nextline->ascii_len : nextline->normalized_len;
                    nextline_txt = ignoreDiacritics ? nextline->ascii_translation : nextline->normalized;
                }
            }

            // search each position in this line
            j = backward ? m - len : 0;
            p = txt + j;
            while (backward ? j >= 0 : j <= m - (nextline_txt ? 1 : len)) {
                bool wholeWordStartIsOk, wholeWordEndIsOk;
                if (wholeWord) {
                    wholeWordStartIsOk = j == 0 || !unicodeTypeAlphaNum(txt[j - 1]);
                    if (nextline_txt) {
                        wholeWordEndIsOk = true; // word end may be in next line, so we'll check it later
                    } else {
                        wholeWordEndIsOk = j + len == m || !unicodeTypeAlphaNum(txt[j + len]);
                    }
                }
                if (!wholeWord || (wholeWordStartIsOk && wholeWordEndIsOk)) {
                    int n = 0;
                    bool spaceConsumedByNewline = false;
                    bool found_it;

                    // compare the strings
                    for (k = 0; k < len; ++k) {
                        bool last_char_of_line = j + k == m - 1;
                        bool last_char_of_search_term = k == len - 1;
                        bool match_started = static_cast<bool>(k);

                        if (p[k] != s2[k] || (nextline_txt && last_char_of_line && !last_char_of_search_term)) {
                            // now check if the comparison failed at the end-of-line hyphen,
                            // and if so, keep on comparing at the next line
                            // (only ASCII '-', U+002D; other hyphen code points are not matched across lines)
                            nextlineAfterHyphen = false;

                            if (s2[k] == p[k]) {
                                if (p[k] != static_cast<Unicode>('-') && !UnicodeIsWhitespace(s2[k + 1])) {
                                    break;
                                }
                                k++;
                            } else if (!match_started || p[k] != static_cast<Unicode>('-') || !last_char_of_line || UnicodeIsWhitespace(s2[k])) {
                                break;
                            } else {
                                nextlineAfterHyphen = true;
                            }

                            for (; n < nextline_len && k < len; ++k, ++n) {
                                if (nextline_txt[n] != s2[k]) {
                                    if (!spaceConsumedByNewline && !n && UnicodeIsWhitespace(s2[k])) {
                                        n = -1;
                                        spaceConsumedByNewline = true;
                                        continue;
                                    }
                                    break;
                                }
                            }
                            break;
                        }
                    }

                    found_it = k == len;
                    if (found_it && nextline_txt && wholeWord) { // check word end for nextline case
                        if (n) { // Match ended at next line
                            wholeWordEndIsOk = n == nextline_len || !unicodeTypeAlphaNum(nextline_txt[n]);
                        } else { // Match ended on same line
                            wholeWordEndIsOk = j + len == m || !unicodeTypeAlphaNum(txt[j + len]);
                        }

                        if (!wholeWordEndIsOk) {
                            found_it = false;
                        }
                    }
                    // found it
                    if (found_it) {
                        bool nextLineMatch = static_cast<bool>(n);
                        if (spaceConsumedByNewline) {
                            k--;
                        }
                        // where s2 matches a subsequence of a compatibility equivalence
                        // decomposition, highlight the entire glyph, since we don't know
                        // the internal layout of subglyph components
                        int normStart, normAfterEnd;
                        if (ignoreDiacritics) {
                            normStart = line->ascii_idx[j];
                            if (nextline_txt) {
                                normAfterEnd = line->ascii_idx[j + k - n];
                            } else {
                                normAfterEnd = line->ascii_idx[j + len - 1] + 1;
                            }
                        } else {
                            normStart = line->normalized_idx[j];
                            if (nextline_txt) {
                                normAfterEnd = line->normalized_idx[j + k - n];
                            } else {
                                normAfterEnd = line->normalized_idx[j + len - 1] + 1;
                            }
                        }

                        adjustRotation(line, normStart, normAfterEnd, &xMin1, &xMax1, &yMin1, &yMax1);

                        if (backward) {
                            if ((startAtTop || yMin1 < yStart || (yMin1 == yStart && xMin1 < xStart)) && (stopAtBottom || yMin1 > yStop || (yMin1 == yStop && xMin1 > xStop))) {
                                if (!found || yMin1 > yMin0 || (yMin1 == yMin0 && xMin1 > xMin0)) {
                                    xMin0 = xMin1;
                                    xMax0 = xMax1;
                                    yMin0 = yMin1;
                                    yMax0 = yMax1;
                                    found = true;
                                }
                            }
                        } else {
                            if ((startAtTop || yMin1 > yStart || (yMin1 == yStart && xMin1 > xStart)) && (stopAtBottom || yMin1 < yStop || (yMin1 == yStop && xMin1 < xStop))) {
                                if (!found || yMin1 < yMin0 || (yMin1 == yMin0 && xMin1 < xMin0)) {
                                    xMin0 = xMin1;
                                    xMax0 = xMax1;
                                    yMin0 = yMin1;
                                    yMax0 = yMax1;
                                    found = true;
                                    if (nextLineMatch) { // set the out parameters
                                        if (ignoredHyphen) {
                                            *ignoredHyphen = nextlineAfterHyphen;
                                        }

                                        if (continueMatch) {
                                            adjustRotation(nextline, 0, n, &xMin2, &xMax2, &yMin2, &yMax2);
                                            continueMatch->x1 = xMin2;
                                            continueMatch->y1 = yMax2;
                                            continueMatch->x2 = xMax2;
                                            continueMatch->y2 = yMin2;
                                        }
                                    } else if (continueMatch && continueMatch->x1 != std::numeric_limits<double>::max()) {
                                        if (ignoredHyphen) {
                                            *ignoredHyphen = false;
                                        }

                                        continueMatch->x1 = std::numeric_limits<double>::max();
                                    }
                                }
                            }
                        }
                    }
                }
                if (backward) {
                    --j;
                    --p;
                } else {
                    ++j;
                    ++p;
                }
            }

            if (nextline_txt && nextline_txt != nextline->ascii_translation && nextline_txt != nextline->normalized) {
                gfree(nextline_txt);
            }
        }
    }

    gfree(s2);
    gfree(reordered);
    if (!caseSensitive) {
        gfree(txt);
    }

    if (found) {
        *xMin = xMin0;
        *xMax = xMax0;
        *yMin = yMin0;
        *yMax = yMax0;
        lastFindXMin = xMin0;
        lastFindYMin = yMin0;
        haveLastFind = true;
        return true;
    }

    return false;
}

GooString TextPage::getText(const std::optional<PDFRectangle> &area, EndOfLineKind textEOL, bool physLayout, EndOfLineHyphenMode hyphenMode) const
{
    TextOutputFunc dumpToString = [](void *stream, const char *text, int len) {
        auto *s = static_cast<GooString *>(stream);
        s->append(text, len);
    };

    GooString s;

    if (!physLayout && !rawOrder && area) {
        error(errInternal, -1, "physical layout false, rawOrder false and an area does not work well together");
    }

    dump(&s, dumpToString, physLayout, textEOL, false, true, area, hyphenMode);
    return s;
}

class TextSelectionVisitor
{
public:
    explicit TextSelectionVisitor(TextPage *page);
    virtual ~TextSelectionVisitor();
    TextSelectionVisitor(const TextSelectionVisitor &) = delete;
    TextSelectionVisitor &operator=(const TextSelectionVisitor &) = delete;
    virtual void visitBlock(TextBlock *block, TextLine *begin, TextLine *end, const PDFRectangle &selection) = 0;
    virtual void visitLine(TextLine *line, TextWord *begin, TextWord *end, int edge_begin, int edge_end, const PDFRectangle &selection) = 0;
    virtual void visitWord(TextWord *word, int begin, int end, const PDFRectangle &selection) = 0;

protected:
    TextPage *page;
};

TextSelectionVisitor::TextSelectionVisitor(TextPage *p) : page(p) { }

TextSelectionVisitor::~TextSelectionVisitor() = default;

class TextSelectionDumper : public TextSelectionVisitor
{
public:
    explicit TextSelectionDumper(TextPage *page);
    ~TextSelectionDumper() override;

    void visitBlock(TextBlock * /*block*/, TextLine * /*begin*/, TextLine * /*end*/, const PDFRectangle & /*selection*/) override { };
    void visitLine(TextLine *line, TextWord *begin, TextWord *end, int edge_begin, int edge_end, const PDFRectangle &selection) override;
    void visitWord(TextWord *word, int begin, int end, const PDFRectangle &selection) override;
    void endPage();

    GooString getText();
    std::vector<std::vector<std::unique_ptr<TextWordSelection>>> takeWordList();

private:
    void startLine();
    void finishLine();

    std::vector<std::vector<std::unique_ptr<TextWordSelection>>> lines;
    std::vector<std::unique_ptr<TextWordSelection>> words;
    int tableId;
    TextBlock *currentBlock;
};

TextSelectionDumper::TextSelectionDumper(TextPage *p) : TextSelectionVisitor(p)
{
    tableId = -1;
    currentBlock = nullptr;
}

TextSelectionDumper::~TextSelectionDumper() = default;

void TextSelectionDumper::startLine()
{
    finishLine();
    words.clear();
}

void TextSelectionDumper::finishLine()
{
    if (!words.empty()) {
        // Reverse word order for RTL text. Fixes #53 for glib backend (Evince)
        if (!page->primaryLR) {
            std::ranges::reverse(words);
        }

        lines.push_back(std::move(words));
    }
    words.clear();
}

void TextSelectionDumper::visitLine(TextLine *line, TextWord * /*begin*/, TextWord * /*end*/, int edge_begin, int edge_end, const PDFRectangle & /*selection*/)
{
    TextLineFrag frag;

    frag.init(line, edge_begin, edge_end - edge_begin);

    if (tableId >= 0 && frag.line->blk->tableId < 0) {
        finishLine();

        tableId = -1;
        currentBlock = nullptr;
    }

    if (frag.line->blk->tableId >= 0) { // a table
        if (tableId == -1) {
            tableId = frag.line->blk->tableId;
            currentBlock = frag.line->blk;
        }

        if (currentBlock == frag.line->blk) { // the same block
            startLine();
        } else { // another block
            if (currentBlock->tableEnd) { // previous block ended its row
                startLine();
            }
            currentBlock = frag.line->blk;
        }
    } else { // not a table
        startLine();
    }
}

void TextSelectionDumper::visitWord(TextWord *word, int begin, int end, const PDFRectangle & /*selection*/)
{
    words.push_back(std::make_unique<TextWordSelection>(word, begin, end));
}

void TextSelectionDumper::endPage()
{
    finishLine();
}

GooString TextSelectionDumper::getText()
{
    GooString text;
    const UnicodeMap *uMap;
    char space[8], eol[16];
    int spaceLen, eolLen;

    if (!(uMap = globalParams->getTextEncoding())) {
        return text;
    }

    spaceLen = uMap->mapUnicode(0x20, space, sizeof(space));
    eolLen = uMap->mapUnicode(0x0a, eol, sizeof(eol));

    std::vector<Unicode> uText;
    for (size_t i = 0; i < lines.size(); i++) {
        const auto &line = lines[i];
        for (size_t j = 0; j < line.size(); j++) {
            const auto &sel = line[j];

            uText.resize(sel->end - sel->begin);
            std::transform(sel->word->chars.begin() + sel->begin, sel->word->chars.begin() + sel->end, uText.begin(), [](auto &c) { return c.text; });
            page->dumpFragment(uText.data(), uText.size(), uMap, &text);

            if (j < line.size() - 1 && sel->word->spaceAfter) {
                text.append(space, spaceLen);
            }
        }
        if (i < lines.size() - 1) {
            text.append(eol, eolLen);
        }
    }

    return text;
}

std::vector<std::vector<std::unique_ptr<TextWordSelection>>> TextSelectionDumper::takeWordList()
{
    std::vector<std::vector<std::unique_ptr<TextWordSelection>>> returnValue;
    std::swap(lines, returnValue);
    return returnValue;
}

class TextSelectionSizer : public TextSelectionVisitor
{
public:
    TextSelectionSizer(TextPage *page, double scale);
    ~TextSelectionSizer() override { delete list; }

    void visitBlock(TextBlock * /*block*/, TextLine * /*begin*/, TextLine * /*end*/, const PDFRectangle & /*selection*/) override { };
    void visitLine(TextLine *line, TextWord *begin, TextWord *end, int edge_begin, int edge_end, const PDFRectangle &selection) override;
    void visitWord(TextWord * /*word*/, int /*begin*/, int /*end*/, const PDFRectangle & /*selection*/) override { };

    std::vector<PDFRectangle *> *takeRegion()
    {
        auto *aux = list;
        list = nullptr;
        return aux;
    }

private:
    std::vector<PDFRectangle *> *list;
    double scale;
};

TextSelectionSizer::TextSelectionSizer(TextPage *p, double s) : TextSelectionVisitor(p), scale(s)
{
    list = new std::vector<PDFRectangle *>();
}

void TextSelectionSizer::visitLine(TextLine *line, TextWord * /*begin*/, TextWord * /*end*/, int edge_begin, int edge_end, const PDFRectangle & /*selection*/)
{
    PDFRectangle *rect;
    double x1, y1, x2, y2, margin;

    switch (line->rot) {
    default:
    case 0:
        margin = (line->yMax - line->yMin) / 8;
        x1 = line->edge[edge_begin];
        x2 = line->edge[edge_end];
        y1 = line->yMin - margin;
        y2 = line->yMax + margin;
        break;
    case 1:
        margin = (line->xMax - line->xMin) / 8;
        x1 = line->xMin - margin;
        x2 = line->xMax + margin;
        y1 = line->edge[edge_begin];
        y2 = line->edge[edge_end];
        break;
    case 2:
        margin = (line->yMax - line->yMin) / 8;
        x1 = line->edge[edge_end];
        x2 = line->edge[edge_begin];
        y1 = line->yMin - margin;
        y2 = line->yMax + margin;
        break;
    case 3:
        margin = (line->xMax - line->xMin) / 8;
        x1 = line->xMin - margin;
        x2 = line->xMax + margin;
        y1 = line->edge[edge_end];
        y2 = line->edge[edge_begin];
        break;
    }

    rect = new PDFRectangle(floor(x1 * scale), floor(y1 * scale), ceil(x2 * scale), ceil(y2 * scale));
    list->push_back(rect);
}

class TextSelectionPainter : public TextSelectionVisitor
{
public:
    TextSelectionPainter(TextPage *page, double scale, int rotation, OutputDev *out, const GfxColor &box_color, const GfxColor &glyph_color, double box_opacity, bool draw_glyphs);
    ~TextSelectionPainter() override;

    void visitBlock(TextBlock * /*block*/, TextLine * /*begin*/, TextLine * /*end*/, const PDFRectangle & /*selection*/) override { };
    void visitLine(TextLine *line, TextWord *begin, TextWord *end, int edge_begin, int edge_end, const PDFRectangle &selection) override;
    void visitWord(TextWord *word, int begin, int end, const PDFRectangle &selection) override;
    void endPage();

private:
    OutputDev *out;
    const GfxColor &glyph_color;
    bool draw_glyphs;
    GfxState *state;
    std::vector<TextWordSelection *> *selectionList;
    Matrix ctm, ictm;
    bool hasGlyphLessFont();
};

TextSelectionPainter::TextSelectionPainter(TextPage *p, double scale, int rotation, OutputDev *outA, const GfxColor &box_color, const GfxColor &glyph_colorA, double box_opacity, bool draw_glyphsA)
    : TextSelectionVisitor(p), out(outA), glyph_color(glyph_colorA), draw_glyphs(draw_glyphsA)
{
    PDFRectangle box(0, 0, p->pageWidth, p->pageHeight);

    selectionList = new std::vector<TextWordSelection *>();
    state = new GfxState(72 * scale, 72 * scale, box, rotation, false);

    state->getCTM(&ctm);
    ctm.invertTo(&ictm);

    out->setDefaultCTM(state->getCTM());

    state->setFillColorSpace(std::make_unique<GfxDeviceRGBColorSpace>());
    state->setFillColor(box_color);
    state->setFillOpacity(box_opacity);
    out->updateFillColor(state);
    out->updateFillOpacity(state);
}

TextSelectionPainter::~TextSelectionPainter()
{
    for (auto *entry : *selectionList) {
        delete entry;
    }
    delete selectionList;
    delete state;
}

void TextSelectionPainter::visitLine(TextLine *line, TextWord * /*begin*/, TextWord * /*end*/, int edge_begin, int edge_end, const PDFRectangle & /*selection*/)
{
    double x1, y1, x2, y2, margin;

    switch (line->rot) {
    default:
    case 0:
        margin = (line->yMax - line->yMin) / 8;
        x1 = line->edge[edge_begin];
        x2 = line->edge[edge_end];
        y1 = line->yMin - margin;
        y2 = line->yMax + margin;
        break;
    case 1:
        margin = (line->xMax - line->xMin) / 8;
        x1 = line->xMin - margin;
        x2 = line->xMax + margin;
        y1 = line->edge[edge_begin];
        y2 = line->edge[edge_end];
        break;
    case 2:
        margin = (line->yMax - line->yMin) / 8;
        x1 = line->edge[edge_end];
        x2 = line->edge[edge_begin];
        y1 = line->yMin - margin;
        y2 = line->yMax + margin;
        break;
    case 3:
        margin = (line->xMax - line->xMin) / 8;
        x1 = line->xMin - margin;
        x2 = line->xMax + margin;
        y1 = line->edge[edge_end];
        y2 = line->edge[edge_begin];
        break;
    }

    ctm.transform(x1, y1, &x1, &y1);
    ctm.transform(x2, y2, &x2, &y2);

    if (x1 < x2) {
        x1 = floor(x1);
        x2 = ceil(x2);
    } else {
        x1 = ceil(x1);
        x2 = floor(x2);
    }

    if (y1 < y2) {
        y1 = floor(y1);
        y2 = ceil(y2);
    } else {
        y1 = ceil(y1);
        y2 = floor(y2);
    }

    ictm.transform(x1, y1, &x1, &y1);
    ictm.transform(x2, y2, &x2, &y2);

    state->moveTo(x1, y1);
    state->lineTo(x2, y1);
    state->lineTo(x2, y2);
    state->lineTo(x1, y2);
    state->closePath();
}

void TextSelectionPainter::visitWord(TextWord *word, int begin, int end, const PDFRectangle & /*selection*/)
{
    selectionList->push_back(new TextWordSelection(word, begin, end));
}

bool TextSelectionPainter::hasGlyphLessFont()
{
    if (selectionList && !selectionList->empty()) {
        TextWordSelection *sel = (*selectionList)[0];
        return sel->word->invisible;
    }

    return false;
}

void TextSelectionPainter::endPage()
{
    if (!draw_glyphs) {
        out->fill(state);
        out->endPage();
        return;
    }

    /* Take a shortcut for glyphless fonts (eg. Tesseract scanned documents)
     * cause we just paint a transparent fill over existent text.Issue #157 */
    if (hasGlyphLessFont()) {
        state->setFillOpacity(glyphlessSelectionOpacity);
        out->updateFillOpacity(state);
        out->fill(state);
        out->endPage();
        return;
    }

    out->fill(state);

    out->saveState(state);
    out->clip(state);

    state->clearPath();

    state->setFillColor(glyph_color);

    out->updateFillColor(state);

    std::string string;
    for (const TextWordSelection *sel : *selectionList) {
        int begin = sel->begin;

        while (begin < sel->end) {
            TextFontInfo *font = sel->word->chars[begin].font;
            const Matrix *mat = &sel->word->chars[begin].textMat;

            state->setTextMat(mat->m[0], mat->m[1], mat->m[2], mat->m[3], 0, 0);
            state->setFont(font->gfxFont, 1);
            out->updateFont(state);

            int fEnd = begin + 1;
            while (fEnd < sel->end && font->matches(sel->word->chars[fEnd].font) //
                   && mat->m[0] == sel->word->chars[fEnd].textMat.m[0] && mat->m[1] == sel->word->chars[fEnd].textMat.m[1] //
                   && mat->m[2] == sel->word->chars[fEnd].textMat.m[2] && mat->m[3] == sel->word->chars[fEnd].textMat.m[3]) {
                fEnd++;
            }

            /* The only purpose of this string is to let the output device query
             * it's length.  Might want to change this interface later. */
            string.clear();
            std::for_each(sel->word->chars.begin() + begin, sel->word->chars.begin() + fEnd, [&string](const auto c) { string.push_back(c.charcode); });
            out->beginString(state, string);

            for (int j = begin; j < fEnd; j++) {
                const auto &charJ = sel->word->chars[j];
                if (j != begin && charJ.charPos == sel->word->chars[j - 1].charPos) {
                    continue;
                }
                out->drawChar(state, charJ.textMat.m[4], charJ.textMat.m[5], 0, 0, 0, 0, charJ.charcode, 1, nullptr, 0);
            }
            out->endString(state);
            begin = fEnd;
        }
    }

    out->restoreState(state);
    out->endPage();
}

void TextWord::visitSelection(TextSelectionVisitor *visitor, const PDFRectangle &selection)
{
    double mid, s1, s2;

    if (rot == 0 || rot == 2) {
        s1 = selection.x1;
        s2 = selection.x2;
    } else {
        s1 = selection.y1;
        s2 = selection.y2;
    }

    size_t begin = len();
    size_t end = 0;
    for (size_t i = 0; i < len(); i++) {
        if (i + 1 < len()) {
            mid = (chars[i].edge + chars[i + 1].edge) / 2;
        } else {
            mid = (chars[i].edge + edgeEnd) / 2;
        }
        if (XBetweenAB(mid, s1, s2)) {
            if (i < begin) {
                begin = i;
            }

            end = i + 1;
        }
    }

    /* Skip empty selection. */
    if (end <= begin) {
        return;
    }

    visitor->visitWord(this, begin, end, selection);
}

void TextLine::visitSelection(TextSelectionVisitor *visitor, const PDFRectangle &selection, SelectionStyle style)
{
    TextWord *p, *begin, *end, *current;
    int i, edge_begin, edge_end;
    PDFRectangle child_selection;
    double s1, s2, pMin, pMax;

    if (rot == 0 || rot == 2) {
        s1 = selection.x1;
        s2 = selection.x2;
    } else {
        s1 = selection.y1;
        s2 = selection.y2;
    }

    begin = nullptr;
    end = nullptr;
    current = nullptr;
    for (p = words; p != nullptr; p = p->next) {
        if (rot == 0 || rot == 2) {
            pMin = p->xMin;
            pMax = p->xMax;
        } else {
            pMin = p->yMin;
            pMax = p->yMax;
        }

        if (blk->page->primaryLR) {
            if (((s1 < pMax) || (s2 < pMax)) && begin == nullptr) {
                begin = p;
            }

            if (((s1 > pMin) || (s2 > pMin)) && begin != nullptr) {
                end = p->next;
                current = p;
            }
        } else {
            if (((s1 > pMin) || (s2 > pMin)) && begin == nullptr) {
                begin = p;
            }

            if (((s1 < pMax) || (s2 < pMax)) && begin != nullptr) {
                end = p->next;
                current = p;
            }
        }
    }

    if (!current) {
        current = begin;
    }

    child_selection = selection;
    if (style == selectionStyleWord) {
        if (rot == 0 || rot == 2) {
            child_selection.x1 = begin ? begin->xMin : xMin;
            if (end && end->xMax != -1) {
                child_selection.x2 = current->xMax;
            } else {
                child_selection.x2 = xMax;
            }
        } else {
            child_selection.y1 = begin ? begin->yMin : yMin;
            if (end && end->yMax != -1) {
                child_selection.y2 = current->yMax;
            } else {
                child_selection.y2 = yMax;
            }
        }
    }

    if (rot == 0 || rot == 2) {
        s1 = child_selection.x1;
        s2 = child_selection.x2;
    } else {
        s1 = child_selection.y1;
        s2 = child_selection.y2;
    }

    edge_begin = len;
    edge_end = 0;
    for (i = 0; i < len; i++) {
        double mid = (edge[i] + edge[i + 1]) / 2;
        if (XBetweenAB(mid, s1, s2)) {
            if (i < edge_begin) {
                edge_begin = i;
            }

            edge_end = i + 1;
        }
    }

    /* Skip empty selection. */
    if (edge_end <= edge_begin) {
        return;
    }

    visitor->visitLine(this, begin, end, edge_begin, edge_end, child_selection);

    for (p = begin; p != end; p = p->next) {
        p->visitSelection(visitor, child_selection);
    }
}

void TextBlock::visitSelection(TextSelectionVisitor *visitor, const PDFRectangle &selection, SelectionStyle style)
{
    PDFRectangle child_selection;
    double x[2], y[2], d, best_d[2];
    TextLine *p, *best_line[2];
    int i, count = 0, best_count[2], start, stop;
    bool all[2];

    x[0] = selection.x1;
    y[0] = selection.y1;
    x[1] = selection.x2;
    y[1] = selection.y2;

    for (i = 0; i < 2; i++) {
        // the first/last lines are often not nearest
        // the corners, so we have to force them to be
        // selected when the selection runs outside this
        // block.
        if (page->primaryLR) {
            all[i] = x[i] >= this->xMax && y[i] >= this->yMax;
            if (x[i] <= this->xMin && y[i] <= this->yMin) {
                best_line[i] = this->lines;
                best_count[i] = 1;
            } else {
                best_line[i] = nullptr;
                best_count[i] = 0;
            }
        } else {
            all[i] = x[i] <= this->xMin && y[i] >= this->yMax;
            if (x[i] >= this->xMax && y[i] <= this->yMin) {
                best_line[i] = this->lines;
                best_count[i] = 1;
            } else {
                best_line[i] = nullptr;
                best_count[i] = 0;
            }
        }
        best_d[i] = 0;
    }

    // find the nearest line to the selection points
    // using the manhattan distance.
    for (p = this->lines; p; p = p->next) {
        count++;
        for (i = 0; i < 2; i++) {
            d = fmax(p->xMin - x[i], 0.0) + fmax(x[i] - p->xMax, 0.0) + fmax(p->yMin - y[i], 0.0) + fmax(y[i] - p->yMax, 0.0);
            if (!best_line[i] || all[i] || d < best_d[i]) {
                best_line[i] = p;
                best_count[i] = count;
                best_d[i] = d;
            }
        }
    }
    // assert: best is always set.
    if (!best_line[0] || !best_line[1]) {
        return;
    }

    // Now decide which point was first.
    if (best_count[0] < best_count[1] || (best_count[0] == best_count[1] && y[0] < y[1])) {
        start = 0;
        stop = 1;
    } else {
        start = 1;
        stop = 0;
    }

    visitor->visitBlock(this, best_line[start], best_line[stop], selection);

    for (p = best_line[start]; p; p = p->next) {
        if (page->primaryLR) {
            child_selection.x1 = p->xMin;
            child_selection.x2 = p->xMax;
        } else {
            child_selection.x1 = p->xMax;
            child_selection.x2 = p->xMin;
        }
        child_selection.y1 = p->yMin;
        child_selection.y2 = p->yMax;
        if (style == selectionStyleLine) {
            if (p == best_line[start]) {
                child_selection.x1 = 0;
                child_selection.y1 = 0;
            }
            if (p == best_line[stop]) {
                child_selection.x2 = page->pageWidth;
                child_selection.y2 = page->pageHeight;
            }
        } else {
            if (p == best_line[start]) {
                child_selection.x1 = fmax(p->xMin, fmin(p->xMax, x[start]));
                child_selection.y1 = fmax(p->yMin, fmin(p->yMax, y[start]));
            }
            if (p == best_line[stop]) {
                child_selection.x2 = fmax(p->xMin, fmin(p->xMax, x[stop]));
                child_selection.y2 = fmax(p->yMin, fmin(p->yMax, y[stop]));
            }
        }
        p->visitSelection(visitor, child_selection, style);
        if (p == best_line[stop]) {
            return;
        }
    }
}

void TextPage::visitSelection(TextSelectionVisitor *visitor, const PDFRectangle &selection, SelectionStyle style)
{
    PDFRectangle child_selection;
    double x[2], y[2], d, best_d[2];
    double xMin, yMin, xMax, yMax;
    TextFlow *flow, *best_flow[2];
    TextBlock *blk, *best_block[2];
    int i, count = 0, best_count[2], start, stop;

    if (!flows) {
        return;
    }

    x[0] = selection.x1;
    y[0] = selection.y1;
    x[1] = selection.x2;
    y[1] = selection.y2;

    xMin = pageWidth;
    yMin = pageHeight;
    xMax = 0.0;
    yMax = 0.0;

    for (i = 0; i < 2; i++) {
        best_block[i] = nullptr;
        best_flow[i] = nullptr;
        best_count[i] = 0;
        best_d[i] = 0;
    }

    // find the nearest blocks to the selection points
    // using the manhattan distance.
    for (flow = flows; flow; flow = flow->next) {
        for (blk = flow->blocks; blk; blk = blk->next) {
            count++;
            // the first/last blocks in reading order are
            // often not the closest to the page corners;
            // track the corners, force those blocks to
            // be selected if the selection runs across
            // multiple pages.
            xMin = fmin(xMin, blk->xMin);
            yMin = fmin(yMin, blk->yMin);
            xMax = fmax(xMax, blk->xMax);
            yMax = fmax(yMax, blk->yMax);
            for (i = 0; i < 2; i++) {
                d = fmax(blk->xMin - x[i], 0.0) + fmax(x[i] - blk->xMax, 0.0) + fmax(blk->yMin - y[i], 0.0) + fmax(y[i] - blk->yMax, 0.0);
                if (!best_block[i] || d < best_d[i] || (!blk->next && !flow->next && x[i] >= fmin(xMax, pageWidth) && y[i] >= fmin(yMax, pageHeight))) {
                    best_block[i] = blk;
                    best_flow[i] = flow;
                    best_count[i] = count;
                    best_d[i] = d;
                }
            }
        }
    }
    for (i = 0; i < 2; i++) {
        if (primaryLR) {
            if (x[i] < xMin && y[i] < yMin) {
                best_block[i] = flows->blocks;
                best_flow[i] = flows;
                best_count[i] = 1;
            }
        } else {
            if (x[i] > xMax && y[i] < yMin) {
                best_block[i] = flows->blocks;
                best_flow[i] = flows;
                best_count[i] = 1;
            }
        }
    }
    // assert: best is always set.
    if (!best_block[0] || !best_block[1]) {
        return;
    }

    // Now decide which point was first.
    if (best_count[0] < best_count[1] || (best_count[0] == best_count[1] && y[0] < y[1])) {
        start = 0;
        stop = 1;
    } else {
        start = 1;
        stop = 0;
    }

    for (flow = best_flow[start]; flow; flow = flow->next) {
        if (flow == best_flow[start]) {
            blk = best_block[start];
        } else {
            blk = flow->blocks;
        }
        for (; blk; blk = blk->next) {
            if (primaryLR) {
                child_selection.x1 = blk->xMin;
                child_selection.x2 = blk->xMax;
            } else {
                child_selection.x1 = blk->xMax;
                child_selection.x2 = blk->xMin;
            }
            child_selection.y1 = blk->yMin;
            child_selection.y2 = blk->yMax;
            if (blk == best_block[start]) {
                child_selection.x1 = fmax(blk->xMin, fmin(blk->xMax, x[start]));
                child_selection.y1 = fmax(blk->yMin, fmin(blk->yMax, y[start]));
            }
            if (blk == best_block[stop]) {
                child_selection.x2 = fmax(blk->xMin, fmin(blk->xMax, x[stop]));
                child_selection.y2 = fmax(blk->yMin, fmin(blk->yMax, y[stop]));
                blk->visitSelection(visitor, child_selection, style);
                return;
            }
            blk->visitSelection(visitor, child_selection, style);
        }
    }
}

void TextPage::drawSelection(OutputDev *out, double scale, int rotation, const PDFRectangle &selection, SelectionStyle style, const GfxColor &glyph_color, const GfxColor &box_color, double box_opacity, bool draw_glyphs)
{
    TextSelectionPainter painter(this, scale, rotation, out, box_color, glyph_color, box_opacity, draw_glyphs);

    visitSelection(&painter, selection, style);
    painter.endPage();
}

std::vector<PDFRectangle *> *TextPage::getSelectionRegion(const PDFRectangle &selection, SelectionStyle style, double scale)
{
    TextSelectionSizer sizer(this, scale);

    visitSelection(&sizer, selection, style);

    return sizer.takeRegion();
}

GooString TextPage::getSelectionText(const PDFRectangle &selection, SelectionStyle style)
{
    TextSelectionDumper dumper(this);

    visitSelection(&dumper, selection, style);
    dumper.endPage();

    return dumper.getText();
}

std::vector<std::vector<std::unique_ptr<TextWordSelection>>> TextPage::getSelectionWords(const PDFRectangle &selection, SelectionStyle style)
{
    TextSelectionDumper dumper(this);

    visitSelection(&dumper, selection, style);
    dumper.endPage();

    return dumper.takeWordList();
}

bool TextPage::findCharRange(int pos, int length, double *xMin, double *yMin, double *xMax, double *yMax) const
{
    TextBlock *blk;
    TextLine *line;
    TextWord *word;
    double xMin0, xMax0, yMin0, yMax0;
    double xMin1, xMax1, yMin1, yMax1;
    bool first;

    if (rawOrder) {
        return false;
    }

    //~ this doesn't correctly handle ranges split across multiple lines
    //~ (the highlighted region is the bounding box of all the parts of
    //~ the range)
    first = true;
    xMin0 = xMax0 = yMin0 = yMax0 = 0; // make gcc happy
    xMin1 = xMax1 = yMin1 = yMax1 = 0; // make gcc happy
    for (int i = 0; i < nBlocks; ++i) {
        blk = blocks[i];
        for (line = blk->lines; line; line = line->next) {
            for (word = line->words; word; word = word->next) {
                if (pos < word->charPosEnd && pos + length > word->chars.front().charPos) {
                    size_t j0, j1;
                    for (j0 = 0; (j0 + 1) < word->len() && pos >= word->chars[j0 + 1].charPos; ++j0) {
                        ;
                    }
                    for (j1 = word->len(); j1 > j0 && pos + length <= word->chars[j1].charPos; --j1) {
                        ;
                    }
                    auto startingEdge = word->chars[j0].edge;
                    auto endingEdge = (j1 + 1 == word->len()) ? word->edgeEnd : word->chars[j1 + 1].edge;
                    switch (line->rot) {
                    case 0:
                        xMin1 = startingEdge;
                        xMax1 = endingEdge;
                        yMin1 = word->yMin;
                        yMax1 = word->yMax;
                        break;
                    case 1:
                        xMin1 = word->xMin;
                        xMax1 = word->xMax;
                        yMin1 = startingEdge;
                        yMax1 = endingEdge;
                        break;
                    case 2:
                        xMin1 = endingEdge;
                        xMax1 = startingEdge;
                        yMin1 = word->yMin;
                        yMax1 = word->yMax;
                        break;
                    case 3:
                        xMin1 = word->xMin;
                        xMax1 = word->xMax;
                        yMin1 = endingEdge;
                        yMax1 = startingEdge;
                        break;
                    }
                    if (first || xMin1 < xMin0) {
                        xMin0 = xMin1;
                    }
                    if (first || xMax1 > xMax0) {
                        xMax0 = xMax1;
                    }
                    if (first || yMin1 < yMin0) {
                        yMin0 = yMin1;
                    }
                    if (first || yMax1 > yMax0) {
                        yMax0 = yMax1;
                    }
                    first = false;
                }
            }
        }
    }
    if (!first) {
        *xMin = xMin0;
        *xMax = xMax0;
        *yMin = yMin0;
        *yMax = yMax0;
        return true;
    }
    return false;
}

std::pair<int, int> TextLine::getLineBounds(const PDFRectangle &area) const
{
    const auto bBox = getBBox();
    auto clipped { bBox };
    clipped.clipTo(area);

    if (clipped.isEmpty()) {
        return { 0, 0 };
    }
    if (bBox == clipped) {
        return { 0, len };
    }

    if (rot == 0 || rot == 2) {
        auto centerY = (bBox.y1 + bBox.y2) * 0.5;
        if (centerY < area.y1 || centerY > area.y2) {
            return { 0, 0 };
        }
    } else {
        auto centerX = (bBox.x1 + bBox.x2) * 0.5;
        if (centerX < area.x1 || centerX > area.x2) {
            return { 0, 0 };
        }
    }

    using Cmp = bool (*)(double, double);
    auto findEdge = [this](int start, double wantedEdge, Cmp cmp) -> int {
        for (int i = start; i < len; i++) {
            if (cmp(edge[i + 1], wantedEdge)) {
                auto center = (edge[i] + edge[i + 1]) * 0.5;
                if (cmp(center, wantedEdge)) {
                    return i;
                }
                return i + 1;
            }
        }
        return len;
    };

    if (rot == 0 || rot == 1) {
        auto edge1 = (rot == 0) ? clipped.x1 : clipped.y1;
        auto edge2 = (rot == 0) ? clipped.x2 : clipped.y2;
        int l = findEdge(0, edge1, [](double a, double b) { return a > b; });
        int u = findEdge(l, edge2, [](double a, double b) { return a > b; });
        return { l, u - l };
    }
    auto edge1 = (rot == 2) ? clipped.x2 : clipped.y2;
    auto edge2 = (rot == 2) ? clipped.x1 : clipped.y1;
    int l = findEdge(0, edge1, [](double a, double b) { return a < b; });
    int u = findEdge(l, edge2, [](double a, double b) { return a < b; });
    return { l, u - l };
}

static bool isHyphenChar(Unicode c)
{
    return c == static_cast<Unicode>('-') || c == static_cast<Unicode>(0x2010) || c == static_cast<Unicode>(0xFE63) || c == static_cast<Unicode>(0xFF0D);
}

void TextPage::dump(void *outputStream, TextOutputFunc outputFunc, bool physLayout, EndOfLineKind textEOL, bool pageBreaks, bool suppressLastEol, std::optional<PDFRectangle> area, EndOfLineHyphenMode hyphenMode) const
{
    const UnicodeMap *uMap;
    char space[8], eol[16], eop[8];
    int spaceLen, eolLen, eopLen;

    // get the output encoding
    if (!(uMap = globalParams->getTextEncoding())) {
        return;
    }
    spaceLen = uMap->mapUnicode(0x20, space, sizeof(space));
    eolLen = 0; // make gcc happy
    switch (textEOL) {
    case eolUnix:
        eolLen = uMap->mapUnicode(0x0a, eol, sizeof(eol));
        break;
    case eolDOS:
        eolLen = uMap->mapUnicode(0x0d, eol, sizeof(eol));
        eolLen += uMap->mapUnicode(0x0a, eol + eolLen, sizeof(eol) - eolLen);
        break;
    case eolMac:
        eolLen = uMap->mapUnicode(0x0d, eol, sizeof(eol));
        break;
    }
    eopLen = uMap->mapUnicode(0x0c, eop, sizeof(eop));

    if (area && area->x1 <= 0.0 && area->y1 <= 0.0 //
        && area->x2 >= pageWidth && area->y2 >= pageHeight) {
        area = std::nullopt;
    }

    //~ writing mode (horiz/vert)

    // output the page in raw (content stream) order
    if (rawOrder) {

        GooString s;
        std::vector<Unicode> uText;

        for (TextWord *word = rawWords; word; word = word->next) {
            if (area) {
                auto bBox = word->getBBox();
                if (!area->contains(bBox.x1, bBox.y1) && !area->contains(bBox.x2, bBox.y2)) {
                    continue;
                }
            }
            s.clear();
            uText.resize(word->len());
            std::ranges::transform(word->chars, uText.begin(), [](auto &c) { return c.text; });
            dumpFragment(uText.data(), uText.size(), uMap, &s);
            (*outputFunc)(outputStream, s.c_str(), s.size());

            if (!word->next) {
                continue;
            }
            if (fabs(word->next->base - word->base) >= maxIntraLineDelta * word->fontSize) {
                (*outputFunc)(outputStream, eol, eolLen);
            } else if (fabs(word->next->xMin - word->xMax) > minDupBreakOverlap * word->fontSize) {
                (*outputFunc)(outputStream, space, spaceLen);
            }
        }

        // output the page, maintaining the original physical layout
    } else if (physLayout) {
        int rotations = 0;

        // collect the line fragments for the page and sort them
        std::vector<TextLineFrag> frags;
        frags.reserve(256);
        for (int i = 0; i < nBlocks; ++i) {
            TextBlock *blk = blocks[i];
            if (area) {
                auto bBox = blk->getBBox();
                bBox.clipTo(area.value());
                if (bBox.isEmpty()) {
                    continue;
                }
            }
            for (TextLine *line = blk->lines; line; line = line->next) {
                const auto [start, len] = area ? line->getLineBounds(*area) : std::pair { 0, line->len };
                if (len) {
                    rotations |= 1 << line->rot;
                    frags.push_back({});
                    frags.back().init(line, start, len);
                }
            }
        }

        bool oneRot = (rotations == 1) || (rotations == 2) || (rotations == 4) || (rotations == 8);
        for (auto &frag : frags) {
            frag.computeCoords(oneRot);
        }
        if (!frags.empty() && area) {
            // Recalculate columns, as some blocks/lines were possibly discarded above
            assignColumns(frags.data(), frags.size(), oneRot);
        }
        if (oneRot) {
            std::ranges::sort(frags, &TextLineFrag::cmpYXLineRot);
        } else {
            std::ranges::sort(frags, &TextLineFrag::cmpYXPrimaryRot);
        }
        for (auto it = frags.begin(); it != frags.end();) {
            double delta = maxIntraLineDelta * it->line->words->fontSize;
            double base = it->base;

            auto end = std::find_if(it + 1, frags.end(), [base, delta](const TextLineFrag &frag) { //
                return fabs(frag.base - base) >= delta;
            });
            std::sort(it, end, oneRot ? &TextLineFrag::cmpXYColumnLineRot : &TextLineFrag::cmpXYColumnPrimaryRot);
            it = end;
        }

#if 0 // for debugging
    printf("*** line fragments ***\n");
    for (const auto& frag : frags) {
      printf("frag: x=%.2f..%.2f y=%.2f..%.2f base=%.2f '",
	     frag.xMin, frag.xMax, frag.yMin, frag.yMax, frag.base);
      for (int n = 0; n < frag.len; ++n) {
	fputc(frag.line->text[frag.start + n] & 0xff, stdout);
      }
      printf("'\n");
    }
    printf("\n");
#endif

        GooString s;
        // generate output
        int col = 0;
        for (size_t i = 0; i < frags.size(); ++i) {
            const auto &frag = frags[i];

            // column alignment
            for (; col < frag.col; ++col) {
                (*outputFunc)(outputStream, space, spaceLen);
            }

            // print the line
            s.clear();
            col += dumpFragment(frag.line->text + frag.start, frag.len, uMap, &s);
            (*outputFunc)(outputStream, s.c_str(), s.size());

            // print one or more returns if necessary
            if (i == frags.size() - 1) {
                if (!suppressLastEol) {
                    (*outputFunc)(outputStream, eol, eolLen);
                }
            } else if (frags[i + 1].col < col || fabs(frags[i + 1].base - frag.base) > maxIntraLineDelta * frag.line->words->fontSize) {
                int d = static_cast<int>((frags[i + 1].base - frag.base) / frag.line->words->fontSize);
                d = std::clamp(d, 1, 5);
                for (; d > 0; --d) {
                    (*outputFunc)(outputStream, eol, eolLen);
                }
                col = 0;
            }
        }

        // output the page, "undoing" the layout
    } else {
        GooString s;
        for (TextFlow *flow = flows; flow; flow = flow->next) {
            for (TextBlock *blk = flow->blocks; blk; blk = blk->next) {
                for (TextLine *line = blk->lines; line; line = line->next) {
                    const bool suppressHyphen = [&]() -> bool {
                        if (!line->next && !blk->next) {
                            return false;
                        }
                        if (hyphenMode == EndOfLineHyphenMode::Keep) {
                            return false;
                        }
                        if (line->text[line->len - 1] == static_cast<Unicode>(0x00AD)) {
                            return true;
                        }
                        if (hyphenMode == EndOfLineHyphenMode::RemoveAll && isHyphenChar(line->text[line->len - 1])) {
                            return true;
                        }
                        return false;
                    }();
                    const int n = suppressHyphen ? line->len - 1 : line->len;
                    s.clear();
                    dumpFragment(line->text, n, uMap, &s);
                    (*outputFunc)(outputStream, s.c_str(), s.size());
                    if (!suppressHyphen) {
                        (*outputFunc)(outputStream, eol, eolLen);
                    }
                }
            }
            (*outputFunc)(outputStream, eol, eolLen);
        }
    }

    // end of page
    if (pageBreaks) {
        (*outputFunc)(outputStream, eop, eopLen);
    }
}

void TextPage::setMergeCombining(bool merge)
{
    mergeCombining = merge;
}

void TextPage::assignColumns(TextLineFrag *frags, int nFrags, bool oneRot)
{
    TextLineFrag *frag0, *frag1;
    int rot, col1, col2, i, j, k;

    // all text in the region has the same rotation -- recompute the
    // column numbers based only on the text in the region
    if (oneRot) {
        std::sort(frags, frags + nFrags, &TextLineFrag::cmpXYLineRot);
        rot = frags[0].line->rot;
        for (i = 0; i < nFrags; ++i) {
            frag0 = &frags[i];
            col1 = 0;
            for (j = 0; j < i; ++j) {
                frag1 = &frags[j];
                col2 = 0; // make gcc happy
                switch (rot) {
                case 0:
                    if (frag0->xMin >= frag1->xMax) {
                        col2 = frag1->col + (frag1->line->col[frag1->start + frag1->len] - frag1->line->col[frag1->start]) + 1;
                    } else {
                        for (k = frag1->start; k < frag1->start + frag1->len && frag0->xMin >= 0.5 * (frag1->line->edge[k] + frag1->line->edge[k + 1]); ++k) {
                            ;
                        }
                        col2 = frag1->col + frag1->line->col[k] - frag1->line->col[frag1->start];
                    }
                    break;
                case 1:
                    if (frag0->yMin >= frag1->yMax) {
                        col2 = frag1->col + (frag1->line->col[frag1->start + frag1->len] - frag1->line->col[frag1->start]) + 1;
                    } else {
                        for (k = frag1->start; k < frag1->start + frag1->len && frag0->yMin >= 0.5 * (frag1->line->edge[k] + frag1->line->edge[k + 1]); ++k) {
                            ;
                        }
                        col2 = frag1->col + frag1->line->col[k] - frag1->line->col[frag1->start];
                    }
                    break;
                case 2:
                    if (frag0->xMax <= frag1->xMin) {
                        col2 = frag1->col + (frag1->line->col[frag1->start + frag1->len] - frag1->line->col[frag1->start]) + 1;
                    } else {
                        for (k = frag1->start; k < frag1->start + frag1->len && frag0->xMax <= 0.5 * (frag1->line->edge[k] + frag1->line->edge[k + 1]); ++k) {
                            ;
                        }
                        col2 = frag1->col + frag1->line->col[k] - frag1->line->col[frag1->start];
                    }
                    break;
                case 3:
                    if (frag0->yMax <= frag1->yMin) {
                        col2 = frag1->col + (frag1->line->col[frag1->start + frag1->len] - frag1->line->col[frag1->start]) + 1;
                    } else {
                        for (k = frag1->start; k < frag1->start + frag1->len && frag0->yMax <= 0.5 * (frag1->line->edge[k] + frag1->line->edge[k + 1]); ++k) {
                            ;
                        }
                        col2 = frag1->col + frag1->line->col[k] - frag1->line->col[frag1->start];
                    }
                    break;
                }
                if (col2 > col1) {
                    col1 = col2;
                }
            }
            frag0->col = col1;
        }

        // the region includes text at different rotations -- use the
        // globally assigned column numbers, offset by the minimum column
        // number (i.e., shift everything over to column 0)
    } else {
        col1 = frags[0].col;
        for (i = 1; i < nFrags; ++i) {
            if (frags[i].col < col1) {
                col1 = frags[i].col;
            }
        }
        for (i = 0; i < nFrags; ++i) {
            frags[i].col -= col1;
        }
    }
}

int TextPage::dumpFragment(const Unicode *text, int len, const UnicodeMap *uMap, GooString *s) const
{
    if (uMap->isUnicode()) {
        return reorderText(text, len, uMap, primaryLR, s, nullptr);
    }
    int nCols = 0;

    char buf[8];
    int buflen = 0;

    for (int i = 0; i < len; ++i) {
        buflen = uMap->mapUnicode(text[i], buf, sizeof(buf));
        s->append(buf, buflen);
        nCols += buflen;
    }

    return nCols;
}

std::unique_ptr<TextWordList> TextPage::makeWordList(bool physLayout)
{
    return std::make_unique<TextWordList>(this, physLayout);
}

//------------------------------------------------------------------------
// ActualText
//------------------------------------------------------------------------
ActualText::ActualText(TextPage *out)
{
    text = out;
    actualTextNBytes = 0;
}

ActualText::~ActualText() = default;

void ActualText::addChar(const GfxState *state, double x, double y, double dx, double dy, CharCode c, int nBytes, const Unicode *u, int uLen)
{
    if (!actualText) {
        text->addChar(state, x, y, dx, dy, c, nBytes, u, uLen);
        return;
    }

    // Inside ActualText span.
    if (!actualTextNBytes) {
        actualTextX0 = x;
        actualTextY0 = y;
    }
    actualTextX1 = x + dx;
    actualTextY1 = y + dy;
    actualTextNBytes += nBytes;
}

void ActualText::begin(const GfxState * /*state*/, const std::string &t)
{
    actualText = t;
    actualTextNBytes = 0;
}

void ActualText::end(const GfxState *state)
{
    // ActualText span closed. Output the span text and the
    // extents of all the glyphs inside the span

    if (actualTextNBytes) {
        // now that we have the position info for all of the text inside
        // the marked content span, we feed the "ActualText" back through
        // text->addChar()
        assert(actualText.has_value());
        std::vector<Unicode> uni = TextStringToUCS4(actualText.value());
        text->addChar(state, actualTextX0, actualTextY0, actualTextX1 - actualTextX0, actualTextY1 - actualTextY0, 0, actualTextNBytes, uni.data(), uni.size());
    }

    actualText.reset();
    actualTextNBytes = 0;
}

//------------------------------------------------------------------------
// TextOutputDev
//------------------------------------------------------------------------

static void TextOutputDev_outputToFile(void *stream, const char *text, int len)
{
    fwrite(text, 1, len, static_cast<FILE *>(stream));
}

TextOutputDev::TextOutputDev(const char *fileName, bool physLayoutA, double fixedPitchA, bool rawOrderA, bool append, bool discardDiagA)
{
    text = nullptr;
    physLayout = physLayoutA;
    fixedPitch = physLayout ? fixedPitchA : 0;
    rawOrder = rawOrderA;
    discardDiag = discardDiagA;
    doHTML = false;
    textEOL = EndOfLineKind::eolUnix;
    textPageBreaks = true;
    ok = true;
    minColSpacing1 = minColSpacing1_default;

    // open file
    needClose = false;
    if (fileName) {
        if (!strcmp(fileName, "-")) {
            outputStream = stdout;
#if defined(_WIN32) || defined(__CYGWIN__)
            // keep DOS from munging the end-of-line characters
            _setmode(fileno(stdout), O_BINARY);
#endif
        } else if ((outputStream = openFile(fileName, append ? "ab" : "wb"))) {
            needClose = true;
        } else {
            error(errIO, -1, "Couldn't open text file '{0:s}'", fileName);
            ok = false;
            actualText = nullptr;
            return;
        }
        outputFunc = &TextOutputDev_outputToFile;
    } else {
        outputStream = nullptr;
    }

    // set up text object
    text = std::make_unique<TextPage>(rawOrderA, discardDiagA);
    actualText = std::make_unique<ActualText>(text.get());
}

TextOutputDev::TextOutputDev(TextOutputFunc func, void *stream, bool physLayoutA, double fixedPitchA, bool rawOrderA, bool discardDiagA)
{
    outputFunc = func;
    outputStream = stream;
    needClose = false;
    physLayout = physLayoutA;
    fixedPitch = physLayout ? fixedPitchA : 0;
    rawOrder = rawOrderA;
    discardDiag = discardDiagA;
    doHTML = false;
    text = std::make_unique<TextPage>(rawOrderA, discardDiagA);
    actualText = std::make_unique<ActualText>(text.get());
    textEOL = EndOfLineKind::eolUnix;
    textPageBreaks = true;
    ok = true;
    minColSpacing1 = minColSpacing1_default;
}

TextOutputDev::~TextOutputDev()
{
    if (needClose) {
        fclose(static_cast<FILE *>(outputStream));
    }
}

void TextOutputDev::startPage(int /*pageNum*/, GfxState *state, XRef * /*xref*/)
{
    text->startPage(state);
}

void TextOutputDev::endPage()
{
    text->endPage();
    text->coalesce(physLayout, fixedPitch, doHTML, minColSpacing1);
    if (outputStream) {
        text->dump(outputStream, outputFunc, physLayout, textEOL, textPageBreaks, false, std::nullopt, hyphenMode);
    }
}

void TextOutputDev::restoreState(GfxState *state)
{
    text->updateFont(state);
}

void TextOutputDev::updateFont(GfxState *state)
{
    text->updateFont(state);
}

void TextOutputDev::beginString(GfxState * /*state*/, const std::string & /*s*/) { }

void TextOutputDev::endString(GfxState * /*state*/) { }

void TextOutputDev::drawChar(GfxState *state, double x, double y, double dx, double dy, double /*originX*/, double /*originY*/, CharCode c, int nBytes, const Unicode *u, int uLen)
{
    actualText->addChar(state, x, y, dx, dy, c, nBytes, u, uLen);
}

void TextOutputDev::incCharCount(int nChars)
{
    text->incCharCount(nChars);
}

void TextOutputDev::beginActualText(GfxState *state, const std::string &t)
{
    actualText->begin(state, t);
}

void TextOutputDev::endActualText(GfxState *state)
{
    actualText->end(state);
}

void TextOutputDev::stroke(GfxState *state)
{
    double x[2], y[2];

    if (!doHTML) {
        return;
    }
    const GfxPath *path = state->getPath();
    if (path->getNumSubpaths() != 1) {
        return;
    }
    const GfxSubpath *subpath = path->getSubpath(0);
    if (subpath->getNumPoints() != 2) {
        return;
    }
    state->transform(subpath->getX(0), subpath->getY(0), &x[0], &y[0]);
    state->transform(subpath->getX(1), subpath->getY(1), &x[1], &y[1]);

    // look for a vertical or horizontal line
    if (x[0] == x[1] || y[0] == y[1]) {
        text->addUnderline(x[0], y[0], x[1], y[1]);
    }
}

void TextOutputDev::fill(GfxState *state)
{
    double x[5], y[5];
    double rx0, ry0, rx1, ry1, t;
    int i;

    if (!doHTML) {
        return;
    }
    const GfxPath *path = state->getPath();
    if (path->getNumSubpaths() != 1) {
        return;
    }
    const GfxSubpath *subpath = path->getSubpath(0);
    if (subpath->getNumPoints() != 5) {
        return;
    }
    for (i = 0; i < 5; ++i) {
        if (subpath->getCurve(i)) {
            return;
        }
        state->transform(subpath->getX(i), subpath->getY(i), &x[i], &y[i]);
    }

    // look for a rectangle
    if (x[0] == x[1] && y[1] == y[2] && x[2] == x[3] && y[3] == y[4] && x[0] == x[4] && y[0] == y[4]) {
        rx0 = x[0];
        ry0 = y[0];
        rx1 = x[2];
        ry1 = y[1];
    } else if (y[0] == y[1] && x[1] == x[2] && y[2] == y[3] && x[3] == x[4] && x[0] == x[4] && y[0] == y[4]) {
        rx0 = x[0];
        ry0 = y[0];
        rx1 = x[1];
        ry1 = y[2];
    } else {
        return;
    }
    if (rx1 < rx0) {
        t = rx0;
        rx0 = rx1;
        rx1 = t;
    }
    if (ry1 < ry0) {
        t = ry0;
        ry0 = ry1;
        ry1 = t;
    }

    // skinny horizontal rectangle
    if (ry1 - ry0 < rx1 - rx0) {
        if (ry1 - ry0 < maxUnderlineWidth) {
            ry0 = 0.5 * (ry0 + ry1);
            text->addUnderline(rx0, ry0, rx1, ry0);
        }

        // skinny vertical rectangle
    } else {
        if (rx1 - rx0 < maxUnderlineWidth) {
            rx0 = 0.5 * (rx0 + rx1);
            text->addUnderline(rx0, ry0, rx0, ry1);
        }
    }
}

void TextOutputDev::eoFill(GfxState *state)
{
    if (!doHTML) {
        return;
    }
    fill(state);
}

void TextOutputDev::processLink(AnnotLink *link)
{
    double x1, y1, x2, y2;
    int xMin, yMin, xMax, yMax, x, y;

    if (!doHTML) {
        return;
    }
    link->getRect(&x1, &y1, &x2, &y2);
    cvtUserToDev(x1, y1, &x, &y);
    xMin = xMax = x;
    yMin = yMax = y;
    cvtUserToDev(x1, y2, &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    cvtUserToDev(x2, y1, &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    cvtUserToDev(x2, y2, &x, &y);
    if (x < xMin) {
        xMin = x;
    } else if (x > xMax) {
        xMax = x;
    }
    if (y < yMin) {
        yMin = y;
    } else if (y > yMax) {
        yMax = y;
    }
    text->addLink(xMin, yMin, xMax, yMax, link);
}

bool TextOutputDev::findText(const Unicode *s, int len, bool startAtTop, bool stopAtBottom, bool startAtLast, bool stopAtLast, bool caseSensitive, bool backward, bool wholeWord, double *xMin, double *yMin, double *xMax, double *yMax) const
{
    return text->findText(s, len, startAtTop, stopAtBottom, startAtLast, stopAtLast, caseSensitive, backward, wholeWord, xMin, yMin, xMax, yMax);
}

GooString TextOutputDev::getText(const std::optional<PDFRectangle> &area) const
{
    return text->getText(area, textEOL, physLayout, hyphenMode);
}

void TextOutputDev::drawSelection(OutputDev *out, double scale, int rotation, const PDFRectangle &selection, SelectionStyle style, const GfxColor &glyph_color, const GfxColor &box_color, double box_opacity, bool draw_glyphs)
{
    text->drawSelection(out, scale, rotation, selection, style, glyph_color, box_color, box_opacity, draw_glyphs);
}

std::vector<PDFRectangle *> *TextOutputDev::getSelectionRegion(const PDFRectangle &selection, SelectionStyle style, double scale)
{
    return text->getSelectionRegion(selection, style, scale);
}

GooString TextOutputDev::getSelectionText(const PDFRectangle &selection, SelectionStyle style)
{
    return text->getSelectionText(selection, style);
}

bool TextOutputDev::findCharRange(int pos, int length, double *xMin, double *yMin, double *xMax, double *yMax) const
{
    return text->findCharRange(pos, length, xMin, yMin, xMax, yMax);
}

void TextOutputDev::setMergeCombining(bool merge)
{
    text->setMergeCombining(merge);
}

std::unique_ptr<TextWordList> TextOutputDev::makeWordList()
{
    return text->makeWordList(physLayout);
}

std::unique_ptr<TextPage> TextOutputDev::takeText()
{
    auto ret = std::move(text);

    text = std::make_unique<TextPage>(rawOrder, discardDiag);
    actualText = std::make_unique<ActualText>(text.get());
    return ret;
}

const TextFlow *TextOutputDev::getFlows() const
{
    return text->getFlows();
}
