examples/activex_cpp/edit_text/edit_text.cpp
126 lines
// ============================================================================
// edit_text -- C++ ActiveX (COM) port of
// examples/delphi/edit_text/edit_text.dpr, cross-checked against
// examples/activex/edit_text/edit_text.vbs
//
// Imports a PDF, then uses the content parser (Psr* exports) to find every
// occurrence of a string and replace it in place.
//
// STATUS: COMPILES (x64 + x86), FAILS AT RUNTIME -- same root cause its
// VBScript sibling records as "*** NOT RUNNABLE ***", reached one call later.
//
// C++ solves the half that stopped VBScript: TTextSelection and TContent are
// packed in/out buffers that a script host cannot build, and RecVar<T> is
// exactly that buffer, so PsrParsePage's TContent out-param now marshals
// correctly where VBScript could not even express it.
//
// What remains is a marshalling gap no caller can code around: the OPTIONAL
// pointer parameters (PsrCreateParserContext's Parms, PsrParsePage's Funcs and
// Parms, PsrFindText's Area and Last) all take the TVarLocks.InPtr path, and
// Lock() REJECTS Empty/Null -- "this parameter needs a packed numeric array".
// So there is no way to say "nil" for them. Passing the integer 0 (what the
// VBScript port does) yields a pointer to a single zero byte, which the engine
// then reads as a TFltRect/TOptimizeParams and rejects: "engine call failed".
// A zeroed record is not equivalent either -- an all-zero Area means an EMPTY
// search rectangle, not "the whole page".
//
// The fix belongs in the wrapper: these params need to accept Empty as a nil
// pointer. That is a per-parameter recipe in tools/gen_axidl.py (the same
// mechanism rec_out uses), NOT a blanket change to Lock() -- its Empty guard
// was added deliberately, because a nil buffer paired with a separate element
// count is an access violation rather than a clean error.
// ============================================================================
#import "..\\..\\wrappers\\activex\\LumasPdfAX.tlb" no_namespace named_guids
#include <windows.h>
#include "axcommon.h"
#include <cstdio>
#include <string>
static const char* IN_FILE = "E:\\LUMASPDFSDK\\sample_multipage.pdf";
// cpfEnableTextSelection / stDefault / rtfDefault / ofDefault are all real
// enumerations in the type library, so they arrive with the #import -- unlike
// the table tf* flags, which are a Cardinal bitmask and have to be declared.
int main() {
ChdirToExe();
ComInit com;
if (!com.ok()) return 1;
int rc = 0;
try {
ILumasPDFPtr pdf;
if (FAILED(pdf.CreateInstance(L"LumasPdf.PDF"))) _com_issue_error(E_FAIL);
// NOT RaiseExceptions here. PsrFindText returning FALSE is how the
// search loop ENDS -- "no more matches" is the normal exit, not an
// error. With exceptions on, that false return throws before the loop
// can break, and the whole example dies with "engine call failed" after
// replacing everything successfully. The Delphi original uses an error
// callback, where a false return is just a false return.
pdf->RaiseExceptions = VARIANT_FALSE;
std::string outFile = OutFile();
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();
// The optional PVoid/record parameters take the InPtr path, whose Lock()
// REJECTS Empty/Null outright ("this parameter needs a packed numeric
// array"). There is therefore no way to express a nil pointer for them;
// the integer 0 is what the VBScript sibling passes, which Lock turns
// into a pointer to a single zero byte.
_variant_t nil((long)0);
long ctx = pdf->PsrCreateParserContext(ofDefault, nil);
_bstr_t searchText = B("PDF");
_bstr_t replaceText = B("XDF");
long pages = pdf->GetPageCount();
long replaced = 0;
for (long i = 1; i <= pages; ++i) {
// PsrParsePage's AValue is a TContent RECORD out-param (16 bytes on
// x64), not a scalar -- RecFromVar rejects anything else outright.
RecVar<TContent> content;
if (pdf->PsrParsePage(ctx, 0, nil, i, cpfEnableTextSelection, nil,
content.Addr())) {
// `last` seeds the search position; the first pass starts at nil.
RecVar<TTextSelection> sel;
bool haveLast = false;
for (;;) {
RecVar<TTextSelection> hit;
VARIANT_BOOL found = pdf->PsrFindText(
ctx, nil, stDefault, haveLast ? sel.Var() : nil,
searchText, (long)searchText.length(), hit.Addr());
if (!found) break;
pdf->PsrReplaceSelText(ctx, rtfDefault, hit.Addr(), replaceText,
(long)replaceText.length());
*sel.Ptr() = *hit.Ptr();
haveLast = true;
++replaced;
}
pdf->PsrWriteToPage(ctx, ofDefault, nil);
}
}
_variant_t ctxVar((long)ctx);
pdf->PsrDeleteParserContext(&ctxVar);
if (pdf->HaveOpenDoc())
if (!pdf->OpenOutputFileA(B(outFile))) return 1;
if (pdf->CloseFile())
std::printf("OK: %s (%ld replacement(s) over %ld page(s))\n",
outFile.c_str(), replaced, pages);
} catch (const _com_error& e) {
const _bstr_t d = e.Description();
std::fprintf(stderr, "%ws\n", d.length() ? (const wchar_t*)d : L"COM error");
rc = 1;
}
return rc;
}
This file is in the SDK at examples/activex_cpp/edit_text/edit_text.cpp.
The build fails if this page and that file ever differ.