{"id":19385810,"url":"https://github.com/smile-io/paperdragon","last_synced_at":"2026-06-11T01:31:00.975Z","repository":{"id":49902687,"uuid":"195121700","full_name":"smile-io/paperdragon","owner":"smile-io","description":"Smile.io's private fork of paperdragon. https://github.com/apotonick/paperdragon","archived":false,"fork":false,"pushed_at":"2022-01-17T16:57:31.000Z","size":149,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":16,"default_branch":"main","last_synced_at":"2025-02-24T17:50:45.402Z","etag":null,"topics":["library"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smile-io.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-07-03T20:23:19.000Z","updated_at":"2022-01-17T16:57:34.000Z","dependencies_parsed_at":"2022-09-02T06:03:20.906Z","dependency_job_id":null,"html_url":"https://github.com/smile-io/paperdragon","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/smile-io/paperdragon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smile-io%2Fpaperdragon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smile-io%2Fpaperdragon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smile-io%2Fpaperdragon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smile-io%2Fpaperdragon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smile-io","download_url":"https://codeload.github.com/smile-io/paperdragon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smile-io%2Fpaperdragon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34178819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["library"],"created_at":"2024-11-10T10:03:16.620Z","updated_at":"2026-06-11T01:31:00.960Z","avatar_url":"https://github.com/smile-io.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Paperdragon\n\n_Explicit image processing._\n\n## Summary\n\nPaperdragon gives you image processing as known from [Paperclip](https://github.com/thoughtbot/paperclip), [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) or [Dragonfly](https://github.com/markevans/dragonfly). It allows uploading, cropping, resizing, watermarking, maintaining different versions of an image, and so on.\n\nIt provides a very explicit DSL: **No magic is happening behind the scenes, paperdragon makes _you_ implement the processing steps.**\n\nWith only a little bit of more code you are fully in control of what gets uploaded where, which image version gets resized when and what gets sent to a background job.\n\nPaperdragon uses the excellent [Dragonfly](https://github.com/markevans/dragonfly) gem for processing, resizing, storing, etc.\n\nPaperdragon is database-agnostic, doesn't know anything about ActiveRecord and _does not_ hook into AR's callbacks.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'paperdragon'\n```\n\n\n## Example\n\nThis README only documents the public DSL. You're free to use the public API [documented here](# TODO) if you don't like the DSL.\n\n### Model\n\nPaperdragon has only one requirement for the model: It needs to have a column `image_meta_data`. This is a serialised hash where paperdragon saves UIDs for the different image versions. We'll learn about this in a minute.\n\n```ruby\nclass User \u003c ActiveRecord::Base # this could be just anything.\n  include Paperdragon::Model\n\n  processable :image\n\n  serialize :image_meta_data\nend\n```\n\nCalling `::processable` advises paperdragon to create a `User#image` reader to the attachment. Nothing else is added to the class.\n\n\n## Uploading\n\nProcessing and storing an uploaded image is an explicit step - you have to code it! This code usually goes to a separate class or an [Operation in Trailblazer](https://github.com/trailblazer/trailblazer#operation), don't leave it in the controller if you don't have to.\n\n```ruby\ndef create\n  file = params.delete(:image)\n\n  user = User.create(params) # this is your code.\n\n  # upload code:\n  user.image(file) do |v|\n    v.process!(:original)                                      # save the unprocessed.\n    v.process!(:thumb)   { |job| job.thumb!(\"75x75#\") }        # resizing.\n    v.process!(:cropped) { |job| job.thumb!(\"140x140+20+20\") } # cropping.\n    v.process!(:public)  { |job| job.watermark! }              # watermark.\n  end\n\n  user.save\nend\n```\n\nThis is a completely transparent process.\n\n1. Calling `#image` usually returns the image attachment. However, passing a `file` to it allows to create different versions of the uploaded image in the block.\n2. `#process!` requires you to pass in a name for that particular image version. It is a convention to call the unprocessed image `:original`.\n3. The `job` object is responsible for creating the final version. This is simply a `Dragonfly::Job` object and gives you [everything that can be done with dragonfly](http://markevans.github.io/dragonfly/imagemagick/).\n4. After the block is run, paperdragon pushes a hash with all the images meta data to the model via `model.image_meta_data=`.\n\nFor a better understanding and to see how simple it is, go and check out the `image_meta_data` field.\n\n```ruby\n user.image_meta_data #=\u003e {original: {uid: \"original-logo.jpg\", width: 240, height: 800},\n                      #    thumb:    {uid: \"thumb-logo.jpg\", width: 140, height: 140},\n                      #   ..and so on..\n                      #   }\n ```\n\n\n## Rendering Images\n\nAfter processing, you may want to render those image versions in your app.\n\n```ruby\nuser.image[:thumb].url\n```\n\nThis is all you need to retrieve the URL/path for a stored image. Use this for your image tags.\n\n```haml\n= img_tag user.image[:thumb].url\n```\n\nInternally, Paperdragon will call `model#image_meta_data` and use this hash to find the address of the image.\n\nWhile gems like paperclip often use several fields of the model to compute UIDs (addresses) at run-time, paperdragon does that once and then dumps it to the database. This completely removes the dependency to the model.\n\n\n## Reprocessing And Cropping\n\nOnce an image has been processed to several versions, you might need to reprocess some of them. As an example, users could re-crop their thumbs.\n\n```ruby\ndef crop\n  user = User.find(params[:id]) # this is your code.\n\n  # reprocessing code:\n  cropping = \"#{params[:w]}x#{params[:h]}#\"\n\n  user.image do |v|\n    v.reprocess!(:thumb, Time.now) { |job| job.thumb!(cropping) } # re-crop.\n  end\n\n  user.save\nend\n```\n\nOnly a few things have changed compared to the initial processing.\n\n1. We do not pass a file to `#image` anymore. This makes sense as reprocessing will re-use the existing original file.\n2. Note that it's not `#process!` but `#reprocess!` indicating a surprising reprocessing.\n3. As a second argument to `#reprocess!` a fingerprint string is required. To understand what this does, let's inspect `image_meta_data` once again. (The fingerprint feature is optional but extremely helpful.)\n\n\n```ruby\n user.image_meta_data #   ..original..\n                      #    thumb:    {uid: \"thumb-logo-1234567890.jpg\", width: 48, height: 48},\n                      #   ..and so on..\n                      #   }\n```\n\nSee how the file name has changed? Paperdragon will automatically append the fingerprint you pass into `#reprocess!` to the existing version's file name.\n\n\n## Renaming\n\nSometimes you just want to rename files without processing them. For instance, when a new fingerprint for an image is introduced, you want to apply that to all versions.\n\n```ruby\nfingerprint = Time.now\n\nuser.image do |v|\n  v.reprocess!(:thumb, fingerprint) { |job| job.thumb!(cropping) } # re-crop.\n  v.rename!(:original, fingerprint) # just rename it.\nend\n```\n\nThis will re-crop the thumb and _rename_ the original.\n\n```ruby\n user.image_meta_data #=\u003e {original: {uid: \"original-logo-1234567890.jpg\", ..},\n                      #    thumb:    {uid: \"thumb-logo-1234567890.jpg\", ..},\n                      #   ..and so on..\n                      #   }\n ```\n\n\n## Deleting\n\nWhile making images is a wonderful thing, sometimes you need to destroy to create. This is why paperdragon gives you a deleting mechanism, too.\n\n```ruby\nuser.image do |v|\n  v.delete!(:thumb)\nend\n```\n\nThis will also remove the associated metadata from the model.\n\nYou can delete all versions of an attachment by omitting the style.\n\n```ruby\nuser.image do |v|\n  v.delete! # deletes :original and :thumb.\nend\n```\n\n\n## Replacing Images\n\nIt's ok to run `#process!` again on a model with an existing attachment.\n\n```ruby\nuser.image_meta_data  #=\u003e {original: {uid: \"original-logo-1234567890.jpg\", ..},\n```\n\nProcessing here will overwrite the existing attachment.\n\n```ruby\nuser.image(new_file) do |v|\n  v.process!(:original) # overwrites the existing, deletes old.\nend\n```\n\n```ruby\nuser.image_meta_data  #=\u003e {original: {uid: \"original-new-file01.jpg\", ..},\n```\n\nWhile replacing the old with the new upload, the old file also gets deleted.\n\n\n## Fingerprints\n\nPaperdragon comes with a very simple built-in file naming.\n\nComputing a file UID (or, name, or path) happens in the `Attachment` class. You need to provide your own implementation if you want to change things.\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Paperdragon::Model\n\n  class Attachment \u003c Paperdragon::Attachment\n    def build_uid(style, file)\n      \"/path/to/#{style}/#{obfuscator}/#{file.name}\"\n    end\n\n    def obfuscator\n      Obfuscator.call # this is your code.\n    end\n  end\n\n  processable :image, Attachment # use the class you just wrote.\n```\n\nThe `Attachment#build_uid` method is invoked when processing images.\n\n```ruby\nuser.image(file) do |v|\n  v.process!(:thumb)   { |job| job.thumb!(\"75x75#\") }\nend\n```\n\nTo create the image UID, _your_ attachment is now being used.\n\n```ruby\n user.image_meta_data #   ..original..\n                      #    thumb:    {uid: \"/path/to/thumb/ac97dnxid8/logo.jpg\", ..},\n                      #   ..and so on..\n                      #   }\n```\n\nWhat a beautiful, cryptic and mysterious filename you just created!\n\nThe same pattern applies for _re-building_ UIDs when reprocessing images.\n\n```ruby\nclass Attachment \u003c Paperdragon::Attachment\n  # def build_uid and the other code from above..\n\n  def rebuild_uid(file, fingerprint)\n    file.uid.sub(\"logo.png\", \"logo-#{fingerprint}.png\")\n  end\nend\n```\n\nThis code is used to re-compute UIDs in `#reprocess!`.\n\nThat example is stupid, I know, but it shows how you have access to the `Paperdragon::File` instance that represents the existing version of the reprocessed image.\n\n\n## Local Rails Configuration\n\nConfiguration of paperdragon completely relies on [configuring dragonfly](http://markevans.github.io/dragonfly/configuration/). As an example, for a Rails app with a local file storage, I use the following configuration in `config/initializers/paperdragon.rb`.\n\n```ruby\nDragonfly.app.configure do\n  plugin :imagemagick\n\n  datastore :file,\n    :server_root =\u003e 'public',\n    :root_path =\u003e 'public/images'\nend\n```\n\nThis would result in image UIDs being prefixed accordingly.\n\n```ruby\nuser.image[:thumb].url #=\u003e \"/images/logo-1234567890.png\"\n```\n\n\n## S3\n\nAs [dragonfly allows S3](https://github.com/markevans/dragonfly-s3_data_store), using the amazon cloud service is straight-forward.\n\nAll you need to do is configuring your bucket. The API for paperdragon remains unchanged.\n\n```ruby\nrequire 'dragonfly/s3_data_store'\n\nDragonfly.app.configure do\n  datastore :s3,\n    bucket_name: 'my-bucket',\n    access_key_id: 'blahblahblah',\n    secret_access_key: 'blublublublu'\nend\n```\n\nImages will be stored \"in the cloud\" when using `#process!`, renaming, deleting and re-processing do the same!\n\n\n## Background Processing\n\nThe explicit design of paperdragon makes it incredibly simple to move all or certain processing steps to background jobs.\n\n```ruby\nclass Image::Processor\n  include Sidekiq::Worker\n\n  def perform(params)\n    user = User.find(params[:id])\n\n    user.image(params[:file]) do |v|\n      v.process!(:original)\n    end\n  end\nend\n```\n\nDocumentation how to use Sidekiq and paperdragon in Traiblazer will be added shortly.\n\n## Validations\n\nValidating uploads are discussed in the _Callbacks_ chapter of the [Trailblazer\nbook](https://leanpub.com/trailblazer). We use [file_validators](https://github.com/musaffa/file_validators).\n\n## Model: Reader and Writer\n\nIf you don't like `Paperdragon::Model#image`'s fuzzy API you can use `Reader` and `Writer`.\n\nThe `Writer` will usually be mixed into a form.\n\n```ruby\nclass AlbumForm \u003c Reform::Form\n  extend Paperdragon::Model::Writer\n  processable_writer :image\n```\n\nThis provides the `image!` writer for processing a file.\n\n```ruby\nform.image!(file) { |v| v.thumb!(\"64x64\") }\n```\n\nLikewise, `Reader` will usually be used in cells or decorators.\n\n```ruby\nclass AlbumCell \u003c Cell::ViewModel\n  extend Paperdragon::Model::Reader\n  processable_reader :image\n  property :image_meta_data\n```\n\nYou can now access the `Attachment` via `image`.\n\n```ruby\ncell.image[:thumb].url\n```\n\n\n## Paperclip compatibility\n\nI wrote paperdragon as an explicit alternative to paperclip. In the process of doing so, I step-wise replaced upload code, but left the rendering code unchanged. Paperclip has a slightly different API for rendering.\n\n```ruby\nuser.image.url(:thumb)\n```\n\nAllowing your paperdragon-backed model to expose this API is piece-of-cake.\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  include Paperdragon::Paperclip::Model\n```\n\nThis will allow both APIs for a smooth transition.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmile-io%2Fpaperdragon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmile-io%2Fpaperdragon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmile-io%2Fpaperdragon/lists"}