Class: C2PA::Manifest
- Inherits:
-
Object
- Object
- C2PA::Manifest
- Defined in:
- lib/c2pa/manifest.rb
Constant Summary collapse
- INTENTS =
Intents this gem can express.
:edit — this asset derives from a parent. c2pa-rs generates the parent ingredient from the source file and adds a c2pa.opened action wired to it by hashed URI. :update — a restricted edit for non-editorial changes, such as fixing metadata. The parent is the source file itself; an explicit ingredient, if given, must be that same file.
Omitting the intent produces a manifest for a newly created asset.
i[edit update].freeze
- GEM_FIELD =
c2pa-rs records itself in a namespaced field alongside the generator name, so the gem does the same when an application supplies its own.
"org.rubygems.ruby_c2pa".freeze
Instance Attribute Summary collapse
-
#ingredient_files ⇒ Array<Hash>
readonly
Ingredients supplied as files, for the signing layer to hand to c2pa-rs.
-
#intent ⇒ Symbol?
readonly
The builder intent, if any.
Instance Method Summary collapse
-
#add_action(action, when_time: nil, software_agent: nil, digital_source_type: nil, changed: nil, parameters: nil) ⇒ self
Add a C2PA action to this manifest.
-
#add_assertion(label:, data:) ⇒ self
Add an arbitrary assertion to this manifest.
-
#add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil) ⇒ self
Add an ingredient (source asset) to this manifest.
-
#initialize(title:, intent: nil, generator_name: nil, generator_version: nil) ⇒ Manifest
constructor
A new instance of Manifest.
-
#to_json ⇒ String
Serialize to the JSON structure expected by c2pa-rs.
Constructor Details
#initialize(title:, intent: nil, generator_name: nil, generator_version: nil) ⇒ Manifest
Returns a new instance of Manifest.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/c2pa/manifest.rb', line 31 def initialize(title:, intent: nil, generator_name: nil, generator_version: nil) unless intent.nil? || INTENTS.include?(intent) raise InvalidManifestError, "unknown intent #{intent.inspect}. Valid options: #{INTENTS.map(&:inspect).join(', ')}" end @title = title @intent = intent @generator_name = generator_name @generator_version = generator_version @actions = [] @assertions = [] @ingredients = [] @ingredient_files = [] end |
Instance Attribute Details
#ingredient_files ⇒ Array<Hash> (readonly)
Ingredients supplied as files, for the signing layer to hand to c2pa-rs. Each is a Hash with the ingredient's JSON description, its format, and the path to read.
52 53 54 |
# File 'lib/c2pa/manifest.rb', line 52 def ingredient_files @ingredient_files end |
#intent ⇒ Symbol? (readonly)
Returns the builder intent, if any.
22 23 24 |
# File 'lib/c2pa/manifest.rb', line 22 def intent @intent end |
Instance Method Details
#add_action(action, when_time: nil, software_agent: nil, digital_source_type: nil, changed: nil, parameters: nil) ⇒ self
Add a C2PA action to this manifest.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/c2pa/manifest.rb', line 64 def add_action(action, when_time: nil, software_agent: nil, digital_source_type: nil, changed: nil, parameters: nil) if action == Actions::OPENED raise InvalidManifestError, "#{Actions::OPENED} cannot be added directly. The specification requires it to " \ "reference a parentOf ingredient by hashed URI, and that hash is computed over " \ "the ingredient as c2pa-rs serialises it, so Ruby cannot construct one. Pass " \ "intent: :edit to C2PA::Manifest.new instead and the action will be added for you." end # Required as of c2pa-rs 0.90. Earlier versions accepted its absence, so # manifests signed by releases before 0.3.0 are rejected by current # verifiers. No default is supplied: c2pa-rs accepts any string here, so # a guess would validate while asserting something untrue about where the # asset came from. Use DigitalSourceTypes::UNSPECIFIED to decline. if action == Actions::CREATED && to_s_or_nil(digital_source_type).nil? raise InvalidManifestError, "#{Actions::CREATED} requires a digital_source_type. Choose the value that " \ "describes how the asset was produced — for example " \ "C2PA::DigitalSourceTypes::DIGITAL_CAPTURE for a camera original, or " \ "TRAINED_ALGORITHMIC_MEDIA for generative AI. If the origin is genuinely " \ "unknown, use C2PA::DigitalSourceTypes::UNSPECIFIED rather than guessing." end # Also new in c2pa-rs 0.90. if action == Actions::TRANSLATED missing = %w[sourceLanguage targetLanguage].reject { |key| param_present?(parameters, key) } unless missing.empty? raise InvalidManifestError, "#{Actions::TRANSLATED} requires #{missing.join(' and ')} in parameters, " \ "as RFC 5646 language codes" end end entry = { "action" => action } entry["when"] = when_time if when_time entry["softwareAgent"] = software_agent || "ruby-c2pa/#{VERSION}" entry["digitalSourceType"] = digital_source_type if digital_source_type entry["changed"] = changed if changed entry["parameters"] = parameters if parameters @actions << entry self end |
#add_assertion(label:, data:) ⇒ self
Add an arbitrary assertion to this manifest.
117 118 119 120 |
# File 'lib/c2pa/manifest.rb', line 117 def add_assertion(label:, data:) @assertions << { "label" => label, "data" => data } self end |
#add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil) ⇒ self
Add an ingredient (source asset) to this manifest.
With file:, c2pa-rs reads the ingredient itself. If that file carries
content credentials, its manifest is embedded and the ingredient points
at it, so provenance chains from the original through to this asset. A
verifier can then follow and check the whole history.
For a file with no credentials there is nothing to carry forward, and
the result is the same as the description alone. (Thumbnails would be
the other contribution, but they need c2pa-rs's add_thumbnails
feature, which this gem does not enable.)
Without file:, only the description is recorded. Nothing binds it to
any actual bytes. This form is kept for compatibility.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/c2pa/manifest.rb', line 144 def add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil) description = { "title" => title, "format" => format, "instance_id" => instance_id, "relationship" => relationship } if file.nil? @ingredients << description return self end unless File.file?(file) && File.readable?(file) raise InvalidManifestError, "ingredient file not readable: #{file.inspect}" end @ingredient_files << { "json" => JSON.generate(description), "format" => format, "path" => File.(file) } self end |
#to_json ⇒ String
Serialize to the JSON structure expected by c2pa-rs.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/c2pa/manifest.rb', line 174 def to_json raise InvalidManifestError, "at least one action is required" if @actions.empty? manifest = { "title" => @title, "claim_generator_info" => [claim_generator_info], "assertions" => [ { "label" => "c2pa.actions.v2", "data" => { "actions" => @actions } }, *@assertions ] } manifest["ingredients"] = @ingredients unless @ingredients.empty? begin JSON.generate(manifest) rescue JSON::GeneratorError => e # Typically a string that is not valid UTF-8 — a filename or caption # read in another encoding and passed through untouched. Without this # the caller gets a JSON::GeneratorError, which is not a C2PA::Error # and so escapes `rescue C2PA::Error`. raise InvalidManifestError, "manifest contains text that cannot be encoded as JSON: #{e.message}" end end |