Word ルビ振りマクロ

下にスクロールして、コードをコピーして使ってください。

[手順]
Wordファイルを開く
Alt + F11 で、VBAエディタを開く
[挿入]→[標準モジュール]
出てきた Module1 などに、以下のVBAコードをコピーして貼り付ける
Word画面に戻る → Alt + F8
実行したいマクロ名を選択 → [実行](最後にチェック&微調整)

ルビマクロ 簡易ガイド

① Word標準ルビ(横書き文書用)

マクロ名 機能
ルビ01_選択範囲に標準ルビをふる 選択範囲のすべての対象に標準ルビ
ルビ02_全文に標準ルビをふる 文書全体に標準ルビ
ルビ03_選択範囲の初出のみ標準ルビ 選択範囲で、同じ漢字・漢字列は最初の1回だけ
ルビ04_全文に初出のみ標準ルビ 文書全体で、同じ漢字・漢字列は最初の1回だけ

② テキストボックス型ルビ(国語などの縦書き文書用)

Word標準ルビで行間などのレイアウトを変えたくない場合に使う方式です。 元の文字の近くに、小さなテキストボックスとしてルビを配置します。

マクロ名 機能
ルビ11_選択範囲に縦書きルビ用テキストボックス 選択範囲のすべての対象に付ける
ルビ12_全文に縦書きルビ用テキストボックス 文書全体に付ける
ルビ13_選択範囲に初出のみ縦書きルビ用テキストボックス 選択範囲で、同じ漢字・漢字列は最初の1回だけ
ルビ14_全文に初出のみ縦書きルビ用テキストボックス 文書全体で、同じ漢字・漢字列は最初の1回だけ
ルビ15_選択範囲に古文縦書きルビ用テキストボックス 選択範囲に古文読みを優先して付ける

使用上の注意

  • 実行前にWordファイルをコピーしてバックアップしてください。
  • テキストボックス型はファイルが重くなるため、 印刷用にコピーしたWordファイルでの使用を推奨します。
  • 縦書きルビのテキストボックス型は処理が遅いため、 複数ページは1ページずつファイルを分割して、同時に複数ファイルで実行するのがおすすめです。
  • 人名・地名・専門用語などは誤読することがあります。 印刷前に目視確認してください。
VBA
    

Option Explicit

'============================================================
' 安全中断用(Escキー監視)
'
' wdDialogPhoneticGuide.Show(1) の 1 は TimeOut 指定であり、
' ダイアログの戻り値 0 をそのまま『ユーザーがCancelした』と
' 判定すると自動タイムアウトと衝突する可能性がある。
' そのため、処理中断はWindowsのEscキー状態を直接監視する。
'============================================================
#If VBA7 Then
Private Declare PtrSafe Function GetAsyncKeyState Lib "user32" ( _
        ByVal vKey As Long) As Integer
#Else
Private Declare Function GetAsyncKeyState Lib "user32" ( _
        ByVal vKey As Long) As Integer
#End If

Private Const RUBY_VK_ESCAPE As Long = &H1B
Private Const RUBY_USER_INTERRUPT_ERROR As Long = 18

'============================================================
' 元コード用
'============================================================

'ルビを振った漢字を格納するArray
Public kanjiArray(9999) As String

'KanjiArrayのインデックス
Public KI As Long


'============================================================
' 縦書き・横書き自動判別RubyBox用・モジュール変数
'============================================================

'親文字に対するルビ文字サイズ
Private Const RUBY_FONT_RATIO As Double = 0.5

'テキストボックス内部余白 最低値(pt)
Private Const RUBY_MIN_MARGIN As Single = 0.5

'親文字とルビの最低間隔(pt)
Private Const RUBY_MIN_GAP As Single = 0.5

'最終手動微調整(縦書き)
Private Const RUBY_X_ADJUST As Single = 0
Private Const RUBY_Y_ADJUST As Single = 0

'最終手動微調整(横書き)
Private Const RUBY_H_X_ADJUST As Single = 0

Private Const RUBY_H_Y_REFERENCE_FONT_SIZE As Single = 11
Private Const RUBY_H_Y_REFERENCE_ADJUST As Single = 5
Private Const RUBY_H_Y_FINE_ADJUST As Single = 5

'読みモード
Private Const RUBY_READ_MODE_WORD As Long = 0
Private Const RUBY_READ_MODE_KOBUN As Long = 1

'高速読みモード
' True : キャッシュ → Excel.GetPhonetic → Word標準ルビ fallback
' False: 従来どおり毎回Word標準ルビ
Private Const FAST_RUBY_MODE As Boolean = True

'古文読み辞書(遅延初期化)
Private mKobunExact As Object
Private mKobunContext As Object

'GetPoint実測用
'同一縦列の近傍文字から ScreenPixel / PagePoint の比率を測る。
Private Const RUBY_SCALE_SEARCH_CHARS As Long = 24

'実測値の異常判定
Private Const RUBY_SCALE_MIN As Double = 0.2
Private Const RUBY_SCALE_MAX As Double = 20#
Private Const RUBY_WIDTH_MIN_RATIO As Double = 0.5
Private Const RUBY_WIDTH_MAX_RATIO As Double = 12#
Private Const RUBY_HEIGHT_MIN_RATIO As Double = 0.35
Private Const RUBY_HEIGHT_MAX_RATIO As Double = 4#

'Word再レイアウト後の座標安定判定
Private Const RUBY_GEOMETRY_TOLERANCE As Single = 0.2
Private Const RUBY_GEOMETRY_RETRY As Long = 4

'生成するShapeの識別文字
Private Const RUBYBOX_PREFIX As String = "RubyTB_"

'読み取得用一時文書
Private mRubyTempDoc As Word.Document

'高速読み用キャッシュ / Excel.Application(遅延初期化・参照設定不要)
Private mRubyReadCache As Object
Private mRubyExcelApp As Object

'高速読み統計
Private mRubyCacheHit As Long
Private mRubyExcelHit As Long
Private mRubyWordFallback As Long

'Shape連番
Private mRubyShapeSeq As Long

'処理件数
Private mRubyOK As Long
Private mRubyEmpty As Long
Private mRubySkip As Long
Private mRubyErr As Long
Private mRubyKobunHit As Long

'ユーザーによる処理中断
Private mRubyCancelled As Boolean


'============================================================
' Escキーの古い押下状態を読み捨てる
'============================================================
Private Sub ResetRubyCancelState()

    Dim dummy As Integer

    mRubyCancelled = False

    On Error Resume Next
    dummy = GetAsyncKeyState(RUBY_VK_ESCAPE)
    On Error GoTo 0

End Sub


'============================================================
' Escが押されたか確認
'
' High bit : 現在押下中
' Low bit  : 前回呼出し以降に押された
' のどちらかを拾う。
'============================================================
Private Function RubyCancelRequested() As Boolean

    Dim keyState As Integer

    If mRubyCancelled = True Then
        RubyCancelRequested = True
        Exit Function
    End If

    On Error Resume Next
    Err.Clear
    keyState = GetAsyncKeyState(RUBY_VK_ESCAPE)

    If Err.Number = 0 Then

        If ((CLng(keyState) And &H8000&) <> 0) _
           Or ((CLng(keyState) And 1&) <> 0) Then

            mRubyCancelled = True

        End If

    End If

    Err.Clear
    On Error GoTo 0

    RubyCancelRequested = mRubyCancelled

End Function


'################################################################
'
' 元のWord標準ルビマクロ
'
'################################################################


'============================================================
' 選択した範囲内の文字列にルビ設定 MakeRubiPartial
'============================================================
Public Sub ルビ01_選択範囲に標準ルビをふる()

    SetPhoneticRange Selection.Range, False

End Sub


'============================================================
' 文書全体にルビ設定 MakeRubiAll
'============================================================
Public Sub ルビ02_全文に標準ルビをふる()

    SetPhoneticRange ActiveDocument.Range, False

End Sub


'============================================================
' 選択した範囲内の文字列にルビ設定 MakeFirstRubiPartial
' 最初の漢字のみ
'============================================================
Public Sub ルビ03_選択範囲の初出のみ標準ルビ()

    SetPhoneticRange Selection.Range, True

End Sub


'============================================================
' 文書全体にルビ設定 MakeFirstRubiAll
' 最初の漢字のみ
'============================================================
Public Sub ルビ04_全文に初出のみ標準ルビ()

    SetPhoneticRange ActiveDocument.Range, True

End Sub


'============================================================
' 元の標準ルビ処理
'============================================================
Private Sub SetPhoneticRange( _
        ByVal rng As Word.Range, _
        ByVal FirstFlag As Boolean)

    Dim r As Word.Range
    Dim s As Word.Range
    Dim i As Long
    Dim dFlag As Boolean

    KI = 0

    For Each r In rng.Words

        If r.Fields.Count < 1 Then

            If ChkKanjiRange2(r) = True Then

                If ChkKanjiRange(r) = True Then

                    If FirstFlag = False Then

                        r.Select
                        Application.Dialogs( _
                            wdDialogPhoneticGuide).Show 1

                    Else

                        If inKanjiArray(r.Text) = False Then

                            addKanjiArray r.Text
                            r.Select
                            Application.Dialogs( _
                                wdDialogPhoneticGuide).Show 1

                        End If

                    End If

                Else

                    i = 1

                    For Each s In r.Characters

                        If ChkKanjiRange(s) = True Then

                            dFlag = False

                            If i < Len(r.Text) Then

                                If Len(Mid$(r.Text, i + 1, 1)) > 0 Then

                                    If isKanji( _
                                        Mid$(r.Text, i + 1, 1)) = True Then

                                        s.End = s.End + 1
                                        dFlag = True

                                    End If

                                End If

                            End If

                            If FirstFlag = False Then

                                s.Select
                                Application.Dialogs( _
                                    wdDialogPhoneticGuide).Show 1

                            Else

                                If inKanjiArray(s.Text) = False Then

                                    If dFlag = True Then

                                        addKanjiArray _
                                            Mid$(r.Text, i, 1)

                                        addKanjiArray _
                                            Mid$(r.Text, i + 1, 1)

                                    End If

                                    addKanjiArray s.Text
                                    s.Select
                                    Application.Dialogs( _
                                        wdDialogPhoneticGuide).Show 1

                                End If

                            End If

                        End If

                        i = i + 1

                    Next s

                End If

            End If

        End If

    Next r

End Sub


'============================================================
' 指定Rangeが全部漢字だったらTrue
'============================================================
Private Function ChkKanjiRange( _
        ByVal rng As Word.Range) As Boolean

    Dim i As Long

    ChkKanjiRange = True

    For i = 1 To Len(rng.Text)

        If isKanji(Mid$(rng.Text, i, 1)) = False Then

            ChkKanjiRange = False
            Exit Function

        End If

    Next i

End Function


'============================================================
' 指定Rangeに漢字が1文字でも含まれていたらTrue
'============================================================
Private Function ChkKanjiRange2( _
        ByVal rng As Word.Range) As Boolean

    Dim i As Long

    ChkKanjiRange2 = False

    For i = 1 To Len(rng.Text)

        If isKanji(Mid$(rng.Text, i, 1)) = True Then

            ChkKanjiRange2 = True
            Exit Function

        End If

    Next i

End Function


'============================================================
' 漢字判定
'============================================================
Private Function isKanji( _
        ByVal strIn As String) As Boolean

    Dim re As Object

    Set re = CreateObject("VBScript.RegExp")
    re.Pattern = "[一-龠〃々〆〇]"

    isKanji = re.Test(strIn)

End Function


'============================================================
' kanjiArrayに存在するか
'============================================================
Private Function inKanjiArray( _
        ByVal str As String) As Boolean

    Dim i As Long

    inKanjiArray = False

    For i = 0 To KI + 1

        If StrComp(kanjiArray(i), str) = 0 Then

            inKanjiArray = True
            Exit Function

        End If

    Next i

End Function


'============================================================
' kanjiArrayへ追加
'============================================================
Private Function addKanjiArray( _
        ByVal str As String) As Boolean

    kanjiArray(KI) = str
    KI = KI + 1
    addKanjiArray = True

End Function


'################################################################
'
' 縦書き・横書き自動判別テキストボックス型ルビ
'
'################################################################


'============================================================
' 選択範囲 / 全対象 MakeRubyBoxPartial
'============================================================
Public Sub ルビ11_選択範囲に縦書きルビ用テキストボックス()

    RunRubyBoxProcess _
        Selection.Range, _
        False, _
        False

End Sub


'============================================================
' 選択範囲 / 古文読み優先 MakeKobunRubyBoxPartial
'
' 古文辞書に一致した場合は古文読みを使用し、
' 一致しない対象は従来どおりWord標準ルビへフォールバックする。
'============================================================
Public Sub ルビ15_選択範囲に古文縦書きルビ用テキストボックス()

    RunRubyBoxProcess _
        Selection.Range, _
        False, _
        False, _
        RUBY_READ_MODE_KOBUN

End Sub


'============================================================
' 文書全体 / 全対象 MakeRubyBoxAll
'
' MainTextStoryに加え、既存TextFrameStoryも処理する。
'============================================================
Public Sub ルビ12_全文に縦書きルビ用テキストボックス()

    RunRubyBoxProcess _
        ActiveDocument.Range, _
        False, _
        True

End Sub


'============================================================
' 選択範囲 / 初出のみ MakeFirstRubyBoxPartial
'============================================================
Public Sub ルビ13_選択範囲に初出のみ縦書きルビ用テキストボックス()

    RunRubyBoxProcess _
        Selection.Range, _
        True, _
        False

End Sub


'============================================================
' 文書全体 / 初出のみ MakeFirstRubyBoxAll
'============================================================
Public Sub ルビ14_全文に初出のみ縦書きルビ用テキストボックス()

    RunRubyBoxProcess _
        ActiveDocument.Range, _
        True, _
        True

End Sub


'============================================================
' RubyBox処理全体の管理
'
' FullFlag=True の場合:
'   1. 既存RubyBoxを全削除
'   2. その時点のTextFrameStoryをスナップショット
'   3. MainTextStoryを処理
'   4. スナップショットしたTextFrameStoryを処理
'
' RubyBox自身もTextFrameを持つため、処理開始前に
' スナップショットしておくことで新規RubyBoxの再処理を防ぐ。
'============================================================
Private Sub RunRubyBoxProcess( _
        ByVal rng As Word.Range, _
        ByVal FirstFlag As Boolean, _
        ByVal FullFlag As Boolean, _
        Optional ByVal readMode As Long = RUBY_READ_MODE_WORD)

    Dim mainDoc As Word.Document
    Dim workRng As Word.Range
    Dim savedSel As Word.Range

    Dim textFrameStories As Collection
    Dim storyRng As Word.Range
    Dim i As Long

    Dim storyKey As String
    Dim storyPage As Long
    Dim modeInfo As String

    On Error GoTo ERR_HANDLER

    Set mainDoc = rng.Document

    '現在選択位置を保存
    On Error Resume Next

    If Selection.Document Is mainDoc Then
        Set savedSel = Selection.Range.Duplicate
    End If

    On Error GoTo ERR_HANDLER

    '初期化
    KI = 0
    mRubyOK = 0
    mRubyEmpty = 0
    mRubySkip = 0
    mRubyErr = 0
    mRubyKobunHit = 0
    mRubyCacheHit = 0
    mRubyExcelHit = 0
    mRubyWordFallback = 0
    mRubyShapeSeq = 0

    Set mRubyReadCache = Nothing
    Set mRubyExcelApp = Nothing

    ResetRubyCancelState

    '削除処理へ入る前にもEscを確認する。
    If RubyCancelRequested() = True Then
        GoTo CLEAN_EXIT
    End If

    If FullFlag = True Then

        '既存RubyBoxを先に消す。
        'これによりStoryRangesのスナップショットへRubyBox自身が入らない。
        DeleteAllRubyBoxes mainDoc

        Set textFrameStories = _
            SnapshotTextFrameStories(mainDoc)

        Set workRng = _
            mainDoc.Range.Duplicate

    Else

        Set workRng = _
            rng.Duplicate

        storyKey = _
            GetRangeStoryKey(workRng)

        storyPage = _
            GetRangePageNumber(workRng)

        DeleteRubyBoxesInRange _
            mainDoc, _
            CLng(workRng.StoryType), _
            storyKey, _
            storyPage, _
            workRng.Start, _
            workRng.End

    End If

    '既存RubyBox削除中などに押されたEscもここで拾う。
    If RubyCancelRequested() = True Then
        GoTo CLEAN_EXIT
    End If

    '読み取得専用一時文書
    Set mRubyTempDoc = _
        Documents.Add(Visible:=True)

    mainDoc.Activate

    'MainTextStoryまたは選択されたStoryを処理
    ProcessRubyBoxRange _
        workRng, _
        mainDoc, _
        FirstFlag, _
        readMode

    If mRubyCancelled = True Then
        GoTo CLEAN_EXIT
    End If

    '全文処理時は、開始時点に存在したTextFrameStoryも処理
    If FullFlag = True Then

        If Not textFrameStories Is Nothing Then

            For i = 1 To textFrameStories.Count

                If RubyCancelRequested() = True Then
                    Exit For
                End If

                Set storyRng = _
                    textFrameStories(i)

                ProcessRubyBoxRange _
                    storyRng, _
                    mainDoc, _
                    FirstFlag, _
                    readMode

                If mRubyCancelled = True Then
                    Exit For
                End If

            Next i

        End If

    End If

CLEAN_EXIT:

    On Error Resume Next

    '高速読みで起動したExcel.Applicationを確実に解放する。
    ReleaseFastRubyResources

    If Not mRubyTempDoc Is Nothing Then

        mRubyTempDoc.Close _
            SaveChanges:=wdDoNotSaveChanges

    End If

    Set mRubyTempDoc = Nothing

    If Not mainDoc Is Nothing Then
        mainDoc.Activate
    End If

    If Not savedSel Is Nothing Then
        savedSel.Select
    End If

    On Error GoTo 0

    modeInfo = ""

    If FAST_RUBY_MODE = True Then
        modeInfo = _
            modeInfo & vbCrLf & _
            "高速読取 Excel : " & mRubyExcelHit & vbCrLf & _
            "高速読取 Cache : " & mRubyCacheHit & vbCrLf & _
            "Word fallback   : " & mRubyWordFallback
    End If

    If readMode = RUBY_READ_MODE_KOBUN Then
        modeInfo = _
            modeInfo & vbCrLf & _
            "古文辞書ヒット: " & mRubyKobunHit
    End If

    If mRubyCancelled = True Then

        MsgBox _
            "ルビ処理を中断しました。" & _
            vbCrLf & vbCrLf & _
            "作成済みRubyBox : " & mRubyOK & vbCrLf & _
            "空RubyBox       : " & mRubyEmpty & vbCrLf & _
            "スキップ        : " & mRubySkip & vbCrLf & _
            "エラー          : " & mRubyErr & _
            modeInfo & _
            vbCrLf & vbCrLf & _
            "※中断前に作成済みのRubyBoxは残しています。", _
            vbInformation

    Else

        MsgBox _
            "テキストボックス型ルビ処理が終了しました。" & _
            vbCrLf & vbCrLf & _
            "通常RubyBox : " & mRubyOK & vbCrLf & _
            "空RubyBox   : " & mRubyEmpty & vbCrLf & _
            "スキップ    : " & mRubySkip & vbCrLf & _
            "エラー      : " & mRubyErr & _
            modeInfo, _
            vbInformation

    End If

    Exit Sub

ERR_HANDLER:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
        Resume CLEAN_EXIT
    End If

    mRubyErr = mRubyErr + 1
    Resume CLEAN_EXIT

End Sub


'============================================================
' 処理開始時点のTextFrameStoryを保存
'
' Wordではテキストボックス本文はwdTextFrameStoryという
' MainTextStoryとは別のStoryとして管理される。
'============================================================
Private Function SnapshotTextFrameStories( _
        ByVal doc As Word.Document) As Collection

    Dim result As Collection
    Dim curStory As Word.Range
    Dim copyRng As Word.Range

    Set result = New Collection

    On Error Resume Next

    Set curStory = _
        doc.StoryRanges(wdTextFrameStory)

    On Error GoTo ERR_HANDLER

    Do While Not curStory Is Nothing

        Set copyRng = _
            curStory.Duplicate

        result.Add copyRng

        Set curStory = _
            curStory.NextStoryRange

    Loop

    Set SnapshotTextFrameStories = result
    Exit Function

ERR_HANDLER:

    '取得途中で問題が起きても、取得済みStoryは返す。
    Set SnapshotTextFrameStories = result

End Function


'============================================================
' 1つのStory/Rangeを元コード同様に単語単位で処理
'============================================================
Private Sub ProcessRubyBoxRange( _
        ByVal workRng As Word.Range, _
        ByVal mainDoc As Word.Document, _
        ByVal FirstFlag As Boolean, _
        ByVal readMode As Long)

    Dim r As Word.Range
    Dim s As Word.Range
    Dim targetRng As Word.Range
    Dim contextRng As Word.Range

    Dim i As Long
    Dim dFlag As Boolean
    Dim skipNext As Boolean

    Dim targetStart As Long
    Dim targetEnd As Long

    On Error GoTo ERR_HANDLER

    For Each r In workRng.Words

        If RubyCancelRequested() = True Then
            Exit Sub
        End If

        If r.Fields.Count < 1 Then

            If ChkKanjiRange2(r) = True Then

                If ChkKanjiRange(r) = True Then

                    Set targetRng = _
                        r.Duplicate

                    Set contextRng = _
                        r.Duplicate

                    If FirstFlag = False Then

                        CreateRubyBoxForRange _
                            targetRng, _
                            mainDoc, _
                            contextRng, _
                            readMode

                    Else

                        If inKanjiArray( _
                            targetRng.Text) = False Then

                            addKanjiArray _
                                targetRng.Text

                            CreateRubyBoxForRange _
                                targetRng, _
                                mainDoc, _
                                contextRng, _
                                readMode

                        End If

                    End If

                Else

                    '漢字・かな等が混在
                    i = 1
                    skipNext = False

                    For Each s In r.Characters

                        If RubyCancelRequested() = True Then
                            Exit Sub
                        End If

                        If skipNext = True Then

                            skipNext = False

                        ElseIf ChkKanjiRange(s) = True Then

                            'Rangeオブジェクトを直接伸縮させず、
                            'この時点のStart/Endを数値で固定してから対象Rangeを作る。
                            'Shape作成・選択変更・再レイアウトによる
                            'Range境界の追随ズレをメタ情報へ持ち込まないため。
                            targetStart = s.Start
                            targetEnd = s.End

                            dFlag = False

                            '次の文字も漢字なら2文字まとめる
                            If i < Len(r.Text) Then

                                If Len(Mid$(r.Text, i + 1, 1)) > 0 Then

                                    If isKanji( _
                                        Mid$(r.Text, i + 1, 1)) = True Then

                                        targetEnd = targetEnd + 1
                                        dFlag = True

                                    End If

                                End If

                            End If

                            Set targetRng = _
                                r.Duplicate

                            targetRng.SetRange _
                                Start:=targetStart, _
                                End:=targetEnd

                            Set contextRng = _
                                r.Duplicate

                            If FirstFlag = False Then

                                CreateRubyBoxForRange _
                                    targetRng, _
                                    mainDoc, _
                                    contextRng, _
                                    readMode

                            Else

                                If inKanjiArray( _
                                    targetRng.Text) = False Then

                                    If dFlag = True Then

                                        addKanjiArray _
                                            Mid$(r.Text, i, 1)

                                        addKanjiArray _
                                            Mid$(r.Text, i + 1, 1)

                                    End If

                                    addKanjiArray _
                                        targetRng.Text

                                    CreateRubyBoxForRange _
                                        targetRng, _
                                        mainDoc, _
                                        contextRng, _
                                        readMode

                                End If

                            End If

                            If dFlag = True Then
                                skipNext = True
                            End If

                        End If

                        i = i + 1

                    Next s

                End If

            End If

        End If

    Next r

    Exit Sub

ERR_HANDLER:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
        Exit Sub
    End If

    '1 Storyの異常で全文処理を止めない。
    mRubyErr = mRubyErr + 1

End Sub


'============================================================
' 1対象の処理
'
' 読み取得だけ失敗した場合は処理を中止せず、
' rubyText="" の空RubyBoxを同じ位置計算で作る。
'============================================================
Private Sub CreateRubyBoxForRange( _
        ByVal target As Word.Range, _
        ByVal mainDoc As Word.Document, _
        ByVal contextRng As Word.Range, _
        ByVal readMode As Long)

    Dim rubyText As String
    Dim rubyReadOK As Boolean
    Dim usedKobun As Boolean

    Dim baseSize As Single
    Dim rubySize As Single
    Dim baseFont As String

    Dim baseLeft As Single
    Dim baseTop As Single
    Dim baseWidth As Single
    Dim baseHeight As Single

    '元Rangeの識別情報は、Dialog/Shape作成より前に固定する。
    Dim sourceStoryType As Long
    Dim sourceStoryKey As String
    Dim sourcePage As Long
    Dim sourceStart As Long
    Dim sourceEnd As Long

    On Error GoTo ERR_HANDLER

    If RubyCancelRequested() = True Then
        Exit Sub
    End If

    If Len(target.Text) = 0 Then

        mRubySkip = mRubySkip + 1
        Exit Sub

    End If

    sourceStoryType = _
        CLng(target.StoryType)

    sourceStart = target.Start
    sourceEnd = target.End

    sourceStoryKey = _
        GetRangeStoryKey(target)

    sourcePage = _
        GetRangePageNumber(target)

    '読み取得。
    '通常モードは従来どおりWord標準ルビ。
    '古文モードは「文脈辞書 → 単語辞書 → Word標準ルビ」。
    usedKobun = False

    If readMode = RUBY_READ_MODE_KOBUN Then

        rubyText = _
            GetKobunRubyText( _
                target, _
                contextRng, _
                rubyReadOK, _
                usedKobun)

    Else

        rubyText = _
            GetSmartRubyText( _
                target, _
                contextRng, _
                rubyReadOK)

    End If

    '読み取得中にEscが押された場合、この対象のBoxは作らない。
    If mRubyCancelled = True Then
        Exit Sub
    End If

    If usedKobun = True Then
        mRubyKobunHit = mRubyKobunHit + 1
    End If

    If rubyReadOK = False Then
        rubyText = ""
    End If

    baseSize = _
        GetBaseFontSize(target)

    If baseSize <= 0 Then

        mRubySkip = mRubySkip + 1
        Exit Sub

    End If

    rubySize = _
        baseSize * RUBY_FONT_RATIO

    baseFont = _
        GetBaseFontName(target)

    mainDoc.Activate

    If GetStableTargetGeometry( _
        target, _
        baseLeft, _
        baseTop, _
        baseWidth, _
        baseHeight) = False Then

        If mRubyCancelled = True Then
            Exit Sub
        End If

        '座標が取得できない場合は同位置のBoxを作れないためスキップ。
        mRubySkip = mRubySkip + 1
        Exit Sub

    End If

    If AddRubyTextBox( _
        mainDoc, _
        target, _
        rubyText, _
        baseSize, _
        rubySize, _
        baseFont, _
        baseLeft, _
        baseTop, _
        baseWidth, _
        baseHeight, _
        sourceStoryType, _
        sourceStoryKey, _
        sourcePage, _
        sourceStart, _
        sourceEnd) = False Then

        If mRubyCancelled = True Then
            Exit Sub
        End If

        mRubyErr = mRubyErr + 1
        Exit Sub

    End If

    If rubyReadOK = True Then
        mRubyOK = mRubyOK + 1
    Else
        mRubyEmpty = mRubyEmpty + 1
    End If

    Exit Sub

ERR_HANDLER:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
        Exit Sub
    End If

    mRubyErr = mRubyErr + 1

End Sub


'============================================================
' 高速読みルータ
'
' FAST_RUBY_MODE=True:
'   1. キャッシュ
'   2. Excel.Application.GetPhonetic
'   3. Word標準ルビ fallback
'
' FAST_RUBY_MODE=False:
'   従来のGetWordRubyTextを直接呼ぶ。
'============================================================
Private Function GetSmartRubyText( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range, _
        ByRef readOK As Boolean) As String

    Dim cacheKey As String
    Dim rubyText As String

    readOK = False
    GetSmartRubyText = ""

    On Error GoTo WORD_FALLBACK

    If FAST_RUBY_MODE = False Then
        GetSmartRubyText = _
            GetWordRubyText( _
                src, _
                contextRng, _
                readOK)
        Exit Function
    End If

    If RubyCancelRequested() = True Then
        Exit Function
    End If

    EnsureRubyReadCache

    cacheKey = _
        BuildRubyReadCacheKey( _
            src, _
            contextRng)

    If Not mRubyReadCache Is Nothing Then

        If Len(cacheKey) > 0 Then

            If mRubyReadCache.Exists(cacheKey) Then

                rubyText = _
                    CStr(mRubyReadCache(cacheKey))

                If Len(rubyText) > 0 Then
                    mRubyCacheHit = mRubyCacheHit + 1
                    readOK = True
                    GetSmartRubyText = rubyText
                    Exit Function
                End If

            End If

        End If

    End If

    rubyText = _
        GetExcelRubyText( _
            src, _
            contextRng, _
            readOK)

    If readOK = True And Len(rubyText) > 0 Then

        mRubyExcelHit = mRubyExcelHit + 1

        If Not mRubyReadCache Is Nothing _
           And Len(cacheKey) > 0 Then

            mRubyReadCache(cacheKey) = rubyText

        End If

        GetSmartRubyText = rubyText
        Exit Function

    End If

WORD_FALLBACK:

    If mRubyCancelled = True Then
        readOK = False
        GetSmartRubyText = ""
        Exit Function
    End If

    Err.Clear

    mRubyWordFallback = mRubyWordFallback + 1

    rubyText = _
        GetWordRubyText( _
            src, _
            contextRng, _
            readOK)

    If readOK = True And Len(rubyText) > 0 Then

        If Not mRubyReadCache Is Nothing _
           And Len(cacheKey) > 0 Then

            mRubyReadCache(cacheKey) = rubyText

        End If

        GetSmartRubyText = rubyText

    Else

        GetSmartRubyText = ""

    End If

End Function


'============================================================
' 高速読みキャッシュを初期化
'============================================================
Private Sub EnsureRubyReadCache()

    On Error GoTo ERR_HANDLER

    If mRubyReadCache Is Nothing Then

        Set mRubyReadCache = _
            CreateObject("Scripting.Dictionary")

        mRubyReadCache.CompareMode = vbBinaryCompare

    End If

    Exit Sub

ERR_HANDLER:

    Set mRubyReadCache = Nothing

End Sub


'============================================================
' Excel.Applicationを遅延初期化
'
' 参照設定は不要。専用の非表示Excelを1プロセスだけ起動し、
' RubyBox処理終了時にReleaseFastRubyResourcesでQuitする。
'============================================================
Private Function EnsureRubyExcelApp() As Boolean

    EnsureRubyExcelApp = False

    On Error GoTo ERR_HANDLER

    If mRubyExcelApp Is Nothing Then

        Set mRubyExcelApp = _
            CreateObject("Excel.Application")

        On Error Resume Next
        mRubyExcelApp.Visible = False
        mRubyExcelApp.DisplayAlerts = False
        On Error GoTo ERR_HANDLER

    End If

    EnsureRubyExcelApp = True
    Exit Function

ERR_HANDLER:

    Set mRubyExcelApp = Nothing
    EnsureRubyExcelApp = False

End Function


'============================================================
' 高速読み用リソースを解放
'============================================================
Private Sub ReleaseFastRubyResources()

    On Error Resume Next

    If Not mRubyExcelApp Is Nothing Then
        mRubyExcelApp.Quit
    End If

    Set mRubyExcelApp = Nothing
    Set mRubyReadCache = Nothing

    On Error GoTo 0

End Sub


'============================================================
' 読みキャッシュ用キー
'
' 同じ表記でも送り仮名・前後文脈が違えば別キーにする。
'============================================================
Private Function BuildRubyReadCacheKey( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range) As String

    Dim contextText As String
    Dim relStart As Long
    Dim relEnd As Long

    BuildRubyReadCacheKey = ""

    If GetRubyReadContextInfo( _
        src, _
        contextRng, _
        contextText, _
        relStart, _
        relEnd) = False Then

        Exit Function

    End If

    BuildRubyReadCacheKey = _
        contextText & vbTab & _
        CStr(relStart) & ":" & CStr(relEnd) & vbTab & _
        src.Text

End Function


'============================================================
' srcとcontextRngから読み取得用の文脈情報を作る
'
' relStart / relEnd は0始まり・End非包含。
'============================================================
Private Function GetRubyReadContextInfo( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range, _
        ByRef contextText As String, _
        ByRef relStart As Long, _
        ByRef relEnd As Long) As Boolean

    contextText = ""
    relStart = 0
    relEnd = 0
    GetRubyReadContextInfo = False

    On Error GoTo ERR_HANDLER

    If src Is Nothing Then
        Exit Function
    End If

    If contextRng Is Nothing Then

        contextText = src.Text
        relStart = 0
        relEnd = Len(src.Text)

    ElseIf contextRng.Document Is src.Document _
       And contextRng.StoryType = src.StoryType _
       And contextRng.Start <= src.Start _
       And contextRng.End >= src.End Then

        contextText = contextRng.Text
        relStart = src.Start - contextRng.Start
        relEnd = src.End - contextRng.Start

    Else

        contextText = src.Text
        relStart = 0
        relEnd = Len(src.Text)

    End If

    contextText = _
        NormalizeRubyContextText(contextText)

    If Len(contextText) = 0 Then
        Exit Function
    End If

    If relStart < 0 _
       Or relEnd <= relStart _
       Or relEnd > Len(contextText) Then

        contextText = _
            NormalizeRubyContextText(src.Text)

        relStart = 0
        relEnd = Len(contextText)

    End If

    If Len(contextText) = 0 Then
        Exit Function
    End If

    GetRubyReadContextInfo = True
    Exit Function

ERR_HANDLER:

    contextText = ""
    relStart = 0
    relEnd = 0
    GetRubyReadContextInfo = False

End Function


'============================================================
' WordのStory末尾制御文字だけ除去
'============================================================
Private Function NormalizeRubyContextText( _
        ByVal textValue As String) As String

    Dim result As String

    result = textValue
    result = Replace(result, vbCr, "")
    result = Replace(result, vbLf, "")
    result = Replace(result, ChrW(7), "")

    NormalizeRubyContextText = result

End Function


'============================================================
' Excel.GetPhoneticを使って読みを取得
'
' context全体の読みから、srcの外側にある「かな」だけを
' 前後から安全に除去できる場合に限って採用する。
' 少しでも対応関係が曖昧ならFalseとしてWordへfallbackする。
'============================================================
Private Function GetExcelRubyText( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range, _
        ByRef readOK As Boolean) As String

    Dim contextText As String
    Dim relStart As Long
    Dim relEnd As Long

    Dim rawRuby As String
    Dim rubyText As String

    Dim prefixSurface As String
    Dim suffixSurface As String
    Dim prefixRuby As String
    Dim suffixRuby As String

    readOK = False
    GetExcelRubyText = ""

    On Error GoTo ERR_HANDLER

    If EnsureRubyExcelApp() = False Then
        Exit Function
    End If

    If GetRubyReadContextInfo( _
        src, _
        contextRng, _
        contextText, _
        relStart, _
        relEnd) = False Then

        Exit Function

    End If

    rawRuby = _
        CStr(mRubyExcelApp.GetPhonetic(contextText))

    rubyText = _
        NormalizeRubyReading(rawRuby)

    If Len(rubyText) = 0 Then
        Exit Function
    End If

    '対象が文脈全体なら、そのまま採用できる。
    If relStart = 0 _
       And relEnd = Len(contextText) Then

        readOK = True
        GetExcelRubyText = rubyText
        Exit Function

    End If

    prefixSurface = _
        Left$(contextText, relStart)

    suffixSurface = _
        Mid$(contextText, relEnd + 1)

    If TrySurfaceKanaToRubyBoundary( _
        prefixSurface, _
        prefixRuby) = False Then

        Exit Function

    End If

    If TrySurfaceKanaToRubyBoundary( _
        suffixSurface, _
        suffixRuby) = False Then

        Exit Function

    End If

    If Len(prefixRuby) > 0 Then

        If Len(rubyText) < Len(prefixRuby) Then
            Exit Function
        End If

        If StrComp( _
            Left$(rubyText, Len(prefixRuby)), _
            prefixRuby, _
            vbBinaryCompare) <> 0 Then

            Exit Function

        End If

        rubyText = _
            Mid$(rubyText, Len(prefixRuby) + 1)

    End If

    If Len(suffixRuby) > 0 Then

        If Len(rubyText) < Len(suffixRuby) Then
            Exit Function
        End If

        If StrComp( _
            Right$(rubyText, Len(suffixRuby)), _
            suffixRuby, _
            vbBinaryCompare) <> 0 Then

            Exit Function

        End If

        rubyText = _
            Left$( _
                rubyText, _
                Len(rubyText) - Len(suffixRuby))

    End If

    If Len(rubyText) = 0 Then
        Exit Function
    End If

    readOK = True
    GetExcelRubyText = rubyText
    Exit Function

ERR_HANDLER:

    readOK = False
    GetExcelRubyText = ""

End Function


'============================================================
' Excelの読みをひらがなへ統一
'============================================================
Private Function NormalizeRubyReading( _
        ByVal rubyText As String) As String

    Dim result As String

    On Error GoTo ERR_HANDLER

    result = rubyText
    result = Replace(result, " ", "")
    result = Replace(result, " ", "")
    result = Replace(result, vbCr, "")
    result = Replace(result, vbLf, "")

    If Len(result) = 0 Then
        Exit Function
    End If

    result = StrConv(result, vbHiragana)

    NormalizeRubyReading = result
    Exit Function

ERR_HANDLER:

    NormalizeRubyReading = ""

End Function


'============================================================
' srcの外側にある表記を、読み境界として安全に使えるか判定
'
' かな・空白・一般的な句読点だけならTrue。
' 漢字 / 英数字などが混じれば対応付けが曖昧なのでFalse。
'============================================================
Private Function TrySurfaceKanaToRubyBoundary( _
        ByVal surfaceText As String, _
        ByRef rubyBoundary As String) As Boolean

    Dim i As Long
    Dim ch As String
    Dim codePoint As Long
    Dim result As String

    rubyBoundary = ""
    TrySurfaceKanaToRubyBoundary = False

    On Error GoTo ERR_HANDLER

    For i = 1 To Len(surfaceText)

        ch = Mid$(surfaceText, i, 1)
        codePoint = AscW(ch)

        'ひらがな
        If codePoint >= &H3041 _
           And codePoint <= &H3096 Then

            result = result & ch

        'カタカナ
        ElseIf codePoint >= &H30A1 _
           And codePoint <= &H30FA Then

            result = _
                result & StrConv(ch, vbHiragana)

        '長音符は読み側にも残る。
        ElseIf ch = "ー" Then

            result = result & ch

        '空白・句読点・括弧類は読み境界には含めない。
        ElseIf IsRubyIgnorableBoundaryChar(ch) = True Then

            '何もしない

        Else

            Exit Function

        End If

    Next i

    rubyBoundary = result
    TrySurfaceKanaToRubyBoundary = True
    Exit Function

ERR_HANDLER:

    rubyBoundary = ""
    TrySurfaceKanaToRubyBoundary = False

End Function


'============================================================
' 読み境界で無視してよい記号
'============================================================
Private Function IsRubyIgnorableBoundaryChar( _
        ByVal ch As String) As Boolean

    Select Case ch

        Case " ", " ", vbTab, _
             "、", "。", ",", ".", ",", ".", _
             "・", "!", "?", "!", "?", _
             "「", "」", "『", "』", _
             "(", ")", "(", ")", _
             "[", "]", "[", "]", _
             "【", "】", "〈", "〉", "《", "》", _
             "…", "‥", ":", ";", ":", ";"

            IsRubyIgnorableBoundaryChar = True

        Case Else

            IsRubyIgnorableBoundaryChar = False

    End Select

End Function


'============================================================
' Word標準ルビから読み取得
'
' contextRng全体を一時文書へコピーし、src部分だけを選択する。
' これにより「食べる」→「食」=「た」のような送り仮名文脈を
' 元コードと同様に残す。
'
' 読み取得失敗時:
'   戻り値 = ""
'   readOK = False
'============================================================
Private Function GetWordRubyText( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range, _
        ByRef readOK As Boolean) As String

    Dim trAll As Word.Range
    Dim tr As Word.Range
    Dim fld As Word.Field

    Dim fldCode As String
    Dim rubyPart As String
    Dim rubyText As String

    Dim baseSize As Single
    Dim baseFont As String

    Dim contextText As String
    Dim relStart As Long
    Dim relEnd As Long

    Dim dlgResult As Long

    readOK = False
    GetWordRubyText = ""

    On Error GoTo ERR_HANDLER

    If mRubyTempDoc Is Nothing Then
        Exit Function
    End If

    If contextRng Is Nothing Then

        contextText = src.Text
        relStart = 0
        relEnd = Len(src.Text)

    ElseIf contextRng.Document Is src.Document _
       And contextRng.StoryType = src.StoryType _
       And contextRng.Start <= src.Start _
       And contextRng.End >= src.End Then

        contextText = contextRng.Text
        relStart = src.Start - contextRng.Start
        relEnd = src.End - contextRng.Start

    Else

        contextText = src.Text
        relStart = 0
        relEnd = Len(src.Text)

    End If

    If Len(contextText) = 0 Then
        Exit Function
    End If

    If relStart < 0 _
       Or relEnd <= relStart _
       Or relEnd > Len(contextText) Then

        contextText = src.Text
        relStart = 0
        relEnd = Len(src.Text)

    End If

    ResetRubyTempDocument

    '送り仮名込みの単語全体をコピー
    Set trAll = _
        mRubyTempDoc.Range(0, 0)

    trAll.Text = contextText

    Set trAll = _
        mRubyTempDoc.Range( _
            Start:=0, _
            End:=Len(contextText))

    baseSize = _
        GetBaseFontSize(src)

    baseFont = _
        GetBaseFontName(src)

    If baseSize > 0 Then
        trAll.Font.Size = baseSize
    End If

    If Len(baseFont) > 0 Then

        On Error Resume Next

        trAll.Font.NameFarEast = baseFont
        trAll.Font.Name = baseFont

        On Error GoTo ERR_HANDLER

    End If

    Set tr = _
        mRubyTempDoc.Range( _
            Start:=relStart, _
            End:=relEnd)

    If tr.Characters.Count < 1 Then
        Exit Function
    End If

    If RubyCancelRequested() = True Then
        Exit Function
    End If

    mRubyTempDoc.Activate
    tr.Select

    dlgResult = _
        Application.Dialogs( _
            wdDialogPhoneticGuide).Show(1)

    DoEvents

    '重要:
    ' .Show(1) の 1 は約1msのTimeOut指定。
    ' そのため dlgResult=0 を無条件にユーザーCancelとは判定しない。
    ' EscはGetAsyncKeyStateで独立して検知する。
    If RubyCancelRequested() = True Then
        readOK = False
        GetWordRubyText = ""
        Exit Function
    End If

    '読み取得失敗の場合はFieldが無いので空Boxへ回す。
    rubyText = ""

    For Each fld In mRubyTempDoc.Fields

        fldCode = fld.Code.Text

        rubyPart = _
            ExtractRubyFromFieldCode(fldCode)

        If Len(rubyPart) > 0 Then
            rubyText = rubyText & rubyPart
        End If

    Next fld

    If Len(rubyText) > 0 Then

        readOK = True
        GetWordRubyText = rubyText

    Else

        readOK = False
        GetWordRubyText = ""

    End If

    Exit Function

ERR_HANDLER:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
    End If

    readOK = False
    GetWordRubyText = ""

End Function


'============================================================
' 古文読み優先でルビ文字列を取得
'
' 優先順位:
'   1. 文脈付き古文辞書
'   2. 対象文字列そのものの古文辞書
'   3. Word標準ルビへフォールバック
'
' 例:本文が「給ふ」で対象Rangeが「給」の場合、
' ルビは「たま」にする。送り仮名「ふ」は本文側に残るため、
' 辞書には「たまふ」ではなく「たま」を登録する。
'============================================================
Private Function GetKobunRubyText( _
        ByVal src As Word.Range, _
        ByVal contextRng As Word.Range, _
        ByRef readOK As Boolean, _
        ByRef usedKobun As Boolean) As String

    Dim targetText As String
    Dim contextText As String
    Dim rubyText As String

    readOK = False
    usedKobun = False
    GetKobunRubyText = ""

    On Error GoTo FALLBACK_WORD

    If RubyCancelRequested() = True Then
        Exit Function
    End If

    targetText = _
        NormalizeKobunLookupText(src.Text)

    If contextRng Is Nothing Then
        contextText = targetText
    Else
        contextText = _
            NormalizeKobunLookupText(contextRng.Text)
    End If

    If Len(targetText) = 0 Then
        GoTo FALLBACK_WORD
    End If

    EnsureKobunDictionaries

    'まず送り仮名・活用形を含む文脈辞書を優先。
    rubyText = _
        LookupKobunContextRuby( _
            targetText, _
            contextText)

    '文脈辞書に無ければ、対象文字列の完全一致辞書。
    If Len(rubyText) = 0 Then

        If Not mKobunExact Is Nothing Then

            If mKobunExact.Exists(targetText) Then
                rubyText = CStr(mKobunExact(targetText))
            End If

        End If

    End If

    If Len(rubyText) > 0 Then

        usedKobun = True
        readOK = True
        GetKobunRubyText = rubyText
        Exit Function

    End If

FALLBACK_WORD:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
    End If

    If mRubyCancelled = True Then
        readOK = False
        GetKobunRubyText = ""
        Exit Function
    End If

    '辞書に無い語は、高速読み経路へ。
    'FAST_RUBY_MODE=Falseなら従来どおりWord標準ルビを使う。
    GetKobunRubyText = _
        GetSmartRubyText( _
            src, _
            contextRng, _
            readOK)

End Function


'============================================================
' 古文辞書検索用にRange文字列を正規化
' Wordの段落末・セル末などだけ除き、本文中の仮名は変更しない。
'============================================================
Private Function NormalizeKobunLookupText( _
        ByVal textValue As String) As String

    Dim result As String

    result = textValue

    result = Replace(result, vbCr, "")
    result = Replace(result, vbLf, "")
    result = Replace(result, ChrW(7), "")

    NormalizeKobunLookupText = Trim$(result)

End Function


'============================================================
' 古文読み辞書の初期化
'
' 【追加方法】
' 単語そのものが古文読みになるもの:
'   AddKobunExactEntry "今日", "けふ"
'
' 送り仮名・活用形まで見て決めたいもの:
'   AddKobunContextEntry "給ふ", "給", "たま"
'
' 第2引数は実際にRubyBoxを付ける漢字Range、
' 第3引数はその漢字部分だけの読みを指定する。
'============================================================
Private Sub EnsureKobunDictionaries()

    On Error GoTo ERR_HANDLER

    If Not mKobunExact Is Nothing Then

        If Not mKobunContext Is Nothing Then
            Exit Sub
        End If

    End If

    Set mKobunExact = _
        CreateObject("Scripting.Dictionary")

    Set mKobunContext = _
        CreateObject("Scripting.Dictionary")

    mKobunExact.CompareMode = vbBinaryCompare
    mKobunContext.CompareMode = vbBinaryCompare

    '--------------------------------------------------------
    ' 単語完全一致辞書
    ' 必要な語はこの欄へ追加する。
    '--------------------------------------------------------
    AddKobunExactEntry "今日", "けふ"
    AddKobunExactEntry "昨日", "きのふ"
    AddKobunExactEntry "一昨日", "をととひ"
    AddKobunExactEntry "蝶", "てふ"

    '--------------------------------------------------------
    ' 文脈辞書
    ' 送り仮名を本文に残すため、Ruby文字列は漢字部分だけを登録。
    '--------------------------------------------------------

    '給ふ → たまふ
    AddKobunContextEntry "給は", "給", "たま"
    AddKobunContextEntry "給ひ", "給", "たま"
    AddKobunContextEntry "給ふ", "給", "たま"
    AddKobunContextEntry "給へ", "給", "たま"

    '宣ふ → のたまふ
    AddKobunContextEntry "宣は", "宣", "のたま"
    AddKobunContextEntry "宣ひ", "宣", "のたま"
    AddKobunContextEntry "宣ふ", "宣", "のたま"
    AddKobunContextEntry "宣へ", "宣", "のたま"

    '候ふ → さうらふ
    AddKobunContextEntry "候は", "候", "さうら"
    AddKobunContextEntry "候ひ", "候", "さうら"
    AddKobunContextEntry "候ふ", "候", "さうら"
    AddKobunContextEntry "候へ", "候", "さうら"

    '侍り → はべり
    AddKobunContextEntry "侍ら", "侍", "はべ"
    AddKobunContextEntry "侍り", "侍", "はべ"
    AddKobunContextEntry "侍る", "侍", "はべ"
    AddKobunContextEntry "侍れ", "侍", "はべ"

    '参る → まゐる
    AddKobunContextEntry "参ら", "参", "まゐ"
    AddKobunContextEntry "参り", "参", "まゐ"
    AddKobunContextEntry "参る", "参", "まゐ"
    AddKobunContextEntry "参れ", "参", "まゐ"

    '居る → ゐる
    AddKobunContextEntry "居ら", "居", "ゐ"
    AddKobunContextEntry "居り", "居", "ゐ"
    AddKobunContextEntry "居る", "居", "ゐ"
    AddKobunContextEntry "居れ", "居", "ゐ"

    '用ゐる → もちゐる
    AddKobunContextEntry "用ゐ", "用", "もち"

    Exit Sub

ERR_HANDLER:

    Set mKobunExact = Nothing
    Set mKobunContext = Nothing

End Sub


'============================================================
' 古文・単語完全一致辞書へ追加
'============================================================
Private Sub AddKobunExactEntry( _
        ByVal targetText As String, _
        ByVal rubyText As String)

    If mKobunExact Is Nothing Then
        Exit Sub
    End If

    If Len(targetText) = 0 Or Len(rubyText) = 0 Then
        Exit Sub
    End If

    mKobunExact(targetText) = rubyText

End Sub


'============================================================
' 古文・文脈辞書へ追加
' キー = 文脈文字列 + TAB + 対象漢字Range
'============================================================
Private Sub AddKobunContextEntry( _
        ByVal contextText As String, _
        ByVal targetText As String, _
        ByVal rubyText As String)

    Dim keyText As String

    If mKobunContext Is Nothing Then
        Exit Sub
    End If

    If Len(contextText) = 0 _
       Or Len(targetText) = 0 _
       Or Len(rubyText) = 0 Then

        Exit Sub

    End If

    keyText = _
        contextText & vbTab & targetText

    mKobunContext(keyText) = rubyText

End Sub


'============================================================
' 文脈辞書を検索
' contextRngがWord.Words由来で前後を少し含んでも拾えるよう、
' 文脈文字列は完全一致ではなく包含一致で判定する。
'============================================================
Private Function LookupKobunContextRuby( _
        ByVal targetText As String, _
        ByVal contextText As String) As String

    Dim keyValue As Variant
    Dim parts() As String
    Dim phraseText As String
    Dim keyTarget As String

    LookupKobunContextRuby = ""

    On Error GoTo ERR_HANDLER

    If mKobunContext Is Nothing Then
        Exit Function
    End If

    For Each keyValue In mKobunContext.Keys

        parts = Split(CStr(keyValue), vbTab)

        If UBound(parts) >= 1 Then

            phraseText = parts(0)
            keyTarget = parts(1)

            If StrComp( _
                keyTarget, _
                targetText, _
                vbBinaryCompare) = 0 Then

                If InStr( _
                    1, _
                    contextText, _
                    phraseText, _
                    vbBinaryCompare) > 0 Then

                    LookupKobunContextRuby = _
                        CStr(mKobunContext(keyValue))

                    Exit Function

                End If

            End If

        End If

    Next keyValue

    Exit Function

ERR_HANDLER:

    LookupKobunContextRuby = ""

End Function


'============================================================
' EQフィールドコードから読み文字列を取得
'============================================================
Private Function ExtractRubyFromFieldCode( _
        ByVal fldCode As String) As String

    Dim p As Long
    Dim pOpen As Long
    Dim pClose As Long

    On Error GoTo ERR_HANDLER

    p = _
        InStr(1, fldCode, "\up", vbTextCompare)

    If p = 0 Then
        p = InStr(1, fldCode, "\do", vbTextCompare)
    End If

    If p = 0 Then
        Exit Function
    End If

    pOpen = _
        InStr(p, fldCode, "(")

    If pOpen = 0 Then
        Exit Function
    End If

    pClose = _
        InStr(pOpen + 1, fldCode, "),")

    If pClose = 0 Then
        pClose = InStr(pOpen + 1, fldCode, ")")
    End If

    If pClose = 0 Then
        Exit Function
    End If

    ExtractRubyFromFieldCode = _
        Mid$( _
            fldCode, _
            pOpen + 1, _
            pClose - pOpen - 1)

    Exit Function

ERR_HANDLER:

    ExtractRubyFromFieldCode = ""

End Function


'============================================================
' 一時文書を空にする
'============================================================
Private Sub ResetRubyTempDocument()

    Dim rr As Word.Range

    On Error Resume Next

    Set rr = mRubyTempDoc.Content

    If rr.End > rr.Start Then
        rr.End = rr.End - 1
    End If

    rr.Delete

    On Error GoTo 0

End Sub


'============================================================
' 対象Rangeのページ上実座標を取得
'
' TextFrameStoryでも、対象を画面へ出した後で
' wdHorizontal/VerticalPositionRelativeToPageを取得する。
'============================================================
Private Function GetTargetGeometry( _
        ByVal target As Word.Range, _
        ByRef baseLeft As Single, _
        ByRef baseTop As Single, _
        ByRef baseWidth As Single, _
        ByRef baseHeight As Single) As Boolean

    On Error GoTo ERR_HANDLER

    If IsVerticalRubyTarget(target) = True Then

        GetTargetGeometry = _
            GetVerticalTargetGeometry( _
                target, _
                baseLeft, _
                baseTop, _
                baseWidth, _
                baseHeight)

    Else

        GetTargetGeometry = _
            GetHorizontalTargetGeometry( _
                target, _
                baseLeft, _
                baseTop, _
                baseWidth, _
                baseHeight)

    End If

    Exit Function

ERR_HANDLER:

    GetTargetGeometry = False

End Function


'============================================================
' 対象Rangeが縦書きか判定
'
' Word.Range.Orientation を第一判定に使う。
' 日本語縦書きは通常 wdTextOrientationVerticalFarEast。
' wdTextOrientationVertical も縦書きとして扱う。
' 取得失敗時は近傍文字のページ座標から進行方向を推定する。
'============================================================
Private Function IsVerticalRubyTarget( _
        ByVal target As Word.Range) As Boolean

    Dim orientationValue As Long

    On Error GoTo GEOMETRY_FALLBACK

    orientationValue = _
        CLng(target.Orientation)

    Select Case orientationValue

        Case wdTextOrientationVerticalFarEast, _
             wdTextOrientationVertical

            IsVerticalRubyTarget = True
            Exit Function

        Case wdTextOrientationHorizontal, _
             wdTextOrientationHorizontalRotatedFarEast

            IsVerticalRubyTarget = False
            Exit Function

        Case Else

            'Downward/Upward等の特殊方向や予期しない値は
            '実座標から進行方向を推定する。
            GoTo GEOMETRY_FALLBACK

    End Select

GEOMETRY_FALLBACK:

    IsVerticalRubyTarget = _
        InferVerticalFromRangeGeometry(target)

End Function


'============================================================
' Orientation取得不能時の進行方向フォールバック
'
' 対象自身または同一Story内の近傍1文字とのページ座標差を見て、
' Y方向の移動量がX方向より大きければ縦書きと推定する。
' あくまでOrientation取得不能時だけ使う保険であり、
' 通常の縦横判定には影響しない。
'============================================================
Private Function InferVerticalFromRangeGeometry( _
        ByVal target As Word.Range) As Boolean

    Dim refChar As Word.Range
    Dim candidate As Word.Range
    Dim storyRng As Word.Range

    Dim offset As Long
    Dim pos As Long

    Dim dx As Double
    Dim dy As Double

    On Error GoTo FALLBACK_HORIZONTAL

    If target Is Nothing Then
        Exit Function
    End If

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    Set refChar = _
        target.Characters(1).Duplicate

    If target.Characters.Count >= 2 Then

        Set candidate = _
            target.Characters( _
                target.Characters.Count).Duplicate

        If TryGetRangeDirectionDelta( _
            refChar, _
            candidate, _
            dx, _
            dy) = True Then

            InferVerticalFromRangeGeometry = _
                (dy > dx)

            Exit Function

        End If

    End If

    Set storyRng = _
        target.Duplicate

    storyRng.Expand _
        Unit:=wdStory

    For offset = 1 To RUBY_SCALE_SEARCH_CHARS

        pos = refChar.Start + offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetRangeDirectionDelta( _
                refChar, _
                candidate, _
                dx, _
                dy) = True Then

                InferVerticalFromRangeGeometry = _
                    (dy > dx)

                Exit Function

            End If

        End If

        pos = refChar.Start - offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetRangeDirectionDelta( _
                refChar, _
                candidate, _
                dx, _
                dy) = True Then

                InferVerticalFromRangeGeometry = _
                    (dy > dx)

                Exit Function

            End If

        End If

    Next offset

FALLBACK_HORIZONTAL:

    '判定材料が無い極端に短いStoryではWord既定の横書き扱い。
    InferVerticalFromRangeGeometry = False

End Function


'============================================================
' 2文字Range間のページ座標差を取得
'============================================================
Private Function TryGetRangeDirectionDelta( _
        ByVal a As Word.Range, _
        ByVal b As Word.Range, _
        ByRef dx As Double, _
        ByRef dy As Double) As Boolean

    Dim pageA As Long
    Dim pageB As Long

    Dim xA As Single
    Dim yA As Single
    Dim xB As Single
    Dim yB As Single

    On Error GoTo ERR_HANDLER

    dx = 0
    dy = 0

    pageA = _
        CLng(a.Information( _
            wdActiveEndPageNumber))

    pageB = _
        CLng(b.Information( _
            wdActiveEndPageNumber))

    If pageA <= 0 Or pageB <= 0 Then
        Exit Function
    End If

    If pageA <> pageB Then
        Exit Function
    End If

    xA = CSng(a.Information( _
        wdHorizontalPositionRelativeToPage))

    yA = CSng(a.Information( _
        wdVerticalPositionRelativeToPage))

    xB = CSng(b.Information( _
        wdHorizontalPositionRelativeToPage))

    yB = CSng(b.Information( _
        wdVerticalPositionRelativeToPage))

    If xA < 0 Or yA < 0 Or xB < 0 Or yB < 0 Then
        Exit Function
    End If

    dx = Abs(CDbl(xB) - CDbl(xA))
    dy = Abs(CDbl(yB) - CDbl(yA))

    If dx < 0.5 And dy < 0.5 Then
        Exit Function
    End If

    TryGetRangeDirectionDelta = True
    Exit Function

ERR_HANDLER:

    dx = 0
    dy = 0
    TryGetRangeDirectionDelta = False

End Function


'============================================================
' 縦書き対象Rangeのページ上実座標を取得
'============================================================
Private Function GetVerticalTargetGeometry( _
        ByVal target As Word.Range, _
        ByRef baseLeft As Single, _
        ByRef baseTop As Single, _
        ByRef baseWidth As Single, _
        ByRef baseHeight As Single) As Boolean

    Dim firstChar As Word.Range
    Dim lastChar As Word.Range

    Dim x1 As Single
    Dim y1 As Single
    Dim x2 As Single
    Dim y2 As Single

    Dim fs As Single
    Dim measuredWidth As Single

    On Error GoTo ERR_HANDLER

    target.Document.Activate

    If ActiveWindow.View.Type = wdOutlineView Then
        Exit Function
    End If

    'まずScrollIntoView。TextFrameで失敗する環境ではSelectをフォールバック。
    On Error Resume Next

    Err.Clear
    ActiveWindow.ScrollIntoView _
        Obj:=target, _
        Start:=True

    If Err.Number <> 0 Then
        Err.Clear
        target.Select
    End If

    On Error GoTo ERR_HANDLER

    DoEvents

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    Set firstChar = _
        target.Characters(1).Duplicate

    Set lastChar = _
        target.Characters( _
            target.Characters.Count).Duplicate

    x1 = CSng( _
        firstChar.Information( _
            wdHorizontalPositionRelativeToPage))

    y1 = CSng( _
        firstChar.Information( _
            wdVerticalPositionRelativeToPage))

    x2 = CSng( _
        lastChar.Information( _
            wdHorizontalPositionRelativeToPage))

    y2 = CSng( _
        lastChar.Information( _
            wdVerticalPositionRelativeToPage))

    '画面外なら一度だけSelectして再試行
    If x1 < 0 Or y1 < 0 Or x2 < 0 Or y2 < 0 Then

        target.Select
        DoEvents

        x1 = CSng( _
            firstChar.Information( _
                wdHorizontalPositionRelativeToPage))

        y1 = CSng( _
            firstChar.Information( _
                wdVerticalPositionRelativeToPage))

        x2 = CSng( _
            lastChar.Information( _
                wdHorizontalPositionRelativeToPage))

        y2 = CSng( _
            lastChar.Information( _
                wdVerticalPositionRelativeToPage))

    End If

    If x1 < 0 Or y1 < 0 Or x2 < 0 Or y2 < 0 Then
        Exit Function
    End If

    fs = _
        GetBaseFontSize(target)

    If fs <= 0 Then
        Exit Function
    End If

    '縦書きで同一列か確認
    If Abs(x1 - x2) > _
        MaxSingle(fs * 0.8, 2) Then

        Exit Function

    End If

    baseLeft = _
        MinSingle(x1, x2)

    baseTop = _
        MinSingle(y1, y2)

    '--------------------------------------------------------
    ' ここが今回の本修正。
    '
    ' 旧:
    '   baseWidth = fs
    '
    ' 新:
    '   Window.GetPoint が返す実画面幅(px)を、
    '   同一縦列の「Page.Y(pt)差 / Screen.Top(px)差」から
    '   その場で求めた px/pt 比率でptへ戻す。
    '
    ' 実測に失敗したときだけ旧仕様(fs)へフォールバックする。
    '--------------------------------------------------------
    measuredWidth = _
        GetActualBaseWidthInPoints( _
            target, _
            fs)

    If measuredWidth > 0 Then
        baseWidth = measuredWidth
    Else
        baseWidth = fs
    End If

    'Y方向は今回のX問題と独立しているため従来計算を維持。
    baseHeight = _
        Abs(y2 - y1) + fs

    GetVerticalTargetGeometry = True
    Exit Function

ERR_HANDLER:

    GetVerticalTargetGeometry = False

End Function


'============================================================
' 横書き対象Rangeのページ上実座標を取得
'
' 縦書き版と同じく、Wordが実際に描画した結果を基準にする。
' 横書きでは同一行を確認し、GetPointの画面矩形を
' 同一行の文字間隔から求めたpx/pt比率で文書ptへ戻す。
' 実測できない場合だけ文字座標+フォントサイズへフォールバック。
'============================================================
Private Function GetHorizontalTargetGeometry( _
        ByVal target As Word.Range, _
        ByRef baseLeft As Single, _
        ByRef baseTop As Single, _
        ByRef baseWidth As Single, _
        ByRef baseHeight As Single) As Boolean

    Dim firstChar As Word.Range
    Dim lastChar As Word.Range

    Dim x1 As Single
    Dim y1 As Single
    Dim x2 As Single
    Dim y2 As Single

    Dim fs As Single
    Dim measuredWidth As Single
    Dim measuredHeight As Single

    On Error GoTo ERR_HANDLER

    target.Document.Activate

    If ActiveWindow.View.Type = wdOutlineView Then
        Exit Function
    End If

    On Error Resume Next

    Err.Clear
    ActiveWindow.ScrollIntoView _
        Obj:=target, _
        Start:=True

    If Err.Number <> 0 Then
        Err.Clear
        target.Select
    End If

    On Error GoTo ERR_HANDLER

    DoEvents

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    Set firstChar = _
        target.Characters(1).Duplicate

    Set lastChar = _
        target.Characters( _
            target.Characters.Count).Duplicate

    x1 = CSng( _
        firstChar.Information( _
            wdHorizontalPositionRelativeToPage))

    y1 = CSng( _
        firstChar.Information( _
            wdVerticalPositionRelativeToPage))

    x2 = CSng( _
        lastChar.Information( _
            wdHorizontalPositionRelativeToPage))

    y2 = CSng( _
        lastChar.Information( _
            wdVerticalPositionRelativeToPage))

    If x1 < 0 Or y1 < 0 Or x2 < 0 Or y2 < 0 Then

        target.Select
        DoEvents

        x1 = CSng( _
            firstChar.Information( _
                wdHorizontalPositionRelativeToPage))

        y1 = CSng( _
            firstChar.Information( _
                wdVerticalPositionRelativeToPage))

        x2 = CSng( _
            lastChar.Information( _
                wdHorizontalPositionRelativeToPage))

        y2 = CSng( _
            lastChar.Information( _
                wdVerticalPositionRelativeToPage))

    End If

    If x1 < 0 Or y1 < 0 Or x2 < 0 Or y2 < 0 Then
        Exit Function
    End If

    fs = _
        GetBaseFontSize(target)

    If fs <= 0 Then
        Exit Function
    End If

    '横書きで同一行か確認。
    If Abs(y1 - y2) > _
        MaxSingle(fs * 0.8, 2) Then

        Exit Function

    End If

    baseLeft = _
        MinSingle(x1, x2)

    baseTop = _
        MinSingle(y1, y2)

    If GetActualHorizontalBaseSizeInPoints( _
        target, _
        fs, _
        measuredWidth, _
        measuredHeight) = True Then

        baseWidth = measuredWidth
        baseHeight = measuredHeight

    Else

        '実測不能時のみ従来型の幾何計算へフォールバック。
        baseWidth = _
            Abs(x2 - x1) + fs

        baseHeight = fs

    End If

    GetHorizontalTargetGeometry = True
    Exit Function

ERR_HANDLER:

    GetHorizontalTargetGeometry = False

End Function


'============================================================
' Window.GetPointでRangeの画面矩形を取得
'
' GetPointはRange全体が画面に見えていないとエラーになるため、
' 呼び出し側でScrollIntoViewした状態を前提にFalseで返す。
'============================================================
Private Function GetRangeScreenRect( _
        ByVal target As Word.Range, _
        ByRef pxLeft As Long, _
        ByRef pxTop As Long, _
        ByRef pxWidth As Long, _
        ByRef pxHeight As Long) As Boolean

    On Error GoTo ERR_HANDLER

    pxLeft = 0
    pxTop = 0
    pxWidth = 0
    pxHeight = 0

    ActiveWindow.GetPoint _
        pxLeft, _
        pxTop, _
        pxWidth, _
        pxHeight, _
        target

    If pxWidth <= 0 Or pxHeight <= 0 Then
        Exit Function
    End If

    GetRangeScreenRect = True
    Exit Function

ERR_HANDLER:

    GetRangeScreenRect = False

End Function


'============================================================
' Wordが対象Rangeへ実際に確保した横方向幅をptで取得
'
' 1) targetのGetPoint.Width(px)を取得
' 2) 同じ縦列の近傍文字からpx/ptを実測
' 3) WidthPx / (px/pt) で文書ptへ戻す
'
' 実測スケールが取れない場合は、
' Word.Application.PixelsToPoints + 現在Zoomをフォールバックに使う。
'============================================================
Private Function GetActualBaseWidthInPoints( _
        ByVal target As Word.Range, _
        ByVal baseSize As Single) As Single

    Dim pxLeft As Long
    Dim pxTop As Long
    Dim pxWidth As Long
    Dim pxHeight As Long

    Dim pxPerPt As Double
    Dim widthPt As Double

    Dim zoomPct As Long

    On Error GoTo ERR_HANDLER

    If baseSize <= 0 Then
        Exit Function
    End If

    If GetRangeScreenRect( _
        target, _
        pxLeft, _
        pxTop, _
        pxWidth, _
        pxHeight) = False Then

        Exit Function

    End If

    pxPerPt = _
        GetScreenPixelsPerPoint( _
            target, _
            baseSize)

    If pxPerPt > 0 Then

        widthPt = _
            CDbl(pxWidth) / pxPerPt

    Else

        '近傍2点でスケールが測れない極端に短いStory等の保険。
        'PixelsToPointsは表示デバイス換算なので、
        '現在のWord Zoom分だけ文書座標へ戻す。
        widthPt = _
            CDbl(Application.PixelsToPoints( _
                CSng(pxWidth), _
                False))

        zoomPct = _
            ActiveWindow.View.Zoom.Percentage

        If zoomPct > 0 Then
            widthPt = _
                widthPt * 100# / CDbl(zoomPct)
        End If

    End If

    'GetPoint失敗や一時的な再レイアウト値を弾く。
    If widthPt < _
            CDbl(baseSize) * RUBY_WIDTH_MIN_RATIO _
       Or widthPt > _
            CDbl(baseSize) * RUBY_WIDTH_MAX_RATIO Then

        Exit Function

    End If

    GetActualBaseWidthInPoints = _
        CSng(widthPt)

    Exit Function

ERR_HANDLER:

    GetActualBaseWidthInPoints = 0

End Function


'============================================================
' 同一縦列の2文字から ScreenPixel / PagePoint を実測
'
' targetが2文字以上ならまず先頭/末尾を使用。
' 1文字の場合や差が取れない場合は、Story内の前後24文字を探索する。
'============================================================
Private Function GetScreenPixelsPerPoint( _
        ByVal target As Word.Range, _
        ByVal baseSize As Single) As Double

    Dim refChar As Word.Range
    Dim candidate As Word.Range
    Dim storyRng As Word.Range

    Dim offset As Long
    Dim pos As Long

    Dim scaleValue As Double

    On Error GoTo ERR_HANDLER

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    Set refChar = _
        target.Characters(1).Duplicate

    '対象自身が複数文字なら、まず同一対象内で測る。
    If target.Characters.Count >= 2 Then

        Set candidate = _
            target.Characters( _
                target.Characters.Count).Duplicate

        If TryGetPixelsPerPointFromPair( _
            refChar, _
            candidate, _
            baseSize, _
            scaleValue) = True Then

            GetScreenPixelsPerPoint = scaleValue
            Exit Function

        End If

    End If

    Set storyRng = _
        target.Duplicate

    storyRng.Expand _
        Unit:=wdStory

    For offset = 1 To RUBY_SCALE_SEARCH_CHARS

        '後方
        pos = refChar.Start + offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetPixelsPerPointFromPair( _
                refChar, _
                candidate, _
                baseSize, _
                scaleValue) = True Then

                GetScreenPixelsPerPoint = scaleValue
                Exit Function

            End If

        End If

        '前方
        pos = refChar.Start - offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetPixelsPerPointFromPair( _
                refChar, _
                candidate, _
                baseSize, _
                scaleValue) = True Then

                GetScreenPixelsPerPoint = scaleValue
                Exit Function

            End If

        End If

    Next offset

    Exit Function

ERR_HANDLER:

    GetScreenPixelsPerPoint = 0

End Function


'============================================================
' 2つの1文字Rangeが同一縦列にある場合、px/ptを返す
'============================================================
Private Function TryGetPixelsPerPointFromPair( _
        ByVal a As Word.Range, _
        ByVal b As Word.Range, _
        ByVal baseSize As Single, _
        ByRef scaleValue As Double) As Boolean

    Dim pageA As Long
    Dim pageB As Long

    Dim xA As Single
    Dim yA As Single
    Dim xB As Single
    Dim yB As Single

    Dim leftA As Long
    Dim topA As Long
    Dim widthA As Long
    Dim heightA As Long

    Dim leftB As Long
    Dim topB As Long
    Dim widthB As Long
    Dim heightB As Long

    Dim dyPt As Double
    Dim dyPx As Double
    Dim candidateScale As Double

    On Error GoTo ERR_HANDLER

    scaleValue = 0

    If a Is Nothing Then
        Exit Function
    End If

    If b Is Nothing Then
        Exit Function
    End If

    pageA = _
        CLng(a.Information( _
            wdActiveEndPageNumber))

    pageB = _
        CLng(b.Information( _
            wdActiveEndPageNumber))

    If pageA <= 0 Or pageB <= 0 Then
        Exit Function
    End If

    If pageA <> pageB Then
        Exit Function
    End If

    xA = CSng(a.Information( _
        wdHorizontalPositionRelativeToPage))

    yA = CSng(a.Information( _
        wdVerticalPositionRelativeToPage))

    xB = CSng(b.Information( _
        wdHorizontalPositionRelativeToPage))

    yB = CSng(b.Information( _
        wdVerticalPositionRelativeToPage))

    If xA < 0 Or yA < 0 Or xB < 0 Or yB < 0 Then
        Exit Function
    End If

    '同じ縦列だけを採用する。
    If Abs(xA - xB) > _
        MaxSingle(baseSize * 0.8, 2) Then

        Exit Function

    End If

    dyPt = _
        Abs(CDbl(yB) - CDbl(yA))

    If dyPt < _
        CDbl(MaxSingle(baseSize * 0.4, 2)) Then

        Exit Function

    End If

    If GetRangeScreenRect( _
        a, _
        leftA, _
        topA, _
        widthA, _
        heightA) = False Then

        Exit Function

    End If

    If GetRangeScreenRect( _
        b, _
        leftB, _
        topB, _
        widthB, _
        heightB) = False Then

        Exit Function

    End If

    dyPx = _
        Abs(CDbl(topB) - CDbl(topA))

    If dyPx <= 0 Then
        Exit Function
    End If

    candidateScale = _
        dyPx / dyPt

    If candidateScale < RUBY_SCALE_MIN _
       Or candidateScale > RUBY_SCALE_MAX Then

        Exit Function

    End If

    scaleValue = candidateScale
    TryGetPixelsPerPointFromPair = True
    Exit Function

ERR_HANDLER:

    scaleValue = 0
    TryGetPixelsPerPointFromPair = False

End Function


'============================================================
' 横書き対象の実幅・実高さをptで取得
'
' GetPointのWidth/Height(px)を、同一行の近傍文字から実測した
' ScreenPixel / PagePoint 比率で文書ptへ戻す。
'============================================================
Private Function GetActualHorizontalBaseSizeInPoints( _
        ByVal target As Word.Range, _
        ByVal baseSize As Single, _
        ByRef widthPt As Single, _
        ByRef heightPt As Single) As Boolean

    Dim pxLeft As Long
    Dim pxTop As Long
    Dim pxWidth As Long
    Dim pxHeight As Long

    Dim pxPerPt As Double
    Dim measuredWidth As Double
    Dim measuredHeight As Double

    Dim zoomPct As Long

    On Error GoTo ERR_HANDLER

    widthPt = 0
    heightPt = 0

    If baseSize <= 0 Then
        Exit Function
    End If

    If GetRangeScreenRect( _
        target, _
        pxLeft, _
        pxTop, _
        pxWidth, _
        pxHeight) = False Then

        Exit Function

    End If

    pxPerPt = _
        GetHorizontalScreenPixelsPerPoint( _
            target, _
            baseSize)

    If pxPerPt > 0 Then

        measuredWidth = _
            CDbl(pxWidth) / pxPerPt

        measuredHeight = _
            CDbl(pxHeight) / pxPerPt

    Else

        measuredWidth = _
            CDbl(Application.PixelsToPoints( _
                CSng(pxWidth), _
                False))

        measuredHeight = _
            CDbl(Application.PixelsToPoints( _
                CSng(pxHeight), _
                True))

        zoomPct = _
            ActiveWindow.View.Zoom.Percentage

        If zoomPct > 0 Then

            measuredWidth = _
                measuredWidth * 100# / CDbl(zoomPct)

            measuredHeight = _
                measuredHeight * 100# / CDbl(zoomPct)

        End If

    End If

    If measuredWidth < _
            CDbl(baseSize) * RUBY_WIDTH_MIN_RATIO _
       Or measuredWidth > _
            CDbl(baseSize) * RUBY_WIDTH_MAX_RATIO Then

        Exit Function

    End If

    If measuredHeight < _
            CDbl(baseSize) * RUBY_HEIGHT_MIN_RATIO _
       Or measuredHeight > _
            CDbl(baseSize) * RUBY_HEIGHT_MAX_RATIO Then

        Exit Function

    End If

    widthPt = CSng(measuredWidth)
    heightPt = CSng(measuredHeight)

    GetActualHorizontalBaseSizeInPoints = True
    Exit Function

ERR_HANDLER:

    widthPt = 0
    heightPt = 0
    GetActualHorizontalBaseSizeInPoints = False

End Function


'============================================================
' 横書き同一行の2文字から ScreenPixel / PagePoint を実測
'============================================================
Private Function GetHorizontalScreenPixelsPerPoint( _
        ByVal target As Word.Range, _
        ByVal baseSize As Single) As Double

    Dim refChar As Word.Range
    Dim candidate As Word.Range
    Dim storyRng As Word.Range

    Dim offset As Long
    Dim pos As Long

    Dim scaleValue As Double

    On Error GoTo ERR_HANDLER

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    Set refChar = _
        target.Characters(1).Duplicate

    If target.Characters.Count >= 2 Then

        Set candidate = _
            target.Characters( _
                target.Characters.Count).Duplicate

        If TryGetHorizontalPixelsPerPointFromPair( _
            refChar, _
            candidate, _
            baseSize, _
            scaleValue) = True Then

            GetHorizontalScreenPixelsPerPoint = scaleValue
            Exit Function

        End If

    End If

    Set storyRng = _
        target.Duplicate

    storyRng.Expand _
        Unit:=wdStory

    For offset = 1 To RUBY_SCALE_SEARCH_CHARS

        pos = refChar.Start + offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetHorizontalPixelsPerPointFromPair( _
                refChar, _
                candidate, _
                baseSize, _
                scaleValue) = True Then

                GetHorizontalScreenPixelsPerPoint = scaleValue
                Exit Function

            End If

        End If

        pos = refChar.Start - offset

        If pos >= storyRng.Start _
           And pos < storyRng.End Then

            Set candidate = _
                storyRng.Duplicate

            candidate.SetRange _
                Start:=pos, _
                End:=pos + 1

            If TryGetHorizontalPixelsPerPointFromPair( _
                refChar, _
                candidate, _
                baseSize, _
                scaleValue) = True Then

                GetHorizontalScreenPixelsPerPoint = scaleValue
                Exit Function

            End If

        End If

    Next offset

    Exit Function

ERR_HANDLER:

    GetHorizontalScreenPixelsPerPoint = 0

End Function


'============================================================
' 2つの1文字Rangeが同一横行にある場合、px/ptを返す
'============================================================
Private Function TryGetHorizontalPixelsPerPointFromPair( _
        ByVal a As Word.Range, _
        ByVal b As Word.Range, _
        ByVal baseSize As Single, _
        ByRef scaleValue As Double) As Boolean

    Dim pageA As Long
    Dim pageB As Long

    Dim xA As Single
    Dim yA As Single
    Dim xB As Single
    Dim yB As Single

    Dim leftA As Long
    Dim topA As Long
    Dim widthA As Long
    Dim heightA As Long

    Dim leftB As Long
    Dim topB As Long
    Dim widthB As Long
    Dim heightB As Long

    Dim dxPt As Double
    Dim dxPx As Double
    Dim candidateScale As Double

    On Error GoTo ERR_HANDLER

    scaleValue = 0

    If a Is Nothing Then
        Exit Function
    End If

    If b Is Nothing Then
        Exit Function
    End If

    pageA = _
        CLng(a.Information( _
            wdActiveEndPageNumber))

    pageB = _
        CLng(b.Information( _
            wdActiveEndPageNumber))

    If pageA <= 0 Or pageB <= 0 Then
        Exit Function
    End If

    If pageA <> pageB Then
        Exit Function
    End If

    xA = CSng(a.Information( _
        wdHorizontalPositionRelativeToPage))

    yA = CSng(a.Information( _
        wdVerticalPositionRelativeToPage))

    xB = CSng(b.Information( _
        wdHorizontalPositionRelativeToPage))

    yB = CSng(b.Information( _
        wdVerticalPositionRelativeToPage))

    If xA < 0 Or yA < 0 Or xB < 0 Or yB < 0 Then
        Exit Function
    End If

    If Abs(yA - yB) > _
        MaxSingle(baseSize * 0.8, 2) Then

        Exit Function

    End If

    dxPt = _
        Abs(CDbl(xB) - CDbl(xA))

    If dxPt < _
        CDbl(MaxSingle(baseSize * 0.4, 2)) Then

        Exit Function

    End If

    If GetRangeScreenRect( _
        a, _
        leftA, _
        topA, _
        widthA, _
        heightA) = False Then

        Exit Function

    End If

    If GetRangeScreenRect( _
        b, _
        leftB, _
        topB, _
        widthB, _
        heightB) = False Then

        Exit Function

    End If

    dxPx = _
        Abs(CDbl(leftB) - CDbl(leftA))

    If dxPx <= 0 Then
        Exit Function
    End If

    candidateScale = _
        dxPx / dxPt

    If candidateScale < RUBY_SCALE_MIN _
       Or candidateScale > RUBY_SCALE_MAX Then

        Exit Function

    End If

    scaleValue = candidateScale
    TryGetHorizontalPixelsPerPointFromPair = True
    Exit Function

ERR_HANDLER:

    scaleValue = 0
    TryGetHorizontalPixelsPerPointFromPair = False

End Function


'============================================================
' Word再レイアウト後の対象Range座標を安定取得
'============================================================
Private Function GetStableTargetGeometry( _
        ByVal target As Word.Range, _
        ByRef baseLeft As Single, _
        ByRef baseTop As Single, _
        ByRef baseWidth As Single, _
        ByRef baseHeight As Single) As Boolean

    Dim i As Long

    Dim curLeft As Single
    Dim curTop As Single
    Dim curWidth As Single
    Dim curHeight As Single

    Dim prevLeft As Single
    Dim prevTop As Single
    Dim prevWidth As Single
    Dim prevHeight As Single

    Dim hasPrev As Boolean
    Dim hasValue As Boolean

    On Error GoTo ERR_HANDLER

    For i = 1 To RUBY_GEOMETRY_RETRY

        DoEvents

        If RubyCancelRequested() = True Then
            Exit Function
        End If

        If GetTargetGeometry( _
            target, _
            curLeft, _
            curTop, _
            curWidth, _
            curHeight) = True Then

            hasValue = True

            If hasPrev = True Then

                If Abs(curLeft - prevLeft) <= _
                        RUBY_GEOMETRY_TOLERANCE _
                   And Abs(curTop - prevTop) <= _
                        RUBY_GEOMETRY_TOLERANCE _
                   And Abs(curWidth - prevWidth) <= _
                        RUBY_GEOMETRY_TOLERANCE _
                   And Abs(curHeight - prevHeight) <= _
                        RUBY_GEOMETRY_TOLERANCE Then

                    baseLeft = curLeft
                    baseTop = curTop
                    baseWidth = curWidth
                    baseHeight = curHeight

                    GetStableTargetGeometry = True
                    Exit Function

                End If

            End If

            prevLeft = curLeft
            prevTop = curTop
            prevWidth = curWidth
            prevHeight = curHeight
            hasPrev = True

        End If

    Next i

    If hasValue = True Then

        baseLeft = prevLeft
        baseTop = prevTop
        baseWidth = prevWidth
        baseHeight = prevHeight

        GetStableTargetGeometry = True
        Exit Function

    End If

ERR_HANDLER:

    GetStableTargetGeometry = False

End Function


'============================================================
' RubyBoxを置くための安全なアンカーRangeを取得
'
' Word.Shapes.AddTextboxにはAnchor引数が無い。
' そのため:
'   MainTextStory → 対象段落先頭を選択
'   TextFrameStory等 → 対象ページのMainTextStoryを選択
' してからAddTextboxする。
'
' Shapeはアンカーと同じページに留まるため、
' TextFrame本文から作る場合でもRubyBox自体はMainTextStory側へ
' アンカーし、Left/Topはページ絶対座標で合わせる。
'============================================================
Private Function GetRubyAnchorRange( _
        ByVal target As Word.Range, _
        ByVal mainDoc As Word.Document) As Word.Range

    Dim anchorRng As Word.Range
    Dim pageNo As Long

    On Error GoTo FALLBACK

    If target.StoryType = wdMainTextStory Then

        If target.Paragraphs.Count > 0 Then

            Set anchorRng = _
                target.Paragraphs(1).Range.Duplicate

            anchorRng.Collapse _
                wdCollapseStart

            Set GetRubyAnchorRange = anchorRng
            Exit Function

        End If

    End If

    pageNo = _
        GetRangePageNumber(target)

    If pageNo > 0 Then

        Set anchorRng = _
            mainDoc.GoTo( _
                What:=wdGoToPage, _
                Which:=wdGoToAbsolute, _
                Count:=pageNo)

        anchorRng.Collapse _
            wdCollapseStart

        Set GetRubyAnchorRange = anchorRng
        Exit Function

    End If

FALLBACK:

    On Error Resume Next

    Set anchorRng = _
        mainDoc.Range(0, 0)

    Set GetRubyAnchorRange = anchorRng

    On Error GoTo 0

End Function


'============================================================
' テキストボックス作成
'============================================================
Private Function AddRubyTextBox( _
        ByVal mainDoc As Word.Document, _
        ByVal target As Word.Range, _
        ByVal rubyText As String, _
        ByVal baseSize As Single, _
        ByVal rubySize As Single, _
        ByVal fontName As String, _
        ByVal baseLeft As Single, _
        ByVal baseTop As Single, _
        ByVal baseWidth As Single, _
        ByVal baseHeight As Single, _
        ByVal sourceStoryType As Long, _
        ByVal sourceStoryKey As String, _
        ByVal sourcePage As Long, _
        ByVal sourceStart As Long, _
        ByVal sourceEnd As Long) As Boolean

    Dim shp As Word.Shape
    Dim anchorRng As Word.Range

    Dim rubyMargin As Single
    Dim rubyGap As Single

    Dim initWidth As Single
    Dim initHeight As Single

    Dim rubyLeft As Single
    Dim rubyTop As Single

    Dim lineAdjust As Single

    Dim shapeName As String

    Dim isEmptyRuby As Boolean
    Dim isVertical As Boolean
    Dim rubyOrientation As Long

    On Error GoTo ERR_HANDLER

    If RubyCancelRequested() = True Then
        Exit Function
    End If

    isEmptyRuby = _
        (Len(rubyText) = 0)

    isVertical = _
        IsVerticalRubyTarget(target)

    If isVertical = True Then
        rubyOrientation = msoTextOrientationVerticalFarEast
    Else
        rubyOrientation = msoTextOrientationHorizontal
    End If

    rubyMargin = _
        MaxSingle( _
            RUBY_MIN_MARGIN, _
            rubySize * 0.15)

    rubyGap = _
        MaxSingle( _
            RUBY_MIN_GAP, _
            rubySize * 0.1)

    If isVertical = True Then

        '縦書きRubyBox:細い幅+読みの長さに応じた高さ。
        initWidth = _
            rubySize * 2 + _
            rubyMargin * 2

        If isEmptyRuby = True Then

            initHeight = _
                MaxSingle( _
                    baseHeight, _
                    rubySize * 2 + _
                    rubyMargin * 2 + _
                    2)

        Else

            initHeight = _
                rubySize * Len(rubyText) + _
                rubyMargin * 2 + _
                2

            If initHeight < rubySize * 2 Then
                initHeight = rubySize * 2
            End If

        End If

    Else

        '横書きRubyBox:読みの長さに応じた幅+低い高さ。
        initHeight = _
            rubySize * 2 + _
            rubyMargin * 2

        If isEmptyRuby = True Then

            initWidth = _
                MaxSingle( _
                    baseWidth, _
                    rubySize * 2 + _
                    rubyMargin * 2 + _
                    2)

        Else

            initWidth = _
                rubySize * Len(rubyText) + _
                rubyMargin * 2 + _
                2

            If initWidth < rubySize * 2 Then
                initWidth = rubySize * 2
            End If

        End If

    End If

    '--------------------------------------------------------
    ' TextFrameStoryを選択したままShapeを作らない。
    ' RubyBoxのアンカー用MainTextStory Rangeを先に選択する。
    '--------------------------------------------------------
    Set anchorRng = _
        GetRubyAnchorRange( _
            target, _
            mainDoc)

    mainDoc.Activate

    If Not anchorRng Is Nothing Then
        anchorRng.Select
    End If

    DoEvents

    If RubyCancelRequested() = True Then
        Exit Function
    End If

    Set shp = _
        mainDoc.Shapes.AddTextbox( _
            Orientation:=rubyOrientation, _
            Left:=0, _
            Top:=0, _
            Width:=initWidth, _
            Height:=initHeight)

    If shp Is Nothing Then
        GoTo ERR_HANDLER
    End If

    '作成直後に本文非干渉・ページ基準を確定
    shp.WrapFormat.Type = wdWrapNone

    shp.RelativeHorizontalPosition = _
        wdRelativeHorizontalPositionPage

    shp.RelativeVerticalPosition = _
        wdRelativeVerticalPositionPage

    'Shape識別名
    mRubyShapeSeq = _
        mRubyShapeSeq + 1

    shapeName = _
        RUBYBOX_PREFIX & _
        Format$( _
            CLng(Timer * 100), _
            "00000000") & _
        "_" & _
        Format$( _
            mRubyShapeSeq, _
            "000000")

    On Error Resume Next
    shp.Name = shapeName
    On Error GoTo ERR_HANDLER

    '元Range情報。
    'Dialog/Shape作成前に固定した値を使い、
    'Range追随によるStart/Endの1文字ズレを防ぐ。
    shp.AlternativeText = _
        "RUBYBOX|" & _
        "ST=" & CStr(sourceStoryType) & "|" & _
        "SK=" & sourceStoryKey & "|" & _
        "P=" & CStr(sourcePage) & "|" & _
        "S=" & CStr(sourceStart) & "|" & _
        "E=" & CStr(sourceEnd) & "|" & _
        "O=" & IIf(isVertical, "V", "H") & "|" & _
        "EMPTY=" & CStr(Abs(CLng(isEmptyRuby))) & "|"

    With shp

        .Fill.Visible = msoFalse
        .Line.Visible = msoFalse

        On Error Resume Next
        .Shadow.Visible = msoFalse
        On Error GoTo ERR_HANDLER

        .WrapFormat.Type = wdWrapNone

        .RelativeHorizontalPosition = _
            wdRelativeHorizontalPositionPage

        .RelativeVerticalPosition = _
            wdRelativeVerticalPositionPage

        .LockAnchor = True

        On Error Resume Next
        .LayoutInCell = False
        On Error GoTo ERR_HANDLER

    End With

    With shp.TextFrame

        .Orientation = rubyOrientation

        .MarginLeft = rubyMargin
        .MarginRight = rubyMargin
        .MarginTop = rubyMargin
        .MarginBottom = rubyMargin

        .TextRange.Text = rubyText

        With .TextRange.Font

            .Size = rubySize

            If Len(fontName) > 0 Then

                On Error Resume Next
                .NameFarEast = fontName
                .Name = fontName
                On Error GoTo ERR_HANDLER

            End If

        End With

        With .TextRange.ParagraphFormat

            .SpaceBefore = 0
            .SpaceAfter = 0
            .LineSpacingRule = wdLineSpaceSingle
            .Alignment = wdAlignParagraphCenter

        End With

        '空文字をAutoSizeするとWordがBoxを極端に縮める場合がある。
        If isEmptyRuby = True Then
            .AutoSize = False
        Else
            .AutoSize = True
        End If

    End With

    'TextFrame2が利用可能なら折返し防止+自動サイズ
    On Error Resume Next

    shp.TextFrame2.WordWrap = msoFalse

    If isEmptyRuby = True Then
        shp.TextFrame2.AutoSize = msoAutoSizeNone
    Else
        shp.TextFrame2.AutoSize = msoAutoSizeShapeToFitText
    End If

    On Error GoTo ERR_HANDLER

    DoEvents

    If RubyCancelRequested() = True Then
        GoTo ERR_HANDLER
    End If

    'Shape追加後の再レイアウトを吸収するため親文字座標を再取得
    If GetStableTargetGeometry( _
        target, _
        baseLeft, _
        baseTop, _
        baseWidth, _
        baseHeight) = False Then

        GoTo ERR_HANDLER

    End If

    '空Boxは親文字の進行方向サイズに追従させる。
    If isEmptyRuby = True Then

        If isVertical = True Then

            If shp.Height < baseHeight Then
                shp.Height = baseHeight
            End If

        Else

            If shp.Width < baseWidth Then
                shp.Width = baseWidth
            End If

        End If

    End If

    If isVertical = True Then

        '----------------------------------------------------
        ' 縦書き:本文の右側へ配置。
        ' 既存Final版の実測セル幅補正をそのまま維持する。
        '----------------------------------------------------
        lineAdjust = _
            ((baseWidth - baseSize) - _
             (shp.Width - rubySize)) / 2

        rubyLeft = _
            baseLeft + _
            baseSize + _
            rubyGap + _
            lineAdjust + _
            RUBY_X_ADJUST

        rubyTop = _
            baseTop + _
            ((baseHeight - shp.Height) / 2) + _
            RUBY_Y_ADJUST

    Else
    
        '横書きは、実測した本文上端(baseTop)を基準に
        'RubyBoxの下端を本文へ寄せる。
        '
        '旧lineAdjustは大フォント時に過剰補正になるため、
        '横書きY位置には使用しない。
        
        rubyLeft = _
            baseLeft + _
            ((baseWidth - shp.Width) / 2) + _
            RUBY_H_X_ADJUST
        
        rubyTop = _
            baseTop - _
            shp.Height + _
            GetHorizontalRubyYAdjust(baseSize)

    End If

    If rubyLeft < 0 Then
        rubyLeft = 0
    End If

    If rubyTop < 0 Then
        rubyTop = 0
    End If

    shp.Left = rubyLeft
    shp.Top = rubyTop

    AddRubyTextBox = True
    Exit Function

ERR_HANDLER:

    If Err.Number = RUBY_USER_INTERRUPT_ERROR Then
        mRubyCancelled = True
    End If

    On Error Resume Next

    If Not shp Is Nothing Then
        shp.Delete
    End If

    On Error GoTo 0

    AddRubyTextBox = False

End Function


'============================================================
' 親文字フォントサイズ
'============================================================
Private Function GetBaseFontSize( _
        ByVal target As Word.Range) As Single

    Dim fs As Single

    On Error GoTo ERR_HANDLER

    If target.Characters.Count < 1 Then
        Exit Function
    End If

    fs = _
        target.Characters(1).Font.Size

    If fs <= 0 Then
        Exit Function
    End If

    GetBaseFontSize = fs
    Exit Function

ERR_HANDLER:

    GetBaseFontSize = 0

End Function


'============================================================
' 親文字日本語フォント
'============================================================
Private Function GetBaseFontName( _
        ByVal target As Word.Range) As String

    Dim strFont As String

    On Error Resume Next

    strFont = _
        target.Characters(1).Font.NameFarEast

    If Len(strFont) = 0 Then
        strFont = target.Characters(1).Font.Name
    End If

    GetBaseFontName = strFont

    On Error GoTo 0

End Function


'============================================================
' Rangeが存在する実ページ番号
'============================================================
Private Function GetRangePageNumber( _
        ByVal target As Word.Range) As Long

    Dim pageNo As Long

    GetRangePageNumber = -1

    On Error GoTo ERR_HANDLER

    target.Document.Activate

    pageNo = _
        CLng(target.Information( _
            wdActiveEndPageNumber))

    If pageNo > 0 Then
        GetRangePageNumber = pageNo
    End If

    Exit Function

ERR_HANDLER:

    GetRangePageNumber = -1

End Function


'============================================================
' Story識別キー
'
' TextFrameStoryでは複数の独立したStoryが同じStart/Endを
' 持ち得るため、StoryTypeだけでは不十分。
'
' Story先頭のページ座標+先頭文字列の簡易ハッシュを使って
' 誤削除を抑える。
'============================================================
Private Function GetRangeStoryKey( _
        ByVal target As Word.Range) As String

    Dim storyRng As Word.Range
    Dim firstChar As Word.Range

    Dim pageNo As Long
    Dim x As Single
    Dim y As Single

    Dim x10 As Long
    Dim y10 As Long

    Dim sampleText As String
    Dim h As Long

    On Error GoTo ERR_HANDLER

    If target.StoryType = wdMainTextStory Then

        GetRangeStoryKey = "MAIN"
        Exit Function

    End If

    Set storyRng = _
        target.Duplicate

    storyRng.Expand _
        Unit:=wdStory

    sampleText = _
        Left$(storyRng.Text, 64)

    h = _
        SimpleTextHash(sampleText)

    pageNo = _
        GetRangePageNumber(storyRng)

    x = -1
    y = -1

    If storyRng.Characters.Count > 0 Then

        Set firstChar = _
            storyRng.Characters(1).Duplicate

        On Error Resume Next

        target.Document.Activate

        ActiveWindow.ScrollIntoView _
            Obj:=firstChar, _
            Start:=True

        DoEvents

        x = CSng(firstChar.Information( _
            wdHorizontalPositionRelativeToPage))

        y = CSng(firstChar.Information( _
            wdVerticalPositionRelativeToPage))

        On Error GoTo ERR_HANDLER

    End If

    x10 = CLng(x * 10)
    y10 = CLng(y * 10)

    GetRangeStoryKey = _
        "ST" & CStr(CLng(target.StoryType)) & _
        "P" & CStr(pageNo) & _
        "X" & CStr(x10) & _
        "Y" & CStr(y10) & _
        "H" & CStr(h)

    Exit Function

ERR_HANDLER:

    GetRangeStoryKey = _
        "ST" & CStr(CLng(target.StoryType))

End Function


'============================================================
' 文字列の簡易ハッシュ
' VBA Longのオーバーフローを避けるためDoubleで剰余計算する。
'============================================================
Private Function SimpleTextHash( _
        ByVal textValue As String) As Long

    Dim i As Long
    Dim h As Double
    Dim codeValue As Long

    h = 5381#

    On Error GoTo ERR_HANDLER

    For i = 1 To Len(textValue)

        codeValue = _
            AscW(Mid$(textValue, i, 1))

        If codeValue < 0 Then
            codeValue = codeValue + 65536
        End If

        h = h * 33# + CDbl(codeValue)
        h = h - _
            Int(h / 2147483647#) * 2147483647#

    Next i

    SimpleTextHash = CLng(h)
    Exit Function

ERR_HANDLER:

    SimpleTextHash = 0

End Function


'============================================================
' 文書全体のRubyBox削除
'============================================================
Private Sub DeleteAllRubyBoxes( _
        ByVal doc As Word.Document)

    Dim i As Long

    On Error Resume Next

    For i = doc.Shapes.Count To 1 Step -1

        If IsRubyBoxShape(doc.Shapes(i)) Then
            doc.Shapes(i).Delete
        End If

    Next i

    On Error GoTo 0

End Sub


'============================================================
' 選択範囲内RubyBox削除
'
' MainTextStoryとTextFrameStoryではStart/Endの座標系が別。
' TextFrameStory同士でも独立StoryのStart/Endが重なるため、
' ST/SK/P/S/Eを組み合わせて判定する。
'============================================================
Private Sub DeleteRubyBoxesInRange( _
        ByVal doc As Word.Document, _
        ByVal rangeStoryType As Long, _
        ByVal rangeStoryKey As String, _
        ByVal rangePage As Long, _
        ByVal rangeStart As Long, _
        ByVal rangeEnd As Long)

    Dim i As Long
    Dim shp As Word.Shape

    Dim srcStoryType As Long
    Dim srcPage As Long
    Dim srcStart As Long
    Dim srcEnd As Long
    Dim srcStoryKey As String

    Dim storyOK As Boolean
    Dim pageOK As Boolean
    Dim keyOK As Boolean

    On Error Resume Next

    For i = doc.Shapes.Count To 1 Step -1

        Set shp = doc.Shapes(i)

        If IsRubyBoxShape(shp) Then

            srcStoryType = _
                GetMetaLong( _
                    shp.AlternativeText, _
                    "ST=")

            srcPage = _
                GetMetaLong( _
                    shp.AlternativeText, _
                    "P=")

            srcStart = _
                GetMetaLong( _
                    shp.AlternativeText, _
                    "S=")

            srcEnd = _
                GetMetaLong( _
                    shp.AlternativeText, _
                    "E=")

            srcStoryKey = _
                GetMetaText( _
                    shp.AlternativeText, _
                    "SK=")

            If srcStart >= 0 And srcEnd >= 0 Then

                '旧版メタ情報(ST無し)はMainTextStoryだけ互換削除。
                If srcStoryType < 0 Then

                    storyOK = _
                        (rangeStoryType = wdMainTextStory)

                Else

                    storyOK = _
                        (srcStoryType = rangeStoryType)

                End If

                If srcPage < 0 Or rangePage < 0 Then
                    pageOK = True
                Else
                    pageOK = (srcPage = rangePage)
                End If

                If Len(srcStoryKey) = 0 _
                   Or Len(rangeStoryKey) = 0 _
                   Or rangeStoryType = wdMainTextStory Then

                    keyOK = True

                Else

                    keyOK = _
                        (StrComp( _
                            srcStoryKey, _
                            rangeStoryKey, _
                            vbBinaryCompare) = 0)

                End If

                If storyOK = True _
                   And pageOK = True _
                   And keyOK = True Then

                    If srcEnd > rangeStart _
                       And srcStart < rangeEnd Then

                        shp.Delete

                    End If

                End If

            Else

                'メタ情報が無い非常に古いRubyBox。
                'MainTextStory選択時だけアンカー位置で代替判定する。
                If rangeStoryType = wdMainTextStory Then

                    If shp.Anchor.Start >= rangeStart _
                       And shp.Anchor.Start <= rangeEnd Then

                        shp.Delete

                    End If

                End If

            End If

        End If

    Next i

    On Error GoTo 0

End Sub


'============================================================
' RubyBox Shapeか
'============================================================
Private Function IsRubyBoxShape( _
        ByVal shp As Word.Shape) As Boolean

    On Error Resume Next

    If Left$( _
        shp.Name, _
        Len(RUBYBOX_PREFIX)) = _
        RUBYBOX_PREFIX Then

        IsRubyBoxShape = True
        Exit Function

    End If

    If Left$( _
        shp.AlternativeText, _
        8) = "RUBYBOX|" Then

        IsRubyBoxShape = True
        Exit Function

    End If

    IsRubyBoxShape = False

    On Error GoTo 0

End Function


'============================================================
' Shapeメタ情報からLong取得
'============================================================
Private Function GetMetaLong( _
        ByVal metaText As String, _
        ByVal keyText As String) As Long

    Dim tmp As String

    GetMetaLong = -1

    tmp = _
        GetMetaText( _
            metaText, _
            keyText)

    If Len(tmp) = 0 Then
        Exit Function
    End If

    If IsNumeric(tmp) Then

        On Error Resume Next
        GetMetaLong = CLng(tmp)
        On Error GoTo 0

    End If

End Function


'============================================================
' Shapeメタ情報から文字列取得
'============================================================
Private Function GetMetaText( _
        ByVal metaText As String, _
        ByVal keyText As String) As String

    Dim p As Long
    Dim q As Long

    GetMetaText = ""

    On Error GoTo ERR_HANDLER

    p = _
        InStr( _
            1, _
            metaText, _
            keyText, _
            vbTextCompare)

    If p = 0 Then
        Exit Function
    End If

    p = p + Len(keyText)

    q = _
        InStr(p, metaText, "|")

    If q = 0 Then
        q = Len(metaText) + 1
    End If

    GetMetaText = _
        Mid$( _
            metaText, _
            p, _
            q - p)

    Exit Function

ERR_HANDLER:

    GetMetaText = ""

End Function


'============================================================
' Single用 Max
'============================================================
Private Function MaxSingle( _
        ByVal a As Single, _
        ByVal b As Single) As Single

    If a > b Then
        MaxSingle = a
    Else
        MaxSingle = b
    End If

End Function


'============================================================
' Single用 Min
'============================================================
Private Function MinSingle( _
        ByVal a As Single, _
        ByVal b As Single) As Single

    If a < b Then
        MinSingle = a
    Else
        MinSingle = b
    End If

End Function

Private Function GetHorizontalRubyYAdjust( _
        ByVal baseSize As Single) As Single

    Dim adjustValue As Double

    If baseSize <= 0 Then
        GetHorizontalRubyYAdjust = _
            RUBY_H_Y_FINE_ADJUST
        Exit Function
    End If

    adjustValue = _
        CDbl(RUBY_H_Y_REFERENCE_ADJUST) * _
        CDbl(baseSize) / _
        CDbl(RUBY_H_Y_REFERENCE_FONT_SIZE)

    GetHorizontalRubyYAdjust = _
        CSng(adjustValue) + _
        RUBY_H_Y_FINE_ADJUST

End Function

    

コピーしました!