Skip to content

Commit 0a13e99

Browse files
Add PDF to image and AI extraction support
Introduces ConvertTo-AITImage for converting PDFs to images, with automatic pdf2img installation and AI vision extraction. Updates README with usage examples, adds tests and test data for PDF extraction, and refactors prompt/context handling with new private helper functions. Bumps module version to 1.0.20.
1 parent 9b70dfe commit 0a13e99

25 files changed

+3563
-1655
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ publish.ps1
44
.vscode/settings.json
55
/.claude
66
nul
7+
Tests/pdf/immunization_page_001.png

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,64 @@ This demonstrates what agentic CLIs do well: read complex requirements, maintain
299299

300300
## Advanced Usage
301301

302+
### PDF to Structured Data Extraction
303+
304+
Convert PDFs to images and extract structured data using AI vision:
305+
306+
```powershell
307+
# Convert PDF to images and extract data as JSON
308+
$params = @{
309+
Tool = "Claude"
310+
Prompt = "Extract pet immunization data from this image"
311+
Context = "./Tests/pdf/immunization.json"
312+
}
313+
314+
Get-ChildItem ./Tests/pdf/immunization.pdf | ConvertTo-AITImage | Invoke-AITool @params
315+
```
316+
317+
`ConvertTo-AITImage` automatically installs [pdf2img](https://github.com/potatoqualitee/pdf2img) on first use and converts PDFs to PNG images optimized for AI vision models. When you pass a single `.json` file as context, aitools automatically instructs the AI to return raw JSON (no markdown fences) for direct parsing with `ConvertFrom-Json`.
318+
319+
**Free Tier Example: Extract and Write to SQL Server**
320+
321+
Using Copilot's free tier with GPT-5-mini, you can extract structured data and write directly to a database:
322+
323+
```powershell
324+
$params = @{
325+
Tool = "Copilot"
326+
Model = "gpt-5-mini"
327+
Raw = $true
328+
Prompt = "Extract data from this image as JSON matching the context schema."
329+
Context = "./Tests/pdf/immunization.json"
330+
}
331+
332+
Get-ChildItem ./Tests/pdf/immunization.pdf |
333+
ConvertTo-AITImage |
334+
Invoke-AITool @params |
335+
ConvertFrom-Json |
336+
Select-Object -ExpandProperty vaccinations |
337+
Write-DbaDataTable -SqlInstance localhost -Database tempdb -Table vaccinations -AutoCreateTable
338+
```
339+
340+
> **Tip:** Free tier models can be inconsistent. For reliable results at minimal cost, use Claude Haiku 4.5 (`claude-haiku-4-5`) at $0.33 per 1K calls - it's remarkably capable for structured extraction. GPT-5-mini is the best free option; avoid GPT-4.1 for this task.
341+
342+
If the model returns extra data, filter the JSON objects:
343+
344+
```powershell
345+
# Filter to only records containing "immunization"
346+
... | ConvertFrom-Json | Where-Object { $PSItem -match "immunization" }
347+
```
348+
349+
```powershell
350+
# Pipeline-friendly: returns FileInfo objects
351+
$images = Get-ChildItem *.pdf | ConvertTo-AITImage
352+
353+
# Adjust DPI for quality vs file size tradeoff
354+
Get-ChildItem document.pdf | ConvertTo-AITImage -DPI 300
355+
356+
# Mix PDFs with other files using -PassThru
357+
Get-ChildItem ./docs | ConvertTo-AITImage -PassThru | Invoke-AITool -Prompt "Analyze these files"
358+
```
359+
302360
### Working with Images
303361

304362
**Image Analysis and Code Generation (Codex, Claude, Gemini)**

Tests/aitools.Tests.ps1

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ Describe 'AITools Module Integration Tests' {
2929
'Update-AITool',
3030
'Uninstall-AITool',
3131
'Update-PesterTest',
32-
'Get-AITPrompt'
32+
'Get-AITPrompt',
33+
'ConvertTo-AITImage'
3334
)
3435

3536
foreach ($command in $commands) {
@@ -257,6 +258,78 @@ function Get-TestData {
257258
}
258259
}
259260

261+
Context 'ConvertTo-AITImage' {
262+
BeforeAll {
263+
$script:pdfPath = Join-Path $PSScriptRoot 'pdf' 'immunization.pdf'
264+
$script:jsonSchemaPath = Join-Path $PSScriptRoot 'pdf' 'immunization.json'
265+
}
266+
267+
It 'Should have test PDF file' {
268+
Test-Path $script:pdfPath | Should -Be $true
269+
}
270+
271+
It 'Should have JSON schema file' {
272+
Test-Path $script:jsonSchemaPath | Should -Be $true
273+
}
274+
275+
It 'Should convert PDF to PNG images' {
276+
$images = Get-ChildItem $script:pdfPath | ConvertTo-AITImage
277+
$images | Should -Not -BeNullOrEmpty
278+
$images | ForEach-Object {
279+
$_.Extension | Should -Be '.png'
280+
$_.Exists | Should -Be $true
281+
}
282+
Write-Host "Generated $($images.Count) image(s) from PDF"
283+
284+
# Store for cleanup
285+
$script:generatedImages = $images
286+
}
287+
288+
It 'Should return FileInfo objects' {
289+
$images = Get-ChildItem $script:pdfPath | ConvertTo-AITImage
290+
$images | ForEach-Object {
291+
$_ | Should -BeOfType [System.IO.FileInfo]
292+
}
293+
}
294+
295+
It 'Should extract immunization data from PDF using AI vision' {
296+
$images = Get-ChildItem $script:pdfPath | ConvertTo-AITImage
297+
298+
$prompt = @"
299+
Extract the pet immunization data from this veterinary record image.
300+
Return ONLY valid JSON matching the schema structure provided in the context file.
301+
Parse all vaccination entries including vaccine names, dates administered, and veterinarian names.
302+
"@
303+
304+
$result = $images | Invoke-AITool -Tool Claude -Prompt $prompt -Context $script:jsonSchemaPath
305+
306+
$result | Should -Not -BeNullOrEmpty
307+
$result.Success | Should -Be $true
308+
$result.Result | Should -Not -BeNullOrEmpty
309+
310+
# Try to parse the result as JSON to verify it's valid
311+
try {
312+
# Extract JSON from the result (may be wrapped in markdown code blocks)
313+
$jsonText = $result.Result -replace '(?s)```json\s*', '' -replace '(?s)```\s*$', ''
314+
$jsonResult = $jsonText | ConvertFrom-Json
315+
Write-Host "Parsed JSON with pet_name: $($jsonResult.pet_name)"
316+
} catch {
317+
Write-Host "Note: Result may not be pure JSON: $($result.Result.Substring(0, [Math]::Min(200, $result.Result.Length)))"
318+
}
319+
320+
Write-Host "AI extraction completed successfully"
321+
}
322+
323+
AfterAll {
324+
# Clean up generated images
325+
if ($script:generatedImages) {
326+
$script:generatedImages | Remove-Item -Force -ErrorAction SilentlyContinue
327+
}
328+
# Also clean up any leftover images from the PDF directory
329+
$pdfDir = Split-Path $script:pdfPath -Parent
330+
Get-ChildItem -Path $pdfDir -Filter '*.png' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
331+
}
332+
}
260333
}
261334

262335
AfterAll {

Tests/pdf/immunization.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "pet",
3+
"strict": true,
4+
"schema": {
5+
"type": "object",
6+
"properties": {
7+
"pet_name": {
8+
"type": "string",
9+
"description": "What is the name of the pet?"
10+
},
11+
"owner_name": {
12+
"type": "string",
13+
"description": "What is the name of the pet's owner?"
14+
},
15+
"pet_breed": {
16+
"type": "string",
17+
"description": "What is the breed of the pet?"
18+
},
19+
"vaccinations": {
20+
"type": "array",
21+
"items": {
22+
"type": "object",
23+
"properties": {
24+
"vaccine_name": {
25+
"type": "string",
26+
"description": "What is the name of the vaccine administered?"
27+
},
28+
"date_administered_1": {
29+
"type": "string",
30+
"description": "When was the first dose of the vaccine administered?"
31+
},
32+
"date_administered_2": {
33+
"type": "string",
34+
"description": "When was the second dose of the vaccine administered?"
35+
},
36+
"date_administered_3": {
37+
"type": "string",
38+
"description": "When was the third dose of the vaccine administered?"
39+
},
40+
"veterinarian": {
41+
"type": "string",
42+
"description": "Who is the veterinarian that administered the first dose?"
43+
}
44+
},
45+
"required": [
46+
"vaccine_name",
47+
"date_administered_1",
48+
"date_administered_2",
49+
"date_administered_3",
50+
"veterinarian"
51+
],
52+
"additionalProperties": false
53+
},
54+
"description": "What vaccinations were given to the pet, including the vaccine names, administration dates, and veterinarian names?"
55+
}
56+
},
57+
"required": [
58+
"pet_name",
59+
"owner_name",
60+
"pet_breed",
61+
"vaccinations"
62+
],
63+
"additionalProperties": false
64+
}
65+
}

Tests/pdf/immunization.pdf

345 KB
Binary file not shown.

aitools.psd1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
RootModule = 'aitools.psm1'
1010

1111
# Version number of this module.
12-
ModuleVersion = '1.0.15'
12+
ModuleVersion = '1.0.20'
1313

1414
# ID used to uniquely identify this module
1515
GUID = 'c90f5001-c492-4fbe-8ab3-f03599951bd0'
@@ -18,7 +18,7 @@
1818
Author = 'Chrissy LeMaire'
1919

2020
# Copyright statement for this module
21-
Copyright = '2025 Chrissy LeMaire'
21+
Copyright = '2026 Chrissy LeMaire'
2222

2323
# Description of the functionality provided by this module
2424
Description = 'PowerShell wrapper for AI coding assistants. Batch process files using Claude Code, Aider, Gemini CLI, GitHub Copilot CLI, and Codex CLI with unified commands and pipeline support.'
@@ -38,6 +38,7 @@
3838
# Public functions
3939
FunctionsToExport = @(
4040
'Clear-AIToolConfig',
41+
'ConvertTo-AITImage',
4142
'Get-AITool',
4243
'Get-AIToolConfig',
4344
'Get-AITPrompt',

aitools.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ Register-PSFTeppArgumentCompleter -Command Get-AITPrompt -Parameter Name -Name P
286286

287287
$exportedFunctions = @(
288288
'Clear-AIToolConfig',
289+
'ConvertTo-AITImage',
289290
'Get-AITool',
290291
'Get-AIToolConfig',
291292
'Get-AITPrompt',

0 commit comments

Comments
 (0)