Module: Pdfrb::Color::DefaultProfile

Defined in:
lib/pdfrb/color/default_profile.rb

Overview

Default sRGB ICC profile accessor. Builds a usable sRGB v4 ICC profile (header + tag table + tagged elements) that real PDF readers can consume for ICCBased color spaces. The profile includes:

* desc  — profile description (sRGB by pdfrb)
* wtpt  — D50 media white point
* rXYZ, gXYZ, bXYZ — sRGB colorant matrix (D50-adapted)
* rTRC, gTRC, bTRC — gamma 2.2 tone reproduction curves

Tagged-element types follow ICC.1:2022 §10 (multiLocalizedUnicodeType for desc, XYZType for colorants, curveType for TRCs).

Constant Summary collapse

SRGB_SIGNATURE =
"acsp".b
ICC_VERSION_V4 =
[0x04, 0x40, 0x00, 0x00].freeze

Class Method Summary collapse

Class Method Details

.build_srgb_header(tag_section_size) ⇒ Object

---- Header construction ----



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/pdfrb/color/default_profile.rb', line 55

def build_srgb_header(tag_section_size)
  header = +""
  header << ([0] * 4).pack("C*")                # Profile size placeholder
  header << ([0] * 4).pack("C*")                # Preferred CMM
  header << ICC_VERSION_V4.pack("C*")           # Version 4.4
  header << "mntr"                              # Device class
  header << "RGB "                              # Color space
  header << "XYZ "                              # PCS
  header << date_bytes                          # Date
  header << SRGB_SIGNATURE # 'acsp'
  header << "APPL"                              # Primary platform
  header << ([0] * 4).pack("C*")                # Profile flags
  header << ([0] * 4).pack("C*")                # Device manufacturer
  header << ([0] * 4).pack("C*")                # Device model
  header << ([0] * 8).pack("C*")                # Device attributes
  header << ([0] * 4).pack("C*")                # Rendering intent (perceptual)
  header << d50_illuminant_bytes                # PCS illuminant (D50)
  header << ([0] * 4).pack("C*")                # Profile creator
  header << ([0] * 16).pack("C*")               # Profile ID (MD5, patched later)
  header << ([0] * 28).pack("C*")               # Reserved

  total_size = header.bytesize + tag_section_size
  header[0, 4] = [total_size].pack("N")
  header
end

.build_srgb_tagsObject

Returns a Hash of { tag_signature => tag_bytes } in the order they should appear after the tag table.



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/pdfrb/color/default_profile.rb', line 96

def build_srgb_tags
  {
    "desc" => desc_tag_bytes("sRGB by pdfrb"),
    "wtpt" => xyz_tag_bytes(d50_xyz),
    "rXYZ" => xyz_tag_bytes(srgb_r_xyz),
    "gXYZ" => xyz_tag_bytes(srgb_g_xyz),
    "bXYZ" => xyz_tag_bytes(srgb_b_xyz),
    "rTRC" => curve_tag_bytes,
    "gTRC" => curve_tag_bytes,
    "bTRC" => curve_tag_bytes,
  }
end

.build_tag_table(tags) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/pdfrb/color/default_profile.rb', line 109

def build_tag_table(tags)
  out = +""
  count = tags.length
  out << [count].pack("N")

  # Compute offsets: tag table is 4 + count*12 bytes; each tag
  # element is appended after the table in order.
  offset = 4 + (count * 12)
  tags.each do |sig, bytes|
    out << sig
    out << [offset].pack("N")
    out << [bytes.bytesize].pack("N")
    offset += bytes.bytesize
  end
  out
end

.curve_tag_bytesObject

curveType with a single gamma value, per ICC.10.16. 2.2 is the canonical sRGB approximation (real sRGB uses a piecewise curve, but a single 2.2 gamma is acceptable for most rendering).



160
161
162
163
164
165
166
167
# File 'lib/pdfrb/color/default_profile.rb', line 160

def curve_tag_bytes
  body = +""
  body << "curv"                              # type signature
  body << ([0] * 4).pack("C*")                # reserved
  body << [0].pack("N") # count = 0 → gamma follows in u8Fixed8
  body << u8fixed8(2.2)
  body
end

.d50_illuminant_bytesObject



86
87
88
89
90
# File 'lib/pdfrb/color/default_profile.rb', line 86

def d50_illuminant_bytes
  # D50 X=0.9642, Y=1.0, Z=0.8249 in s15Fixed16.
  [0x00, 0x00, 0xF6, 0xD6, 0x00, 0x01, 0x00, 0x00,
   0x00, 0x00, 0xD3, 0x2D].pack("C*")
end

.d50_xyzObject

---- sRGB colorants (D50-adapted, per ICC.1:2022 Annex A) ----



183
184
185
# File 'lib/pdfrb/color/default_profile.rb', line 183

def d50_xyz
  [0.9642, 1.0000, 0.8249]
end

.date_bytesObject



81
82
83
84
# File 'lib/pdfrb/color/default_profile.rb', line 81

def date_bytes
  [0x07, 0xE8, 0x00, 0x01, 0x00, 0x01,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00].pack("C*")
end

.desc_tag_bytes(text) ⇒ Object

multiLocalizedUnicodeType (mluc) for desc, per ICC.10.13.



133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/pdfrb/color/default_profile.rb', line 133

def desc_tag_bytes(text)
  utf16 = text.encode("UTF-16BE").bytes
  body = +""
  body << "mluc"                              # type signature
  body << ([0] * 4).pack("C*")                # reserved
  body << [1].pack("N")                        # number of records
  body << [12].pack("N")                       # record size in bytes
  body << "enUS" # language + country
  body << [utf16.length].pack("N")             # string length in bytes
  body << [28].pack("N")                       # string offset (from tag start)
  body << utf16.pack("C*")
  body
end

.patch_profile_id(profile) ⇒ Object

Patch the Profile ID (offset 84..99) with the MD5 of the profile bytes computed with the ID slot zeroed (per ICC.1 §7.1.1).



204
205
206
207
208
209
210
# File 'lib/pdfrb/color/default_profile.rb', line 204

def patch_profile_id(profile)
  copy = profile.b
  copy[84, 16] = ([0] * 16).pack("C*")
  md5 = Digest::MD5.digest(copy)
  copy[84, 16] = md5
  copy
end

.s15fixed16(value) ⇒ Object

Encode a Float as ICC s15Fixed16 (signed 16.16).



170
171
172
173
# File 'lib/pdfrb/color/default_profile.rb', line 170

def s15fixed16(value)
  v = (value * 65536.0).round.to_i
  [v].pack("N")
end

.srgb_b_xyzObject



195
196
197
# File 'lib/pdfrb/color/default_profile.rb', line 195

def srgb_b_xyz
  [0.1431, 0.0606, 0.7141]
end

.srgb_bytesObject

Returns the sRGB ICC profile bytes (~ 600 bytes). Includes the structural header + tag table + tagged elements needed for a real ICCBased color space.



28
29
30
31
32
33
34
35
36
37
38
# File 'lib/pdfrb/color/default_profile.rb', line 28

def srgb_bytes
  tags = build_srgb_tags
  tag_table = build_tag_table(tags)
  header = build_srgb_header(tag_table.bytesize + tags_total_size(tags))
  profile = header + tag_table
  tags.each_value { |tag_bytes| profile << tag_bytes }
  # Compute MD5 ProfileID over the on-disk profile (sans the
  # existing ID slot, which we'll patch in). ICC.1:2022 §7.1.
  profile = patch_profile_id(profile)
  profile.force_encoding(Encoding::BINARY)
end

.srgb_color_space(document) ⇒ Object

Build an ICCBased color space array referencing a new stream containing the sRGB profile bytes. Returns [:ICCBased, ref] suitable for /ColorSpace resources.



43
44
45
46
47
48
49
50
51
# File 'lib/pdfrb/color/default_profile.rb', line 43

def srgb_color_space(document)
  bytes = srgb_bytes
  stream = document.add(
    { N: 3, Length: bytes.bytesize },
    type: Pdfrb::Model::Cos::Stream
  )
  stream.stream = bytes
  [:ICCBased, Pdfrb::Model::Reference.new(stream.oid, stream.gen)]
end

.srgb_g_xyzObject



191
192
193
# File 'lib/pdfrb/color/default_profile.rb', line 191

def srgb_g_xyz
  [0.3851, 0.7169, 0.0971]
end

.srgb_r_xyzObject



187
188
189
# File 'lib/pdfrb/color/default_profile.rb', line 187

def srgb_r_xyz
  [0.4360, 0.2225, 0.0139]
end

.tags_total_size(tags) ⇒ Object



126
127
128
# File 'lib/pdfrb/color/default_profile.rb', line 126

def tags_total_size(tags)
  tags.values.sum(&:bytesize)
end

.u8fixed8(value) ⇒ Object

Encode a Float as ICC u8Fixed8 (unsigned 8.8).



176
177
178
179
# File 'lib/pdfrb/color/default_profile.rb', line 176

def u8fixed8(value)
  v = (value * 256.0).round.to_i
  [v].pack("n")
end

.xyz_tag_bytes(xyz_triplet) ⇒ Object

XYZType for colorants and white points, per ICC.10.22.



148
149
150
151
152
153
154
# File 'lib/pdfrb/color/default_profile.rb', line 148

def xyz_tag_bytes(xyz_triplet)
  body = +""
  body << "XYZ "                              # type signature
  body << ([0] * 4).pack("C*")                # reserved
  xyz_triplet.each { |v| body << s15fixed16(v) }
  body
end