国際的な慣例、前置きとして。#
今回は必ず SEO タグを満たします!2024 年に単色黒の QR コードの生成方法が検索できないなんて許しません!
QR コードを生成するのは手があればできる?#
コードで QR コードを生成するのは難しいことではありません。多くの人が入社して最初に目に見えるプログラムとして QR コードを生成したと言われています。会社の業務バックエンドにも QR コードを生成する機能があり、これらの QR コードは印刷され、最終的に紙の本に表示されます。
ある日、ビジネスサイドから連絡があり、私たちが生成した QR コードは四色黒で、印刷コストが高いため、印刷前に手動で単色黒に処理する必要があると言われました。一度にバックエンドから単色黒としてエクスポートできるか尋ねられました。
私たちは何も大したことは見たことがないので、深く考えた後、当場で DingTalk グループに知恵に満ちた一言を打ちました:単色黒とは何か - -!
単色黒?四色黒?#
単色黒と四色黒は印刷業界特有の用語のように思えますが、実際には私たちはすでに接触しているかもしれません。
私たちは中学校(もしかしたら小学校?)で色光三原色という概念を学びました。三色の光を重ねることで、さまざまな色を得ることができます。三色の光が最終的に白色になるため、この色彩モデルは加色法とも呼ばれ、現在ではディスプレイや写真などのシーンで広く使用されています(これにより形成される色彩モードはRGB色彩モードと呼ばれます)。
印刷や絵画などの分野にも、私たちがよく知っている色料三原色があります。これはシアン、マゼンタ、イエローの三色の顔料を重ねることで、同様にさまざまな色を得ることができます。
理論的には、シアン、マゼンタ、イエローの三色の顔料の濃度が最大に達すると、得られる色は黒色ですが、実際の生産では効果が期待外れで、主に以下の点が含まれます:
- 三色の重ね合わせによって形成される黒色は不純で、灰色がかった黒色が現れ、効果が良くない;
- 三色の重ね合わせによって形成される黒色はインクを多く消費し、印刷コストが高い;
- 三原色を整然と重ねるためには印刷の精度が非常に高く要求され、そうでなければエッジに明らかなぼやけが現れる;
そのため、印刷業界は三原色の基礎の上に第四の原色として黒色を追加しました。これにより、シアン (Cyan)、マゼンタ (Magenta)、イエロー (Yellow)、黒 (blacK) の四つの原色に基づくカラー印刷の色彩モードが誕生しました。四つの色の英単語からそれぞれ一文字を取って、その名前がCMYK色彩モードとなりました。
これが現在のカラープリンターのインクがこの四色である理由を説明できます。例えば👉小米のインクジェットプリンターのインク(本当に広告ではありません……
CMYK色彩モードは、四つの原色の濃度を 4 つのパーセンテージで表現します。四つの原色のうちシアン、マゼンタ、イエローがそれぞれ 0% で、黒色が 100% のときに形成される黒色は単色黒と呼ばれ、CMYK(0% 0% 0% 100%)として表現されます。(実際には黒色チャンネルの値が 100% である必要は厳密にはなく、人間の目は飽和度の微細な差を識別できません)
前述のように、単色黒はインクが一層必要で、印刷の精度要求もそれほど高くないため、カラー印刷における大面積の黒色は通常単色黒が優先されます。
では問題が生じます:私たちが生成しているのは何ですか?#
生成した QR コードを PhotoShop にインポートして色彩モードを確認すると、それはインデックスモードとして認識されました。これは非常に限られた色数の色彩モードで、通常はウェブ表示などのシーンで使用されます。色が限られているため、サイズは比較的小さくなります。しかし、より多くの色の要求がある場合、通常はインデックスモードをRGBモードに変換して処理します。後に白黒 QR コードにカラーモノグラムを追加する必要があるため、ここではインデックスモードについてはあまり詳しく議論せず、より一般的なRGBモードで代替します。
RGB色彩モードでは、通常RGB(0, 0, 0)で黒色を表現します(時には 16 進数の#000000も使用されますが、本質的には同じものです)。では、単色黒との違いは何でしょうか?
Wiki で RGB と CMYK の変換公式を調べることができますが、残念ながら、一方では専門ソフトウェアを使用して処理しようとすると変換結果が常に合わないことがわかりますし、もう一方では実際には RGB 色彩モードが表現できる色の範囲は CMYK 色彩モードよりもはるかに広いのです。
PhotoShop でRGB(0, 0, 0)の画像を開き、その中の黒色部分を吸い取ってCMYK色彩モードでの色値を確認すると、さまざまな異なる結果が表示されるかもしれません。これらの結果はすべてICC色彩記述ファイル(国際色彩印刷協会の色彩工業標準)を参考にして計算されています。異なる記述ファイルを選択して変換を試みることができるこのサイトでは、得られる結果はすべて異なります。現在業界で一般的なのはJapan Color 2001 Coatedです。
このように 4 つのチャンネルが重なってできた黒色が、前述の四色黒です(実際にはどのICC規範ファイルに切り替えても、RGB(0, 0, 0)はおそらく単色黒には変換されないでしょう)。
以下は Adobe Acrobat でRGB(0, 0, 0)で塗りつぶされた画像を開き、その 4 つのチャンネルの色情報を確認したスクリーンショットです。黒色チャンネルと黄色チャンネルをオフにしても他の色の QR コードが見えることがわかります。これは、この QR コードの黒色が複数の色の重ね合わせによって実現されていることを意味します。
手を抜く:グレースケールモード#
印刷と色彩の基本原理を理解した後、私たちは CMYK 色彩モードを使用すべきであることを簡単に理解できますが、その前に少し脱線したいと思います。ファイル形式に特定の要件がなく、QR コードに他のカラフルな内容を重ねるつもりがない場合、実際には手を抜く方法があります —— グレースケールモードです。
グレースケールモードは通常 8 ビットのグレースケールチャンネルを使用して表現され、0 は黒色、255 は白色、127 は灰色を表します。
一見すると、CMYK 色彩モードとは何の関係もないように見えますが、別の角度から考えると、グレースケールモードは黒、白、灰の三色だけで構成されており、これは CMYK 色彩モードの黒色チャンネルの異なる飽和度にちょうど対応しています。
そのため、実際には QR コードをグレースケールモードに変換するだけで、Adobe Acrobat で出力プレビューを通じてレイヤー情報を確認すると、その画像の黒色がすべて K チャンネルに位置していることがわかります。これが私たちが求めている単色黒です。
TIFF ファイル規格#
世界は常に私の思い通りにはいきません。QR コードの中央にカラフルなアプリのロゴを追加することは非常に一般的な要求です。画像全体がグレースケールモードである場合、カラフルなロゴも色彩情報を失います。
印刷シーンでは CMYK 色彩モードを使用するべきですが、1 つの障害があります:すべての画像ファイル形式が CMYK 色彩モードをサポートしているわけではありません。
偶然にもビジネスサイドにはそのような要求があり、彼らは生成される QR コードが tif 形式であることを望んでいます。なぜなら、そのようなファイルは Adobe InDesign でのレイアウト作業に便利だからです。
TIFF ファイル形式でカラフルなロゴを持つ単色黒 QR コードを実現するには、2 つのアプローチがあります:
- 同じ TIFF ファイル内に 2 つの画像ファイルを保持する。一つはグレースケールモード、もう一つはアプリロゴの元の色彩モード(例えば
RGBモード)を使用する; - CMYK 色彩モードを使用して QR コード部分とアプリロゴ部分を再描画する;
IFD#
TIFF 6.0規格を読むことで、TIFF ファイルは複数のIFD(Image File Directory)で構成されており、各IFDは独自の色彩モードを持つことができることがわかります。この方法を使用することで、1 つのファイル内にグレースケールとRGBなどの複数の色彩モードを同時に含めることができます。
ここでは柴樹杉さんのブログの内容を直接引用します。面白いことに、1 つのIFDには通常、画像の属性を説明するための複数のTagがありますが、色彩モードを定義するのは特定のTagではなく、一連のTagによって共同で定義されます。
+------------------------------------------------------------------------------+
| TIFF Structure |
| IFH |
| +------------------+ |
| | II/MM | |
| +------------------+ |
| | 42 | IFD |
| +------------------+ +------------------+ |
| | Next IFD Address |--->| IFD Entry Num | |
| +------------------+ +------------------+ |
| | IFD Entry 1 | |
| +------------------+ |
| | IFD Entry 2 | |
| +------------------+ |
| | | IFD |
| +------------------+ +------------------+ |
| IFD Entry | Next IFD Address |--->| IFD Entry Num | |
| +---------+ +------------------+ +------------------+ |
| | Tag | | IFD Entry 1 | |
| +---------+ +------------------+ |
| | Type | | IFD Entry 2 | |
| +---------+ +------------------+ |
| | Count | | | |
| +---------+ +------------------+ |
| | Offset |--->Value | Next IFD Address |--->NULL |
| +---------+ +------------------+ |
| |
+------------------------------------------------------------------------------+
ただし、複数の IFD で構成される TIFF ファイルのサイズは比較的大きくなるため、この方法は考慮しません。(実際には、私が見つけたライブラリは結合時にすべての素材を 1 つの IFD に統合することを選択したため…… そうでなければ、グレースケールモードに直接ロゴを重ねることを考えていたのですが……
CMYK 色彩モードに基づく実装#
TIFF 6.0 規格の拡張章では、TIFF ファイル形式が CMYK 色彩モードを明確にサポートしていることが示されています。CMYK 色彩を基に QR コードを再描画することについては特に説明することはありませんので、直接コードを示します。
package main
import (
"bytes"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"github.com/skip2/go-qrcode"
"github.com/sunshineplan/tiff"
)
func main() {
// QRコードを生成
qrCode, err := qrcode.Encode("https://www.xiongyuchi.com/2024/06/18/wu-cai-ban-lan-de-hei-lao-yi-lao-dan-se-hei-yu-si-se-hei/", qrcode.Medium, 2048)
if err != nil {
panic(err)
}
// 画像オブジェクトに変換
img, err := png.Decode(bytes.NewReader(qrCode))
if err != nil {
panic(err)
}
// ウォーターマークファイルを開く
wmb_file, err := os.Open("../watermark.png")
if err != nil {
panic(err)
}
defer wmb_file.Close()
// ウォーターマークファイルをデコード
wmb_img, err := png.Decode(wmb_file)
if err != nil {
panic(err)
}
// ウォーターマークを追加
offset := image.Pt((img.Bounds().Dx() - wmb_img.Bounds().Dx()) / 2, (img.Bounds().Dy() - wmb_img.Bounds().Dy()) / 2)
b := img.Bounds()
m := image.NewCMYK(b)
draw.Draw(m, b, img, image.ZP, draw.Src)
draw.Draw(m, wmb_img.Bounds().Add(offset), wmb_img, image.ZP, draw.Over)
// case 1 CMYK色値モードのTIFFファイルをエクスポート
SaveAsTiff(m, "result_cmyk.tif")
// case 2 グレースケールモードのTIFFファイルをエクスポート
grayImg := ConvertToGrayScale(m)
SaveAsTiff(grayImg, "result_gray.tif")
}
// 画像をCMYK色彩モードに変換
func ConvertToCMYK(img image.Image) *image.CMYK {
bounds := img.Bounds()
cmykImg := image.NewCMYK(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
originalColor := img.At(x, y)
cmykColor := color.CMYKModel.Convert(originalColor)
cmykImg.Set(x, y, cmykColor)
}
}
return cmykImg
}
// 画像をグレースケールモードに変換
func ConvertToGrayScale(img image.Image) *image.Gray {
bounds := img.Bounds()
grayImg := image.NewGray(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
originalColor := img.At(x, y)
grayColor := color.GrayModel.Convert(originalColor)
grayImg.Set(x, y, grayColor)
}
}
return grayImg
}
// 画像をTIFFファイルとして保存
func SaveAsTiff(img image.Image, outputPath string) error {
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
return tiff.Encode(file, img, &tiff.Options{Compression: tiff.LZW})
}
最終的な結果:おっと!崩れた