Skip to content

kt-paperclip 8 and ImageMagick 7 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Update kt-paperclip from 7.3.0 to 8.0.0 and make Easy8's Paperclip integration use Debian 13 ImageMagick 7 commands without ImageMagick 6 compatibility.

Architecture: Let kt-paperclip 8 own ImageMagick availability and command selection by deleting Easy8's Paperclip::Helpers#with_imagemagick? override. Keep Easy8-specific attachment lifecycle and processing behavior, but migrate deprecated command APIs and audit each remaining Paperclip patch against 8.0.0. Verify behavior through focused adapter, patch, avatar, and thumbnail tests inside the Debian 13 runtime.

Tech Stack: Ruby 4.0, Rails 8.1, RSpec, kt-paperclip 8.0.0, ImageMagick 7.1.1 on Debian 13, Bundler 4.


File Structure

  • Modify plugins/easyproject/Gemfile: constrain direct dependency to kt-paperclip ~> 8.0.0.
  • Delete easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb: remove legacy standalone identify and convert probes completely.
  • Modify app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb: use kt-paperclip 8 ImageMagick 7 command and geometry APIs.
  • Modify remaining files under easy_engines/easy_extensions/easy_patch/paperclip/ only where 8.0.0 compatibility requires it; preserve Easy8-specific behavior.
  • Create spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb: command and geometry behavior against Paperclip 8 API.
  • Create spec/models/easy_avatar_spec.rb: real attachment declaration, style processing contract, and callback behavior.
  • Create easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb: prove remaining patches apply to 8.0.0 targets and obsolete helper patch is absent.
  • Keep Gemfile.lock local and untracked; never stage or commit it.

Task 1: Prepare Dedicated Branch and Baseline

Files: - Inspect: plugins/easyproject/Gemfile:21 - Inspect: Gemfile.lock:446-451,1032 - Inspect: easy_engines/easy_extensions/easy_patch/paperclip/*.rb

  • [ ] Step 1: Inspect repository state and prerequisites

Run:

git status --short --branch
git diff
git log --oneline -10
git remote get-url origin
glab auth status

Expected: unrelated untracked files may exist, but no conflicting edits to planned files. Record remote host for glab commands.

  • [ ] Step 2: Refresh base and create branch

Run:

git fetch origin
git switch next/minor
git pull --ff-only
git switch --no-track -c feature/692469_update_kt_paperclip_gem origin/next/minor
git status --short --branch
git branch -vv

Expected: current branch is feature/692469_update_kt_paperclip_gem and does not track origin/next/minor.

  • [ ] Step 3: Capture baseline dependency graph

Run:

bundle info kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'
bundle exec ruby -e 'require "paperclip"; puts Gem.loaded_specs.fetch("kt-paperclip").dependencies.select { |d| d.type == :runtime }.map { |d| "#{d.name} #{d.requirement}" }'

Expected: version 7.3.0; runtime dependencies match local Gemfile.lock.

Task 2: Add ImageMagick 7 API Characterization Specs

Files: - Create: spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb - Create: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

  • [ ] Step 1: Write failing adapter API specs

Create spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb with focused examples that stub command execution, not external processes:

require "easy_extensions/spec_helper"

RSpec.describe EasyExtensions::ImageProcessing::Adapters::ImagemagickAdapter do
  describe ".get_geometry" do
    subject(:get_geometry) { described_class.get_geometry("image.png") }

    let(:geometry) { Paperclip::Geometry.new(100, 50) }

    before do
      allow(Paperclip::Commands::ImageMagick::GeometryParser)
        .to receive(:from_file)
        .with("image.png")
        .and_return(geometry)
    end

    it { is_expected.to eq(geometry) }
  end

  describe ".resize_image_to_fit" do
    subject(:resize_image_to_fit) do
      described_class.resize_image_to_fit(source.path, 64, 64, dst: destination.path)
    end

    let(:source) { Tempfile.new(["source", ".png"]) }
    let(:destination) { Tempfile.new(["destination", ".png"]) }

    before do
      allow(Paperclip::Commands::ImageMagick).to receive(:convert)
    end

    after do
      source.close!
      destination.close!
    end

    it "uses kt-paperclip ImageMagick command API" do
      resize_image_to_fit
      expect(Paperclip::Commands::ImageMagick).to have_received(:convert)
    end
  end
end

Adjust tempfile setup only if current adapter requires valid image bytes before command dispatch.

  • [ ] Step 2: Write failing patch compatibility specs

Create easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb:

require "easy_extensions/spec_helper"

RSpec.describe "Paperclip patches" do
  it "does not override ImageMagick availability detection" do
    expect(Paperclip::Helpers.ancestors.map(&:name))
      .not_to include("EasyPatch::Paperclip::HelpersPatch")
  end

  it "applies remaining patches to kt-paperclip 8 targets", :aggregate_failures do
    class_methods_patch = EasyPatch::Paperclip::GeometryPatch::ClassMethods

    expect(Paperclip::Thumbnail.ancestors).to include(EasyPatch::Paperclip::ThumbnailPatch)
    expect(Paperclip::HasAttachedFile.ancestors).to include(EasyPatch::Paperclip::HasAttachedFilePatch)
    expect(Paperclip::MediaTypeSpoofDetector.ancestors).to include(EasyPatch::Paperclip::MediaTypeSpoofDetectorPatch)
    expect(Paperclip::Geometry.singleton_class.ancestors).to include(class_methods_patch)
  end
end

Note: assert on the module NAME string for HelpersPatch (ancestors.map(&:name)) because the constant will not exist after Task 4 deletes its only definition — referencing the constant would raise NameError instead of passing.

If EasyPatchManager applies patches with inclusion rather than ancestor-visible prepend, assert the installed wrapped methods instead. Keep assertions tied to observable patch application.

  • [ ] Step 3: Run characterization specs and confirm failure

Run:

bundle exec rspec spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

Expected: failure because 7.3.0 lacks Paperclip::Commands::ImageMagick, helper patch is still installed, or direct adapter calls still use legacy APIs.

Task 3: Update kt-paperclip and Inspect Dependency Cascade

Files: - Modify: plugins/easyproject/Gemfile:21 - Local only: Gemfile.lock

  • [ ] Step 1: Change direct constraint

Change:

gem "kt-paperclip", "~> 7.3.0"

to:

gem "kt-paperclip", "~> 8.0.0"
  • [ ] Step 2: Resolve only requested dependency

Run:

bundle update kt-paperclip

Expected: kt-paperclip 8.0.0; no unrelated direct dependency updates.

  • [ ] Step 3: Inspect local lockfile changes

Run:

git diff --no-index /dev/null Gemfile.lock
bundle info kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'
bundle exec ruby -e 'require "paperclip"; puts Gem.loaded_specs.fetch("kt-paperclip").dependencies.select { |d| d.type == :runtime }.map { |d| "#{d.name} #{d.requirement}" }'

Expected: root gem is 8.0.0. Any changed transitive lock-only gem must be required by Bundler and documented; any changed explicitly declared gem triggers full dependency-cascade analysis before proceeding.

  • [ ] Step 4: Verify Ruby and Rails compatibility in resolved bundle

Run:

bundle exec ruby -e 'spec = Gem.loaded_specs.fetch("kt-paperclip"); puts spec.required_ruby_version; puts RUBY_VERSION'
bundle exec rails runner 'puts [Rails.version, Paperclip::VERSION].join(" / ")'

Expected: required Ruby >= 2.7.0, project Ruby 4.0.6, Rails boot succeeds.

Task 4: Remove Legacy ImageMagick Detection Patch

Files: - Delete: easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb - Test: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

  • [ ] Step 1: Delete helper patch completely

Delete easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb. Do not replace it with delegation or fallback logic.

  • [ ] Step 2: Verify no Easy8 legacy command probes remain

Run:

rg 'run\("(identify|convert)"|Paperclip\.run|GeometryDetector|command_path' app easy_engines plugins lib config

Expected after all migration tasks: no executable legacy Paperclip calls. Configuration documentation may still mention Redmine's separate thumbnail command and must be assessed independently.

  • [ ] Step 3: Verify Paperclip 8 detects ImageMagick 7

Run in Debian 13 runtime:

magick -version
bundle exec rails runner 'abort "ImageMagick 7 not detected" unless Paperclip::Commands::ImageMagick::VersionDetector.detected_version == 7; puts 7'

Expected: ImageMagick 7.1.1-43 family and output 7.

Task 5: Migrate Adapter to ImageMagick 7 APIs

Files: - Modify: app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb:10-92 - Test: spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb

  • [ ] Step 1: Replace deprecated geometry detector

Change .get_geometry to:

def get_geometry(src)
  geometry = ::Paperclip::Commands::ImageMagick::GeometryParser.from_file(src)
  geometry || raise(EasyExtensions::ImageProcessing::AdapterProcessException)
rescue ::Paperclip::Errors::NotIdentifiedByImageMagickError
  raise EasyExtensions::ImageProcessing::AdapterProcessException
end
  • [ ] Step 2: Route conversion through Paperclip 8 command API

Replace creation of Paperclip::Thumbnail solely to call protected/legacy convert with direct calls to:

::Paperclip::Commands::ImageMagick.convert(parameters, interpolation_values)

Build parameters with Terrapin placeholders rather than embedding source/destination shell quoting. For example:

parameters = [":source", "-auto-orient", "-strip", "-resize", %(") + geometry.to_s + %(")]
parameters << crop_option(crop) if crop
parameters << ":destination"

::Paperclip::Commands::ImageMagick.convert(
  parameters.join(" "),
  source: File.expand_path(src),
  destination: "#{format}#{File.expand_path(options[:dst] || src)}"
)

Preserve existing resize, crop, format, PDF conversion, and exception translation semantics. Do not add convert fallback.

  • [ ] Step 3: Run adapter specs

Run:

bundle exec rspec spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb

Expected: PASS and command expectation targets Paperclip::Commands::ImageMagick.convert.

  • [ ] Step 4: Run RuboCop for adapter and spec

Run:

bundle exec rubocop app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb

Expected: no offenses.

Task 6: Audit and Modernize Remaining Paperclip Patches

Files: - Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/thumbnail_patch.rb - Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/has_attached_file_patch.rb - Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/geometry_patch.rb - Modify if needed: easy_engines/easy_extensions/easy_patch/paperclip/media_type_spoof_detector_patch.rb - Test: easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

  • [ ] Step 1: Compare each patch target with installed 8.0.0 source

Run:

bundle show kt-paperclip
bundle exec ruby -e 'require "paperclip"; puts Paperclip::Thumbnail.instance_method(:make).source_location; puts Paperclip::HasAttachedFile.instance_method(:define).source_location; puts Paperclip::MediaTypeSpoofDetector.instance_method(:spoofed?).source_location; puts Paperclip::Geometry.method(:from_file).source_location'

Expected: all target constants and methods exist. Record source paths and compare signatures/private state used by patches.

  • [ ] Step 2: Replace alias_method_chain only where Paperclip 8 breaks patch installation

Use minimal prepend modules when required. Example for HasAttachedFile:

module EasyPatch
  module Paperclip
    module HasAttachedFilePatch
      def define
        super
        return unless EasyExtensions::EasyProjectSettings.enable_copying_easy_images_to_public

        define_easy_assets_callbacks_methods
        add_easy_assets_callbacks
      end

      private

      def add_easy_assets_callbacks
        @klass.after_save :copy_to_public
        @klass.after_destroy :remove_from_public
      end

      def define_easy_assets_callbacks_methods
        @klass.define_method(:copy_to_public) { EasyExtensions::EasyAssets.copy_to_public(self) }
        @klass.define_method(:remove_from_public) { EasyExtensions::EasyAssets.remove_from_public(self) }
      end
    end
  end
end

Use repository patch-manager registration expected for prepend-capable patches. Do not refactor patches that already apply and pass behavior tests.

  • [ ] Step 3: Preserve explicit spoof-detector policy

Keep existing behavior returning false; do not silently enable validation during dependency update. Remove dead commented code if touched by required compatibility changes. Document security policy in MR description.

  • [ ] Step 4: Run patch compatibility specs

Run:

bundle exec rspec easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

Expected: PASS; helper availability override absent; remaining required patches applied.

  • [ ] Step 5: Run RuboCop on touched patch files and spec

Run:

bundle exec rubocop easy_engines/easy_extensions/easy_patch/paperclip/thumbnail_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/has_attached_file_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/geometry_patch.rb easy_engines/easy_extensions/easy_patch/paperclip/media_type_spoof_detector_patch.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb

Pass only files actually touched if some patches need no changes. Expected: no offenses.

Task 7: Add Real EasyAvatar Integration Coverage

Files: - Create: spec/models/easy_avatar_spec.rb - Inspect fixtures: test/fixtures/files/ or existing image fixtures

  • [ ] Step 1: Write real attachment contract specs

Create model examples using an existing small PNG/JPEG fixture:

require "easy_extensions/spec_helper"

RSpec.describe EasyAvatar, type: :model do
  describe "image attachment" do
    subject(:avatar) do
      described_class.new(entity: user, image: Rack::Test::UploadedFile.new(image_path, "image/png"))
    end

    let(:user) { build_stubbed(:user) }
    let(:image_path) { Rails.root.join("test/fixtures/files/image.png") }

    it "defines all generated styles", :aggregate_failures do
      expect(avatar.image.styles.keys).to contain_exactly(:original, :large, :medium, :small)
      expect(avatar.image.options[:processors]).to eq([:cropper])
    end

    it { is_expected.to be_valid }
  end
end

Use a persisted entity only if polymorphic validation or callback behavior requires it. Choose an existing fixture path verified in repository; do not create a large binary fixture.

  • [ ] Step 2: Add public-copy callback assertion

Assert EasyAvatar responds to copy_to_public and remove_from_public when copying is enabled, proving HasAttachedFile patch behavior rather than implementation internals.

  • [ ] Step 3: Run model spec

Run:

bundle exec rspec spec/models/easy_avatar_spec.rb

Expected: PASS without deprecation warnings from GeometryDetector, Paperclip.run, or command_path.

  • [ ] Step 4: Run RuboCop

Run:

bundle exec rubocop spec/models/easy_avatar_spec.rb

Expected: no offenses.

Task 8: Verify Real ImageMagick 7 Processing Paths

Files: - No committed file expected unless a failing check exposes required code changes.

  • [ ] Step 1: Confirm bundle installation

Run:

bundle install

Expected: exit code 0, kt-paperclip 8.0.0. Gemfile.lock remains local only.

  • [ ] Step 2: Run focused specs

Run:

bundle exec rspec spec/models/easy_avatar_spec.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb spec/models/attachment_spec.rb spec/utils/easy_extensions/easy_assets_spec.rb spec/libs/easy_extensions/avatar_spec.rb

Expected: all pass.

  • [ ] Step 3: Exercise actual ImageMagick 7 conversion

Run against an existing image fixture:

bundle exec rails runner 'src = Rails.root.join("test/fixtures/files/image.png"); dst = Tempfile.new(["kt-paperclip-8", ".png"]); EasyExtensions::ImageProcessing::Adapters::ImagemagickAdapter.resize_image_to_fit(src.to_s, 32, 32, dst: dst.path); abort "empty thumbnail" unless File.size?(dst.path); puts dst.path'

Expected: output file exists and is non-empty. Replace fixture path with verified existing PNG path if needed.

  • [ ] Step 4: Check for legacy command usage and deprecations

Run:

rg 'run\("(identify|convert)"|Paperclip\.run|GeometryDetector|command_path' app easy_engines plugins lib

Expected: no production matches requiring migration. Any remaining match must be comments, historical migration text, or unrelated Redmine configuration and documented.

  • [ ] Step 5: Run all touched-file lint checks

Run:

bundle exec rubocop plugins/easyproject/Gemfile app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb spec/utils/easy_extensions/image_processing/adapters/imagemagick_adapter_spec.rb spec/models/easy_avatar_spec.rb easy_engines/easy_extensions/spec/easy_patch/paperclip/patches_spec.rb
git diff --check

Do not pass plugins/easyproject/Gemfile to RuboCop if project configuration excludes Gemfiles; lint every touched .rb file. Expected: no offenses and no whitespace errors.

Task 9: Review, Commit, Push, and Create MR

Files: - Commit only intended source and spec files. - Never commit: Gemfile.lock.

  • [ ] Step 1: Review final diff and lock resolution

Run:

git status --short
git diff -- plugins/easyproject/Gemfile app/utils/easy_extensions/image_processing/adapters/imagemagick_adapter.rb easy_engines/easy_extensions/easy_patch/paperclip spec
bundle exec ruby -e 'require "paperclip"; puts Paperclip::VERSION'

Expected: only intended tracked changes; version 8.0.0.

  • [ ] Step 2: Stage intended files only

Run explicit git add with actual touched paths. Include deletion of easy_engines/easy_extensions/easy_patch/paperclip/paperclip_patch.rb. Never use git add ..

  • [ ] Step 3: Enforce lockfile safety check

Run:

git diff --cached --name-only

Expected: Gemfile.lock absent. If present, run git restore --staged Gemfile.lock and repeat check.

  • [ ] Step 4: Commit

Run:

git commit -m "chore(gems): update kt-paperclip to 8.0.0 (refs #692469)"

Expected: commit succeeds with only intended files.

  • [ ] Step 5: Verify branch tracking and dry-run push

Run:

git status --short --branch
git branch -vv
git push --dry-run origin HEAD:refs/heads/feature/692469_update_kt_paperclip_gem

Expected destination: exactly refs/heads/feature/692469_update_kt_paperclip_gem.

  • [ ] Step 6: Push explicit branch ref

Run:

git push -u origin HEAD:refs/heads/feature/692469_update_kt_paperclip_gem
  • [ ] Step 7: Create merge request

Create MR into next/minor with:

Title: chore(gems): update kt-paperclip to 8.0.0 (refs #692469)
Reviewer: merge_request_pool
Assignee: lukasp
Options: remove source branch, squash before merge

MR description must summarize:

  • kt-paperclip 7.3.0 -> 8.0.0;
  • Debian 13/ImageMagick 7-only support;
  • deleted Easy8 legacy availability patch;
  • migrated geometry and conversion APIs;
  • remaining patch audit results;
  • spoof detection remains intentionally disabled by existing policy;
  • exact test and RuboCop commands/results;
  • Gemfile.lock remained local and was not committed.

  • [ ] Step 8: Return MR URL and update issue

Post final verification and MR URL to Easy8 issue #692469, then report branch, commit, MR URL, tests, lint, and lockfile safety result.

Self-Review

  • Spec coverage: dependency update, patch audit, complete helper-patch deletion, ImageMagick 7-only migration, usage verification, issue/MR workflow all mapped to tasks.
  • Scope: one requested root gem, one branch, one commit, one MR. Required integration changes remain in same update.
  • Dependency safety: local lockfile resolution inspected but never staged.
  • Compatibility: no ImageMagick 6 fallback introduced.
  • Security: existing spoof-detector override preserved and surfaced rather than changed implicitly.