Module: Labimotion::CoverImageHelpers

Extended by:
Grape::API::Helpers
Included in:
ElementHelpers
Defined in:
lib/labimotion/helpers/cover_image_helpers.rb

Overview

Element cover images (chem-generic-ui#1050, labimotion#259). cover_images is the one element-metadata key whose shape the gem owns: validation and normalization of the stored references, their direct save, the resolution of a reference to the attachment holding its picture, and the bytes served for the display area (originals), the picker rows (thumbnails) and the docx export.

Constant Summary collapse

COVER_THUMB_RESIZE =
'256x256>'
SVG_TRANSPARENT_RE =

ImageMagick's own SVG renderer reads the CSS keyword transparent as black — the defect behind the ELN thumbnails' black background — so paint properties are normalized to none first and the result is a PNG flattened onto white. Nil when conversion is unavailable or fails; callers fall back.

/((?:fill|stroke|stop-color|flood-color|background(?:-color)?)\s*[:=]\s*["']?)transparent\b/i

Instance Method Summary collapse

Instance Method Details

#analysis_preview_attachment(container) ⇒ Object

Server twin of the ELN client's getContainerImageData: among the thumbnail-bearing attachments of the analysis' dataset children, the container's preferred_thumbnail wins while it still exists, then the newest "combined" image, then the newest overall.



91
92
93
94
95
96
97
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 91

def analysis_preview_attachment(container)
  dataset_ids = container.children.where(container_type: 'dataset').pluck(:id)
  return nil if dataset_ids.empty?

  candidates = Attachment.where(attachable_type: 'Container', attachable_id: dataset_ids, thumb: true).to_a
  pick_preview_attachment(container, candidates)
end

#cover_image_attachment(element, source, sid) ⇒ Object

Resolves a stored cover-image reference to the Attachment holding its picture, or nil. Ownership is part of the resolution: an attachment is looked up through the element's own association, an analysis must be one of the element's analyses — a foreign id answers nil, never a file.



77
78
79
80
81
82
83
84
85
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 77

def cover_image_attachment(element, source, sid)
  case source
  when 'attachment'
    element.attachments.find_by(id: sid)
  when 'analysis'
    container = element.analyses.find_by(id: sid)
    container && analysis_preview_attachment(container)
  end
end

#cover_image_body(att, variant) ⇒ Object

The picker's rows ask for variant=thumbnail — the small uniform PNG; everything else means the original (display area). An svg's STORED thumbnail is black (see cover_rasterize), so it is re-rasterized from the original instead, falling back to the stored one.



119
120
121
122
123
124
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 119

def cover_image_body(att, variant)
  return cover_image_payload(att) unless variant == 'thumbnail'

  thumb = svg_cover_thumbnail(att) || att.read_thumbnail
  [thumb, 'image/png'] if thumb
end

#cover_image_entry_error!Object



54
55
56
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 54

def cover_image_entry_error!
  cover_images_error!("entries must be { source: #{Constants::CoverImage::SOURCES.join(' | ')}, id: integer }")
end

#cover_image_payload(att) ⇒ Object

The bytes + content type to serve for a cover attachment: the original file whenever a browser can render it, otherwise the PNG thumbnail (TIFF originals, raw-data files behind an analysis preview). Nil when neither is readable.



172
173
174
175
176
177
178
179
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 172

def cover_image_payload(att)
  if att.type_image? && !att.type_image_tiff?
    data = att.read_file
    return [data, att.content_type] if data
  end
  thumb = att.read_thumbnail
  [thumb, 'image/png'] if thumb
end

#cover_images_error!(reason) ⇒ Object



58
59
60
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 58

def cover_images_error!(reason)
  error!("metadata.cover_images #{reason}", 422)
end

#cover_rasterize(data, mime, resize: nil) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 144

def cover_rasterize(data, mime, resize: nil)
  # Deferred on purpose: mini_magick is a host-bundle gem, not a
  # dependency of this gem — at the top of the file it would break gem
  # load (and the spec suite) on a host without it, while here a miss is
  # rescued below into the nil fallback.
  require 'mini_magick'
  data = data.gsub(SVG_TRANSPARENT_RE, '\1none') if mime.include?('svg')
  image = MiniMagick::Image.read(data, cover_read_ext(mime))
  image.format('png') do |convert|
    convert.background('white')
    convert.flatten
    convert.resize(resize) if resize
  end
  image.to_blob
rescue LoadError, StandardError => e
  Labimotion.log_exception(e)
  nil
end

#cover_read_ext(mime) ⇒ Object



163
164
165
166
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 163

def cover_read_ext(mime)
  subtype = mime.split('/').last.to_s.sub('+xml', '')
  subtype.empty? ? '.img' : ".#{subtype}"
end

#normalized_cover_image!(entry) ⇒ Object



46
47
48
49
50
51
52
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 46

def normalized_cover_image!(entry)
  cover_image_entry_error! unless entry.is_a?(Hash)
  source = entry['source'] || entry[:source]
  id = entry['id'] || entry[:id]
  cover_image_entry_error! unless Constants::CoverImage::SOURCES.include?(source) && id.is_a?(Integer)
  { 'source' => source, 'id' => id }
end

#pick_preview_attachment(container, candidates) ⇒ Object



99
100
101
102
103
104
105
106
107
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 99

def pick_preview_attachment(container, candidates)
  return nil if candidates.empty?

  preferred = preferred_candidate(container, candidates)
  return preferred if preferred

  combined = candidates.select { |att| att.filename.to_s.downcase.include?('combined') }
  (combined.empty? ? candidates : combined).max_by(&:updated_at)
end

#preferred_candidate(container, candidates) ⇒ Object

hstore values are strings — the stored preferred_thumbnail id arrives as '1'.



110
111
112
113
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 110

def preferred_candidate(container, candidates)
  preferred_id = (container. || {})['preferred_thumbnail'].to_i
  candidates.find { |att| att.id == preferred_id }
end

#prepare_element_metadata(stored, incoming) ⇒ Object

Element metadata is a client-round-tripped document with several writers — layer groups/restrictions from the element details page, host regComps through the registry seam — and those writers remove a key by omitting it. So a present :metadata param replaces the stored hash wholesale; only an absent param leaves the stored metadata untouched (an old client that does not send :metadata must never wipe a saved cover selection). cover_images is the one key the gem owns: its entries are validated against the picker's cap and source vocabulary and normalized to exactly source/id, so nothing else reaches the jsonb column through that key.



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 25

def (stored, incoming)
  return stored || {} if incoming.nil?

  # to_h returns self for a plain Hash — dup so the caller's copy stays intact.
   = incoming.to_h.dup
  return  unless .key?('cover_images') || .key?(:cover_images)

  entries = .key?('cover_images') ? ['cover_images'] : [:cover_images]
  .delete(:cover_images)
  ['cover_images'] = validate_cover_images!(entries)
  
end

#save_cover_images(element, entries) ⇒ Object

Direct write of the one gem-owned metadata key (the cover picker's Save): a targeted merge, deliberately narrower than the element save's wholesale-replace contract — this writer owns exactly cover_images and can neither wipe nor resurrect any other key. Live like klass settings: not versioned until the next element save snapshots metadata.



67
68
69
70
71
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 67

def save_cover_images(element, entries)
  validated = validate_cover_images!(entries)
  element.update!(metadata: (element. || {}).merge('cover_images' => validated))
  validated
end

#svg_cover_thumbnail(att) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 128

def svg_cover_thumbnail(att)
  mime = att.content_type.to_s.downcase
  return nil unless mime.include?('svg')

  data = att.read_file
  data && cover_rasterize(data, mime, resize: COVER_THUMB_RESIZE)
end

#validate_cover_images!(entries) ⇒ Object



38
39
40
41
42
43
44
# File 'lib/labimotion/helpers/cover_image_helpers.rb', line 38

def validate_cover_images!(entries)
  cover_images_error!('must be an array') unless entries.is_a?(Array)
  max = Constants::CoverImage::MAX
  cover_images_error!("holds at most #{max} entries") if entries.size > max

  entries.map { |entry| normalized_cover_image!(entry) }
end