├── .ruby-version ├── lib ├── daily_image │ ├── version.rb │ ├── poem.rb │ ├── config.rb │ ├── image.rb │ └── lunar_solar_converter.rb └── daily_image.rb ├── tmp ├── daily_2018-09-14.jpg ├── daily_2018-09-17.jpg └── daily_2018-10-06.jpeg ├── .travis.yml ├── test ├── test_helper.rb ├── daily_image │ └── poem_test.rb └── daily_image_test.rb ├── .gitignore ├── bin ├── setup ├── console └── daily_image ├── Gemfile ├── Rakefile ├── Gemfile.lock ├── Dockerfile ├── daily_image.gemspec ├── README.md └── CODE_OF_CONDUCT.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.4 2 | -------------------------------------------------------------------------------- /lib/daily_image/version.rb: -------------------------------------------------------------------------------- 1 | module DailyImage 2 | VERSION = "0.1.5" 3 | end 4 | -------------------------------------------------------------------------------- /tmp/daily_2018-09-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renyijiu/daily_image/HEAD/tmp/daily_2018-09-14.jpg -------------------------------------------------------------------------------- /tmp/daily_2018-09-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renyijiu/daily_image/HEAD/tmp/daily_2018-09-17.jpg -------------------------------------------------------------------------------- /tmp/daily_2018-10-06.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renyijiu/daily_image/HEAD/tmp/daily_2018-10-06.jpeg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.5.1 5 | before_install: gem install bundler -v 1.16.1 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "daily_image" 3 | 4 | require "minitest/autorun" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /.idea/ 9 | /.DS_Store 10 | Gemfile.lock -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in daily_image.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /test/daily_image/poem_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DailyImage::PoemTest < Minitest::Test 4 | 5 | def test_it_should_get_poem_txt 6 | flag, res = ::DailyImage::Poem.new.txt 7 | 8 | assert flag 9 | assert_kind_of Hash, res 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "daily_image" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | daily_image (0.1.5) 5 | ruby-vips (~> 2.0.17) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ffi (1.11.1) 11 | minitest (5.11.3) 12 | rake (12.3.3) 13 | ruby-vips (2.0.17) 14 | ffi (~> 1.9) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | bundler (~> 1.16) 21 | daily_image! 22 | minitest (~> 5.0) 23 | rake (~> 12.3, >= 12.3.3) 24 | 25 | BUNDLED WITH 26 | 1.17.2 27 | -------------------------------------------------------------------------------- /lib/daily_image/poem.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "json" 3 | 4 | # 5 | # 使用 一言·古诗词 API, 获取一条古诗词 6 | # 7 | # API返回结果: 8 | # { 9 | # content: "陌上风光浓处。第一寒梅先吐。", 10 | # origin: "十样花·陌上风光浓处", 11 | # author: "李弥逊", 12 | # category: "古诗文-植物-梅花" 13 | # } 14 | # 15 | module DailyImage 16 | class Poem 17 | 18 | API_URL = "https://v1.jinrishici.com/all.json" 19 | 20 | def txt 21 | get(API_URL) 22 | end 23 | 24 | private 25 | 26 | def get(url, params={}) 27 | uri = URI(url) 28 | uri.query = URI.encode_www_form(params) 29 | 30 | res = Net::HTTP.get_response(uri) 31 | format_res(res) 32 | end 33 | 34 | def format_res(res) 35 | return [false, {}] unless res.is_a?(Net::HTTPSuccess) 36 | 37 | [true, JSON.parse(res.body)] 38 | end 39 | 40 | end 41 | end -------------------------------------------------------------------------------- /lib/daily_image.rb: -------------------------------------------------------------------------------- 1 | require "date" 2 | require "vips" 3 | require "daily_image/version" 4 | require "daily_image/lunar_solar_converter" 5 | require "daily_image/poem" 6 | require "daily_image/image" 7 | require "daily_image/config" 8 | 9 | module DailyImage 10 | 11 | class << self 12 | def configure 13 | config = DailyImage::Config.instance 14 | yield config 15 | end 16 | 17 | def config 18 | DailyImage::Config.instance.configuration 19 | end 20 | 21 | def draw_image(output_path = nil, date = Date.today) 22 | output_path ||= Dir.pwd 23 | date = Date.parse(date.to_s) rescue Date.today 24 | 25 | output_file = File.join(output_path, "daily_#{date}.jpeg") 26 | 27 | image = DailyImage::Image.new(date: date).draw_image 28 | 29 | image.write_to_file(output_file, Q: 100) 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/daily_image/config.rb: -------------------------------------------------------------------------------- 1 | require "singleton" 2 | 3 | module DailyImage 4 | class Config 5 | include Singleton 6 | 7 | # 背景颜色 8 | attr_accessor :bg_color 9 | 10 | # 边框颜色 11 | attr_accessor :frame_color 12 | 13 | # 文字颜色 14 | attr_accessor :text_color 15 | 16 | # 中间日期颜色 17 | attr_accessor :date_color 18 | 19 | # 进度条未使用颜色 20 | attr_accessor :unused_color 21 | 22 | # 进度条已使用颜色 23 | attr_accessor :used_color 24 | 25 | # 文字默认字体 26 | attr_accessor :font 27 | 28 | # 外层边框偏移量 29 | attr_accessor :out_frame_offset 30 | 31 | # 下半部分内层边框偏移量 32 | attr_accessor :in_frame_offset 33 | 34 | def configuration 35 | @config ||= {}.tap do |config| 36 | config[:bg_color] = bg_color || [255, 255, 255] 37 | config[:frame_color] = frame_color || [100, 145, 170] 38 | config[:text_color] = text_color || [100, 145, 170] 39 | config[:date_color] = date_color || [100, 145, 170] 40 | config[:unused_color] = unused_color || [200, 205, 215] 41 | config[:used_color] = used_color || [100, 145, 170] 42 | config[:out_frame_offset] = out_frame_offset || 15 43 | config[:in_frame_offset] = in_frame_offset || 50 44 | config[:font] = font || 'Hiragino Sans GB' 45 | end 46 | end 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | ARG VIPS_VERSION=8.9.1 4 | ARG VIPS_URL=https://github.com/libvips/libvips/releases/download 5 | 6 | RUN apk update && apk upgrade 7 | 8 | # basic packages libvips likes 9 | RUN apk add \ 10 | build-base \ 11 | autoconf \ 12 | automake \ 13 | libtool \ 14 | bc \ 15 | zlib-dev \ 16 | expat-dev \ 17 | jpeg-dev \ 18 | tiff-dev \ 19 | glib-dev \ 20 | libjpeg-turbo-dev \ 21 | libexif-dev \ 22 | lcms2-dev \ 23 | fftw-dev \ 24 | giflib-dev \ 25 | libpng-dev \ 26 | libwebp-dev \ 27 | orc-dev \ 28 | libgsf-dev 29 | 30 | # text rendering ... we have to download some fonts and rebuild the font 31 | # cache 32 | RUN apk add \ 33 | pango-dev \ 34 | msttcorefonts-installer \ 35 | wqy-zenhei --update-cache --repository http://nl.alpinelinux.org/alpine/edge/testing --allow-untrusted 36 | RUN update-ms-fonts \ 37 | && fc-cache -f 38 | 39 | # there are other optional deps you can add for openslide / openexr / 40 | # imagmagick support / Matlab support etc etc 41 | 42 | # installing to /usr is not the best idea, but it's easy 43 | RUN wget -O- ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp 44 | RUN cd /tmp/vips-${VIPS_VERSION} \ 45 | && ./configure --prefix=/usr --disable-static --disable-debug \ 46 | && make V=0 \ 47 | && make install 48 | 49 | RUN apk add ruby-dev 50 | RUN rm -rf /var/cache/apk/* 51 | RUN gem install bundler && gem install json && gem install daily_image -------------------------------------------------------------------------------- /daily_image.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "daily_image/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "daily_image" 8 | spec.version = DailyImage::VERSION 9 | spec.authors = ["renyijiu"] 10 | spec.email = ["lanyuejin1108@gmail.com"] 11 | 12 | spec.summary = %q{A gem generate a daily image. Just For Fun 😊} 13 | spec.description = %q{A gem generate a daily image. Just For Fun 😊} 14 | spec.homepage = "https://github.com/renyijiu/daily_image" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 20 | else 21 | raise "RubyGems 2.0 or newer is required to protect against " \ 22 | "public gem pushes." 23 | end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{^(test|spec|features)/}) 27 | end 28 | spec.bindir = "bin" 29 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_dependency "ruby-vips", "~> 2.0.17" 33 | 34 | spec.add_development_dependency "bundler", "~> 1.16" 35 | spec.add_development_dependency "rake", "~> 12.3", ">= 12.3.3" 36 | spec.add_development_dependency "minitest", "~> 5.0" 37 | end 38 | -------------------------------------------------------------------------------- /test/daily_image_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DailyImageTest < Minitest::Test 4 | 5 | def test_should_has_version 6 | refute_nil ::DailyImage::VERSION 7 | end 8 | 9 | def test_should_get_config 10 | config = DailyImage.config 11 | 12 | assert config[:bg_color] 13 | assert config[:frame_color] 14 | assert config[:text_color] 15 | assert config[:date_color] 16 | assert config[:unused_color] 17 | assert config[:used_color] 18 | assert config[:font] 19 | assert config[:out_frame_offset] 20 | assert config[:in_frame_offset] 21 | end 22 | 23 | def test_should_set_config 24 | Singleton.__init__(DailyImage::Config) 25 | color = [rand(0..255), rand(0..255), rand(0..255)] 26 | 27 | DailyImage.configure do |config| 28 | config.bg_color = color 29 | end 30 | 31 | config = DailyImage.config 32 | assert_equal color, config[:bg_color] 33 | end 34 | 35 | def test_should_generate_image 36 | filename = "daily_#{Date.today}.jpeg" 37 | file_path = File.join('./tmp/', filename) 38 | 39 | DailyImage.draw_image('./tmp/') 40 | 41 | assert File.file?(file_path) 42 | File.delete(file_path) 43 | end 44 | 45 | def test_should_generate_image_when_set_date 46 | (0..10).each do |i| 47 | date = Date.today - i 48 | 49 | filename = "daily_#{date}.jpeg" 50 | file_path = File.join('./tmp/', filename) 51 | 52 | DailyImage.draw_image('./tmp/', date) 53 | 54 | assert File.file?(file_path) 55 | File.delete(file_path) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DailyImage 2 | 3 | 日签 Or 日历,生成一张指定日期的图片,可作为日历。Just For Fun!👏 4 | 5 | [PHP实现版本](https://github.com/dolphin836/make-calendar-image) 6 | [Python版本实现](https://github.com/wnma3mz/Tools/tree/master/daily_image) 7 | [Android桌面小部件](https://github.com/fairytale110/DailyImageWidget) 8 | 9 | ## 使用 10 | 11 | 如果需要在项目中使用,在你的gemfile中添加以下代码 12 | 13 | ```ruby 14 | gem 'daily_image' 15 | ``` 16 | 17 | 然后执行下面的命令 18 | 19 | $ bundle 20 | 21 | 或者,你可以直接安装使用 22 | 23 | $ gem install daily_image 24 | 25 | ## 使用 26 | 27 | ### 配置使用 28 | 29 | 当你需要项目使用时,可以通过下面方式修改默认配置: 30 | 31 | ```ruby 32 | 33 | # 初始项目配置 34 | DailyImage.configure do |config| 35 | config.bg_color = [255, 255, 255] # 背景颜色 36 | config.frame_color = [151, 158, 160] # 边框颜色 37 | config.text_color = [100, 145, 170] # 文字颜色 38 | config.date_color = [100, 145, 170] # 中间日期颜色 39 | config.unused_color = [200, 205, 215] # 进度条未使用颜色 40 | config.used_color = [100, 145, 170] # 进度条已使用颜色 41 | config.out_frame_offset = 15 # 外层边框偏移量 42 | config.in_frame_offset = 50 # 下半部分内层边框偏移量 43 | config.font = 'Hiragino Sans GB' # 文字默认字体 44 | end 45 | 46 | # 调用方法 47 | DailyImage.draw_image(output_path = nil, date = Date.today) # 默认图片存放地址,包所在的目录; 默认生成为当日 48 | 49 | # 例子 50 | DailyImage.draw_image('./', '2018-09-30') 51 | ``` 52 | 53 | ### 命令行 54 | 55 | 如果是直接安装,可以通过命令行直接调用。**当然,还是需要安装ruby以及对应的依赖** 56 | 57 | ```shell 58 | $ bundle exec daily_image -h 59 | Usage: daily_image [options] 60 | 61 | Specific options: 62 | -b, --bg_color BG_COLOR the image's background color 63 | -r, --frame_color FRAME_COLOR the image's frame color 64 | -t, --text_color TEXT_COLOR the image's text color 65 | -c, --date_color DATE_COLOR the middle date's text color 66 | -n, --unused_color UNUSED_COLOR unused color of the progress bar 67 | -u, --used_color USED_COLOR used color of the progress bar 68 | -o OUT_FRAME_OFFSET, the outside frame offset 69 | --out_offset 70 | -i, --in_offset IN_FRAME_OFFSET the inside frame offset 71 | -f, --font FONT the text font 72 | -s, --output OUTPUT the output path, save the new image 73 | -d, --date Specific date the date you want to generate 74 | 75 | Common options: 76 | -h, --help Show the help message 77 | -v, --version Show version 78 | 79 | ``` 80 | 81 | ### docker 82 | 83 | 打包镜像 84 | 85 | ```shell 86 | $ docker build . -t ruby-image 87 | 88 | ``` 89 | 90 | 使用 91 | 92 | ```shell 93 | $ docker run -it --mount src="$(pwd)",target=/home,type=bind ruby-image daily_image -s /home/ 94 | ``` 95 | 96 | 然后你就可以在当前目录看到新生成的图片文件了 97 | 98 | ## 示例 99 | 100 | ![](./tmp/daily_2018-10-06.jpeg) 101 | 102 | ## 感谢🙏 103 | 104 | 站在巨人的肩膀上 105 | 106 | 1. 画图使用了 [ruby-vips](https://github.com/jcupitt/ruby-vips) 107 | 108 | 2. 诗词来自于 [一言·古诗词 API](https://github.com/xenv/gushici) 109 | 110 | ## 待完成 111 | 112 | - [ ] 使用机器学习生成古诗词,替代一言·古诗词API 113 | 114 | ## 如何贡献 115 | 116 | 1. Fork it 117 | 2. Create your feature branch (`git checkout -b my-new-feature`) 118 | 3. Commit your changes (`git commit -am 'Add some feature'`) 119 | 4. Push to the branch (`git push origin my-new-feature`) 120 | 5. Create new Pull Request 121 | 122 | 欢迎贡献相关代码或是反馈使用时遇到的问题👏,另外请记得为你的代码编写测试。 123 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lanyuejin1108@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /bin/daily_image: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "optparse" 5 | require "json" 6 | require "daily_image" 7 | 8 | class DailyImageOptparser 9 | 10 | class Parser 11 | attr_accessor :bg_color, 12 | :frame_color, 13 | :text_color, 14 | :date_color, 15 | :unused_color, 16 | :used_color, 17 | :font, 18 | :out_frame_offset, 19 | :in_frame_offset, 20 | :output_path, 21 | :date 22 | 23 | def initialize 24 | end 25 | 26 | def define_options(parser) 27 | parser.banner = "Usage: daily_image [options]" 28 | parser.separator "" 29 | parser.separator "Specific options:" 30 | 31 | bg_color_option(parser) 32 | frame_color_option(parser) 33 | text_color_option(parser) 34 | date_color_option(parser) 35 | unused_color_option(parser) 36 | used_color_option(parser) 37 | out_frame_offset_option(parser) 38 | in_frame_offset_option(parser) 39 | font_option(parser) 40 | output_path_option(parser) 41 | date_option(parser) 42 | 43 | parser.separator "" 44 | parser.separator "Common options:" 45 | 46 | parser.on_tail("-h", "--help", "Show the help message") do 47 | puts parser 48 | exit 49 | end 50 | # Another typical switch to print the version. 51 | parser.on_tail("-v", "--version", "Show version") do 52 | puts DailyImage::VERSION 53 | exit 54 | end 55 | end 56 | 57 | private 58 | 59 | def bg_color_option(parser) 60 | parser.on("-b", "--bg_color BG_COLOR", "the image's background color") do |bg_color| 61 | self.bg_color = bg_color 62 | end 63 | end 64 | 65 | def frame_color_option(parser) 66 | parser.on("-r", "--frame_color FRAME_COLOR", "the image's frame color") do |frame_color| 67 | self.frame_color = frame_color 68 | end 69 | end 70 | 71 | def text_color_option(parser) 72 | parser.on("-t", "--text_color TEXT_COLOR", "the image's text color") do |text_color| 73 | self.text_color = text_color 74 | end 75 | end 76 | 77 | def date_color_option(parser) 78 | parser.on("-c", "--date_color DATE_COLOR", "the middle date's text color") do |date_color| 79 | self.date_color = date_color 80 | end 81 | end 82 | 83 | def unused_color_option(parser) 84 | parser.on("-n", "--unused_color UNUSED_COLOR", "unused color of the progress bar") do |unused_color| 85 | self.unused_color = unused_color 86 | end 87 | end 88 | 89 | def used_color_option(parser) 90 | parser.on("-u", "--used_color USED_COLOR", "used color of the progress bar") do |used_color| 91 | self.used_color = used_color 92 | end 93 | end 94 | 95 | def out_frame_offset_option(parser) 96 | parser.on("-o", "--out_offset OUT_FRAME_OFFSET", "the outside frame offset") do |out_offset| 97 | self.out_frame_offset = out_offset 98 | end 99 | end 100 | 101 | def in_frame_offset_option(parser) 102 | parser.on("-i", "--in_offset IN_FRAME_OFFSET", "the inside frame offset") do |in_offset| 103 | self.in_frame_offset = in_offset 104 | end 105 | end 106 | 107 | def font_option(parser) 108 | parser.on("-f", "--font FONT", "the text font") do |font| 109 | self.font = font 110 | end 111 | end 112 | 113 | def output_path_option(parser) 114 | parser.on("-s", "--output OUTPUT", "the output path, save the new image") do |output| 115 | self.output_path = output 116 | end 117 | end 118 | 119 | def date_option(parser) 120 | parser.on("-d", "--date Specific date", "the date you want to generate") do |date| 121 | self.date = Date.parse(date.to_s) rescue Date.today 122 | end 123 | end 124 | 125 | end 126 | 127 | attr_reader :parser, :options 128 | def parse(args) 129 | @options = Parser.new 130 | @args = OptionParser.new do |parser| 131 | @options.define_options(parser) 132 | parser.parse!(args) 133 | end 134 | @options 135 | end 136 | end 137 | 138 | parser = DailyImageOptparser.new 139 | options = parser.parse(ARGV) 140 | 141 | DailyImage.configure do |config| 142 | config.bg_color = JSON.parse(options.bg_color) if options.bg_color 143 | config.frame_color = JSON.parse(options.frame_color) if options.frame_color 144 | config.text_color = JSON.parse(options.text_color) if options.text_color 145 | config.date_color = JSON.parse(options.date_color) if options.date_color 146 | config.unused_color = JSON.parse(options.unused_color) if options.unused_color 147 | config.used_color = JSON.parse(options.used_color) if options.used_color 148 | config.out_frame_offset = options.out_frame_offset.to_i if options.out_frame_offset 149 | config.in_frame_offset = options.in_frame_offset.to_i if options.in_frame_offset 150 | config.font = options.font if options.font 151 | end 152 | 153 | DailyImage.draw_image(options.output_path, options.date) -------------------------------------------------------------------------------- /lib/daily_image/image.rb: -------------------------------------------------------------------------------- 1 | 2 | module DailyImage 3 | class Image 4 | 5 | def initialize(width = 600, height = 800, date: Date.today) 6 | @width = width 7 | @height = height 8 | @date = date 9 | end 10 | 11 | def draw_image 12 | image = Vips::Image.black(@width, @height) 13 | image = draw_top_half(image) 14 | 15 | draw_down_half(image) 16 | end 17 | 18 | private 19 | 20 | def config 21 | @config ||= DailyImage.config 22 | end 23 | 24 | # 画出上半部分 25 | def draw_top_half(image) 26 | image = draw_background(image) 27 | image = draw_frame(image) 28 | image = draw_day(image) 29 | image = draw_date(image) 30 | image = draw_week(image) 31 | image = draw_lunar_txt(image) 32 | image = draw_progress_txt(image) 33 | 34 | draw_progress(image) 35 | end 36 | 37 | # 画出下半部分 38 | def draw_down_half(image) 39 | image = draw_down_frame(image) 40 | 41 | draw_poem(image) 42 | end 43 | 44 | # 设置背景颜色 45 | def draw_background(image) 46 | image + config[:bg_color] 47 | end 48 | 49 | # 画出外边框 50 | def draw_frame(image) 51 | [0, 1, 2, 3, 4, 10, 11].each do |offset| 52 | offset += config[:out_frame_offset] 53 | 54 | width = @width - 2 * offset 55 | height = @height - 2 * offset 56 | 57 | image = image.draw_rect(config[:frame_color], offset, offset, width, height, fill: false) 58 | end 59 | 60 | image 61 | end 62 | 63 | # 画出中间日期 64 | def draw_day(image) 65 | day = @date.day.to_s 66 | 67 | # 生成字体图片 68 | text = generate_text_image(day, dpi: 1000, text_color: config[:date_color]) 69 | 70 | # 计算字体放置位置 71 | x = (image.width - text.width) / 2 72 | y = (image.height * 0.2).to_i 73 | 74 | image.draw_image text, x, y, mode: :set 75 | end 76 | 77 | # 画出左上角日期 78 | def draw_date(image) 79 | date = @date.strftime("%Y.%m.%d") 80 | text = generate_text_image(date, dpi: 150) 81 | 82 | # 计算放置位置 83 | x = (image.width * 0.1).to_i 84 | y = (image.height * 0.09).to_i 85 | 86 | image.draw_image text, x, y, mode: :set 87 | end 88 | 89 | # 画出右上角信息 90 | def draw_week(image) 91 | week_arr = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] 92 | week = week_arr[@date.cwday - 1] 93 | 94 | text = generate_text_image(week, dpi: 140) 95 | 96 | # 计算放置位置 97 | x = (image.width * 0.78).to_i 98 | y = (image.height * 0.09).to_i 99 | 100 | image.draw_image text, x, y, mode: :set 101 | end 102 | 103 | # 画出农历信息 104 | def draw_lunar_txt(image) 105 | text = DailyImage::LunarSolarConverter.date_to_lunar(@date) 106 | 107 | # 获取文字图片 108 | text_image = generate_text_image(text) 109 | 110 | x = (@width - text_image.width) / 2 111 | y = @height * 0.4 112 | 113 | image.draw_image text_image, x, y, mode: :set 114 | end 115 | 116 | # 画出进度描述信息 117 | def draw_progress_txt(image) 118 | day = @date.yday 119 | percent = (percent_of_year * 100).round(2) 120 | text = "第 #{day} 天,进度已消耗 #{percent}%" 121 | 122 | # 获取文字图片 123 | text_image = generate_text_image(text) 124 | 125 | x = (@width - text_image.width) / 2 126 | y = @height * 0.45 127 | 128 | image.draw_image text_image, x, y, mode: :set 129 | end 130 | 131 | # 画出年进度信息 132 | def draw_progress(image) 133 | height = 30 134 | 135 | unused_width = @width - 2 * config[:in_frame_offset] 136 | used_width = (unused_width * percent_of_year).to_i 137 | 138 | x = config[:in_frame_offset] 139 | y = (@height * 0.5).to_i 140 | 141 | # 画出整年的进度条 142 | image = image.draw_rect(config[:unused_color], x, y, unused_width, height, fill: true) 143 | 144 | # 画出已进行的进度条 145 | image.draw_rect(config[:used_color], x, y, used_width, height, fill: true) 146 | end 147 | 148 | # 画出下半部分边框 149 | def draw_down_frame(image) 150 | width = (@width - 2 * config[:in_frame_offset]).to_i 151 | height = (@height * 0.49 - 2 * config[:in_frame_offset]).to_i 152 | 153 | x = config[:in_frame_offset] 154 | y = (@height * 0.51 + config[:in_frame_offset]).to_i 155 | 156 | image.draw_rect(config[:frame_color], x, y, width, height, fill: false) 157 | end 158 | 159 | def draw_poem(image) 160 | _, res = DailyImage::Poem.new.txt 161 | 162 | title = res['origin'] || '无题' 163 | content = res['content'] || '唯有梦想与爱情,不可辜负。' 164 | author = "--- #{res['author'] || '佚名'}" 165 | 166 | image = draw_poem_title(image, title) 167 | image = draw_poem_content(image, content) 168 | draw_poem_author(image, author) 169 | end 170 | 171 | def draw_poem_title(image, title) 172 | text = generate_text_image(title, dpi: 110) 173 | 174 | x = ((@width - text.width) / 2).to_i 175 | y = (@height * 0.65).to_i 176 | 177 | image.draw_image text, x, y, mode: :set 178 | end 179 | 180 | def draw_poem_content(image, content) 181 | text = generate_text_image(content, dpi: 135) 182 | 183 | x = ((@width - text.width) / 2).to_i 184 | y = (@height * 0.75).to_i 185 | 186 | image.draw_image text, x, y, mode: :set 187 | end 188 | 189 | def draw_poem_author(image, author) 190 | text = generate_text_image(author) 191 | 192 | x = (@width - text.width - config[:in_frame_offset] - 10).to_i 193 | y = (@height * 0.85).to_i 194 | 195 | image.draw_image text, x, y, mode: :set 196 | end 197 | 198 | # 计算当天时间在一年的百分比 199 | def percent_of_year 200 | days = Date.new(@date.year, 12, 31).yday 201 | current_days = @date.yday 202 | 203 | (current_days.to_f / days).round(4) 204 | end 205 | 206 | # 生成字体图片 207 | def generate_text_image(text, opts={}) 208 | dpi = opts.fetch(:dpi, 100) 209 | text_color = opts.fetch(:text_color, config[:text_color]) 210 | bg_color = opts.fetch(:bg_color, config[:bg_color]) 211 | font = opts.fetch(:font, config[:font]) 212 | 213 | text_image = Vips::Image.text text, dpi: dpi, font: font 214 | text_image = text_image.embed 0, 0, text_image.width, text_image.height, extend: :mirror 215 | 216 | text_image.ifthenelse(text_color, bg_color, blend: true) 217 | end 218 | 219 | end 220 | end -------------------------------------------------------------------------------- /lib/daily_image/lunar_solar_converter.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module DailyImage 4 | class Lunar 5 | attr_accessor :lunar_day, :lunar_month, :lunar_year, :is_leap 6 | 7 | def initialize(y = 0, m = 0, d = 0, leap = false) 8 | @lunar_year = y 9 | @lunar_month = m 10 | @lunar_day = d 11 | @is_leap = leap 12 | end 13 | end 14 | 15 | class Solar 16 | attr_accessor :solar_day, :solar_month, :solar_year 17 | 18 | def initialize(y = 0, m = 0, d = 0) 19 | @solar_year = y 20 | @solar_month = m 21 | @solar_day = d 22 | end 23 | end 24 | 25 | class LunarSolarConverter 26 | LUNAR_MONTH_DAYS = [ 27 | 1887, 0x1694, 0x16aa, 0x4ad5, 0xab6, 0xc4b7, 0x4ae, 0xa56, 0xb52a, 0x1d2a, 0xd54, 0x75aa, 0x156a, 0x1096d, 28 | 0x95c, 0x14ae, 0xaa4d, 0x1a4c, 0x1b2a, 0x8d55, 0xad4, 0x135a, 0x495d, 0x95c, 0xd49b, 0x149a, 0x1a4a, 0xbaa5, 29 | 0x16a8, 0x1ad4, 0x52da, 0x12b6, 0xe937, 0x92e, 0x1496, 0xb64b, 0xd4a, 0xda8, 0x95b5, 0x56c, 0x12ae, 0x492f, 30 | 0x92e, 0xcc96, 0x1a94, 0x1d4a, 0xada9, 0xb5a, 0x56c, 0x726e, 0x125c, 0xf92d, 0x192a, 0x1a94, 0xdb4a, 0x16aa, 31 | 0xad4, 0x955b, 0x4ba, 0x125a, 0x592b, 0x152a, 0xf695, 0xd94, 0x16aa, 0xaab5, 0x9b4, 0x14b6, 0x6a57, 0xa56, 32 | 0x1152a, 0x1d2a, 0xd54, 0xd5aa, 0x156a, 0x96c, 0x94ae, 0x14ae, 0xa4c, 0x7d26, 0x1b2a, 0xeb55, 0xad4, 0x12da, 33 | 0xa95d, 0x95a, 0x149a, 0x9a4d, 0x1a4a, 0x11aa5, 0x16a8, 0x16d4, 0xd2da, 0x12b6, 0x936, 0x9497, 0x1496, 0x1564b, 34 | 0xd4a, 0xda8, 0xd5b4, 0x156c, 0x12ae, 0xa92f, 0x92e, 0xc96, 0x6d4a, 0x1d4a, 0x10d65, 0xb58, 0x156c, 0xb26d, 35 | 0x125c, 0x192c, 0x9a95, 0x1a94, 0x1b4a, 0x4b55, 0xad4, 0xf55b, 0x4ba, 0x125a, 0xb92b, 0x152a, 0x1694, 0x96aa, 36 | 0x15aa, 0x12ab5, 0x974, 0x14b6, 0xca57, 0xa56, 0x1526, 0x8e95, 0xd54, 0x15aa, 0x49b5, 0x96c, 0xd4ae, 0x149c, 37 | 0x1a4c, 0xbd26, 0x1aa6, 0xb54, 0x6d6a, 0x12da, 0x1695d, 0x95a, 0x149a, 0xda4b, 0x1a4a, 0x1aa4, 0xbb54, 0x16b4, 38 | 0xada, 0x495b, 0x936, 0xf497, 0x1496, 0x154a, 0xb6a5, 0xda4, 0x15b4, 0x6ab6, 0x126e, 0x1092f, 0x92e, 0xc96, 39 | 0xcd4a, 0x1d4a, 0xd64, 0x956c, 0x155c, 0x125c, 0x792e, 0x192c, 0xfa95, 0x1a94, 0x1b4a, 0xab55, 0xad4, 0x14da, 40 | 0x8a5d, 0xa5a, 0x1152b, 0x152a, 0x1694, 0xd6aa, 0x15aa, 0xab4, 0x94ba, 0x14b6, 0xa56, 0x7527, 0xd26, 0xee53, 41 | 0xd54, 0x15aa, 0xa9b5, 0x96c, 0x14ae, 0x8a4e, 0x1a4c, 0x11d26, 0x1aa4, 0x1b54, 0xcd6a, 0xada, 0x95c, 0x949d, 42 | 0x149a, 0x1a2a, 0x5b25, 0x1aa4, 0xfb52, 0x16b4, 0xaba, 0xa95b, 0x936, 0x1496, 0x9a4b, 0x154a, 0x136a5, 0xda4, 43 | 0x15ac 44 | ] 45 | 46 | SOLAR11 = [ 47 | 1887, 0xec04c, 0xec23f, 0xec435, 0xec649, 0xec83e, 0xeca51, 0xecc46, 0xece3a, 0xed04d, 0xed242, 0xed436, 48 | 0xed64a, 0xed83f, 0xeda53, 0xedc48, 0xede3d, 0xee050, 0xee244, 0xee439, 0xee64d, 0xee842, 0xeea36, 0xeec4a, 49 | 0xeee3e, 0xef052, 0xef246, 0xef43a, 0xef64e, 0xef843, 0xefa37, 0xefc4b, 0xefe41, 0xf0054, 0xf0248, 0xf043c, 50 | 0xf0650, 0xf0845, 0xf0a38, 0xf0c4d, 0xf0e42, 0xf1037, 0xf124a, 0xf143e, 0xf1651, 0xf1846, 0xf1a3a, 0xf1c4e, 51 | 0xf1e44, 0xf2038, 0xf224b, 0xf243f, 0xf2653, 0xf2848, 0xf2a3b, 0xf2c4f, 0xf2e45, 0xf3039, 0xf324d, 0xf3442, 52 | 0xf3636, 0xf384a, 0xf3a3d, 0xf3c51, 0xf3e46, 0xf403b, 0xf424e, 0xf4443, 0xf4638, 0xf484c, 0xf4a3f, 0xf4c52, 53 | 0xf4e48, 0xf503c, 0xf524f, 0xf5445, 0xf5639, 0xf584d, 0xf5a42, 0xf5c35, 0xf5e49, 0xf603e, 0xf6251, 0xf6446, 54 | 0xf663b, 0xf684f, 0xf6a43, 0xf6c37, 0xf6e4b, 0xf703f, 0xf7252, 0xf7447, 0xf763c, 0xf7850, 0xf7a45, 0xf7c39, 55 | 0xf7e4d, 0xf8042, 0xf8254, 0xf8449, 0xf863d, 0xf8851, 0xf8a46, 0xf8c3b, 0xf8e4f, 0xf9044, 0xf9237, 0xf944a, 56 | 0xf963f, 0xf9853, 0xf9a47, 0xf9c3c, 0xf9e50, 0xfa045, 0xfa238, 0xfa44c, 0xfa641, 0xfa836, 0xfaa49, 0xfac3d, 57 | 0xfae52, 0xfb047, 0xfb23a, 0xfb44e, 0xfb643, 0xfb837, 0xfba4a, 0xfbc3f, 0xfbe53, 0xfc048, 0xfc23c, 0xfc450, 58 | 0xfc645, 0xfc839, 0xfca4c, 0xfcc41, 0xfce36, 0xfd04a, 0xfd23d, 0xfd451, 0xfd646, 0xfd83a, 0xfda4d, 0xfdc43, 59 | 0xfde37, 0xfe04b, 0xfe23f, 0xfe453, 0xfe648, 0xfe83c, 0xfea4f, 0xfec44, 0xfee38, 0xff04c, 0xff241, 0xff436, 60 | 0xff64a, 0xff83e, 0xffa51, 0xffc46, 0xffe3a, 0x10004e, 0x100242, 0x100437, 0x10064b, 0x100841, 0x100a53, 61 | 0x100c48, 0x100e3c, 0x10104f, 0x101244, 0x101438, 0x10164c, 0x101842, 0x101a35, 0x101c49, 0x101e3d, 0x102051, 62 | 0x102245, 0x10243a, 0x10264e, 0x102843, 0x102a37, 0x102c4b, 0x102e3f, 0x103053, 0x103247, 0x10343b, 0x10364f, 63 | 0x103845, 0x103a38, 0x103c4c, 0x103e42, 0x104036, 0x104249, 0x10443d, 0x104651, 0x104846, 0x104a3a, 0x104c4e, 64 | 0x104e43, 0x105038, 0x10524a, 0x10543e, 0x105652, 0x105847, 0x105a3b, 0x105c4f, 0x105e45, 0x106039, 0x10624c, 65 | 0x106441, 0x106635, 0x106849, 0x106a3d, 0x106c51, 0x106e47, 0x10703c, 0x10724f, 0x107444, 0x107638, 0x10784c, 66 | 0x107a3f, 0x107c53, 0x107e48 67 | ] 68 | 69 | TIAN_GAN = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"] 70 | DI_ZHI = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"] 71 | MONTH = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "腊月"] 72 | DAY = [ 73 | "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十", 74 | "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", 75 | "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十", "卅一" 76 | ] 77 | 78 | class << self 79 | 80 | def date_to_lunar(date = Date.today) 81 | converter = LunarSolarConverter.new 82 | solar = Solar.new 83 | 84 | solar.solar_year = date.year 85 | solar.solar_month = date.month 86 | solar.solar_day = date.day 87 | 88 | lunar = converter.solar_to_lunar(solar) 89 | lunar_text(lunar) 90 | end 91 | 92 | protected 93 | 94 | def lunar_text(lunar) 95 | tian_gan = TIAN_GAN[(lunar.lunar_year - 4) % 10] 96 | di_zhi = DI_ZHI[(lunar.lunar_year - 4) % 12] 97 | month = "#{lunar.is_leap ? '闰' : ''}#{MONTH[lunar.lunar_month - 1]}" 98 | day = "#{DAY[lunar.lunar_day - 1]}" 99 | 100 | "#{tian_gan}#{di_zhi}年 #{month}#{day}" 101 | end 102 | 103 | end 104 | 105 | def lunar_to_solar(lunar) 106 | offset = 0 107 | days = LUNAR_MONTH_DAYS[lunar.lunar_year - LUNAR_MONTH_DAYS.first] 108 | leap = get_bit_int(days, 4, 13) 109 | loopend = leap 110 | 111 | unless lunar.is_leap 112 | loopend = if lunar.lunar_month <= leap || leap == 0 113 | lunar.lunar_month - 1 114 | else 115 | lunar.lunar_month 116 | end 117 | end 118 | 119 | (0..loopend-1).each do |i| 120 | tmp_offset = get_bit_int(days, 1, 12 - i) == 1 ? 30 : 29 121 | 122 | offset += tmp_offset 123 | end 124 | 125 | offset += lunar.lunar_day 126 | solar11 = SOLAR11[lunar.lunar_year - SOLAR11.first] 127 | 128 | y = get_bit_int(solar11, 12, 9) 129 | m = get_bit_int(solar11, 4, 5) 130 | d = get_bit_int(solar11, 5, 0) 131 | 132 | solar_from_int(solar_to_int(y, m, d) + offset - 1) 133 | end 134 | 135 | def solar_to_lunar(solar) 136 | lunar = Lunar.new(0, 0, 0, false) 137 | index = solar.solar_year - SOLAR11.first 138 | data = (solar.solar_year << 9) | (solar.solar_month << 5) | solar.solar_day 139 | 140 | index -= 1 if SOLAR11[index] > data 141 | solar11 = SOLAR11[index] 142 | 143 | y = get_bit_int(solar11, 12, 9) 144 | m = get_bit_int(solar11, 4, 5) 145 | d = get_bit_int(solar11, 5, 0) 146 | offset = solar_to_int(solar.solar_year, solar.solar_month, solar.solar_day) - solar_to_int(y, m, d) 147 | 148 | days = LUNAR_MONTH_DAYS[index] 149 | leap = get_bit_int(days, 4, 13) 150 | 151 | lunar_year = index + SOLAR11.first 152 | lunar_month = 1 153 | offset += 1 154 | 155 | (0..12).each do |i| 156 | dm = get_bit_int(days, 1, 12 - i) == 1 ? 30 : 29 157 | break if dm >= offset 158 | 159 | lunar_month += 1 160 | offset -= dm 161 | end 162 | 163 | lunar.lunar_year = lunar_year 164 | lunar.lunar_month = lunar_month 165 | lunar.lunar_day = offset.to_i 166 | lunar.is_leap = false 167 | 168 | if leap != 0 && lunar_month > leap 169 | lunar.lunar_month = lunar_month - 1 170 | 171 | lunar.is_leap = true if lunar_month == leap + 1 172 | end 173 | 174 | lunar 175 | end 176 | 177 | private 178 | 179 | def get_bit_int(data, length, shift) 180 | (data & (((1 << length) - 1) << shift)) >> shift 181 | end 182 | 183 | def solar_to_int(y, m, d) 184 | m = (m + 9) % 12 185 | y -= m / 10 186 | 187 | 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (d - 1) 188 | end 189 | 190 | def solar_from_int(g) 191 | y = (10000 * g + 14780) / 3652425 192 | ddd = g - (365 * y + y / 4 - y / 100 + y / 400) 193 | 194 | if ddd < 0 195 | y -= 1 196 | ddd = g - (365 * y + y / 4 - y / 100 + y / 400) 197 | end 198 | 199 | mi = (100 * ddd + 52) / 3060 200 | mm = (mi + 2) % 12 + 1 201 | y += (mi + 2) / 12 202 | dd = ddd - (mi * 306 + 5) / 10 + 1 203 | 204 | Solar.new(y, mm, dd) 205 | end 206 | 207 | end 208 | end 209 | --------------------------------------------------------------------------------