Class: C2PA::Manifest

Inherits:
Object
  • Object
show all
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.

Omitting the intent produces a manifest for a newly created asset.

c2pa-rs also has an :update intent, a restricted edit for non-editorial changes. It is not offered here because it requires an ingredient with real content, and add_ingredient records metadata only — signing with it fails with "ingredient file not found". Tracked separately.

%i[edit].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

Instance Method Summary collapse

Constructor Details

#initialize(title:, intent: nil, generator_name: nil, generator_version: nil) ⇒ Manifest

Returns a new instance of Manifest.

Parameters:

  • title (String)

    human-readable title for this asset

  • intent (Symbol, nil) (defaults to: nil)

    :edit; omit for a new creation

  • generator_name (String, nil) (defaults to: nil)

    the application doing the signing. Defaults to this gem. Supplying it credits your application as the claim generator, with the gem recorded alongside.

  • generator_version (String, nil) (defaults to: nil)

    version of that application

Raises:



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/c2pa/manifest.rb', line 33

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 = []
end

Instance Attribute Details

#intentSymbol? (readonly)

Returns the builder intent, if any.

Returns:

  • (Symbol, nil)

    the builder intent, if any



24
25
26
# File 'lib/c2pa/manifest.rb', line 24

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.

Parameters:

  • action (String)

    one of the C2PA::Actions constants

  • when_time (String, nil) (defaults to: nil)

    ISO 8601 timestamp of when the action occurred

  • software_agent (String, nil) (defaults to: nil)

    name/version of the software that performed the action; defaults to "ruby-c2pa/"

  • digital_source_type (String, nil) (defaults to: nil)

    URI from the C2PA digitalSourceType vocabulary

  • changed (Array<String>, nil) (defaults to: nil)

    list of regions or ingredients that changed

  • parameters (Hash, nil) (defaults to: nil)

    action-specific additional parameters

Returns:

  • (self)


58
59
60
61
62
63
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
# File 'lib/c2pa/manifest.rb', line 58

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.

Parameters:

  • label (String)

    the assertion label, e.g. "stds.schema-org.CreativeWork"

  • data (Hash)

    the assertion data

Returns:

  • (self)


111
112
113
114
# File 'lib/c2pa/manifest.rb', line 111

def add_assertion(label:, data:)
  @assertions << { "label" => label, "data" => data }
  self
end

#add_ingredient(title:, format:, instance_id:, relationship: "parentOf") ⇒ self

Add an ingredient (source asset) to this manifest.

Parameters:

  • title (String)

    human-readable title of the ingredient

  • format (String)

    MIME type of the ingredient, e.g. "image/jpeg"

  • instance_id (String)

    unique identifier for the ingredient instance

  • relationship (String) (defaults to: "parentOf")

    relationship to this asset; defaults to "parentOf"

Returns:

  • (self)


123
124
125
126
127
128
129
130
131
# File 'lib/c2pa/manifest.rb', line 123

def add_ingredient(title:, format:, instance_id:, relationship: "parentOf")
  @ingredients << {
    "title"        => title,
    "format"       => format,
    "instance_id"  => instance_id,
    "relationship" => relationship
  }
  self
end

#to_jsonString

Serialize to the JSON structure expected by c2pa-rs.

Returns:

  • (String)

Raises:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/c2pa/manifest.rb', line 138

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