MCPcopy Create free account
hub / github.com/QodeXcli/QodeX / execute

Method execute

src/tools/filesystem/pdf-read.ts:245–320  ·  view source on GitHub ↗
(args: z.infer<typeof PdfReadArgs>, _ctx: ToolContext)

Source from the content-addressed store, hash-verified

243 argsSchema = PdfReadArgs;
244
245 async execute(args: z.infer<typeof PdfReadArgs>, _ctx: ToolContext): Promise<ToolResult> {
246 try {
247 const data = await fs.readFile(args.path);
248 // Sanity check: starts with %PDF-
249 if (data.subarray(0, 5).toString('latin1') !== '%PDF-') {
250 return { content: `[PDF_ERROR] Not a PDF: ${args.path}`, isError: true };
251 }
252 // Encryption: presence of /Encrypt in trailer dict
253 const tail = data.subarray(Math.max(0, data.length - 4096)).toString('latin1');
254 if (tail.includes('/Encrypt')) {
255 return {
256 content: `[PDF_ENCRYPTED] ${args.path} is encrypted. Decrypt it first with: qpdf --decrypt input.pdf output.pdf`,
257 isError: true,
258 };
259 }
260
261 const objects = parseObjects(data);
262 logger.debug(`pdf_read parsed ${objects.length} objects from ${args.path}`);
263
264 // Find pages — objects whose dict contains /Type /Page (not /Pages)
265 const pageObjects = objects.filter(o => /\/Type\s*\/Page\b(?!s)/.test(o.dict));
266 const totalPages = pageObjects.length;
267 const wanted = args.pages ? parsePageRange(args.pages, totalPages) : null;
268
269 // For each page, follow /Contents reference(s) and decode the stream
270 const pageText: string[] = [];
271 let imagesOnly = 0;
272 for (let i = 0; i < pageObjects.length; i++) {
273 const pageNum = i + 1;
274 if (wanted && !wanted.has(pageNum)) continue;
275 const pageObj = pageObjects[i]!;
276 // /Contents N 0 R OR /Contents [ N 0 R M 0 R ... ]
277 const contentsMatch = pageObj.dict.match(/\/Contents\s+(\d+)\s+\d+\s+R|\/Contents\s*\[([^\]]+)\]/);
278 if (!contentsMatch) { pageText.push(`\n[Page ${pageNum}: no contents]\n`); continue; }
279 const refs: number[] = [];
280 if (contentsMatch[1]) refs.push(parseInt(contentsMatch[1], 10));
281 else if (contentsMatch[2]) {
282 const refRe = /(\d+)\s+\d+\s+R/g;
283 let r: RegExpExecArray | null;
284 while ((r = refRe.exec(contentsMatch[2]))) refs.push(parseInt(r[1]!, 10));
285 }
286 const pageTextParts: string[] = [];
287 for (const ref of refs) {
288 const streamObj = objects.find(o => o.num === ref);
289 if (!streamObj || streamObj.streamStart === undefined || streamObj.streamEnd === undefined) continue;
290 const raw = data.subarray(streamObj.streamStart, streamObj.streamEnd);
291 const decoded = await decodeStream(raw, streamObj.dict);
292 if (!decoded) continue;
293 const streamStr = decoded.toString('latin1');
294 const text = extractTextFromStream(streamStr);
295 pageTextParts.push(text);
296 }
297 const joined = pageTextParts.join('\n').replace(/\n{3,}/g, '\n\n').trim();
298 if (joined.length < 20) imagesOnly++; // probably a scanned page
299 pageText.push(`\n--- Page ${pageNum} ---\n${joined || '[no extractable text — likely scanned image]'}\n`);
300 }
301
302 if (imagesOnly === pageObjects.length && pageObjects.length > 0) {

Callers

nothing calls this directly

Calls 7

parseObjectsFunction · 0.85
parsePageRangeFunction · 0.85
decodeStreamFunction · 0.85
extractTextFromStreamFunction · 0.85
debugMethod · 0.80
hasMethod · 0.45
pushMethod · 0.45

Tested by

no test coverage detected