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....
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
Gzipで元のpdfファイルの7割以下まで圧縮できました。
面白いことに、S3の管理画面上でダウンロードすると、その場で解凍され、拡張子も変化し、foo.pdf
としてダウンロードされます。
そして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=..."
上記メソッドで生成した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,
)