EXCEL VBA AcrobatReaderやAdobeを使わずにPDFのページ数を取得する方法

dタイトルの通り、

EXCEL VBA AcrobatAdobeを使わずにPDFのページ数を取得したい

と思ったときにめちゃくちゃ調べまわったのでその結果をまとめる。

 

結論から言うと、イメージしていたやり方では

PDFのある特定のバージョンで特定の構造の場合は取得できるが、

取得できない場合があった。

 

以下調査結果まとめ

<調査結果まとめ>

VBAでPDFのPages【ページズ】コレクションのCount【カウント】プロパティにはアクセスできない(AdobeWindowsのプロパティはサポートしていないため)

 

◆PDFをバイナリ形式で開き、正規表現でページタグになる部分をカウントしたり、値を取得する。

 

 イメージはこれが一番近いが、取得できないPDFがある。PDFをテキストエディタで開くとわかるが、正規表現に該当しないパターンのファイルはページ数が取得できない。

 

  VBA Page Count | MrExcel Message Boardより

 https://www.mrexcel.com/board/threads/vba-page-count.347911/page-2

 

しかし、実行してみると14行目のページ総数が64となっているが実際は1ページのPDFファイルのため、実行結果にミスがあることがわかる。

そのため、確実に正しい結果が取得できる正規表現にしぼる必要があった。

 

実行画面

f:id:amumukun:20201017121811p:plain

 

フリーソフトでページ数を表示できるものがある

 ComPDFというツールで取得できるらしい。(試していない)

 ツールダウンロード:http://www.ne.jp/asahi/foresth/home/にアクセスしComPDFをダウンロード

 紹介していたページ:「バヤシのブログ」さん

 【時短テクニック】複数のPDFのページ数を開かずにカウントする方法

 https://salaryman-knowhow.com/pdf-page-count/

 

VBAでPDFファイルを開き、文書のプロパティを開き、ページ数を取得する

 これが現段階では最強ではなかろうか。

 64bit版は先頭のライブラリの定義を差し替えるといけると思います。

 とりあえず実行できるところまで試した。(DCをインストールしたくない)

 そのまま実行したらエラーが発生した。

 参照設定「UIAutomationClient」の設定が必要。

 なんとコメントに書いてあったが見逃していたw

 

 「初心者備忘録」さん

 Acrobat Reader DCを利用してPDFファイルのページ数を取得するVBAマクロ

 https://www.ka-net.org/blog/?p=11981

 

◆上記を踏まえ、おまけで私が実装した妥協PDF、エクセル、ワードのページ数取得マクロ

・一部PDFのページ数が取得できません。

・シート名”一覧”としているため、strListの定義を変えれば任意のシート名に変更できます。

・検索対象のフォルダはB2セルに入力するようにしているのでダイアログ表示にするなど変更してください。

・取得できないパターンはエラー文言をセルに出力するようにしています。

実行画面

 

f:id:amumukun:20201017053500p:plain

 

以下ソース


Option Explicit

'※64ビット版Officeで実行する場合は要コード変更
'UIAutomationClient(UIAutomationCore.dll)要参照

#If Win64 Then

Declare PtrSafe Function PostMessage Lib "user32" Alias "PostMessageA" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, ByVal lparam As LongPtr) As Long

Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)

#Else

Declare Function PostMessage Lib "user32" Alias "PostMessageA"(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lparam As Long) As Long

Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)

#End If
Private Const WM_COMMAND As Long = &H111


Dim rowCntOutput As Long
Dim cntListNum As Long

Const strList As String = "一覧"
Const rowTitle As Long = 5
Const colNo As Long = 2
Const colPath As Long = 3
Const colFile As Long = 4
Const colPage As Long = 5
Const rowList As Long = 200 'リスト最大200行

'リストクリア関数
Sub delFileList()
    Dim wsh As Worksheet
    Set wsh = ThisWorkbook.Worksheets(strList)
    
    'タイトル行の次からリスト最大行までのRangeをクリア
    wsh.Range( _
        wsh.Cells(rowTitle + 1, colNo), _
        wsh.Cells(rowList, colPage)).Clear
        
    'ワークシートオブジェクトは後処理不要という記述もあるが、明示的にクリアする。
    Set wsh = Nothing
End Sub

'ファイルリスト取得メイン処理
Sub FileList()
    Dim listwsh As Worksheet
    Dim strPath As String
    
    Set listwsh = ThisWorkbook.Worksheets(strList)
    
    strPath = listwsh.Cells(2, 2).Value
    listwsh.Cells(rowTitle, colNo).Value = "No"
    listwsh.Cells(rowTitle, colPath).Value = "パス"
    listwsh.Cells(rowTitle, colFile).Value = "ファイル名"
    listwsh.Cells(rowTitle, colPage).Value = "ページ総数"
    
    Call delFileList
    
    '出力行初期化、タイトル行の次から出力
    rowCntOutput = rowTitle + 1
    'ファイルリストNo.を初期化
    cntListNum = 1
    
    Call getFileList(strPath)
    listwsh.Select
    Set listwsh = Nothing
End Sub

'ファイルリスト取得処理
Sub getFileList(searchpath As String)

    Dim listwsh As Worksheet
    Dim fso As Object
    Dim objFiles As Object
    Dim objFolders As Object
    Dim separateNum As Long
    
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual
    Application.EnableEvents = False

    Set fso = CreateObject("Scripting.FileSystemObject")
    Set listwsh = ThisWorkbook.Worksheets(strList)

    For Each objFolders In fso.GetFolder(searchpath).subfolders
        Call getFileList(objFolders.Path)
    Next
    
    For Each objFiles In fso.GetFolder(searchpath).Files
        separateNum = InStrRev(objFiles.Path, "\")
       
        If (objFiles.Attributes And 4) > 0 Or _
           (objFiles.Attributes And 2) > 0 Then
            'Systemファイルおよび隠しファイルは処理しない
        Else
            listwsh.Cells(rowCntOutput, colNo).Value = cntListNum
            listwsh.Cells(rowCntOutput, colPath).Value = Left(objFiles.Path, separateNum - 1)
            listwsh.Cells(rowCntOutput, colFile).Value = Right(objFiles.Path, Len(objFiles.Path) - separateNum)
            
            Call getFilePage(objFiles.ShortPath)
            rowCntOutput = rowCntOutput + 1
            cntListNum = cntListNum + 1
        End If
    Next
       

    'リスト範囲に枠線
    listwsh.Range( _
        listwsh.Cells(rowTitle, colNo), _
        listwsh.Cells(rowCntOutput - 1, colPage)).Borders.LineStyle = True
    'リストのタイトル行に色付け
    listwsh.Range( _
        listwsh.Cells(rowTitle, colNo), _
        listwsh.Cells(rowTitle, colPage)).Interior.ColorIndex = 35
    
    '後処理
    Set objFiles = Nothing
    Set objFolders = Nothing
    Set fso = Nothing
    Set listwsh = Nothing

    Application.EnableEvents = True
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
    
End Sub

Sub getFilePage(strFile As String)
    Dim DotPos As Long
    Dim PageNum As Variant
    Dim strType As String
    
    Dim yenPos As Long
    Dim strfname As String
    
    yenPos = InStrRev(strFile, "\")
    If yenPos > 0 Then
        strfname = LCase(Mid(strFile, yenPos + 1))
    End If
    
    PageNum = 0
    DotPos = InStrRev(strFile, ".")
    
    If DotPos > 0 Then
        strType = LCase(Mid(strFile, DotPos + 1))
        If strType = "xlsx" Or _
           strType = "xls" Or _
           strType = "xlsm" Then
            PageNum = getExcelPageNum(strFile)
        ElseIf strType = "docx" Or strType = "doc" Then
            PageNum = getWordPageNum(strFile)
        ElseIf strType = "pdf" Then
            
            'ここを切り替えると他の方のコードと差し替えることが可能
            'PageNum = getPdfPageNum(strFile)
            PageNum = GetPageNum(strFile)
            '「UIAutomationClient」の参照設定が必要
            'PageNum = getPdfPageNumProperty(strFile)
        Else
            'Excel、Word、PDFファイル以外
            PageNum = 0
        End If
        ThisWorkbook.Worksheets(strList).Cells(rowCntOutput, colPage).Value = PageNum
    End If
    
End Sub

Private Function getExcelPageNum(strFile As String) As Variant

    Dim wbk As Workbook
    Dim workwsh As Worksheet
    Dim retVal As Variant
    Dim 表示シート数 As Long
    
    'ファイル有無をチェック
    Dim chkstr As String
    chkstr = Dir(strFile)
    If chkstr = "" Then
        'ファイルなし
        getExcelPageNum = "ファイルなし"
        Exit Function
    End If
    
    'ファイルを開いているかチェック
    Dim chkwsh As worksbook
    For Each chkwsh In Workbooks
        If chkwsh.Name = chkstr Then
            '同名のファイルを開いている
            Exit For
        Else
            Set chkwsh = Nothing
        End If
    Next
    
    If chkwsh Is Nothing Then
        'ファイルを読み取り専用、通知なし、リンク更新なし で開く
        Set wbk = Workbooks.Open(Filename:=strFile, notify:=False, UpdateLinks:=0, ReadOnly:=True)
    Else
        '既に開いているファイルを指定
        Set wbk = chkwsh
    End If
    Set chkwsh = Nothing

    '非表示シートを含めたシート数
    'retVal = wbk.Worksheets.Count

    '表示シートのみのシート数
    表示シート数 = 0
    For Each workwsh In wbk.Worksheets
        If workwsh.Visible Then
            表示シート数 = 表示シート数 + 1
        Else
            '非表示シートをカウントしない
        End If
    Next
    retVal = 表示シート数

    getExcelPageNum = retVal
    
    '開いたファイルを保存せずにクローズ
    '指定フォルダ以下で開いていたファイルも閉じるので注意
    wbk.Close SaveChanges:=False

    Set workwsh = Nothing
    Set wbk = Nothing


End Function

Private Function getWordPageNum(strFile As String) As Variant

    Dim objWord As Object
    Dim objDoc As Object
    Dim retVal As Long
    
    Set objWord = CreateObject("Word.Application")
    Set objDoc = objWord.Documents.Open(Filename:=strFile, ReadOnly:=True)
    
    retVal = objDoc.Content.Information(4)
    objDoc.Close SaveChanges:=False
    objWord.Quit
    
    Set objDoc = Nothing
    Set objWord = Nothing
    
    getWordPageNum = retVal
    
End Function

Private Function getPdfPageNum(strFile As String) As Variant

    Dim objPdf As Object
    Dim objwork As Object
    Dim objResult As Object
    
    Dim xFileNum As Long
    Dim xStr As String
    Dim retVal As Long

    retVal = 0
    'Openはパスに環境依存文字が含まれるとエラー
    'FSOでバイナリ開けない
    'Fso.ShortPathだと環境依存文字が含まれないかも
    'vbFormUnicode=128,vbUnicode=64 参照設定しないので直値指定
    'VBEはUnicode変換するとShift-JIS以外は"?"となるので環境依存文字判定
    Dim temp As String
    temp = StrConv(StrConv(strFile, 128, 1041), 64, 1041)
    
    If InStr(temp, "?") <= 0 Then
        xFileNum = FreeFile
        Open (strFile) For Binary Access Read As #xFileNum
        xStr = Space(LOF(xFileNum))
        Get #xFileNum, , xStr
        Close #xFileNum
        
        Set objPdf = CreateObject("VBscript.RegExp")
        objPdf.Global = True
        
        objPdf.Pattern = "<</Count\s(\d+)"
        Set objResult = objPdf.Execute(xStr)
        For Each objwork In objResult
            getPdfPageNum = objResult.Item(0).SubMatches.Item(0)
            Set objPdf = Nothing
            Set objwork = Nothing
            Set objResult = Nothing
            Exit Function
        Next
        
        objPdf.Pattern = "/Type\s*/Page[^s]"
        Set objResult = objPdf.Execute(xStr)
        For Each objwork In objResult
            getPdfPageNum = objResult.Count
            Set objPdf = Nothing
            Set objwork = Nothing
            Set objResult = Nothing
            Exit Function
        Next
    
        Set objPdf = Nothing
        Set objwork = Nothing
        Set objResult = Nothing
    Else
        getPdfPageNum = "パスに環境依存文字が含まれています"
        Exit Function
    End If
    
    getPdfPageNum = "PDFページ取得失敗..."
   
End Function

' trial ver
' Seiji Fujita  rev.2  June 02, 2020 (base code: 'Haluk 19/10/2008)
' Added a file size check to prevent a zero byte file from being created if the file does not exist
' When file does not exit or filesize equal 0, return value = NOTEXIST
' When If it is impossible to count, return value = UNSUPPORTNUM
Function GetPageNum(ByVal PDF_File As String) As Long
    Const NOTEXIST As Long = 0
    Const UNSUPPORTNUM As Long = -99
    Dim FileNum As Long
    Dim strRetVal As String
    Dim RegExp
    Dim nFileLen As Long
    Dim getpage0 As Long, getpage1 As Long, getpage2 As Long, _
        getpage3 As Long, getpage4 As Long, getpage5 As Long, _
        getpage6 As Long, getpage7 As Long

    Application.Volatile

    ' return NOTEXIST when filesize equal zero or file does not exist
    On Error Resume Next
    nFileLen = FileLen(PDF_File)
    On Error GoTo 0
    If nFileLen <= 0 Then
        GetPageNum = NOTEXIST
        Exit Function
    End If

    FileNum = FreeFile
    Open PDF_File For Binary As #FileNum
        strRetVal = Space(LOF(FileNum))
        Get #FileNum, , strRetVal
    Close #FileNum

    Set RegExp = CreateObject("VBscript.RegExp")
    RegExp.Global = True

    ' getpage0 is the basics, but depending on the PDF,
    ' the number of this pattern differs from the number of pages
    RegExp.Pattern = "/Type\s*/Page[^s]"
    getpage0 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/Resources"
    getpage1 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/ProcSet\s*\[/PDF"
    getpage2 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/Type/Catalog/Page\s*"
    getpage3 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/ExtGState"
    getpage4 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/Type/ObjStm"
    getpage5 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "/Subtype/"
    getpage6 = RegExp.Execute(strRetVal).Count

    RegExp.Pattern = "<rdf:" ' "JFIF"  ' "DeviceRGB"     ' "/Ordering\(Identity"   ' "Registry\(Adobe\)"     ' "/BM/Normal"
    getpage7 = RegExp.Execute(strRetVal).Count

    If getpage0 > 0 Then
        GetPageNum = getpage0
    Else
        If getpage5 > 0 Then
            If getpage7 <= 0 Then
                GetPageNum = getpage6 - getpage5 - getpage4 - 1
            Else
                GetPageNum = UNSUPPORTNUM   ' cannot get right number; getpage6 - getpage7 - 2 * getpage1 ' not logical, adhoc
            End If
        Else
            GetPageNum = MathMax(MathMax(getpage1, getpage2), getpage3)
            ' you can use below with MS Excel, insted above
            ' GetPageNum = Application.WorksheetFunction.Max(getpage1, getpage2, getpage3)
            If GetPageNum > getpage4 Then
                GetPageNum = GetPageNum - getpage4
            End If
        End If
    End If
End Function

Function MathMax(ByVal a As Long, ByVal b As Long) As Long
    If a >= b Then
        MathMax = a
    Else
        MathMax = b
    End If
End Function
 
Private Function getPdfPageNumProperty(PDFPath As String) As Variant
  Dim num As Long
   
  num = GetPDFNumPagesUsingAdobeReader(PDFPath)
  getPdfPageNumProperty = num
End Function
 
Private Function GetPDFNumPagesUsingAdobeReader(ByVal PDFPath As String) As Long
'Adobe Readerの[文書のプロパティ]ダイアログからPDFのページ数を取得
  Dim uiAuto As CUIAutomation
  Dim elmApp As IUIAutomationElement
  Dim elmRoot As IUIAutomationElement
  Dim elmDialog As IUIAutomationElement
  Dim elmTabItem As IUIAutomationElement
  Dim elmTextPageNumCaption As IUIAutomationElement
  Dim elmTextPageNum As IUIAutomationElement
  Dim elmButtonCancel As IUIAutomationElement
  Dim elmButtonClose As IUIAutomationElement
  Dim ptnSel As IUIAutomationSelectionItemPattern
  Dim ptnInvoke As IUIAutomationInvokePattern
  Dim hApp As Long
  Dim readerPath As String
  Dim ret As Long
   
  'Adobe Readerで指定したPDFファイルを表示
  readerPath = GetAdobeReaderPath
  If Len(Trim(GetAdobeReaderPath)) < 1 Then GoTo Fin
  Shell """" & readerPath & """" & " " & """" & PDFPath & """", vbNormalFocus
   
  'Adobe Reader取得
  '※PDF表示まで多少時間が掛かる場合有り
  Set uiAuto = New CUIAutomation
  Set elmRoot = uiAuto.GetRootElement
  Do
    Set elmApp = GetElement(uiAuto, _
                            elmRoot, _
                            UIA_ClassNamePropertyId, _
                            "AcrobatSDIWindow", _
                            UIA_WindowControlTypeId)
    Sleep 200
    DoEvents
  Loop While elmApp Is Nothing
   
  '[文書のプロパティ]ダイアログ取得
  hApp = elmApp.GetCurrentPropertyValue(UIA_NativeWindowHandlePropertyId)
  PostMessage hApp, WM_COMMAND, &H1794, 0 '文書のプロパティ表示
  Do
    Set elmDialog = GetElement(uiAuto, _
                               elmApp, _
                               UIA_NamePropertyId, _
                               "文書のプロパティ", _
                               UIA_WindowControlTypeId)
    Sleep 200
    DoEvents
  Loop While elmDialog Is Nothing
   
  '[概要]タブ選択
  Set elmTabItem = GetElement(uiAuto, _
                              elmDialog, _
                              UIA_NamePropertyId, _
                              "概要", _
                              UIA_TabItemControlTypeId)
  If elmTabItem Is Nothing Then GoTo Fin
  Set ptnSel = elmTabItem.GetCurrentPattern(UIA_SelectionItemPatternId)
  ptnSel.Select
   
  '[ページ数]取得
  Set elmTextPageNumCaption = GetElement(uiAuto, _
                                         elmDialog, _
                                         UIA_NamePropertyId, _
                                         "ページ数 :", _
                                         UIA_TextControlTypeId)
  If elmTextPageNumCaption Is Nothing Then GoTo Fin
  Set elmTextPageNum = uiAuto.RawViewWalker.GetNextSiblingElement(elmTextPageNumCaption)
  If elmTextPageNum Is Nothing Then GoTo Fin
  ret = CLng(elmTextPageNum.CurrentName)
   
  'ダイアログを閉じてアプリケーション終了
  Set elmButtonCancel = GetElement(uiAuto, _
                                   elmDialog, _
                                   UIA_NamePropertyId, _
                                   "キャンセル", _
                                   UIA_ButtonControlTypeId)
  If elmButtonCancel Is Nothing Then GoTo Fin
  Set ptnInvoke = elmButtonCancel.GetCurrentPattern(UIA_InvokePatternId)
  ptnInvoke.Invoke
  Set elmButtonClose = GetElement(uiAuto, _
                                  elmApp, _
                                  UIA_NamePropertyId, _
                                  "閉じる", _
                                  UIA_ButtonControlTypeId)
  If elmButtonClose Is Nothing Then GoTo Fin
  Set ptnInvoke = elmButtonClose.GetCurrentPattern(UIA_InvokePatternId)
  ptnInvoke.Invoke
   
Fin:
  GetPDFNumPagesUsingAdobeReader = ret
End Function
 
Private Function GetAdobeReaderPath()
'Adobe Reader(AcroRd32.exe)のパスを取得
  Dim folderPath As String
  Dim filePath As String
  Dim itm As Object
  Const ExeName As String = "AcroRd32.exe"
    
  '[プログラムと機能]からAdobe Readerのインストール先フォルダを取得
  With CreateObject("Shell.Application").Namespace("shell:::{7b81be6a-ce2b-4676-a29e-eb907a5126c5}")
    For Each itm In .Items
      If InStr(LCase(itm.Name), "adobe") And InStr(LCase(itm.Name), "reader") Then
        '[Extended Asian Language font pack]は除外
        If InStr(LCase(itm.Name), "extended") < 1 Then
          folderPath = .GetDetailsOf(itm, 10)
          Exit For
        End If
      End If
    Next
  End With
   
  'Adobe Readerのパスを取得
  With CreateObject("Scripting.FileSystemObject")
    filePath = .BuildPath(folderPath, "Reader")
    filePath = .BuildPath(filePath, ExeName)
    If .FileExists(filePath) = True Then GetAdobeReaderPath = filePath
  End With
End Function
 
Private Function GetElement(ByVal uiAuto As CUIAutomation, _
                            ByVal elmParent As IUIAutomationElement, _
                            ByVal propertyId As Long, _
                            ByVal propertyValue As Variant, _
                            Optional ByVal ctrlType As Long = 0) As IUIAutomationElement
  Dim cndFirst As IUIAutomationCondition
  Dim cndSecond As IUIAutomationCondition
       
  Set cndFirst = uiAuto.CreatePropertyCondition( _
                   propertyId, _
                   propertyValue _
                 )
  If ctrlType <> 0 Then
    Set cndSecond = uiAuto.CreatePropertyCondition( _
                      UIA_ControlTypePropertyId, _
                      ctrlType _
                    )
    Set cndFirst = uiAuto.CreateAndCondition( _
                     cndFirst, _
                     cndSecond _
                   )
  End If
  Set GetElement = elmParent.FindFirst(TreeScope_Subtree, cndFirst)
End Function