Examples / text_extraction / activex_cpp

Text Extraction

A complete, runnable activex_cpp program — 210 lines, shipped in your download.

Demonstrates Text

Same example, other languages: c cpp cpp_linux csharp delphi php python vbnet

examples/activex_cpp/text_extraction/text_extraction.cpp 210 lines
// ============================================================================
//  text_extraction -- C++ ActiveX (COM) port of
//  examples/delphi/text_extraction/text_extraction.dpr (+ pdf_to_text.pas)
//
//  THIS PORT RUNS, AND ITS VBSCRIPT SIBLING CANNOT. That file is a documented
//  skip: "the COM GetPageText marshals TPDFStack as an opaque byte-blob
//  OleVariant with embedded pointers that VBScript cannot dereference (no
//  VarPtr / CopyMemory / typed struct access)".
//
//  A C++ client has none of those limits. The AX server is IN-PROC, so the
//  pointers inside the returned TPDFStack -- Kerning, an array of TTextRecordW
//  each holding a raw PWideChar + length -- are valid addresses in this very
//  process. RecVar<TPDFStack> is the byte blob, and the fields are just read
//  through. So the real stack walk the Delphi original does is reproduced here
//  rather than substituted with ExtractText (which is what text_extraction3
//  demonstrates, and what the VBScript sibling redirects to).
//
//  The reconstruction logic is the CPDFToText algorithm verbatim:
//    * a text record starts a NEW LINE when its direction changes, or when its
//      start point is not on the line the previous record established
//    * otherwise a SPACE is inserted if the gap exceeds one space width --
//      measured in text space and transformed to user space, because the record
//      distance is in user space
//    * within a record, a negative kerning advance below half a space width is
//      also a space
//  Templates are walked recursively, each handle only once.
// ============================================================================
#import "..\\..\\wrappers\\activex\\LumasPdfAX.tlb" no_namespace named_guids

#include <windows.h>

#include "axcommon.h"

#include <cmath>
#include <cstdio>
#include <string>
#include <vector>

static const char* IN_FILE = "E:\\LUMASPDFSDK\\sample_multipage.pdf";
static const long tfNotInitialized = 5;          // TTextDir
static const double MAX_LINE_ERROR = 4.0;        // square of the allowed error (2*2)

static ILumasPDFPtr pdf;
static FILE* gFile;
static RecVar<TPDFStack>* gStack;
static long gLastTextDir;
static double gLastEndX, gLastEndY, gLastInfX, gLastInfY;
static std::vector<long> gTemplates;

static void W(const wchar_t* p, size_t n) {
    if (p && n) fwrite(p, sizeof(wchar_t), n, gFile);
}
static void WS(const wchar_t* s) { W(s, wcslen(s)); }

static TCTM MulMatrix(const TCTM& M1, const TCTM& M2) {
    TCTM r;
    r.a = M2.a * M1.a + M2.b * M1.c;
    r.b = M2.a * M1.b + M2.b * M1.d;
    r.c = M2.c * M1.a + M2.d * M1.c;
    r.d = M2.c * M1.b + M2.d * M1.d;
    r.x = M2.x * M1.a + M2.y * M1.c + M1.x;
    r.y = M2.x * M1.b + M2.y * M1.d + M1.y;
    return r;
}
static void Transform(const TCTM& M, double& x, double& y) {
    double tx = x;
    x = tx * M.a + y * M.c + M.x;
    y = tx * M.b + y * M.d + M.y;
}
static double CalcDistance(double x1, double y1, double x2, double y2) {
    double dx = x2 - x1, dy = y2 - y1;
    return std::sqrt(dx * dx + dy * dy);
}
static bool IsPointOnLine(double x, double y, double x0, double y0, double x1, double y1) {
    x -= x0; y -= y0;
    double dx = x1 - x0, dy = y1 - y0;
    double di = (x * dx + y * dy) / (dx * dx + dy * dy);
    if (di < 0.0) di = 0.0; else if (di > 1.0) di = 1.0;
    dx = x - di * dx;
    dy = y - di * dy;
    return (dx * dx + dy * dy) < MAX_LINE_ERROR;
}

static void AddText() {
    TPDFStack* st = gStack->Ptr();
    double x1 = 0.0, y1 = 0.0, x2 = 0.0, y2 = st->FontSize;
    TCTM m = MulMatrix(st->ctm, st->tm);
    Transform(m, x1, y1);                 // Start point of the text record
    Transform(m, x2, y2);                 // Second point -> text direction

    long textDir;
    if (y1 == y2) textDir = ((x1 > x2 ? 1 : 0) + 1) * 2;
    else          textDir = (y1 > y2 ? 1 : 0);

    if (textDir != gLastTextDir ||
        !IsPointOnLine(x1, y1, gLastEndX, gLastEndY, gLastInfX, gLastInfY)) {
        gLastInfX = 1000000.0;
        gLastInfY = 0.0;
        Transform(m, gLastInfX, gLastInfY);
        if (gLastTextDir != tfNotInitialized) WS(L"\r\n");
    } else {
        // The space width is measured in TEXT space but the distance between
        // two records is in USER space -- transform before comparing.
        double x3 = st->SpaceWidth, y3 = 0.0;
        Transform(m, x3, y3);
        double spaceWidth = CalcDistance(x1, y1, x3, y3);
        double distance = CalcDistance(gLastEndX, gLastEndY, x1, y1);
        if (distance > spaceWidth) WS(L" ");
    }

    // Half the space width gives better results for intra-record gaps.
    float spw = -st->SpaceWidth * 0.5f;
    const TTextRecordW* rec = (const TTextRecordW*)st->Kerning;
    for (uint32_t i = 0; rec && i < st->KerningCount; ++i, ++rec) {
        if (rec->Advance < spw) WS(L" ");
        // The Kerning array carries Unicode strings (two bytes per character).
        W(rec->Text, (size_t)rec->Length);
    }

    // Deliberately NOT the real end of the string: applications often append a
    // space that slightly overlaps the next record, and IsPointOnLine would
    // then reject a record that really is on the same line.
    gLastEndX = st->TextWidth + spw;      // spw is negative
    gLastEndY = 0.0;
    gLastTextDir = textDir;
    Transform(m, gLastEndX, gLastEndY);
}

static void ParseText() {
    VARIANT_BOOL haveMore = pdf->GetPageText(gStack->Addr());
    if (!haveMore && gStack->Ptr()->TextLen == 0) return;
    AddText();
    if (haveMore)
        while (pdf->GetPageText(gStack->Addr())) AddText();
}

static void ParseTemplates() {
    long count = pdf->GetTemplCount();
    for (long i = 0; i < count; ++i) {
        if (!pdf->EditTemplate(i)) return;
        long tmpl = pdf->GetTemplHandle();
        bool seen = false;
        for (size_t k = 0; k < gTemplates.size(); ++k)
            if (gTemplates[k] == tmpl) { seen = true; break; }
        if (!seen) {
            gTemplates.push_back(tmpl);
            pdf->InitStack(gStack->Addr());
            ParseText();
            long inner = pdf->GetTemplCount();
            for (long j = 0; j < inner; ++j) ParseTemplates();
        }
        pdf->EndTemplate();
    }
}

int main() {
    ChdirToExe();
    ComInit com;
    if (!com.ok()) return 1;

    if (FAILED(pdf.CreateInstance(L"LumasPdf.PDF"))) return 1;
    // Not RaiseExceptions: GetPageText returning FALSE is how the record loop
    // ENDS, and EditTemplate/InitStack returns are tested the same way.
    pdf->RaiseExceptions = VARIANT_FALSE;

    pdf->CreateNewPDFA(B(""));
    pdf->SetImportFlags(ifImportAll | ifImportAsPage);
    if (pdf->OpenImportFileA(B(IN_FILE), ptOpen, B("")) < 0) {
        std::printf("Input file not found: %s\n", IN_FILE);
        return 1;
    }
    pdf->ImportPDFFile(1, 1, 1);
    pdf->CloseImportFile();
    pdf->FlattenAnnots(affMarkupAnnots);
    pdf->FlattenForm();

    std::string outFile = OutFile("out.txt");
    gFile = fopen(outFile.c_str(), "wb");
    if (!gFile) return 1;
    const unsigned char bom[2] = {0xFF, 0xFE};   // UTF-16LE
    fwrite(bom, 1, 2, gFile);

    RecVar<TPDFStack> stack;
    gStack = &stack;

    long pages = pdf->GetPageCount();
    for (long i = 1; i <= pages; ++i) {
        pdf->EditPage(i);
        gTemplates.clear();
        pdf->InitStack(stack.Addr());
        gLastEndX = gLastEndY = gLastInfX = gLastInfY = 0.0;
        gLastTextDir = tfNotInitialized;

        if (i > 1) WS(L"\r\n");
        wchar_t hdr[128];
        swprintf(hdr, 128,
                 L"%%----------------------- Page %ld -----------------------------\r\n", i);
        WS(hdr);

        ParseText();
        ParseTemplates();
        pdf->EndPage();
    }
    fclose(gFile);

    std::printf("Text successfully extracted to \"%s\" (%ld pages)\n", outFile.c_str(), pages);
    pdf = nullptr;
    return 0;
}

This file is in the SDK at examples/activex_cpp/text_extraction/text_extraction.cpp. The build fails if this page and that file ever differ.