2025/12/13

訂正記事:GetAllEntriesByKey は本当に速いのか? - 再検証でわかった“逆転の真実”

2025 年 8 月 24 日に投稿した記事『LotusScript でビューを検索するメソッドの違い』で「GetAllDocumentsByKey より GetAllEntriesByKey のほうがパフォーマンス面で有利」と説明しました(以降、”前回の記事”といいます)。

ところが後日、実際に検証を行うとまったく逆の結果が出ました。まさかと思いつつ、改めて HCL テクニカルサポートに問い合わせたところ、私の検証と同じ結論(前回の結果の撤回)が返ってきました。今回の記事では、前回の記事の訂正とともに、両メソッドの特性について最新の知見をまとめておきます。


前回記事の要旨

前回の記事では、両メソッドの特徴を次のようにまとめました

GetAllDocumentsByKey 文書そのものを一つ一つコレクションすることから比較的に遅い
GetAllEntriesByKey ビュー索引を活用する仕様
索引は軽量であり、メモリ消費も少なく高速に動作

2025 年 8 月の Domino Lounge Osaka でこの話をしたところ、同じ理解をされている方が多数いたのでこれが ”定説” だったことがうかがえます。


検証したら...真逆!?

どれぐらいパフォーマンスに差があるのか気になり、簡易的に検証したところ、GetAllEntriesByKey の方が ”数倍遅い!?” という結果がでました。

測定した項目は次の 5 項目です。GetAllEntriesByKey では高速なはずの ColumnValues による値の取得だけでなく、文書オブジェクトを取得して、値を取得するテストも行いました。

シナリオ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 GetAllDocumentsByKey で検索し、コレクションを取得GetAllEntriesByKey で検索し、コレクションを取得
エントリ取得 GetFirstDocument、GetNextDocument で文書を取得GetFirstEntry、GetNextEntry でエントリを取得
値の取得 文書から値を取得ColumnValues で値を取得
文書の取得 Document プロパティで文書を取得
文書から値の取得 文書から値を取得

結果は次の通りで、GetAllDocumentsByKey の方がずいぶん早かったのです。

はじめは、私のテストケース設定が悪く特徴を捕まえ損ねたと考えましが、より明確にメソッドの特性を理解するチャンスとばかりに、検証に使用した NSF(データと検証のコード)をサポートに送付して質問しました。

冒頭に記載した通り、返答は驚きの結果でした。”定説” とは真逆で『GetAllDocumentsByKey の方が高速』とありました。かなり丁寧な検証を実施いただいたようで、複数のテストケースに対して目的、検証手順、結果、考察をまとめた詳細なレポートと検証に利用した NSF が添付されていました。


検証レポートの要点

レポートでは、両メソッドの挙動が処理フェーズごとに整理されていました。

処理フェーズ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 NotesDocumentCollection は内部構造がシンプルで軽量 NotesViewEntryCollection は内部構造が複雑で初期作成コストが高い
エントリ取得 GetNextDocument はコレクション内のポインタ移動だけで圧倒的に軽量 GetNextEntry は移動のたびに複雑なオブジェクトを生成・破棄が発生
値の取得 NotesDocument は読み込むフィールド数に比例してコスト増 NotesViewEntry は列数が増えると内部的な配列生成など処理コストが増加

表を見てわかるように、すべてのフェーズで GetAllDocumentsByKey  が優勢となっています。


”定説” 逆転の背景

HCL サポートの見解では、主に以下の2点の要因が指摘されていました。

I/O コストの低下 かつての HDD 環境では、文書データへのアクセスのようなランダムアクセスは大きな遅延要因であったが、現在の SSD 環境になり劇的に短縮。
ボトルネックの変化I/O コスト低下により処理時間の主たる要因がディスクアクセスからオブジェクト操作などのプログラムの内部処理へ移行
(ストレージの I/O → CPUやメモリ)

文書を開かずビュー索引を使用したほうが早いという ”定説” は、ストレージが HDD だった過去の環境から導き出されたものだったということです。

現在の環境においては、GetAllEntriesByKey の複雑な内部処理が圧倒的に重く、フィールド数が多いなど文書操作が重くなる状況でも逆転することはないとのことでした。


今どきの”定説”

HCL サポートからのレポートは私の簡易的な検証とも一致する結果でした。

現在の一般的な環境では『GetAllDocumentsByKey のほうがパフォーマンス面で有利』という結論です。

前回の記事の内容は、この知見を踏まえて、完全に撤回し訂正いたします。

ちなみに、GetAllEntriesByKey を使う利点は、次のような用途に限られます。

  • ビューのソート順通りに取得したい
  • ビューの列の値にアクセスしたい


検証は大事

今回のような訂正記事を書くハメになったのは、「メーカサポートの回答だから」と無条件に信じてしまったことが原因です。これは ”サポートを信頼できない” と言っているのではありません。自分で検証すると理解が深まり、結果的に応用の幅も広がるということを体験できてよかったと感じました。検証は技術者としてとても重要だと再確認できました。

今回の記事において、パフォーマンスを左右する要素としては

  • ストレージの I/O
  • CPUやメモリ
  • 文書のサイズやフィールド数
  • ビューの列数

があることを知りました。これらがどのように影響するのか、特性を知っておくとより理解が深まります。調査が難しいものありますが、可能な限り検証してまとめたいと思います。


お詫び

ノーツコンソーシアム主催の次世代エース養成ワークショップ(2025 年 7 月、10 月)では、訂正前の内容を紹介しておりました。当時の私にとっては ”新しい知見” だったので、少し得意げに紹介してしまったことを反省しております。

ここでお詫びしても受講者のみなさま全員に届くとは限りませんが、この場を借りてお詫びいたします。今後はより慎重に検証したた上で、研修テキストの作成をしてまいります。


2025/12/07

QR コードの作画:#9)DXL でリッチテキストに表示

LotusScript で QR コードを作画するこの連載は今回が最終回となります。

DXL がビットマップに対応していないことから、暫定策として GIF ファイルに変換する対応をしました。今回は出来上がった GIF ファイルを文書に貼り付ける部分を作成します。


エージェントの作成と関数の追加

前回作成した CreateQRcode_GIF エージェントをコピペして DrawQRcode を作成します。このエージェントに関数を追加して作業を進めます。

リッチテキストフィールドに QR コードをインラインイメージ(見える状態)で貼り付ける処理には DXL を活用しなければなりません。この連載の本題ではないので詳細は割愛しますが、DXL にご興味がある方は以下の連載をご確認ください(一部関数は連載より流用)。


◇ 文書を DXL に変換

既存文書を DXL で操作できるよう変換処理を行う関数です。

Function xGetDOMParser(vnd As NotesDocument) As NotesDOMParser
   'Dominoデータ を DXL に変換する準備
   Dim dexp As NotesDXLExporter
   Set dexp = xns.CreateDXLExporter()
   Call dexp.SetInput(vnd)

   'パーサーに変換する DXL をセット
   Dim dprs As NotesDOMParser
   Set dprs = xns.CreateDOMParser()
   Call dprs.SetInput(dexp)

   'DXL 変換を実行
   Call dexp.Process()

   Set xGetDOMParser = dprs
End Function

この関数は『DXL Step-by-Step:#3)文書を DXL で取得』で紹介しています。


◇ QR コードの表示

リッチテキスト内に QR コードをインラインイメージで貼り付ける関数です。

引数 vsFld で指定された名前のリッチテキストフィールドにセットするのですが、フィールドをいったん削除しています。フィールド内の既存コンテンツはクリアされますので注意してください。

Function xSetDXL_GIF(vdprs As NotesDOMParser, ByVal vsFld As String, ByVal vsFP_GIF As String, viX As Integer, viY As Integer)
   Dim ddn As NotesDOMDocumentNode
   Dim den As NotesDOMElementNode
   Dim denDoc As NotesDOMElementNode
   Dim denItem As NotesDOMElementNode
   Dim denRT As NotesDOMElementNode
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim denGIF As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode
   Dim nst As NotesStream

   Set ddn = vdprs.Document

   'document ノード取得
   Set denDoc = ddn.DocumentElement

   '既存フィールドを削除
   Call xRemoveItemByName(denDoc, vsFld)

   'リッチテキストフィールド作成
   Set den = ddn.CreateElementNode("item")
   Call den.SetAttribute("name", vsFld)
   Set denItem = denDoc.AppendChild(den)

   'リッチテキスト作成
   Set den = ddn.CreateElementNode("richtext")
   Set denRT = denItem.AppendChild(den)

   '段落定義 id='1'
   Set den = ddn.CreateElementNode("pardef")
   Call den.SetAttribute("id", "1")
   Call denRT.AppendChild(den)

   '段落の作成
   Set den = ddn.CreateElementNode("par")
   Call den.SetAttribute("def", "1")
   Set denPar = denRT.AppendChild(den)

   'イメージリソースの追加
   Set den = ddn.CreateElementNode("picture")
   Call den.SetAttribute("align", "baseline")
   Call den.SetAttribute("width", CStr(viX) & "px")
   Call den.SetAttribute("height", CStr(viY) & "px")
   Set denPic = denPar.AppendChild(den)

   '画像の作成
   Set den = ddn.CreateElementNode("gif")
   Set denGIF = denPic.AppendChild(den)

   'GIF をストリームで開く
   Set nst = xns.CreateStream()
   Call nst.Open(vsFP_GIF, "binary")

   '画像の中身
   Set dtn = ddn.CreateTextNode(StreamToBase64(nst))
   Call denGIF.AppendChild(dtn)

   Call nst.Close()
End Function

この関数については合致する記事はありませんが、インラインイメージの貼り付けについては『DXL Step-by-Step:#41)インラインイメージの貼り付け』で触れています。

また、今回は画像サイズは QR コードのサイズであり、事前にわかっています。そこで、画像ファイルから取得するのではなく、関数の引数で受け取り処理を簡略化しています。


◇ 指定したフィールドの削除

xSetDXL_GIF からコールされているサブ関数で、DXL でフィールドを削除する関数です。

Function xRemoveItemByName(vden As NotesDOMElementNode, ByVal vsName As String)
   Dim dn As NotesDOMNode
   Dim den As NotesDOMElementNode
   Dim sName As String
   Dim s As String

   sName = LCase(vsName)

   Set dn = vden.FirstChild
   Do Until dn.Isnull
      If dn.NodeType = DOMNODETYPE_ELEMENT_NODE Then
         If dn.NodeName = "item" Then
            Set den = dn
            s = LCase(den.GetAttribute("name"))

            If sName = s Then
               Call vden.RemoveChild(den)
               Exit Function
            End If
         End If
      End If
      Set dn = dn.NextSibling
   Loop
End Function

この関数の参考記事は現時点でありません。フィールドである item ノードから名前が一致するものを探し出し削除しています。


◇ 画像のエンコード

こちらも xSetDXL_GIF からコールされているサブ関数です。DXL 内の画像は Base64 でエンコードしておく必要があります。そのエンコード処理を行う関数です。

'OpenNTF LotusScript Gold Collection より拝借(StreamToBase64)
Function StreamToBase64(streamIn As NotesStream) As String
   Dim s As String

   On Error GoTo theOldWay
   ' ReadEncoded function is not documented. In case it doesn't work have a backup.
   s = streamIn.ReadEncoded(ENC_BASE64, 76)
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
   Exit Function

theOldWay:
   Dim session As New NotesSession
   Dim db As NotesDatabase
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body")
   streamIn.Position = 0
   Call mime.SetContentFromBytes(streamIn, "image/gif", ENC_NONE)
   mime.EncodeContent(ENC_BASE64)
   s = mime.ContentAsText
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
End Function

コメントに記載した通り、 OpenNTF の LotusScript Gold Collection プロジェクトより拝借した関数です。『DXL Step-by-Step:#10)イメージリソースの新規作成』で紹介しています。


◇ 文書の保存

最後の関数は文書を保存する関数です。

Function DXL_Import(vdprs As NotesDOMParser, ByVal viOption As Integer, ByVal vbIsDesign As Boolean) As Boolean
   Dim nst As NotesStream
   Dim ndb As NotesDatabase
   Dim dimp As NotesDXLImporter

   On Error GoTo Err_Proc

   'DXL の抽出準備
   Set nst = xns.CreateStream()
   Call vdprs.SetOutput(nst)
   Call vdprs.Serialize()

   '保存(インポート)
   Set ndb = xns.CurrentDatabase
   Set dimp = xns.CreateDXLImporter()
   If vbIsDesign = True Then
      '設計の保存
      dimp.DesignImportOption = viOption
   Else
      '文書の保存
      dimp.DocumentImportOption = viOption
   End If

   'DXL の保存
   Call dimp.Import(nst.ReadText(), ndb)
   DXL_Import = True

Exit_Proc:
   Exit Function

Err_Proc:
   MsgBox Error$, 16, "DXL_Import"
   DXL_Import = False

   Resume Exit_Proc
End Function

この関数は『DXL Step-by-Step:#23)サンプルコード(段落と文字の装飾①)』で紹介しています。


メインルーチンの修正

エージェントの Initialize を修正し、今回作成した QR コード表示機能を追加します。

Sub Initialize
                  ・・・
   Dim iX As Integer    '画像の幅
   Dim iY As Integer    '画像の高さ
   Dim dprs As NotesDOMParser
                  ・・・
      '② GIF ファイルの作成
      Call DrawQR_GIF(abQR, "c:\tmp\QR.gif")

      '③ リッチテキストにインラインで貼り付け
      Set dprs = xGetDOMParser(nd)
      iX = UBound(abQR, 1) + 1    '画像の幅
      iY = UBound(abQR, 2) + 1    '画像の高さ
      Call xSetDXL_GIF(dprs, "QRcode", "c:\tmp\QR.gif", iX, iY)
      Call DXL_Import(dprs, 5, False)    '文書を更新
   End If
End Sub


ビューの修正と動作検証

エージェントが完成したら、アクションボタンをビューに追加します。

ビューを保存後、動作検証します。正常に実行されると冒頭の画像のように QR コードが表示されます。


まとめ

今回の連載は、LotusScript だけを使って QR コードをビットマップ画像として描画する方法について解説しました。あわせて、ビットマップ画像のフォーマット仕様についても整理し、仕組みを理解しながら作り上げる流れをご紹介しました。

本来であれば、できる限り幅広い環境で動作させるために LotusScript だけで完結させることを目指していましたが、現状の DXL ではビットマップに対応していないため、最終的な GIF 変換には Windows の機能に頼らざるを得ない部分がありました。その結果、完全に LotusScript だけで完結する構成にはできなかった点は少し心残りです。

機会があれば GIF フォーマットのファイルを LotusScript で生成することにも挑戦してみたいと思います。仕様が理解できたらという制約はありますが...


前回 QR コードの作画


2025/12/06

QR コードの作画:#8)BMP → GIF 変換

前回は、DXL はビットマップファイルに対応していないと(個人的に)衝撃的な事実を知ったことを紹介しました。せっかく作ったライブラリを何とか使いたいので、GIF ファイルに変換して利用したいと思います。

こういった汎用的なコンバート処理は、AI に聞きながら開発すると効率的ですよね。そこで、今回は ChatGPT 先生に相談しながら作業することにします。


BMP → GIF 変換

WIA(Windows Image Acquisition)を使うなどいくつかの方法を提案してくれましたが、64 ビット版の Notes でも動作させたいので、今回は PowerShell を利用して実現することにします。次のコマンドを実行すればよいそうです。

Add-Type -AssemblyName System.Drawing
$bmp = [System.Drawing.Bitmap]::FromFile('C:\tmp\BMP.bmp')
$bmp.Save('C:\tmp\BMP.gif', [System.Drawing.Imaging.ImageFormat]::Gif)

各行は、

  1. .NET の System.Drawing 名前空間を PowerShell セッションにロードします。
    (System.Drawing は Bitmap や Image、ImageFormat の定義を含むライブラリ)
  2. 指定した BMP ファイルを読み込み、System.Drawing.Bitmap オブジェクト(ビットマップ画像を表す .NET オブジェクト)を作成して $bmp に代入
  3. $bmp に保持されている画像を、指定したパスに GIF 形式で保存

となっているそうです。


LotusScript で関数化

教えてもらった PowerShell のコマンドを実行する LotusScript の関数の作成も依頼してみます。頼んでないのに複数ファイルを一括変換してみたり、Domino Designer に貼り付けると文法エラーになったりと、少々回り道をしましたが、何とか完成しました。

完成に向けていくつか要望を出してみたのですが、それぞれ叶えてくれました。

PowerShell 実行ウィンドウの非表示 非表示で WSH を起動し、PowerShell を実行させる
全角対応UTF-8 で PowerShell スクリプトを保存
これによりパスやフォルダ名に日本語(全角文字)が含まれても安全に動作

出来上がった関数は以下の通りです。ChatGPT が出力した関数を不要な部分を削除し、命名規則を整えたバージョンです。

Private Function xConvertBMPtoGIF_PS(ByVal vsFP_BMP As String, ByVal vsFP_GIF As String) As Boolean
   Dim oFSO As Variant
   Dim oShell As Variant
   Dim sPS_FP As String
   Dim sPS As String
   Dim sCMD As String
   Dim sCRLF As String

   ' 初期化
   On Error GoTo ErrorHandler

   xConvertBMPtoGIF_PS = False
   sCRLF = Chr(13) & Chr(10)

   ' 変換元ファイルの存在チェック
   Set oFSO = CreateObject("Scripting.FileSystemObject")
   If Not oFSO.FileExists(vsFP_BMP) Then Exit Function

   ' PowerShell スクリプトの作成
   sPS_FP = Environ$("TEMP") & "\convert_bmp_to_gif.ps1"

   ' PowerShellスクリプト内容(全角パス対応)
   sPS = "Add-Type -AssemblyName System.Drawing"
   sPS = sPS & sCRLF & "$src = '" & Replace(vsFP_BMP, "'", "''") & "'"
   sPS = sPS & sCRLF & "$dst = '" & Replace(vsFP_GIF, "'", "''") & "'"
   sPS = sPS & sCRLF & "$bmp = [System.Drawing.Bitmap]::FromFile($src)"
   sPS = sPS & sCRLF & "$bmp.Save($dst, [System.Drawing.Imaging.ImageFormat]::Gif)"
   sPS = sPS & sCRLF & "$bmp.Dispose()"

   ' UTF-8で.ps1を書き出し(全角対応のため)
   Call xWriteUtf8File(sPS_FP, sPS)

   ' PowerShell実行
   Set oShell = CreateObject("WScript.Shell")
   sCMD = |powershell -NoProfile -ExecutionPolicy Bypass -File "| & sPS_FP & |"|
   oShell.Run sCMD, 0, True       ' 0=ウィンドウ非表示, True=完了待ち

   ' 結果確認
   If oFSO.FileExists(vsFP_GIF) Then
      xConvertBMPtoGIF_PS = True
   End If

ExitFunc:
   ' 一時ファイル削除
   On Error Resume Next
   oFSO.DeleteFile sPS_FP

   Exit Function

ErrorHandler:
   xConvertBMPtoGIF_PS = False
   Resume ExitFunc
End Function

' UTF-8 (BOMなし) でテキストファイルを書き出し
Private Sub xWriteUtf8File(ByVal vsFP As String, ByVal vsText As String)
   Dim oStream As Variant
   Set oStream = CreateObject("ADODB.Stream")
   With oStream
      .Charset = "UTF-8"
      .Open
      .WriteText vsText
      .SaveToFile vsFP, 2    ' 上書き
      .Close
   End With
End Sub


ライブラリへの組み込み

上記関数を前回までに作成した lsDrawQRcode ライブラリに追加します。

続いて、GIF ファイルを作成するメインルーチンも作成します。ビットマップ作成と上記の関数を実行するだけの単純な構造です。

%REM
QR コードの論理データ(Boolean 型の 2 次元配列)から GIF ファイルを作成します。

◆ 引数
   vabQR Boolean QR コードの論理データ(Boolean 型の 2 次元配列)
   vsFP_GIF String フルパスで指定されたファイル名

◆ 戻り値 Booelan
   ファイルが作成出来れば True
%END REM

Public Function DrawQR_GIF(vabQR As Variant, ByVal vsFP_GIF As String) As Boolean
   Dim sFP_BMP As String

   sFP_BMP = vsFP_GIF & ".bmp"
   If DrawQR_BMP(vabQR, sFP_BMP) Then
      Call xConvertBMPtoGIF_PS(sFP_BMP, vsFP_GIF)

      ' 一時ファイル削除
      On Error Resume Next
      'Kill sFP_BMP
   End If
End Function

なお、一時ファイルであるビットマップは最後に削除するコードを書いています。検証のためコメントアウトしていますが、確認後はコメント解除してください。


動作検証

関数が完成したので動作検証を行います。

前回作成したエージェントをコピペして CreateQRcode_GIF を作成します。画像を作成する部分を今回作成した関数に差し替えます。

         ・・・
      '② GIF ファイルの作成
      Call DrawQR_GIF(abQR, "c:\tmp\QR.gif")
         ・・・

このエージェントを実行するアクションボタンをビューに追加します。

正常に動作すると出力フォルダに GIF ファイルが作成されます。


次回の予定

これで GIF 形式の QR コードが出力できるようになりました。次回の最終回では DXL を使ってこの画像をリッチテキストにインラインで貼り付けるコードを紹介します。


前回 QR コードの作画 次回


2025/12/04

QR コードの作画:#7)BMP は作成できたけど...

前回でスクリプトライブラリは完成しましたので、正しくビットマップファイルが作成できるか動作検証をしましょう。


フォームの作成

新規でフォームを作成します。名称にこだわりはないのですが、ここでは QR_Pict とします。フィールドは次の 2 個作成します

項目 フィールド名 種類 補足
変換する文字列 StrSrc テキスト 編集可能
QR コード QRcode リッチテキスト 編集可能

入力した文字列を QR コード化にしてリッチテキストに表示することをイメージした単純なフォームとなります。


エージェントの作成

続いてビューで選択した文書に QR コードを添付するエージェントを作成します。とりあえず今回は、QR コードに変換しビットマップファイルとして保存するところまでを作成します。

LotusScript のエージェント CreateQRcode_BMP を新規作成し、プロパティで実行対象を”なし”に設定します。

エージェントのコード全体は次の通りです。

まず、前回までに作成したライブラリ lsDrawQRcode を組み込みます。

選択した文書から変換する文字列を取得して QR コードにエンコードします(①)。ここまでは、連載『ノーツで QR コード』で作成した lsQRCode ライブラリを利用した部分ですね。

今回作成した関数は ② で使用しています。動作検証なので C:\tmp\ フォルダに QR.bmp という名前で固定して出力させています(実行前にはフォルダを用意ください)。

Option Declare
Use "lsDrawQRcode"

Private xns As NotesSession

Sub Initialize
   Dim ndb As NotesDatabase
   Dim ndc As NotesDocumentCollection
   Dim nd As NotesDocument
   Dim sSrcText As String

   Dim sEnc As String             'エンコード文字列
   Dim abQR() As Boolean    'QR コードの論理データ(Boolean 型の 2 次元配列)

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments

   If ndc.Count <> 1 Then
      MsgBox "QR コードを作成する文書を1文書だけ選択してください。", 16, ndb.Title
   Else
      Set nd = ndc.GetNthDocument(1)
      sSrcText = nd.StrSrc(0)    'QRコード化したい文字列

      '① QR コードのエンコード
      sEnc = EncodeBarcode(sSrcText, 3)    '3 = 誤り訂正レベル
      Call bc_2Dms_New(sEnc, abQR)         'QR コードの論理データに変換

      '② ビットマップファイルの作成
      Call DrawQR_BMP(abQR, "c:\tmp\QR.bmp")
   End If
End Sub


ビューの作成

先に作成したフォームの文書を表示するためのビューを作成します。フォーム同様特にこだわりはないので適当に作成します。

ただ、このビューは QR コードを作成を実行する役割があります。[QRコード作成]アクションボタンで作成したエージェントを実行するようにします。


動作検証

テストできる環境が整ったので動作検証を行います。適当な文字列を入力したテストデータを作成し、[QRコード作成]ボタンをクリックします。

正常に動作すると出力フォルダにビットマップファイルが作成されます。


想定外の大どんでん返し...

最難関と考えていた画像生成ができたので、あとは QR コードをフォームに貼り付けるだけです。

リッチテキストフィールドに画像をインライン(見える状態)で貼り付けるには、通常の NotesRichText??? クラスでは実現できません。LotusScript で DXL を使用する必要があります。

他の連載『DXL ことはじめ』『DXL Step-by-Step』でまとめている通り、DXL には心得があります。それを使って実現しようとしたのですが、なんと!DXL は BMP 未対応だったんです !? 

GIF や JPEG などの画像形式に対応しているので、てっきり対応しているものと思っていました...


次回の予定

仕方がないので、次に仕様が簡単そうな GIF 形式を調べてみたのですが、画像圧縮する機能あり、ビットマップよりずいぶん複雑です。現時点では、残念ながら理解に至っていません。

そこで今回は、ビットマップを GIF に変換することで暫定対応としたいと思います。詳しくは次回紹介します。


前回 QR コードの作画


2025/12/01

QR コードの作画:#6)ビットマップファイルの出力

前回はビットマップの画像データを作成する関数 xBMP_MakePictureData を作成しました。今回はメインルーチンからコールしているその他の関数を紹介します。

  • ファイルヘッダの出力
  •  情報ヘッダの出力
  •  カラーパレットの出力
  • 画像データの出力

これらは『#2)白黒ビットマップの構造』ビットマップの構造順にファイルを出力するための処理となります。また、各ブロックの構造については『#3)ビットマップのエンコード仕様』を参照ください。


ファイルヘッダの出力

ファイル出力するエリアとなる Byte 型配列を用意して、そこに出力する値をセットします。すべての値が準備できたら、NotesStream の Write メソッドでファイルに書き込みます。

ファイルヘッダで画像に応じて可変になる項目はファイルサイズだけでした。

ファイルサイズは、前回作成した画像データから取得し、他のブロック数を加算して算出ります。それ以外の項目は固定の値となります。

Private Function xBMP_WriteFileHeader(vnst As NotesStream, vvBitMap As Variant)
   Dim lSize As Long
   Dim sSize As String
   Dim i As Integer
   Dim abyFileHeader(13) As Byte

   '固定
   abyFileHeader(0) = Asc("B")
   abyFileHeader(1) = Asc("M")

   'ファイルサイズ
   lSize = Ubound(vvBitMap(0)) + 1               '1行のバイト数
   lSize = lSize * (UBound(vvBitMap) + 1)    '行数を掛ける
   lSize = lSize + 14 + 40 + 8     '画像データ以外のブロックを加算
   'リトルエンディアンでセット(4 バイト)
   sSize = Right("0000000" & Hex(lSize), 8)
   For i = 0 To 3
      abyFileHeader(5-i) = CByte(xHexToDec(Mid(sSize, i*2+1, 2)))
   Next

   'オフセットビット
   abyFileHeader(10) = 62

   'ファイルヘッダ部をファイルに出力
   Call vnst.Write(abyFileHeader)
End Function


情報ヘッダの出力

情報ヘッダで可変となる項目は、画像のサイズ(幅と高さ)、画像データのバイト数でした。

画像データのサイズはファイルヘッダで算出していたのと同様です。画像サイズは、QR コードの論理データ(Boolean 型の 2 次元配列)から簡単に取得できます。よってこの関数の引数には、QR コードの論理データ vabQR と画像データ vvBitMap が引数になっています。

Private Function xBMP_WriteInfoHeader(vnst As NotesStream, vabQR As Variant, vvBitMap As Variant)
   Dim lSize As Long
   Dim sSize As String
   Dim i As Integer
   Dim abyInfoHeader(39) As Byte

   '情報ヘッダのサイズ
   abyInfoHeader(0) = 40

   '画像の幅(リトルエンディアン)
   lSize = (UBound(vabQR, 1) + 1)
   sSize = Right("0000000" & Hex(lSize), 8)
   For i = 0 To 3
      abyInfoHeader(7-i) = CByte(xHexToDec(Mid(sSize, i*2+1, 2)))
   Next

   '画像の高さ(リトルエンディアン)
   lSize = (UBound(vabQR, 2) + 1)
   sSize = Right("0000000" & Hex(lSize), 8)
   For i = 0 To 3
      abyInfoHeader(11-i) = CByte(xHexToDec(Mid(sSize, i*2+1, 2)))
   Next

   'プレーン数
   abyInfoHeader(12) = 1

   'ビットカウント
   abyInfoHeader(14) = 1

   '画像データのサイズ(リトルエンディアン)
   lSize = UBound(vvBitMap(0)) + 1               '1行のバイト数
   lSize = lSize * (UBound(vvBitMap) + 1)    '行数を掛ける
   sSize = Right("0000000" & Hex(lSize), 8)
   For i = 0 To 3
      abyInfoHeader(23-i) = CByte(xHexToDec(Mid(sSize, i*2+1, 2)))
   Next

   '情報ヘッダ部をファイルに出力
   Call vnst.Write(abyInfoHeader)
End Function


カラーパレットの出力

今回は白黒のビットマップですのでカラーパレットブロックの内容はすべて固定でした。ですので、出力関数内は単純です。


Private Function xBMP_WriteColorPallet(vnst As NotesStream)
   Dim sCol_HEX As String
   Dim i As Integer
   Dim abyColor(7) As Byte

   'パレット0
    sCol_HEX = "00000000"
   For i = 0 To 3
      abyColor(i) = CByte(xHexToDec(Mid(sCol_HEX , i*2+1, 2)))
   Next

   'パレット1
    sCol_HEX = "FFFFFF00"
   For i = 0 To 3
      abyColor(4 + i) = CByte(xHexToDec(Mid(sCol_HEX , i*2+1, 2)))
   Next

   'カラーパレットをファイルに出力
   Call vnst.Write(abyColor)
End Function


画像データの出力

画像データは Variant 型配列の各要素に 1 行分が入っていました。ファイルへの出力は For ループで 1 行ずつ順に出力するだけです。

Private Function xBMP_WritePictureData(vnst As NotesStream, vvBitMap As Variant)
   Dim iY As Integer

   '1 行ずつファイルに出力
   For iY = 0 To UBound(vvBitMap)
      Call vnst.Write(vvBitMap(iY))
   Next
End Function


リトルエンディアンとは?

ビットマップファイルでは画像サイズやファイルサイズなど数値を出力する場合、リトルエンディアンで出力します。これは、複数バイトで表現される整数を下位バイトから順に並べる 方式となります。

例えば 16 進数で 8 桁となる数値を 4 バイトで出力するには、次のようになります。


16 進数 → 10 進数変換

リトルエンディアンのようにバイナリーファイルを扱う場合 16 進数を 10 進数に変換することがあります。10 進数 → 16 進数では Hex という関数があるのですが、逆の関数はありません。そこで関数 xHexToDec を作成します。

Private Function xHexToDec(sHex As String) As Long
   '16 進数を 10 進数に変換
   xHexToDec = xVirtualNumber2Dec(sHex, "0123456789ABCDEF")
End Function

関数は 2 段階になっています。xVirtualNumber2Dec では、引数 vsNumChrList で表現された“仮想的な進数”を 10 進数へ変換する関数 になっています。16 進数からの変換だけであればもっとシンプルに記述できますが、n 進数で使える汎用的な関数としています。

Private Function xVirtualNumber2Dec(ByVal vsNum As String, ByVal vsNumChrList As String) As Long
   'psNumChrListで構成されるLen(psNumCheList)進数のpsNumを10進数に変換
   Dim iNum As Integer
   Dim iTgetNum As Integer
   Dim sTgetNum As String
   Dim sNum As String
   Dim lResult As Long
   Dim iMinus As Integer
   Dim i As Integer
   Dim iNumChrList As Integer

   sNum = vsNum
   If Left(sNum, 1)="-" Then
      sNum = Right(sNum, Len(sNum) - 1)
      iMinus = -1
   Else
      iMinus = 1
   End If
   iNum = Len(sNum)
   iNumChrList = Len(vsNumChrList)
   lResult = 0
   For i = 1 To iNum
      sTgetNum = Mid(sNum, i, 1)
      iTgetNum = InStr(1, vsNumChrList, sTgetNum) - 1
      lResult = lResult + iTgetNum * iNumChrList ^ (iNum - i)
   Next i

   xVirtualNumber2Dec = lResult * iMinus
End Function


前回 QR コードの作画 前回


2025/11/30

QR コードの作画:#5)画像データの変換

今回はビットマップの作成で一番重要な画像データを作成する関数 xBMP_MakePictureData を作成します。少し長い関数になりますので、ポイントとなる点を順にまとめます。最後に関数全体を掲載していますので、そちらを参照しながら読み進めてください。


関数の機能

この関数の機能は QR コードの論理データからビットマップファイルに設定する画像データブロックを作成することですが、データブロックのサイズ(バイト数)を事前に把握する役割もあります。前回 記載しましたが、ビットマップを出力する際に、画像データブロックより先に、ヘッダブロック出力が必要だったためです。

そこで関数のインターフェースは次のように定義します。

◇ 引数

1 vabQR Variant QR コードの論理データ
(Boolean 型の 2 次元配列)
2ravBitMap  Variant (戻り値)画像データ(Variant 型の配列)
1要素に1行分の画像データを保持

ravBitMap で渡される変数は、前回 紹介したメインルーチンで行数分の配列として宣言されていました。

      ReDim avBitMap(UBound(vabQR, 2))     '保存エリア確保
      Call xBMP_MakePictureData(vabQR, avBitMap)

この変数の各要素に 1 行ごとの画像データを格納します。1行分の画像データは1行を構成する Byte 型配列とします。保持仕様を図式化すると次のようになります。


1行分のバイト数

白黒ビットマップの場合、横幅のピクセル数が必要なビット数となります。これをバイトに変換するのですが、エンコード仕様により、4 バイト単位に調整する必要がありました。

具体的には、以下の演算でバイト数を算出して、保存エリアを確保しています。

   '初期化(1行分の保存に必要なエリアを確保)
   iByte = UBound(vabQR, 1) + 1    '画像の幅
   iByte = (iByte + 7) \ 8                       '必要なビット数(バイト単位)
   iByte = ((iByte + 3) \ 4) * 4             '1行は4バイト単位
   ReDim abyLine(iByte - 1)


1行分のデータ作成

ビットマップは画像の下から上にエンコードする仕様でした。これを実現しているのが以下の For ループで、Step が -1 となっています。

   For iY = UBound(vabQR, 2) To 0 Step -1     '行は下から上

ループ内ではまず、1 行分のワークエリアである abyLine を初期化しています。初期化で全要素をいったん 0 にしておき、必要な要素だけ値をセットしようという算段です。

      '1行分のワークエリアの初期化
      iByte = 0
      ReDim abyLine(UBound(abyLine))     '配列内初期化

コード内のコメントの ① の部分は、1 行分のQR コード論理データを順に処理しています。処理はサブルーチン SetBit で行い、8 ビット分たまったら、WriteByte サブルーチンでバイトに変換して、1 行分のワークエリアに格納しています。

画像の幅が 8 の倍数ではないとき、余ったビットを格納するために ② の処理を実行しています。


関数全体

最後に関数 xBMP_MakePictureData の全体を掲載します。

%REM
QR コードの論理データ(Boolean 型の 2 次元配列)から画像データブロックを作成します。

◆ 引数
   vabQR Boolean QR コードの論理データ(Boolean 型の 2 次元配列)
   ravBitMap Variant 画像データ(配列の各要素は1行分の画像データ(Byte 配列))
%END REM

Private Function xBMP_MakePictureData(vabQR As Variant, ravBitMap As Variant) As Boolean
   Dim iY As Integer
   Dim iX As Integer
   Dim iB As Integer                 'ビットカウンタ
   Dim abBit() As Boolean    '1バイト分を保存するワークエリア
   Dim bBit As Boolean
   Dim byByte As Byte
   Dim iByte As Integer         '1行のバイト数(カウンタ)
   Dim abyLine() As Byte     '1行分のワークエリア(バイト数分の配列)
   Dim iLine As Integer

   '初期化(1行分の保存に必要なエリアを確保)
   iByte = UBound(vabQR, 1) + 1    '画像の幅
   iByte = (iByte + 7) \ 8                       '必要なビット数(バイト単位)
   iByte = ((iByte + 3) \ 4) * 4             '1行は4バイト単位
   ReDim abyLine(iByte - 1)

   '1行ずつ順に処理
   iLine = 0
   ReDim abBit(7)     '1バイト分のビット格納エリアのリセット
   iB = 0
   For iY = UBound(vabQR, 2) To 0 Step -1     '行は下から上
      '1行分のワークエリアの初期化
      iByte = 0
      ReDim abyLine(UBound(abyLine))     '配列内初期化

      '① 画像の出力(1ピクセル毎)
      For iX = 0 To UBound(vabQR, 1)
         bBit = vabQR(iX, iY)
         GoSub SetBit
      Next

      '② 1バイトに満たないビットがあればバイトに変換
      If iB > 0 Then GoSub WriteByte

      '戻り値配列にセット
      ravBitMap(iLine) = abyLine
      iLine = iLine + 1
   Next

   Exit Function

SetBit:     '1ビットを格納
   abBit(iB) = bBit
   iB = iB + 1
   '1バイト分たまったら格納
   If iB = 8 Then GoSub WriteByte

   Return

WriteByte:     '1バイトを格納
   byByte = 0
   For iB = 0 To 7
      If abBit(iB) = False Then     '白黒反転
         byByte = byByte + 2 ^ (7-iB)
      End If
   Next

   '1バイト書き込み
   abyLine(iByte) = byByte
   iByte = iByte + 1

   '1バイト分のビット格納エリアのリセット
   ReDim abBit(7)
   iB = 0

   Return
End Function


次回の予定

これでビットマップ作製に必要な情報がそろいました。次回は、バイナリファイルに出力する部分を作成します。


前回 QR コードの作画 前回


2025/11/29

QR コードの作画:#4)作画ライブラリ作成開始

今回からは、いよいよ QR コードの作画(ビットマップファイルの作成)するためのコーディングを開始ます。


QR コードライブラリの準備

まず、QR コードの論理データ(Boolean 型の 2 次元配列)へ変換は、連載『ノーツで QR コード』で作成したスクリプトライブラリ lsQRCode を使用します。連載では、元となった Excel VBA のコードを逐次修正する形で紹介していました。ライブラリの中身を知りたい方は連載をご確認ください。完成したスクリプトライブラリがあればよいという方は MISC Market で無償公開しています。以下のリンクよりダウンロードください。

QR コード変換ライブラリ for LotusScript


◇ QR コードの論理データについて

lsQRCode ライブラリが出力する QR コードの論理データについて補足します。

変数宣言は Boolean 型の 2 次元配列となっており、1 次元目が幅で、2次元目が高さとなります。画像サイズの取得方法は次の通りです。

横幅 UBound(vabQR, 1) + 1
高さ UBound(vabQR, 2) + 1

要素番号が 0 始まりなので + 1 しています。数と要素番号を混同しないように注意しましょう。


作画ライブラリの作成

まず、新規で作成したデータベースに先ほどのライブラリ lsQRCode を配置します。

続いて、新しいライブラリ lsDrawQRcode を新規作成します。このライブラリが、QR コードを作画するライブラリとなります。lsQRCode が出力した QR コードの論理データから QR コードのビットマップファイル作成するコードを記述することになります。

ライブラリ lsQRCode  を呼び出し、NotesSession を初期化した状態からスタートとします。

Option Declare
Use "lsQRcode"     ' QR コードの論理データ(Boolean 型の 2 次元配列)変換

Private xns As NotesSession

Sub Initialize
   Set xns = New NotesSession
End Sub


メインルーチンの処理

ビットマップファイルを出力するには、バイナリデータをファイルに書き込む必要があります。その操作は NotesStream クラスを使用するのが便利です。書き込みには Write メソッドを使用するのですが、ファイルを前から順に書き込む必要があります。

ビットマップの仕様には QR コードのサイズにより可変となる項目があります。画像の幅と高さは QR コード論理データ配列の要素数から取得できますが、画像データのサイズ(バイト数)は不明です。事前に算出しておかないと、すべての項目が埋められません。

ブロック 項目
ファイルヘッダ ファイルサイズ
情報ヘッダ 画像の幅、画像の高さ、画像データサイズ
画像データ ファイルサイズ、画像データサイズを知るために必要

そこで、今回作成するメインルーチンでは、まず、画像データ部を生成する関数を実行しています。これで、ファイルヘッダ、情報ヘッダに設定する値が決定できます。


メインルーチンの作成

作成する関数インターフェースは次の通りです。

◇ 引数

1 vabQR Variant QR コードの論理データ
(Boolean 型の 2 次元配列)
2vsFP_BMP  String 出力するファイル名

◇ 戻り値

Boolean  ファイル出力ができたら True


コードの全体は次の通りです。

%REM
QR コードの論理データ(Boolean 型の 2 次元配列)からビットマップファイルを作成します。

◆ 引数
   vabQR    Boolean    QR コードの論理データ(Boolean 型の 2 次元配列)
   vsFP_BMP   String    フルパスで指定されたファイル名

◆ 戻り値 Booelan
   ファイルが作成出来れば True
%END REM

Public Function DrawQR_BMP(vabQR As Variant, ByVal vsFP_BMP As String) As Boolean
   Dim nst As NotesStream
   Dim avBitMap As Variant    ’
画像データ
   Dim s As String

   On Error GoTo Err_General

   Set nst = xns.CreateStream()
   If nst.Open(vsFP_BMP, "binary") Then
      '画像データ部の変換
      ReDim avBitMap(UBound(vabQR, 2))     '保存エリア確保
      Call xBMP_MakePictureData(vabQR, avBitMap)

      'ファイルの中身をいったん削除
      Call nst.Truncate()

      '① ファイルヘッダの出力

      Call xBMP_WriteFileHeader(nst, avBitMap)

      '② 情報ヘッダの出力
      Call xBMP_WriteInfoHeader(nst, vabQR, avBitMap)

      '③ カラーパレットの出力
      Call xBMP_WriteColorPallet(nst)

      '④ 画像データの出力
      Call xBMP_WritePictureData(nst, avBitMap)

      '保存
      Call nst.Close()
      '戻り値セット
      DrawQR_BMP = True
   End If

Exit_Func:
   Exit Function

Err_General:
   DrawQR_BMP = False

   s = "DrawQR_BMP でエラーが発生しました。" & Chr(10)
   s = s & Error$ & " (Err = " & CStr(Err) & ", Erl = " & CStr(Erl) & ")"
   MsgBox s, 16, "DrawQR_BMP"

   Resume Exit_Func
End Function

① ~ ④ までがビットマップデータの各ブロックをファイル出力する関数です。事前に出力データがそろっているので、ファイルフォーマットに従い出力させるだけの役割となります。


次回の予定

メインルーチンからコールしているサブ関数の詳細は次回以降に順次紹介します。次回は、画像データ部を生成する関数 xBMP_MakePictureData を紹介します。


前回 QR コードの作画 次回



2025/11/26

QR コードの作画:#3)ビットマップのエンコード仕様

前回は、ビットマップデータが 4 つのブロックに分かれていることを紹介しました。今回は各ブロックの詳細な仕様と画像データのエンコードについてまとめます。 


ファイルヘッダ

ファイルヘッダの構造は単純で、14 バイトの固定長となっています。ブロック内は4つの役割に分割されています。


Byte 数
役割 説明
2 ファイルタイプ ビットマップを表す固定の文字列
4 ファイルサイズ ファイル全体のサイズ(バイト数)
4 予約領域 固定で 0 を設定
4 画像データの開始位置 ファイルの先頭からのバイト数を指定


◇ ファイルタイプ

ファイルタイプには 0x42 と 0x4D を指定します。これは文字に変換すると "B" と "M" で BitMap ファイルであることを表します。ファイルを読み込むプログラムではこの最初の 2 バイトでこのファイルはビットマップであると判定できるということですね。

別の記事『DXL Step-by-Step:#13)イメージの形式とサイズの取得』で PNG、GIF、JPEG のフォーマットについて解説しましたが、どのフォーマットも決められた位置にフォーマットを表す文字(値)がセットされています。後続のフォーマットがまねているので、ビットマップが画像ファイルの元祖って感じがしますね。


◇ ファイルサイズ

ビットマップファイル全体のバイト数を指定します。よって、この項目は画像データとして出力データ量が明確になってからでないと決定できない項目となります。


◇ 予約領域

今回利用しない項目です(何に使うか不明)。すべて 0x00 をセットすれば OK です。


◇ 画像データの開始位置

前回記載したように、白黒のビットマップではヘッダ領域のサイズは固定となり、62 バイトでした。ですので、 最後の項目の開始位置には 0x3E、0x00、0x00、0x00 を指定します。


情報ヘッダ

情報ヘッダも固定長で、40 バイトで構成されます。このエリアには、ビットマップファイルの画像フォーマットに関する情報が集約されています。色数や画像の幅や高さだけでなく、解像度の項目もあります。また、圧縮に関する項目もあるようなので、ビットマップって無圧縮だけではないようですね。

今回は白黒の QR コードを前提にしますので、情報ヘッダは次の通りとなります。赤い部分が画像に応じて可変になる部分です。


Byte 数
役割 説明
4 ヘッダサイズ 40 byte
4 画像の幅 ピクセル数
4 画像の高さ ピクセル数
2 プレーン数 1 を指定
2 色数(ビット数) 1 : 白黒、8 : 256色、24 : True Color など
4 圧縮形式
4 画像データサイズ 画像データブロックの byte 数
4 水平解像度
4 垂直解像度
4 パレットの色数 0 で色数から算出
4 重要色数 正確に表示すべき色数(パレットの前から)


カラーパレット

256 色以下のビットマップで存在するブロックとなります。パレットというだけあって、ビットマップ内の色番号を何色で表示するか指定する項目です。4 byte で 1 パレットの色を表します(R G B で 1 byte、予備 1 byte)。

今回は色数が 1 bit = 2 色なので、8 バイトの固定長となります。事例では、1 色目が黒で、2 色目が白としています。


画像データ

◇ エンコードの流れ

まず、画像データブロックの全体像について整理します。エンコードの基本仕様として、画像の下から上に変換するのが一般的です。

情報ヘッダの画像の高さにマイナス値を設定すると上から下にエンコードする仕様が定義されているようなのですが、画像ビューアが対応していないなど一般的ではないこともあるそうで、今回は利用しません。


◇ 1行分のエンコード

次に1行分のエンコード仕様についてまとめます。

今回色数は 1 bitなので画像は 2 進数として表現できます。1行分の各ピクセルを、白は1、黒は0で並べます。

1 行分のブロックは 4 バイトの倍数で表現する仕様なので、余った bit には 0 をセットします。これを 8 bit(= 1 byte)毎にファイルに書き込むという流れになります。


次回の予定

ビットマップの構造とエンコード仕様がわかったので、次回からはいよいよコーディングを開始します。


前回 QR コードの作画 次回