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
transparentas black — the defect behind the ELN thumbnails' black background — so paint properties are normalized tononefirst 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
-
#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.
-
#cover_image_attachment(element, source, sid) ⇒ Object
Resolves a stored cover-image reference to the Attachment holding its picture, or nil.
-
#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).
- #cover_image_entry_error! ⇒ Object
-
#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).
- #cover_images_error!(reason) ⇒ Object
- #cover_rasterize(data, mime, resize: nil) ⇒ Object
- #cover_read_ext(mime) ⇒ Object
- #normalized_cover_image!(entry) ⇒ Object
- #pick_preview_attachment(container, candidates) ⇒ Object
-
#preferred_candidate(container, candidates) ⇒ Object
hstore values are strings — the stored preferred_thumbnail id arrives as '1'.
-
#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.
-
#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.
- #svg_cover_thumbnail(att) ⇒ Object
- #validate_cover_images!(entries) ⇒ Object
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 (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 (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 (element, source, sid) case source when 'attachment' element..find_by(id: sid) when 'analysis' container = element.analyses.find_by(id: sid) container && (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 (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 |