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.