4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails(Ruby)でPDFを作成、圧縮、S3にアップロード

Last updated at Posted at 2023-01-11

TL;DR

# PDFをインラインで作成
require 'wicked_pdf'
pdf = WickedPdf.new.pdf_from_string(
  "Lorem ipsum ロレム・イプサム 呂礼無・維風沙夢" * 500,
  return_file: true,
)


# Tempfileの作成&圧縮
require 'tempfile'
require 'zlib'

tf = Tempfile.open(['foo', '.pdf.gz'], binmode: true)
Zlib::GzipWriter.wrap(tf, Zlib::BEST_COMPRESSION) do |gz|
  gz.print(pdf)
end


# AWS S3アップロード
require 'aws-sdk'

client = Aws::S3::Client.new(
  access_key_id: ENV['AWS_ACCESS_KEY_ID'],
  secret_access_key: ENV['AWS_SECRET_KEY_ID'],
  region: 'ap-northeast-1',
)

client.put_object(
  bucket: "bucket_name",
  content_encoding: "gzip",
  content_type: "application/pdf",
  key: "pdf/foo.pdf.gz",
  body: tf,
)

tf.close!


# Presigned URLでダウンロード
require 'aws-sdk'

bucket = Aws::S3::Resource.new(
  access_key_id: ENV['AWS_ACCESS_KEY_ID'],
  secret_access_key: ENV['AWS_SECRET_KEY_ID'],
  region: 'ap-northeast-1',
).bucket('bucket_name')

obj = bucket.object("pdf/foo.pdf.gz")

obj.presigned_url(
  :get,
  response_content_disposition: 'attachment; filename="FooBar.pdf"',
)

はじめに

令和5年10月より、インボイス制度が始まります。おかげさまで開発者として領収書の保存についていろんな方法を調査・考察していました。

調査で分かったこと

PDFのInline出力

WickedPdf.new.pdf_from_string()などで出力できます、使い方についてはWicked PDFのReadmeで詳しく書いていたので、ここでは割愛します。

特筆したいのは、pdf_from_string()のデフォルトの挙動をよくよく見ると、バイナリのStringを返しています、そのやり方だと、もしファイルサイズが大きい場合、必要以上にたくさんメモリ消耗する可能性が生じています。
なのでpdf_from_string('Foo', return_file: true)のようにフラグを立てば、Tempfileの引用が返してくれます。この引用があれば、S3にアップできます。

pdf = WickedPdf.new.pdf_from_string(
  "Lorem ipsum ロレム・イプサム 呂礼無・維風沙夢" * 500,
  return_file: true,
)
=> #<File:/var/folders/1r/npvfcq595hs4fvhwwwnjj73472pvds/T/wicked_pdf_generated_file20230111-94541-6jmf8s.pdf>

# S3 client事前設定済みとして
client.put_object(
  bucket: 'bucket_name',
  content_type: "application/pdf",
  key: "pdf/foo.pdf", body: pdf
)
=> #<struct Aws::S3::Types::PutObjectOutput....

Cursor_と_subsc-at-development_-_S3_bucket_001.png

Cursor_と_foo_pdf.png

GZIP圧縮してからアップロード

インボイス制度で大量に請求書・領収書を保管する場合、容量を減らすことはサーバー原価の削減に直結していますので、圧縮を試みます。

Tempfileを利用する場合

pdf = WickedPdf.new.pdf_from_string("Lorem ipsum ロレム・イプサム 呂礼無・維風沙夢" * 500, return_file: true)

# Tempfileの受け皿を作成
tf = Tempfile.open(['gzip', '.gz'], binmode: true) # `binmode: true`がないと失敗します
=> #<File:/var/folders/1r/npvfcq595hs4fvhwwwnjj73472pvds/T/gzip20230111-94541-1i0y35y.gz>

# ZlibでpdfのfileIOと受け皿のTempfileに繋ぐ
chunk = 1024 * 1024
Zlib::GzipWriter.wrap(tf, Zlib::BEST_COMPRESSION) do | gz | # 9 == Zlib::BEST_COMPRESSION
  gz << pdf.read(chunk) until pdf.eof?
end
=> nil

# S3 client事前設定済みとして
client.put_object(
  bucket: 'bucket_name',
  content_encoding: 'gzip', # 設定すれば、DLする際、S3が勝手に解凍してくれる
  content_type: "application/pdf",
  key: "pdf/foo.pdf.gz", body: tf, # 先ほど設定した受け皿のTempfileをアップロード
)

# 最後に2つのTempfileをclose
pdf.close!
=> true
tf.close!
=> true

Cursor_と_subsc-at-development_-_S3_bucket.png

Gzipで元のpdfファイルの7割以下まで圧縮できました。

Cursor_と_subsc-at-development_-_S3_bucket002.png
subsc-at-development_-_S3_bucket.png

面白いことに、S3の管理画面上でダウンロードすると、その場で解凍され、拡張子も変化し、foo.pdfとしてダウンロードされます。

Cursor_と_subsc-at-development_-_S3_bucket.png
Cursor_と_foo_pdf_gz.png

そしてS3の管理画面で「署名付きURL」を生成したら、ファイル名こそ変わってないが、解凍されたPDFとしてプレビューできます。

ダウンロード用署名付きURLの生成

設定して解凍されたにもかかわらず、名前に.gzが入っているのは気持ち悪いので、ファイル名してしてダウンロードすることも可能です。

# S3 Bucketが設定済みとして
obj = bucket.object("pdf/foo.pdf.gz")
obj.presigned_url(
  :get,
  response_content_disposition: 'attachment; filename="FooBar.pdf"',
)
=> "https://foobar.s3.ap-northeast-1.amazonaws.com/pdf/foo.pdf.gz?response-content-disposition=attachment%3B%20filename%3D%22FooBar.pdf%22&X-Amz-Algorithm=..."

foo_pdf_gz.png

上記メソッドで生成したURLでアクセスすると、FooBar.pdfとしてダウンロードされます。

[余談] StringIOを使って圧縮する方法

安定性やメモリ使用などについてはまだ未考察なので、保証いたしません。

io = StringIO.new
io.binmode # 重要

gz = Zlib::GzipWriter.new(io)
gz.write(pdf)
# もしFileIOを使ったの場合は 
# gz << pdf.read until pdf.eof?
gz.close # Closeしないと、書き込みが完了しません

client.put_object(
  bucket: "bucket_name",
  content_encoding: "gzip",
  content_type: "application/pdf",
  key: "foo.pdf.gz",
  body: io.string,
)

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?