diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c292590..999f4bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,13 +4,13 @@ on: push: branches: - main - pull_request: jobs: build: runs-on: ubuntu-latest name: Ruby ${{ matrix.ruby }} + strategy: matrix: ruby: @@ -20,10 +20,21 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false + + # 👇 ESTE STEP FALTABA + - name: Install system dependencies (GD) + run: | + sudo apt update + sudo apt install -y \ + libgd-dev \ + pkg-config \ + build-essential + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true + - name: Run the default task run: bundle exec rake diff --git a/Gemfile b/Gemfile index 5689813..1838841 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ source "https://rubygems.org" gemspec gem "irb" +gem "pry" gem "rake", "~> 13.0" gem "rspec", "~> 3.0" diff --git a/Gemfile.lock b/Gemfile.lock index a4a09df..d93ace1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,11 +2,13 @@ PATH remote: . specs: imprint (0.1.0) + ruby-libgd GEM remote: https://rubygems.org/ specs: ast (2.4.3) + coderay (1.1.3) date (3.5.1) diff-lcs (1.6.2) erb (6.0.1) @@ -18,6 +20,7 @@ GEM json (2.18.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) + method_source (1.1.0) parallel (1.27.0) parser (3.3.10.1) ast (~> 2.4.1) @@ -26,6 +29,10 @@ GEM prettyprint prettyprint (0.2.0) prism (1.8.0) + pry (0.16.0) + coderay (~> 1.1) + method_source (~> 1.0) + reline (>= 0.6.0) psych (5.3.1) date stringio @@ -66,6 +73,7 @@ GEM rubocop-ast (1.49.0) parser (>= 3.3.7.2) prism (~> 1.7) + ruby-libgd (0.2.3) ruby-progressbar (1.13.0) stringio (3.2.0) tsort (0.2.0) @@ -80,12 +88,14 @@ PLATFORMS DEPENDENCIES imprint! irb + pry rake (~> 13.0) rspec (~> 3.0) rubocop (~> 1.21) CHECKSUMS ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5 @@ -95,11 +105,13 @@ CHECKSUMS json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.8.0) sha256=84453a16ef5530ea62c5f03ec16b52a459575ad4e7b9c2b360fd8ce2c39c1254 + pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a @@ -114,6 +126,7 @@ CHECKSUMS rspec-support (3.13.6) sha256=2e8de3702427eab064c9352fe74488cc12a1bfae887ad8b91cba480ec9f8afb2 rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273 rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd + ruby-libgd (0.2.3) sha256=9ddf757d1abcc5d44166e30b3204b95579f561e2ed4f366ef2c5ffa6cc669c50 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f diff --git a/imprint.gemspec b/imprint.gemspec index a98ad58..9e4b452 100644 --- a/imprint.gemspec +++ b/imprint.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |spec| # Uncomment to register a new dependency of your gem # spec.add_dependency "example-gem", "~> 1.0" - + spec.add_dependency 'ruby-libgd' # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html end diff --git a/lib/imprint.rb b/lib/imprint.rb index 7d6c324..441349a 100644 --- a/lib/imprint.rb +++ b/lib/imprint.rb @@ -2,6 +2,8 @@ require 'imprint/version' require 'imprint/signer' +require 'imprint/renderer' +require 'imprint/engine' if defined?(Rails) module Imprint def self.sign(source:, watermark:, expires_in:) @@ -24,6 +26,16 @@ def self.verify(token) payload end + def self.render_from_token(token) + payload = verify(token) + return nil unless payload + + Renderer.render( + source: payload['source'], + watermark: payload['watermark'] + ) + end + def self.secret_key # sin engine todavía; Rails solo si existe if defined?(Rails) diff --git a/lib/imprint/config/routes.rb b/lib/imprint/config/routes.rb new file mode 100644 index 0000000..ec6bfa3 --- /dev/null +++ b/lib/imprint/config/routes.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Imprint::Engine.routes.draw do + get 'render/:token', to: 'renders#show' +end diff --git a/lib/imprint/controllers/imprint/renders_controller.rb b/lib/imprint/controllers/imprint/renders_controller.rb new file mode 100644 index 0000000..3ff4828 --- /dev/null +++ b/lib/imprint/controllers/imprint/renders_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Imprint + class RendersController < ActionController::Base + def show + path = Imprint.render_from_token(params[:token]) + + return head :not_found unless path && File.exist?(path) + + send_file( + path, + type: 'image/png', + disposition: 'inline', + cache_control: 'no-store' + ) + end + end +end diff --git a/lib/imprint/engine.rb b/lib/imprint/engine.rb new file mode 100644 index 0000000..ed9d0d9 --- /dev/null +++ b/lib/imprint/engine.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails' + +module Imprint + class Engine < ::Rails::Engine + isolate_namespace Imprint + end +end diff --git a/lib/imprint/renderer.rb b/lib/imprint/renderer.rb new file mode 100644 index 0000000..b9793ff --- /dev/null +++ b/lib/imprint/renderer.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'pry' +require 'gd' +require 'securerandom' +require 'tmpdir' + +module Imprint + class Renderer + DEFAULT_FONT_SIZE = 24 + + def self.render(source:, watermark:) + raise ArgumentError, 'source image not found' unless File.exist?(source) + + image = GD::Image.open(source) + + apply_watermark(image, watermark) + + output = File.join( + Dir.tmpdir, + "imprint-#{SecureRandom.hex(8)}.png" + ) + + image.save(output) + output + end + + def self.apply_watermark(image, text) + color = GD::Color.rgba(0, 116, 139, 225) + + raise "Font missing" unless File.exist?(default_font) + + image.text( + text, + x: 20, + y: image.height - 20, + size: DEFAULT_FONT_SIZE, + color: color, + font: default_font + ) + end + + def self.default_font + @default_font ||= begin + font = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf' + raise 'Font not found' unless File.exist?(font) + + font + end + end + end +end diff --git a/spec/fixtures/sample.png b/spec/fixtures/sample.png new file mode 100644 index 0000000..3905f93 Binary files /dev/null and b/spec/fixtures/sample.png differ diff --git a/spec/fixtures/source.png b/spec/fixtures/source.png new file mode 100644 index 0000000..dca86c4 Binary files /dev/null and b/spec/fixtures/source.png differ diff --git a/spec/imprint_render_from_token_spec.rb b/spec/imprint_render_from_token_spec.rb new file mode 100644 index 0000000..3b48a38 --- /dev/null +++ b/spec/imprint_render_from_token_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'gd' + +RSpec.describe Imprint do + before do + FileUtils.mkdir_p('spec/fixtures') + + image = GD::Image.new(200, 100) + white = GD::Color.rgb(255, 255, 255) + image.fill(white) + image.save('spec/fixtures/source.png') + end + + it 'renders image when token is valid' do + token = Imprint.sign( + source: 'spec/fixtures/source.png', + watermark: 'CONFIDENTIAL', + expires_in: 60 + ).split('/').last + + output = Imprint.render_from_token(token) + + expect(output).to be_a(String) + expect(File).to exist(output) + end + + it 'returns nil for expired token' do + token = Imprint.sign( + source: 'spec/fixtures/source.png', + watermark: 'CONFIDENTIAL', + expires_in: -1 + ).split('/').last + + expect(Imprint.render_from_token(token)).to be_nil + end +end diff --git a/spec/imprint_renderer_spec.rb b/spec/imprint_renderer_spec.rb new file mode 100644 index 0000000..4a81051 --- /dev/null +++ b/spec/imprint_renderer_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Imprint::Renderer do + it 'renders an image with watermark' do + output = described_class.render( + source: 'spec/fixtures/sample.png', + watermark: 'CONFIDENTIAL' + ) + + expect(File).to exist(output) + expect(File.size(output)).to be > 0 + end +end