colinardo
Goto Top

Powershell: Text anhand seiner Position aus PDF-Dokumenten auslesen

article-picture

back-to-topEinleitung


Möglichkeiten für das automatische Extrahieren von Text aus PDF-Dokumenten gibt es etliche (PDFtk, iTextSharp,...). Das gezielte Extrahieren der gewünschten Inhalte geschieht dann aber oftmals mittels Regular Expressions, das genügt auch in vielen Fällen, so lange sich der Text ausreichend von anderen Teilen des Dokumentes eindeutig abhebt. Oftmals lässt aber die Struktur des Dokumentes ein effizientes auslesen der Texte nicht mehr zu, und genau hier soll folgende Funktion einspringen, indem man mit Ihr Text anhand seiner absoluten Position innerhalb eines genau definierten Rechtecks auslesen kann. Nützlich ist das bspw. bei immer gleich aufgebauten Formularen bei denen aber keine "echten" PDF-Formularfelder mehr vorhanden sind weil das PDF mit der "flatten" Funktion behandelt wurde. Wer dagegen echte PDF-Formulare bei denen die Formularfelder noch im Original vorhanden sind auslesen möchte findet von mir bereits hier ein entsprechendes Skript.

back-to-topWas macht die Funktion


Das unten aufgeführte CMDLet extrahiert bereits vorhandenen maschinenlesbaren Text aus einem oder mehreren PDF-Dokumenten mittels seiner absoluten Positionen im Dokument. Anhand einer Hashtable definiert man die Koordinaten für Rechtecke innerhalb derer der Text ausgelesen werden soll. Als Ergebnis erhält man ein oder mehrere Objekte mit den Texten als separate Eigenschaften für eine objektorientierte Weiterverarbeitung. Für die Verarbeitung der PDF-Dokumente verwendet das Skript als Basis die iTextSharp-DLL welche automatisch von Nuget in das Skript- oder TEMP-Verzeichnis (bei direkter Ausführung ohne Skriptdatei) heruntergeladen wird sofern sie noch nicht vorhanden ist.

back-to-topPowershell CMDLet: Get-PDFTextFromLocation


function Get-PDFTextFromLocation {
    <#
    .Synopsis
       Extracts text from PDF documents
    .DESCRIPTION
       Extracts text from defined locations in PDF documents
    .LINK
       © @colinardo (https://administrator.de/tutorial/powershell-text-anhand-seiner-position-aus-pdf-dokumenten-auslesen-32779450015.html)
    .INPUTS
         System.IO.FileSystemInfo[] / You can pipe the output of Get-ChildItem to the function
    .OUTPUTS
         System.Management.Automation.PSCustomObject[]
    .EXAMPLE
        $locations = @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172    
                'Date' = 130,230,150,245    
            }
        }
        Get-ChildItem "E:\data" -File -Filter *.pdf | Get-PDFTextFromLocation -locations $locations  

       This example extracts 2 fields on page 1 from every PDF coming on the pipeline
    .NOTES
       Hashtable-Format for text location definition by page number:
        - first level of hashtable defines the page index from which to extract the field data
        - second level keys define a user defined unique names for the fields
        - the value in the second level defines the rectangle location as an  array with four items:
            - [LowerLeftX],[LowerLeftY],[UpperRightX],[UpperRightY])
            - coordinate origin is the non rotated lower left corner of the page.
        
        Example 1 (extracts 2 fields from page 1):
        ============
        @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172    
                'Date' = 130,230,150,245    
            }
        }

        Example 2 (extracts 2 fields from page 1, and 1 field from page 2):
        ============
        @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172    
                'Date' = 130,230,150,245    
            }
            2 = [ordered]@{
                'Sum' = 120,80,150,100    
            }
        }
        # ---------------------------------------------
        Hashtable-Format to select pages by regular expression (when the document contains multiple pages of the same format):
        - first level of hashtable defines a regular expression pattern wich selects the pages to be processes
        - second level keys define a user defined unique names for the fields
        - the value in the second level defines the rectangle location as an  array with four items:
            - [LowerLeftX],[LowerLeftY],[UpperRightX],[UpperRightY])
            - coordinate origin is the non rotated lower left corner of the page.
        
        Example 3 (extracts 2 fields from each page wich matches the regular expression pattern '(?ism)Invoice'):  
        ============
        @{
            '(?ism)Invoice' = [ordered]@{  
                'Invoice No.' = 30,123,203,172    
                'Date' = 130,230,150,245    
            }
        }

    #>
    param(
        # Filepath
        [Parameter(mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][Alias('Fullname')][string[]]$path,    
        # hashtable with definition of pages and locations
        [Parameter(mandatory=$true)][hashtable]$locations,
        # optional match page content by Regex instead of page number
        [Parameter(mandatory=$false)][switch]$UsePageSelectRegex,
        # output source fileinfo
        [Parameter(mandatory=$false)][switch]$passthru
    )

    begin{
        # function to load iTextSharp assembly
        function Load-iTextLibrary {
            if($psscriptroot -ne ''){    
                $localpath = join-path $psscriptroot 'itextsharp.dll'    
            }else{
                $localpath = join-path $env:TEMP 'itextsharp.dll'    
            }
            $zip = $null;$tmp = ''    
            $script:iTextDLL = $localpath
            try{
                if(!(Test-Path $localpath)){
                    Add-Type -A System.IO.Compression.FileSystem
                    $tmp = "$env:TEMP\$([IO.Path]::GetRandomFileName())"    
                    write-host "Downloading and extracting required 'iTextSharp.dll' ... " -F Green -NoNewline    

                    (New-Object System.Net.WebClient).DownloadFile('https://www.nuget.org/api/v2/package/iTextSharp/5.5.13.1', $tmp)    
                    $zip = [System.IO.Compression.ZipFile]::OpenRead($tmp)
                    $zip.Entries | ?{$_.Fullname -eq 'lib/itextsharp.dll'} | %{    
                        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_,$localpath)
                    }
                    write-host "OK" -F Green     
                }
                if (Get-Item $localpath -Stream zone.identifier -ea SilentlyContinue){
                    Unblock-File -Path $localpath
                }
                Add-Type -Path $localpath
            }catch{
                throw "Error: $($_.Exception.Message)"    
            }finally{
                if ($zip){$zip.Dispose()}
                if ($tmp -ne ''){del $tmp -Force -EA SilentlyContinue}    
            }
        }

        # load iText library
        Load-iTextLibrary

        # also read protected files
        [iTextSharp.text.pdf.PdfReader]::unethicalreading = $true

        if (!("iText.GetTextFromLocationStrategy" -as [type])){    
            # Create GetTextFromLocationStrategy class
            Add-Type -ReferencedAssemblies $script:iTextDLL -TypeDefinition '    
                using System.Text;
                using System.Collections;
                using System.Collections.Generic;
                using System.Collections.Specialized;
                using iTextSharp.text;
                using iTextSharp.text.pdf.parser;
                using iTextSharp.text.pdf;

                namespace iText {
                    public class GetTextFromLocationStrategy : iTextSharp.text.pdf.parser.ITextExtractionStrategy {
                        StringBuilder result = new StringBuilder();
                        OrderedDictionary resultfields = new OrderedDictionary();
                        Vector lastBaseline = new Vector(0, 0, 0);
                        OrderedDictionary rects;

                        public GetTextFromLocationStrategy(OrderedDictionary locations) {
                            rects = locations;
                        }

                        public void RenderText(TextRenderInfo renderinfo) {
                            Vector curBaseline = renderinfo.GetBaseline().GetStartPoint();
                            Vector topRight = renderinfo.GetAscentLine().GetStartPoint();
                            iTextSharp.text.Rectangle rect = new iTextSharp.text.Rectangle(curBaseline[Vector.I1], curBaseline[Vector.I2], topRight[Vector.I1], topRight[Vector.I2]);
                            foreach (DictionaryEntry field in rects) {
                                Rectangle fieldRect = (Rectangle)field.Value;
                                if (!resultfields.Contains(field.Key)) {
                                    resultfields.Add(field.Key, new StringBuilder(""));    
                                }
                                if (rect.Left >= fieldRect.Left && rect.Top <= fieldRect.Top && rect.Bottom >= fieldRect.Bottom && rect.Right <= fieldRect.Right) {
                                    if (lastBaseline[Vector.I2] > curBaseline[Vector.I2] && ((StringBuilder)resultfields[field.Key]).ToString() != "") {    
                                        ((StringBuilder)resultfields[field.Key]).Append("\r\n");  
                                        result.Append("\r\n");    
                                    }
                                    string txt = renderinfo.GetText();
                                    ((StringBuilder)resultfields[field.Key]).Append(txt);
                                    result.Append(txt);
                                }
                            }
                            lastBaseline = curBaseline;
                        }

                        public string GetResultantText() {
                            return result.ToString();
                        }
                        public OrderedDictionary GetResultTable() {
                            OrderedDictionary dic = new OrderedDictionary();
                            foreach (DictionaryEntry field in resultfields) {
                                dic.Add(field.Key, field.Value.ToString());
                            }
                            return dic;
                        }
                        public void BeginTextBlock() { }
                        public void EndTextBlock() { }
                        public void RenderImage(ImageRenderInfo renderinfo) { }
                    }
                }
            '    
        }
        
        
        # function to convert mm to points
        function M2P($mm){$mm * 2.83465}

        # convert user supplied location values to rectangles with point values
        foreach($page in $locations.Keys){
            [string[]]$locations.$page.Keys | %{$locations.$page.$_ = [iTextSharp.text.Rectangle]::new((M2P $locations.$page.$_[0]),(M2P $locations.$page.$_[1]),(M2P $locations.$page.$_[2]),(M2P $locations.$page.$_[3]))}
        }
    }

    process{
        # Process files
        foreach($file in $path) {
            $reader = $null
            try {
                write-verbose "Processing file '$($file)' ..."    
                # create pdf reader object
                $reader = New-Object iTextSharp.text.pdf.PdfReader $file
                # parse pages defined
                
                if ($UsePageSelectRegex.IsPresent){
                    $pages = 1..($reader.NumberOfPages) | ?{[iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader,$_) -match $locations.Keys[0]}
                    $data = foreach($page in $pages){
                         # create instance from custom Text-Extraction-Strategy
                         $strategy = New-Object iText.GetTextFromLocationStrategy ([System.Collections.Specialized.OrderedDictionary]($locations.([string]$locations.Keys[0])))
                         # execute extraction
                         [void][iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader,$page,$strategy)
                         # output result object
                         [pscustomobject]$strategy.GetResultTable()
                    }
                }else{
                    $data = [ordered]@{}
                    $pages = $locations.Keys | ?{$_ -in (1..$reader.NumberOfPages)}
                    foreach($page in $pages){
                         # create instance from custom Text-Extraction-Strategy
                         $strategy = New-Object iText.GetTextFromLocationStrategy ([System.Collections.Specialized.OrderedDictionary]$locations.$page)
                         # execute extraction
                         [void][iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader,$page,$strategy)
                         # append to result object
                         $data += $strategy.GetResultTable()
                    }
                    if ($passthru.IsPresent){
                        # append file info
                        $data.FileInfo = Get-Item $file
                    }
                    $data = [pscustomobject]$data
                }
                # output data
                $data
            }catch{
                write-host $_.Exception.Message -F Red
            }finally{
                if($reader){$reader.Dispose()}
            }
        }
    }
}

back-to-topPraktische Verwendung der Funktion


Gegeben sei als Beispiel das folgende Muster-Dokument aus dem wir die zwei umrandeten Textfelder extrahieren werden:

screenshot

back-to-topWie definiert man die Positionen?


Für die Definition der Positionen dient eine Hashtable die ein bestimmtes Format aufweisen muss. Auf der ersten Ebene der Hashtable wird die Seitenzahl als Key angegeben auf die sich die folgenden Positionen beziehen. Der Wert des Keys ist dann eine weitere Hashtable die als Keys eindeutige Bezeichner haben und als Wert ein Array aus 4 Zahlen hat, welches ein Rechteck über die Position des Textes legt der darin extrahiert werden soll. Das Rechteck bzw. das Array ist wie folgt definiert:
[LOWER-LEFT-X] , [LOWER-LEFT-Y] , [UPPER-RIGHT-X] , [UPPER-RIGHT-Y]
  • Der Koordinaten-Ursprung liegt dabei immer in der linken unteren Ecke der jeweiligen Seite.
  • Als Einheit kommen Millimeter (mm) zum Einsatz

Das ganze sieht dann in der Praxis folgendermaßen aus.

Hier werden zwei Felder auf Seite 1 ausgelesen:
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}

Es können sowohl mehrere Seiten als auch pro Seite mehrere Positionen in der Hashtable definiert werden. Die Namen der Felder können beliebig gewählt werden, müssen aber alle eindeutig sein damit später eine eindeutige Zuordnung möglich ist.

Hier werden zwei Felder auf Seite 1 und 1 Feld auf Seite 3 ausgelesen.
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
    3 = [ordered]@{
        'Bankleitzahl' = 10,20,40,30  
    }
}

Eine weitere Möglichkeit der Funktion bietet der Switch-Parameter -UsePageSelectRegex. Dieser ist für das Szenario gedacht bei dem man in einem Dokument mehrere gleich aufgebaute Seiten vorliegen. Er verändert das Verhalten der Funktion in der Art, dass man nun in der Hashtable auf der ersten Ebene als Key statt fester Seitennummern einen Regular Expression Pattern hinterlegt, welcher die Seiten anhand eines Musters auswählt welches auf den gewünschten Seiten immer im Text auftaucht. Die definierten Positionen werden dann aus jeder zutreffende Seite des PDFs ausgelesen.

Anwendungsbeispiel bei Verwendung des Parameters -UsePageSelectRegex :

Hier werden zwei Felder für jede Seite des PDFs ausgelesen auf welcher das Regular Expression Pattern 'Abschlagsänderung' zutrifft
$locations = @{
    'Abschlagsänderung' = [ordered]@{  
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
Zu Regular Expressions allgemein ist etwa das Regular Expressions Tutorial als Einstieg empfehlenswert.

Wichtig ist das die Koordinaten-Angaben mit ausreichend Abstand zu dem Text umgebenden Rahmen definiert werden, ansonsten wird der Text darin nicht extrahiert! Erfasst werden nämlich nur Blöcke deren Koordinaten wirklich vollständig innerhalb des vom Benutzer definierten Rechtecks liegen. Sollte also ein Text einmal nicht vollständig oder überhaupt nicht ausgelesen werden, ist meist das Rechteck zu klein gewählt worden, es hilft dann die Koordinaten des Rechtecks zu vergrößern, dabei macht es nichts aus wenn das Rechteck zum Teil anderen Text ansatzweise umfasst so lange dessen umgebender Kasten nicht durch das Rechteck mit erfasst werden.

Für die Bestimmung der exakten Positionen können als digitale Hilfsmittel in PDF-Readern oder Editoren etwa vorhandene Mess-Tools bzw. Lineal-Funktionen benutzt werden. Ein extra Ausdrucken des Dokumentes und Messen mittels Lineal ist somit überflüssig und sollte auch aus ökologischen Gründen vermieden werden wann immer es geht 🌲.

Es folgen nun ein paar praktische Beispiele für die Verwendung des CMDLets.

back-to-topCode-Beispiel 1 - Auslesen eines einzelnen Dokumentes


# Positionen der zu extrahierenden Textfelder definieren
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
# Ein einzelnes PDF-Dokumente auslesen und das Ergebnis auf der Konsole als Liste ausgeben
Get-PDFTextFromLocation -Path "E:\dokument.pdf" -locations $locations | format-list *  

Ergebnis für das o.g. Musterdokument

screenshot

back-to-topCode-Beispiel 2 - Export der Daten mehrerer gleich aufgebauter Dokumente in eine CSV-Datei


# Positionen der zu extrahierenden Textfelder definieren
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
# Alle gleich aufgebauten PDF-Dokumente eines Ordners verarbeiten und das Ergebnis in eine CSV-Datei ausgeben
Get-ChildItem "E:\quelldaten" -File -Filter *.pdf | Get-PDFTextFromLocation -locations $locations | export-csv "E:\output.csv" -Delimiter ";" -NoTypeInformation -Encoding UTF8 -Force  

Ergebnis der CSV-Datei für zwei ähnliche Musterdokumente

"Geschäftspartnernummer";"Empfänger"
"1222334455";"Herrn
Max Mustermann
Musterstr. 99
99999 Musterhausen"
"22342344";"Frau
Monika Musterfrau
Musterweg. 1
88888 Musterdorf"


back-to-topCode-Beispiel 3 - Verwendung des Parameters -UsePageSelectRegex für den Export mehrfach gleich aufgebauter Seiten einer PDF-Datei in eine CSV-Datei


# Regular Expression Pattern der auszuwählenden Seiten und Positionen der zu extrahierenden Textfelder definieren
$locations = @{
    '(?i)Abschlagsänderung' = [ordered]@{  
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
# PDF-Dokument auslesen und das Ergebnis in eine CSV-Datei ausgeben
Get-PDFTextFromLocation -Path "E:\dokument.pdf" -locations $locations -UsePageSelectRegex | export-csv "E:\output.csv" -Delimiter ";" -NoTypeInformation -Encoding UTF8 -Force  

Eventuell ist das dem ein oder anderen von euch ein nützliches Hilfsmittel.

Wie immer alle Angaben ohne Gewähr auf Leib und Leben face-smile.

Content-Key: 32779450015

Url: https://administrator.de/contentid/32779450015

Printed on: April 27, 2024 at 07:04 o'clock

Member: Kraemer
Kraemer Sep 19, 2023 at 09:45:08 (UTC)
Goto Top
Moin,

zwar noch nicht getestet aber viel Lob von meiner Seite für diese tolle Idee.
Member: colinardo
colinardo Sep 19, 2023 updated at 10:11:26 (UTC)
Goto Top
Danke dir. Dachte das taucht hier ja immer wieder mal auf, und gefunden habe ich, speziell das, bisher bis auf Tabula(für Tabellen) nur in kommerziell verfügbaren Programmen.
Member: Crusher79
Crusher79 Sep 19, 2023 updated at 14:12:39 (UTC)
Goto Top
Hallo,

danke für diesen Ansatz hab vor längeren auch was gebaut. Aber nicht ganz so schön.

Hatte mir als Basis pdftotext ausgesucht- https://www.xpdfreader.com/pdftotext-man.html

Hintergrund war, dass man hier schon Layout übernehmen oder unterdrüken kann. Mit Layout wäre es so, wie wir es von früher kennen: Fixe Abstände zum linken Rand. Zumindest so einigermaßen.

Die Ankerpunkte kann man via Regex auch schön finden. Hier ging es darum, wenn ein Sachbearbeiter nichht gefüllt war, dass dann trotzdem das richtige Feld gefunden wurde.

Muster:
A U F T R A G S B E S T Ä T I G U N G
Ihr Ansprechpartner: Kunden-Nr. Auf.-Nr. Datum Seite
Max Mustermann 22292 864987 /V 11.02.21 1

Die Belge stammen aus Crystal Reports. Man kan natürlich auch damit einen "hidden" String - also weiße Schrift auf weißen Grund o.ä. - nehmen und alles in eine Art Token packen.

Das war damals mein Ansatz. Lief aber relativ robust.

@colinardo ansonsten schwebte mir vor Dokument und Text-Stream erst zu slicen und mit tesseract auszuelesen. Weiter unten ein Bsp. dazu für einen Barcode. Zwar kein OCR, aber so die Richtung.

An pdftotext wird wohl nicht wirklich viel weiterentwickelt. Schaue mir daher dein Script mal genauer an. Mir fällt schon dass ein oder andere Projekt ein.

Wir haben ja X-Rechung, ZUGFeRD etc. aber ohne XML Anteil ist so ein PDF dumm wie Stück Brot. Ich mag immer noch den Ansatz mit der Zerlegung.

Die Trefferquote war recht hoch. Ansonsten hab ich einfach "MANUELLE ERFASSUNG" o.ä. in den Dateinamen geschrieben. Dann konnte jeder einfach nacharbeiten. Kam aber selten vor.

Mit Ausschnitten wie beim Barcode lässt sich das ganze noch verfeinern. Sieht man ja an deinen Beispiel gut. Abbyy oder Omnipage machen ja nichts anderes. Nur das man hier die Masken über die GUI definieren kann.

Nimmt man das noch via Script vor, steht eine solche Lösung den kommerziellen kaum in was nach.


# Snippet
$pdftotext = "C:\ps\pdftotext.exe"  

Function Global:FindBelegNr($PDFToTextFile)
    {
		#PDF in durchsuchbaren Text umwandeln. simple2 und clip zum "normalisieren" 
        Start-Process -NoNewWindow -Wait $pdftotext -ArgumentList "-simple2", "-clip", "-f 1", "-l 1", $PDFToTextFile, $TempTextFile  

        $texttest = Get-Content -Path $TempTextFile -Raw

        #$patternBelegRe = "Rech.-Nr. " 
        #$patternBelegAb = "Auf.-Nr. " 
        [string] $DokBelegAbRe =""  
        [string] $DokBelegAn =""  
        [string] $DokBelegLs =""  
                
        #Angebot Nr in einer Zeile
        $regex_An = "(ANGEBOT )(Kunden-Nr\. )(Angebot-Nr\. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"                  
        #Auf ODER Rech übermehrere Zeilen
        $regex_AbRe = "(.*)\r\n(Ihr Ansprechpartner: )(Kunden-Nr\. )((Auf|Rech)\.-Nr\. )(Datum )(Seite)\r\n(.*)\s([\d]+)\s([\d]+)"  
        #Nur Lieferschein
        $regex_Ls = "(LIEFERSCHEIN)\r\n(Lieferschein-Nr\. )(Lieferanten-Nr\. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"  
        #Nur Bestellschein
        $regex_Bs = "(.*)(Lieferanten-Nr. )(Bestell-Nr. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"  
                
        $texttest | select-string -Pattern $regex_AbRe -AllMatches | % { $DokBelegAbRe = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[10].Value }  
        $texttest | select-string -Pattern $regex_An -AllMatches | % { $DokBelegAn = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[7].Value }  
        $texttest | select-string -Pattern $regex_Ls -AllMatches | % { $DokBelegLs = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[6].Value }  
        $texttest | select-string -Pattern $regex_Bs -AllMatches | % { $DokBelegBs = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[7].Value }  

        IF ($DokBelegAbRe.length -gt 0) { $Script:Belegnr = $DokBelegAbRe }
        IF ($DokBelegAn.length -gt 0) { $Script:Belegnr = $DokBelegAn }
        IF ($DokBelegLs.length -gt 0) { $Script:Belegnr = $DokBelegLs }
        IF ($DokBelegBs.length -gt 0) { $Script:Belegnr = $DokBelegBs }        
    }


Barcode ausschneiden und aulsesen:
using namespace System.Drawing;
Add-Type -AssemblyName System.Drawing;

[void] [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")  

$testBild = "C:\temp\code_ls4.png-1.png"  
$src=[System.Drawing.Image]::FromFile($testBild)

$rect = New-Object System.Drawing.Rectangle(740,1500,400,250) # top, left, width, height of slice
$slice = $src.Clone($rect, $src.PixelFormat);
#$slice.Save("c:\temp\test_slice.png", "png"); 

$src = $slice;


[void] [System.Reflection.Assembly]::LoadFrom("c:\temp\BarcodeImaging.dll");      
$barcodes = @{}
[BarcodeImaging]::FullScanPage([ref] $barcodes, $src, 150)

$barcodes
Member: Crusher79
Crusher79 Sep 19, 2023 at 14:29:12 (UTC)
Goto Top
Zitat von @colinardo:

Danke dir. Dachte das taucht hier ja immer wieder mal auf, und gefunden habe ich, speziell das, bisher bis auf Tabula(für Tabellen) nur in kommerziell verfügbaren Programmen.

Gebe dir vollkommen recht! Andere nennen es DocuXXX und verlangen 60.000 bis 120.000 Euro. Workflows und DSGVO mal außen vor gelassen braucht man meist doch im Alltag nur ein paar Infos.

Wenn man so die Datei umbennennt, reicht dass bei manchen DMS schon für einen Import. Die Möglichkeiten die sich damit auftuen sind vielflältig.

Fehlt der Text hilft tesseract weiter. Auch wenn es nur um Ausschnitte - s.o. geht.

Aus eigener Praxis kann ich auf jedenfall bestätigen, dass @colinardo Lösung sehr robust ist! Arbeitet es doch teils so, wie Menschen es auch händisch umsetzen würden.

Passt es mal nicht, kann man immer noch die Suchmuster verfeienern. Auf fehlen von Nummern reagieren etc. Aber sowas im Detail durch zu spielen sollte jeden klar sein. Dann hat man ein sehr robustes System!
Member: TomTomBon
TomTomBon Sep 19, 2023 at 14:41:41 (UTC)
Goto Top
Ich kenne es auch aus kommerziellen Systemen als ZoneOCR.

Sehr sehr ähnlich aufgemacht ist es bei ScannerVision.
Aber halt closed Source und bis auf das Thema der Position des Bereiches ist dort nicht viel zu machen.
Und natürlich die Definition der dort erhobenen Daten und das ein OCR gleich mitgemacht wird...

Aber Colinardo hat dies vorgemacht wie man es selbst bauen kann.
Genial !
Member: MatthiasBP
MatthiasBP Sep 20, 2023 at 09:26:46 (UTC)
Goto Top
Danke, mir hat es jmd in einen Beitrag eingehaengt Link. Ich lerne gerade ueber Funktionen, habe es in ein Modul kopiert und erhalte einen Fehler, es will einen Namen fuer ein Makro haben. Habe gestern gelernt, dass die Funktion eine Klammer () am Ende haben soll. Liegt es daran?
Eine Frage geht es ueberhaupt fuer meine Zwecke?
Habe knapp 500 pdf's mit bis zu 90 oder mehr Seiten. Bestellungen koennen bei einem Kunden bis zu vier auf einer Seite sein, bei zwei Kunden entweder 1/1 oder 1/2 oder 2/1 Bestellungen, also beide je eine oder der erste zwei und der zweite eine und umgekehrt. Die Felder koennte ich jeweils messen und eingeben mit allen Kombinationen, aber nicht fuer jede Seite oder verstehe ich es falsch?
Member: Crusher79
Crusher79 Sep 20, 2023 at 09:43:19 (UTC)
Goto Top
Zitat von @MatthiasBP:

... verstehe ich es falsch?

Ich hab hier eig. auch zu viel geschrieben. Rubrik: Anleitungen ....

Lass es doch drüben diskutieren! Hab auch viel Sauereien gemacht. Nur das sprengt hier den Rahmen.
Member: colinardo
colinardo Sep 20, 2023 updated at 10:36:42 (UTC)
Goto Top
Servus @MatthiasBP.
habe es in ein Modul kopiert und erhalte einen Fehler, es will einen Namen fuer ein Makro haben.
Ähh nee, das ist Powershell kein VBA!
@MatthiasBP habe dir in deinem Thread dazu geantwortet => Pdf in Excel Bestellnummern auslesen, bitte auch nur dort diesbezüglich antworten. Merci.

Grüße Uwe
Member: colinardo
colinardo Oct 24, 2023 updated at 11:02:37 (UTC)
Goto Top
Zur Info:
Die Funktion wurde aufgrund von einigen Useranfragen um den optionalen Switch-Parameter -UsePageSelectRegex erweitert. Dieser ist für das Szenario gedacht, bei dem in einem Dokument mehrere gleich aufgebaute Seiten vorliegen.
Er verändert das Verhalten der Funktion in der Art, dass man nun in der Hashtable auf der ersten Ebene als Key statt feste Seitennummern nun einen Regular Expression Pattern hinterlegt, welcher die Seiten anhand eines Musters auswählt welches auf den gewünschten Seiten immer im Text auftaucht. Die definierten Positionen werden dann auf jeder zutreffenden Seite des PDFs ausgelesen.

Grüße Uwe