出直し!! ヘルプ

連載中

連載 終了

2025/07/31

標準アプリは Notes/Domino ユーザのお手本であれ!

昨日投稿した『有効期限 68 年の DJX ユーザ登録には要注意 !?』に関連して、気になることがあったので追記します。

TimeDifference の戻り値に関する問題により、下記の If 文が正常に判定できず、範囲内の 68 年を指定しても、『認証の有効期限が、設定の範囲(現在時刻~68年後)を超えています』とエラーとなる症状でした。


判定の仕様

この If 文の条件は、現在時刻 ~ 68年後の範囲に含まれないことを判定をしているのですが、私が気になったのは、68 年後を表す MAXADJUSTSEC 定数です。

   If diffs& < 0 Or diffs& > MAXADJUSTSEC Then

この定数の定義は、GeneralConstant ライブラリの Declarations に定義されていました。

   Const MAXADJUSTSEC = 2145916800 ' 68 years in seconds

「なるほど、68 年を秒に変換した値が入っているのか」っと思いつつこの値を検算してみました。その結果、1 年の日数を 365.25 日として計算していることがわかりました。

   68 年 * 365.25 日 * 24 時間 * 60 分 * 60 秒 = 2145916800 秒

この 0.25 は 4 年に一度のうるう年に配慮しているようですね。ただ、うるう年は 100 で割り切れる年は平年、400で割り切れる年は再びうるう年になり、そんなに単純ではありません。正確には、約365.2422日だそうで、それでも ”約” がついています。

よって、この If 文の判定はあまり正確ではないと言えますね。


判定仕様は揃えよう

DJX 管理ツールの有効期間の設定は、年月日のそれぞれを数値で入力する仕様です。 

そして、内部的な日付計算は NotesDatatime クラスの機能を活用して作成されています。ここでも、年月日それぞれの数値を使って算出しています。

このように、入力や演算は日付を基準とした処理で行われていることがわかります。にもかかわらず、上記の If 文では秒数を使って判定が行われていました。つまり、DJX 管理ツールには、こよみ上の「日付」と、時間間隔を表す「秒」という、異なる2種類の基準が混在していることになります。

しかし、「日数」と「その間の秒数」は常に一意に対応するわけではありません。このような設計は、想定外の動作やバグを引き起こす原因になると感じました。


標準アプリは、お手本であれ!

Notes/Domino は、標準テンプレートとして多くのサンプルアプリも用意されています。また、管理用ツールがアプリとして提供されていることが多いです。ユーザはこれらを参考にすることで、アプリ開発の手法を学んだり、自社のアプリ設計のヒントを得たりすることができます。こうした点は、Notes/Domino の面白さであり、大きな魅力のひとつだと感じています。

しかし今回紹介したように、必ずしも参考にすべきでない点も存在します。また、最近の標準テンプレートは機能が過剰で複雑になりすぎており、内容を理解するのが非常に困難になってきています。

標準アプリはすべての Notes/Domino ユーザにとっての「良き手本」であるべきです。誰が見てもエレガントで「イケてる」と感じられる、シンプルかつ洗練された仕様を目指してほしいものです。


2025/07/30

有効期限 68 年の DJX ユーザ登録には要注意 !?

本日は、ノーツコンソーシアム の Domino Lounge(旧 研究会)でネタにしたお話です。今年 5 月に、Domino 11 から 14 にアップデートした時に発生した現象についてご紹介します。


発生した現象

DJX の管理ツールからのユーザ登録は、有効期限を設定できる最大値の 68 年で運用していました。

14.0 にアップデート後、ユーザを登録したところ、『認証の有効期限が、設定の範囲(現在時刻~68年後)を超えています』とエラーが発生しました。

アップデート前は正常に登録できていましたし、設定の変更は行っていません。そもそもエラーメッセージに表示されている日付は、ちょうど 68 年後なので、何がエラーなのかさっぱりわかりません。


エラーの発生個所

原因調査のため、DJX 管理ツールの設計をのぞいてみたところ、ExtReadLib スクリプトライブラリの CreateReqUserCSV 関数の中でエラーを出力していることがわかりました。

このエラーを出力するためには、その直前の If 文でエラーだと判定される必要があります。ということはその値を演算している TimeDifference メソッドに起因する現象ということがわかります。


エラーの原因

このままでは検証しづらいので、この部分だけを抜き出して確認します。変数 dt には 68 年後の日付、dtnow には SetNow メソッドで現在の時刻がセットされています。この部分だけのサンプルプログラムを作成します。

Sub Initialize
   Dim ns As New NotesSession
   Dim dt As New NotesDateTime("2093/05/26 12:57:54")
   Dim dtnow As New NotesDateTime("2025/05/26 12:57:54")

   diffs& = dt.TimeDifference(dtnow)

   MsgBox CStr(diffs&), 0, ns.Notesversion
End Sub

このプログラムを移行前の環境である Notes 11 と 移行後の環境である Notes 14 で実行します。すると、結果が違ってました!

誤差(?)は 1,566,847 秒でした(換算すると18 日 と 3 時間 14 分 7 秒)。68 年間にあるうるう年の回数に近いです(68 / 4 = 17)。うるう年計算を忘れているなんてバカなことはないと思いながら、おそるおそるテクニカルサポートに問い合わせてみました。


誤差の原因はバグ

返答は次の通りでした。

サンプルコードを実行すると、Notes 14 では Long 型の最大値 2,147,483,647 が戻り値となっており、オーバーフローしている。
本来、TimeDifference の値は戻り値が 68 年と 20 日程度を超えると Long 値の最大となるが、Notes 14 ではそれよりも短い期間でオーバーフローしてしまっている。
この問題は、問題報告番号 JPKRDEVSGW として報告され、Notes 14.5、14.0FP5、12.0.2FP7 にて修正が予定されている。

オーバーフローって桁あふれが原因の現象と理解しているのですが、今回の現象は、Notes 14 の方が大きな値が出力されています。どんな演算を行うと ”早く桁があふれる” のか気になりながらも、バグなら仕方がないと飲み込みました。

ちなみにこの問題、記載通り Notes 14.5 では修正されていることを確認しました。

また、サポートの方からは TimeDifferenceDouble というメソッドを使用すると改善するとの提案もいただきました。NotesDatetime クラスは使用したことがないのですが、さまざまな機能がありそうですねぇ...


DJX の対応

ユーザ登録時の意味不明のエラー原因は明確になりました。また、14.5 でも治っていることは確認したのですが、これだけを理由にバージョンアップをするわけにはいかないですね。

仕方がないので、有効期限の設定を 67 年と 11 ヶ月に変更して運用を継続しています。

Notes/Domino 11 のサポート終了に伴い、バージョンアップを実施される環境は多いかと思います。14 を選択される場合、DJX + 68 年のユーザ登録にはご注意ください。レアケースかもしれませんが...


2025/07/20

つないでみよう:#28)画像生成 AI DALL-E3 - Base64 文字列を画像ファイルに変換

OpenAI 社の DALL-E3 を API で利用して画像を生成するシリーズの最終回です。今回はレスポンスの JSON に含まれる画像をファイルとして抽出する部分を作成します。


メインルーチンの修正

画像ファイルを取得するには大きく分けて2つの処理が必要です。

まず、前回紹介したレスポンスの中から画像データの文字列だけを取得します。2つ目の処理では。画像データを画像ファイルに変換します。

この流れに沿ってメインルーチンを構築します。

Sub Initialize
         ・・・(省略)・・・
   'API をコール
   Set jnavResponce = xAskGPT(sJSON_Post)
   Call xSetRT(nd, "JSON_Responce", jnavResponce.Stringify())


   '生成画像を Base64 文字列で取得
   sBase64 = xGetImage_Base64(jnavResponce)

   'Base64 画像をファイルとして保存
   Call xDownloadImage(sBase64, "c:\Temp\DALL-E3#28.jpg")

   Call nd.save(True, False)
End Sub


画像データの取得

レスポンスの JSON はこのような構造でした。

ここから base64_json の値を取得する関数 xGetImage_Base64 を作成します。引数はレスポンスの JSON 全体で、戻り値が画像データです。

関数内の処理では、data ノードが配列になっている点に注意が必要です。

Function xGetImage_Base64(vjnavResponce As NotesJSONNavigator) As String
   Dim je As NotesJSONElement
   Dim ja As NotesJSONArray
   Dim jeData As NotesJSONElement
   Dim jobj As NotesJSONObject
   Dim jeBase64 As NotesJSONElement
   Dim sResponce As String

   ' data を検索
   Set je = vjnavResponce.GetElementByName("data")

   ' data は配列なので NotesJSONArray で受ける
   Set ja = je.Value

   If ja.Size > 0 Then
      '配列の 1 件目を取得
      Set jeData = ja.GetFirstElement()
      Set jobj = jeData.Value

      ' b64_json を検索し値( = 画像)を取得
      Set jeBase64 = jobj.GetElementByName("b64_json")
      xGetImage_Base64 = jeBase64.Value
   End If
End Function


画像データの変換と保存

メインルーチンがコールする2つ目の関数です。Base64 画像をデコードする部分と画像ファイルとして保存する処理に分かれます。それぞれの処理はサブ関数化されています。

Function xDownloadImage(ByVal vsBase64 As String, ByVal vsFP As String) As Boolean
   Dim nst As NotesStream

   'Base64 画像をファイルとして保存
   Set nst = xns.CreateStream()
   Call Base64ToBinary(vsBase64, nst)
   Call StreamToImageFile(nst, vsFP)
End Function


サブ関数は別の連載記事『DXL Step-by-Step:#9)DXL 内の画像のダウンロード』で紹介したもので、そのまま再利用します。今回は関数のみ転載しますので、詳細を知りたい方は、上記リンクをご確認ください。


◇ Base64 のデコード

%REM
   Sub Base64ToBinary
   Description: Given a string of base64-encoded data, write into a binary stream we are passed.
      This is done rather than creating the stream here and returning it, so that you can
   stream directly into a file if you choose.
%END REM

Sub Base64ToBinary(strBase64$, streamOut As NotesStream)
   ' Given a string of base64 encoded data, this routine decodes and writes the original binary data into a NotesStream
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity
   Dim streamIn As NotesStream
   Dim db As NotesDatabase
   Dim session As New NotesSession

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body") ' the mime classes already know how to do this conversion,
   Set streamIn = session.CreateStream
   Call streamIn.WriteText(strBase64)
   streamIn.Position = 0
   Call mime.SetContentFromText(streamIn, "binary", ENC_BASE64)
   Call mime.GetContentAsBytes(streamOut, True) ' decode as you stream out the data.
End Sub

※ OpenNTF の LotusScript Gold Collection プロジェクトから拝借


◇ 画像をファイルとして保存

Sub StreamToImageFile(vnstImage As NotesStream, ByVal vsFP As String)
   Dim nstOut As NotesStream

   On Error Resume Next
   Kill vsFP '存在してたら削除

   Set nstOut = xns.CreateStream()
   Call nstOut.Open(vsFP)

   vnstImage.Position = 0
   Do Until vnstImage.Position >= vnstImage.Bytes
      Call nstOut.Write(vnstImage.Read(16000))
   Loop
End Sub


テスト実行

エージェントが完成したらテスト実行します。前回のテストと同様に『上司と部下が会議室で本気で議論しているシーンを少年漫画のバトルシーンのようなタッチで描いてください。』と指示を出すと以下のような画像が作成されました。


まとめ

今回は画像生成 AI の OpenAI 社の DALL-E3 を API で利用する方法を紹介しました。画像1枚あたりの生成コストは $0.04(約 6 円、今回の設定の場合)と比較的安価です。この金額で、上記のようなクオリティの画像が生成できるのですから、十分に利用価値があるといえるでしょう。

今後はプロンプトエンジニアリングの技術を磨き、より理想に近い画像を生成できるようにしていきたいですね。


前回 連載:つないでみよう


2025/07/19

つないでみよう:#27)画像生成 AI DALL-E3 - テスト実行とレスポンスの確認

前回は API をコールしてレスポンスを取得するまでのエージェントを作成しました。実際に実行し、結果を確認しましょう。


テスト実行 

フォームを開き、生成する画像の説明を入力し、文書を保存します。今回は『上司と部下が会議室で本気で議論しているシーンを少年漫画のバトルシーンのようなタッチで描いてください。』と入力してみました。

作成した文書を選択し、エージェントを実行します。実行後の文書を開くと、送受信した JSON が実行結果に表示されます(数分はかかるのでご注意ください)。


受信した JSON

今回のリクエストでは、responce_format に "b64_json" を指定しているので、受信した JSON 内に画像データが含まれます。大量の文字のほとんどは画像データとなり、見通しが悪く、わかりずらいです。VSCode で整形して、構造を確認します。

整形すると一目瞭然ですね。data ノードの下に画像データである b64_json ノードが存在します。Create Image API では1回のリクエストで複数枚の画像を作成する機能があります。これに対応するため data ノードは配列となっている点がポイントとなります。


リクエストは英語の方がいい?

先ほど紹介した受信した JSON を見て気になった点があります。画像データの次の項目に revised_prompt というノードがあります。ノード名から生成画像の指示だとわかりますが、値は次の通りとなっていました。

Imagine a scene where a male boss of Middle-Eastern descent and a female employee of Black descent are passionately debating in a conference room. The artistic style should evoke the feeling of a battle scene taken from a classic boy's comic, emphasizing dramatic angles and exaggerated expressions, featuring in bold, ink-heavy shading resembling manga styles from the 19th century. Elements such as onomatopoeic words or speed lines may also be included to intensify the scene.

ChatGPT で日本語に翻訳するといろいろと尾ひれがついていることがわかります。

中東系の男性上司と、黒人系の女性社員が会議室で激しく議論している場面を想像してください。 芸術的なスタイルは、まるで少年漫画のバトルシーンのような雰囲気を醸し出し、ドラマチックなアングルや誇張された表情を強調します。 表現には、19世紀の漫画スタイルを思わせる、太くインクの乗った重厚な陰影が使われます。 さらに、擬音語やスピード線といった要素が加わり、シーンの迫力を一層引き立てます。

また、同じ文章でもう一度リクエストしてみたところ、revised_prompt は違うものになっていました。

Create an image of an intense discussion between a supervisor and an employee in a conference room. Depict the scene with the energetic and intense characteristics commonly found in pre-1912 battle scenes from boys' comics. The supervisor is a Middle-Eastern man and the employee is a South Asian woman. She is standing up confidently asserting her points, while the supervisor listens with full attention suggesting his curiosity and openness. The conference room is modern with a large oval table, ergonomic chairs and a wall filled with whiteboards and charts.

会議室で上司と部下が激しい議論を交わしている場面の画像を作成してください。 このシーンは、1912年以前の少年漫画に見られる戦闘シーンのような、エネルギッシュで緊張感のある表現で描写してください。 上司は中東系の男性、部下は南アジア系の女性です。彼女は自信を持って立ち上がり、自分の主張を力強く述べています。一方、上司は興味と受容の姿勢を示しながら、真剣に彼女の話に耳を傾けています。 会議室はモダンなデザインで、大きな楕円形のテーブル、エルゴノミクスチェア、そして壁一面にはホワイトボードとグラフが設置されています。

初回はスピード線など作画に偏った指示でしたが、2回目は会議室の什器について詳細に記述されています。実行回によってこだわるポイントが違うようです。

この結果より、Create Image API では、リクエストを英語で送った方がよりダイレクトに指示が出せそうです。ChatGPT などを使用して、自身の希望をできる限り反映した英文を作成してから送信する方がよさそうですね。


まとめ

今回は、作成したエージェントのテストと Create Image API のレスポンスの JSON を確認しました。次回は、JSON 内の Base64 画像データから画像ファイルを抽出する部分を作成します。


前回 連載:つないでみよう 次回


2025/07/18

つないでみよう:#26)画像生成 AI DALL-E3 - API のコール

OpenAI 社の DALL-E3 を API で利用して画像を生成するシリーズの2回目です。前回は API の仕様を紹介しましたので、今回は具体的なコーディングを行います。

基本的な流れは、これまでこの連載で紹介してきた OpenAI 社の API 利用と同じです。ただ、しばらく期間があいているので、LotusScript のコードはすべてを掲載いたします。


フォームの作成

まずは、どのような画像を作成するのか指示を入力するためのフォームを作成します。

API コールで使用するのは「リクエスト内容」の部分だけです。実行結果は、送受信される JSON を記録する確認用のリッチテキストフィールドとなります。


エージェントの作成

新規で LotusScript のエージェントを作成ます。ビューで選択した文書に対して実行するので、対象に[すべての選択文書]を設定します。

まず、エージェントのメインルーチンは次の通りです。

Option Declare

Private xns As NotesSession
Private Const xcsBearer = "xxxxx" 'APIキーをここにセット

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

   Dim sBase64 As String

   Dim sJSON_Post As String
   Dim sJSON_Responce As String
   Dim jnavResponce As NotesJSONNavigator

   Set xns = New NotesSession

   'ビューの選択文書の1つ目を取得
   Set ndb = xns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments
   If ndc.Count = 0 Then
      MsgBox "文書を選択してください。", 16, ndb.Title
      Exit Sub
   End If
   Set nd = ndc.GetFirstDocument()

   '送信(POST)する JSON を作成
   sJSON_Post = xGetJSON(nd)
   Call xSetRT(nd, "JSON_Send", sJSON_Post)

   'API をコール
   Set jnavResponce = xAskGPT(sJSON_Post)
   Call xSetRT(nd, "JSON_Responce", jnavResponce.Stringify())   '必要なときのみ有効化

   Call nd.save(True, False)
End Sub

まず、このエージェントでは、ビューの選択文書の1つ目を取得して処理するようになっています。送信する JSON の作成と API をコールする2段階に分かれていて、それぞれ関数化しています。


送信する JSON の作成

送信する JSON は次のような構成でした。今回は単純化するために、prompt に文書に入力した「生成する画像の説明」をセットする以外は、固定の値とします。

{
   "model": "dall-e-3",
   "size": "1024x1024",
   "quality": "standard",
   "response_format": "b64_json",
   "prompt": "(ここに生成する画像の説明をセット)"
}

この JSON を作成する関数は次の通りです。引数 vnd が処理する文書で、戻り値は作成した JSON です。

Function xGetJSON(vnd As NotesDocument) As String
   Dim jnav As NotesJSONNavigator
   Dim s As String

   'RequestBody(JSON) の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1.model
   Call jnav.AppendElement("dall-e-3","model")
   '1.size
   Call jnav.AppendElement("1024x1024","size")
   '1.quality
   Call jnav.AppendElement("standard","quality")
   '1.response_format
   Call jnav.AppendElement("b64_json","response_format")
   '1.prompt
   Call jnav.AppendElement(vnd.Prompt(0),"prompt")

   xGetJSON = jnav.Stringify
End Function


API をコール

続いて、API をコールする関数です。コメントに記載している通り、HTTP リクエストの準備して、それにヘッダの設定を行い、エンドポイントに対して POST しています。

ヘッダの Authorization には API キーを 設定しています(キーの取得については #1 を参照)。また、Post メソッドの2つ目の引数に先ほどの xGetJSON 関数で作成した JSON をセットしています。

Function xAskGPT(ByVal vsJSON_Post As String) As NotesJSONNavigator
   Dim sURL As String
   Dim http As NotesHTTPRequest
   Dim jnav As NotesJSONNavigator
   Dim ja As NotesJSONArray
   Dim jobj As NotesJSONObject

   'HTTP リクエストの準備
   Set http = xns.CreateHTTPRequest()

   'HTTP ヘッダーの設定
   Call http.SetHeaderField("Content-Type", "application/json")
   Call http.SetHeaderField("Authorization", "Bearer " & xcsBearer)

   'API 実行
   http.PreferJSONNavigator = True
   sURL = "https://api.openai.com/v1/images/generations"     'エンドポイント
   Set jnav = http.Post(sURL, vsJSON_Post)

   'Responceをセット
   Set xAskGPT = jnav
End Function

関数の戻り値は、API の返答の JSON で、NotesJSONNavigator のオブジェクトとして返します。


JSON の記録

メインルーチンで作成したJSON や API のレスポンスの JSON をリッチテキストフィールドに保存するのが xSetRT 関数です。

Function xSetRT(vnd As NotesDocument, ByVal vsFld As String, ByVal vsVal As String)
   Dim nrti As NotesRichTextItem

   Call vnd.RemoveItem(vsFld)
   Set nrti = vnd.CreateRichTextItem(vsFld)
   Call nrti.AppendText(vsVal)
End Function

上記メインルーチンでは有効化していますが、JSON を確認したい場合に実行し、それ以外はコメントアウトしてください。

今回は、画像生成ですので、レスポンスの JSON が大きくなります。文書内に JSON を記録すると文書を開くのが非常に遅くなります(私の環境では 5 分程度)。ご注意ください。


テスト実行は次回

ここまでで API をコールして、結果を JSONで取得するとことまでができました。次回はこれを実行して結果を確認します。


前回 連載:つないでみよう 次回


2025/07/17

つないでみよう:#25)画像生成 AI DALL-E3

約半年ぶりにこの連載『つないでみよう』の更新です。今回は画像生成にチャレンジします。

画像生成で利用するのは OpenAI 社の DALL-E 3 です。API としては 2023 年 9 月の公開らしいので、すでに 1 年半以上経過しているようです。展開が早くて全然追いつけていないですね...


きっかけ

先日行われた DominoHub 2025 Tokyo で展示ブースを担当することになりました(レポート記事は こちら)。私のブースでは、これまでに作成してきたさまざまなアプリを紹介し、Domino 活用のヒントをご提供することを目標にしました。そのアプリの一つが『AI 絵付師 からくり1号』で、生成 AI を使用してノーツ DB のアイコンを自動作成するアプリです。処理の流れは、

  1.  作成したい画像イメージを文章でリクエストを入力
  2.  DALL-E3 がリクエストに応じた画像を生成(1024 x 1024 Pixel)
  3.  Excel を利用して 64 x 64 Pixel に高品質で変換
  4.  指定した DB のアイコンに DXL を使って設定

となっていまいました。今回のシリーズでは、この処理の 2 の部分を紹介します。


DALL-E3 の利用

OpenAI 社が提供する Create Image API から DALL-E3 を利用できます。この API は数年前にテストしたことがあったのですが、その時点では前バージョンの DALL-E2 までしか利用できませんでした。残念ながら微妙な画像しか生成できなかったので、そのまま放置していました。その間に、DALL-E3 に対応し、劇的に進化していたようです。

DALL-E3 では、こちらからのリクエストの解釈(特に日本語の理解)が進み、希望する(希望に近い)画像が出力されるようになりました。内部的には、著作権保護や不適切な表現をしないよう安全性にも配慮されているとのことです。

API 利用者としてありがたいのは、生成した画像の所有権がユーザ側にあること、そして、商用利用しても問題がないという点だと思います。


Create Image API の使い方

API の基本的な利用方法は、他のシリーズで使用してきた Chat Completions API と同等です。違う点を中心にまとめます。

まず、エンドポイントです。コールする API が違うので、URL が変化します。

POST  https://api.openai.com/v1/images/generations

リクエストヘッダの指定には変化がありません。Authorization に指定する Bearer も同じもが流用できます。

POST 時に送信する RequestBody は次のような JSON となります。 Chat Completions API に比べてずいぶんシンプルですね。

{
   "model": "dall-e-3",
   "size": "1024x1024",
   "quality": "standard",
   "response_format": "b64_json",
   "prompt": "上司と部下が会議室で本気で議論しているシーンを少年漫画のバトルシーンのようなタッチで描いてください。"
}

各項目の説明と設定できる値は次の通りです。必要最低限の情報だけを抜粋しているので、より詳しく知りたい場合には、API の ヘルプ を参照ください。

prompt 生成を希望する画像の説明文です。
model
画像生成のモデルを指定します。
DALL-E3 を指定するには "dall-e-3" を指定。
(デフォルトは前バージョンの "dall-e-2")
quality生成画像の品質を指定します。
DALL-E3 の場合、"hd" と "standard" が利用できます。
(省略するとモデルごとの最適な品質を自動選択)
size 生成する画像サイズで、設定できる値はモデル次第です。
DALL-E3:"1024x1024", "1792x1024", "1024x1792"
DALL-E2:"256x256", "512x512", "1024x1024"
response_format 生成画像の取得方法です。
"url" :生成画像の URL を返す(有効期限は 60 分)
"b64_json" :Base64 エンコードの画像をレスポンスに含む


続きは次回

これで API 利用に必要な情報がそろいました。次回は、リクエストを送信する部分の LotusScript のサンプルコードを紹介します。


前回 連載:つないでみよう 次回


2025/07/02

Notes - Excel 連携:#55)指定した Pixel 数で画像を出力する方法

前回は Export メソッドで画像を出力した際に希望通りのサイズとならないことがある現象(以下、誤差といいます)に関して、詳細な調査を行いました。

発見できた事実は、

  1. 縦と横で誤差が発生する条件が違う
  2. Point(実数)とPixel(整数)間の変換で発生する丸め誤差ではない
  3. 出力画像形式により、誤差が発生する条件が違う

という結果になりました。なぜこのような仕様(?)になっているのかわかりませんが、画像ファイルフォーマットの仕様や画像形式ごとにプログラマが違うなどが原因ではないかと想定します。


対応方針

前回、掲載したグラフから誤差が発生するパターンがつかめそうなのですが、いつ仕様の調整が入るかわかりません。そこで、特性に合わせた対応にするのではなく、出力画像が希望通りのサイズとなっているか確認し、誤差があったら調整して再出力する方式とします。

あまり美しい対応とは言えませんが、ご了承ください...


サンプルプログラム

画像として保存する新しい関数を作成しました。引数は、

  1. 画像化する Chart が配置された Shape オブジェクト
  2. 保存ファイル名
  3. 画像サイズ横(Pixel)
  4. 画像サイズ縦(Pixel)

です。

Function xSaveAsPicture_Px(voShape As Variant, ByVal vsFileName As String, ByVal viX As Integer, ByVal viY As Integer)
   Dim ns As New NotesSession
   Dim nst As NotesStream
   Dim iX As Integer
   Dim iY As Integer
   Dim iType As Integer
   Dim bOK As Boolean

   Dim dX_Org As Double '元の画像の幅(ポイント)
   Dim dY_Org As Double '元の画像の高さ(ポイント)
   Dim dX_Out As Double '出力する画像の幅(ポイント)
   Dim dY_Out As Double '出力する画像の高さ(ポイント)

   '元のサイズ取得
   dX_Org = voShape.Width
   dY_Org = voShape.Height

   '希望の画素数にリサイズ
   dX_Out = dX_Org * (PixcelToPoint(viX)/dX_Org)
   dY_Out = dY_Org * (PixcelToPoint(viY)/dY_Org)
   voShape.Width = dX_Out
   voShape.Height = dY_Out

   '画像として保存し、サイズを検証
   bOK = False
   Do
      '画像として保存
      Call voShape.Chart.Export(vsFileName)

      '出力された画像の種類とサイズを取得
      Set nst = ns.CreateStream()
      Call nst.Open(vsFileName)
      iType = xGetImageFileInfo(nst, iX, iY)
      Call nst.Close()

      '出力された画像の検証
      If iType = DXL_Image_Unknown Then
         'サポートできない画像なのでこれで OK とする
         bOK = True
      Else
         'サイズの確認
         If iX = viX And iY = viY Then
            '希望通りのサイズ
            bOK = True
         Else
            'サイズが違う
            If iX < viX Then
               '小さい場合は 0.5 point 加算
               voShape.Width = voShape.Width + 0.5
            ElseIf iX > viX Then
               '大きい場合は 0.5 point 減算
               voShape.Width = voShape.Width - 0.5
            End If
            If iY < viY Then
               '小さい場合は 0.5 point 加算
               voShape.Height = voShape.Height + 0.5
            ElseIf iY > viY Then
               '大きい場合は 0.5 point 減算
               voShape.Height = voShape.Height - 0.5
            End If
         End If
      End If
   Loop Until bOK = True

   '元のサイズに戻す
   voShape.Width = dX_Org
   voShape.Height = dY_Org
End Function

ポイントは Do ~ Loop の中です。画像を出力し、そのサイズを確認、希望通りになっていなければサイズを微調整して再出力するようにしています。微調整は 1 Pixel 分の 0.75 Point でも問題ないとは思うのですが、特性が完全につかめていないので、0.5 Point と少し遠慮しています。

なお、今回のサンプルも前回同様に『DXL Step-by-Step:#13)イメージの形式とサイズの取得』で掲載した、定数と 2 つの関数を流用しています。また対応していない画像形式に対しては、サイズのチェックを行わず、初回の出力をそのまま採用しています。


テスト結果

この関数を利用して、ある画像を 32 ~ 48 Pixel まで変換してみました。結果は、対応している画像 jpg, gif, png では、指定通りのサイズが出力されるようになりました。そして、対応していない bmp では、一部希望通りとなっていないサイズが発生しています。

通常使用する画像形式は正しく出力できることが確認できました。今後新たな問題は発覚しないことを祈りつつ、指定した Pixel 数で画像を出力する関数はこれで完成とします。


前回 Notes - Excel 連携


2025/07/01

Notes - Excel 連携:#54)指定した Pixel 数で画像が出力できない !?

#52)画像のリサイズ』で、既存の画像ファイルをリサイズして再保存する方法を紹介しました。出力画像の縦横のサイズを Pixel で指定する仕様だったのですが、先日、希望通りのサイズにならない現象に出くわしました。誤差が原因だろうと、『#40)訂正 - 名前アイコン生成 』で紹介した、0.5 Point ずらす技を使いましたが、それでも、100% 改善することができませんでした。

そこで改めて、希望の Pixel と出力される画像サイズを検証してみました。想定外の結果になったので、検証してよかったのですが、#40 で訂正した内容のさらなる訂正記事のような状態となります...。ご了承ください。

 

検証方法

まずは、改めて Excel の挙動を確認します。

1000 x 1000 Pixel の画像を用意して、それをリサイズして再保存し、保存された画像ファイルのサイズをチェックします。リサイズは 760 Point から 770 Point まで 0.1 刻みで変化させます。Pixel で言うと 0.075 刻みとなるので、今回の症状を確実に捕まえられるという算段です。なお、縦横とも同じ Point を指定した正方形の画像が出力し、縦横の挙動の違いも調査します。

Notes - Excel 連携の連載ですので、参考までではありますが、検証プログラムを掲載します。調査対象の画像形式は、asFmt にセットしている通り、jpg, gif, png の 3 種類です。

Option Declare
Use "lsXls"

Public Const DXL_Image_Unknown = 0
Public Const DXL_Image_JPEG = 1
Public Const DXL_Image_GIF = 2
Public Const DXL_Image_PNG = 3

Sub Initialize
   Dim oXls As Variant
   Dim oSheet As Variant
   Dim oShape As Variant
   Dim oImage As Variant
   Dim sFN As String
   Dim ns As New NotesSession
   Dim nst As NotesStream
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer
   Dim asFmt(2) As String
   Dim i As Integer
   Dim iRow As Integer

   asFmt(0) = "jpg"
   asFmt(1) = "gif"
   asFmt(2) = "png"

   'Excel の準備
   Set oXls = CreateObject("Excel.Application")
   Call oXls.Workbooks.Add
   Set oSheet = oXls.Workbooks(1).WorkSheets(1)

   '画像サイズ取得
   Dim dX As Double '画像のサイズ(ポイント)

   'Chart オブジェクトを画像の大きさで作成
   Set oShape = xAddChart(oSheet, 750, 750)

   'Chart 上に画像ファイルを読み込み
   sFN = "C:\Sample.jpg"
   Set oImage = oShape.Chart.Shapes.AddPicture(sFN, msoFalse, msoTrue, 0, 0, -1, -1)

   'リサイズのテスト
   oSheet.Cells(1, 1).Value = "Point"
   oSheet.Cells(1, 2).Value = "Pixcel"
   For i = 0 To UBound(asFmt)
      oSheet.Cells(1, (i*2)+3).Value = "x(" & asFmt(i) & ")"
      oSheet.Cells(1, (i*2)+4).Value = "y(" & asFmt(i) & ")"

      iRow = 1
      For dX = 760 To 770 Step 0.1
         iRow = iRow + 1

         '初回のループのみ Point と 単純変換した Pixel を記録
         If i = 0 Then
            oSheet.Cells(iRow, 1).Value = dX
            oSheet.Cells(iRow, 2).Value = dX / 0.75
         End If

         'リサイズ
         oShape.Width = dX
         oShape.Height = dX

         'リサイズ画像の保存
         sFN = "C:\Resized_" & CStr(CInt(dX*10)) & "." & asFmt(i)
         Call xSaveAsPicture(oShape, sFN)

         '保存した画像サイズの確認
         Set nst = ns.CreateStream()
         Call nst.Open(sFN)
         iType = xGetImageFileInfo(nst, iSizeX, iSizeY)
         Call nst.Close()

         '画像サイズを Excel シートに記録
         oSheet.Cells(iRow, (i*2)+3).Value = iSizeX
         oSheet.Cells(iRow, (i*2)+4).Value = iSizeY
      Next
   Next

   oXls.Visible = True
End Sub

lsXls ライブラリはこれまでの連載で使用してきたライブラリです。また、画像サイズの取得方法として、別の連載『DXL Step-by-Step:#13)イメージの形式とサイズの取得』で掲載した、定数と xGetImageFileInfo、xGetImageFileType の 2 つの関数を流用しています。


実行結果の確認

検証プログラムを実行すると Excel シート上に結果が表示されます(図はセル書式セット後)。

このままでは傾向がつかめないので、Excel でグラフ化します。まずは、jpg 画像の結果は次の通りです。横軸が Point で、縦軸が Pixel です。

この結果より、2 つのことがわかります。

まず、リサイズ後の画像の横幅が青で縦が赤なのですが、ずれていることがわかります。縦横で同じ Point を指定したにもかかわらず、結果に差が出るということは、Pixel に丸める際に、縦横で仕様に差があるということです。

2 点目は少々厄介です。グレーの線は Point を単純に Pixel に変換したもので、実数なので直線になっています。その ± 0.5 が上下の薄いグレーの線です。この範囲内に入っているなら、四捨五入など丸め誤差の範囲と判断できるのですが、上側にはみ出ている箇所があります。この結果より、丸め処理だけで解決できる問題ではないと判定できます。また、グラフの波形が特定のパターンを示しているので、論理的な ”仕様” と考えられます。

続いて、別の画像形式の結果をグラフ化してみました。gif は jpg と同じ傾向だったのですが、png 形式では横幅の結果だけ微妙に違っていました。


対策は次回

これらの結果より、指定した Pixel 数で正確に画像を出力するためには、出力画像タイプごとの特性を把握したうえで、それに応じた微調整を行う必要があります。なぜこういった仕様になっているかは不明ですが、コーティングする上で大変悩ましい状況になりました。

ただ、少々長くなったので、その対策については次回にまとめます。


前回 Notes - Excel 連携 次回