FPDIでPDF1.5以降の圧縮されたファイルを何とかしたい
FPDIで色々していたら This PDF document probably uses a compression technique which is not supported by the free parser shipped with FPDI.
というエラーが。
どうやらPDF1.5以降の圧縮されたPDFの回答は無償版のFPDIではできないとのこと。
んじゃライセンス買おうかと思ったけど、当然高い。
ということで無償で何とかする方法が以下。
環境
Service | Version |
---|---|
CentOS | 7.3 |
PHP | 7.2.2 |
対応
QPDFインストール
必要なパッケージをインストール
$ yum install libjpeg-turbo libjpeg-turbo-devel
どうやらCentoOS7.3のgcc4.8では9.1.1のビルドが通らないらしいので、9.1.0を使用。
$ cd /usr/local/src $ wget https://github.com/qpdf/qpdf/releases/download/release-qpdf-9.1.0/qpdf-9.1.1.tar.gz $ tar xvfz qpdf-9.1.0.tar.gz $ cd ./qpdf-9.1.0 $ ./configure --prefix=/usr/local/qpdf/v9.1.0 $ make $ make install
パスを通す
$ cd /usr/local/qpdf/ $ ln -s ./v9.1.0 ./current $ cd ./current/bin $ ln -s /usr/local/qpdf/current/bin/qpdf
PHPから使ってみる
以下適当なサンプル。
<?php use setasign\Fpdi\TcpdfFpdi; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; /** * 圧縮の解除 * * @param string $inputPath * @param string $outputPath * @return string 圧縮解除したファイルのパス */ function uncompress(string $inputPath, string $outputPath): string { if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) { throw \Exception('skip setlocale() failed'); } $outputPath = escapeshellcmd($outputPath); $inputPath = escapeshellcmd($inputPath); $command = "qpdf --stream-data=uncompress --force-version=1.4 {$inputPath} {$outputPath}"; $process = Process::fromShellCommandline($command); $process->run(); if (! $process->isSuccessful()) { throw new ProcessFailedException($process); } return $outputPath; } $pdf = new TcpdfFpdi(); $pdf->setSourceFile(self::uncompress('/your/inputPath', '/your/outputPath'));
こんな感じで良い感じに書いてください。
雑感
Ghostscriptとかでやる方法などもあるみたいだけど、とりあえずこれで動いてるから一旦終わり!
あとPHPのサンプルは適当なので悪しからず。実際はこんな感じのコードをLaravelで書いて確認しました。
PHPでコマンド実行しようとしたら日本語が消失した
PHPでコマンドを実行する時はOSコマンドインジェクション対策としてescapeshellcmd
やescapeshellarg
などでエスケープすることが多いと思うが、その際にマルチバイト文字が含まれているとマルチバイト文字が空文字に変換されてしまって困った。
環境
Service | Version |
---|---|
PHP | 7.2.2 |
解決
実行前にロケール情報を設定してあげることで回避できた。
<?php if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) { throw \Exception('Not exist Locale.'); } $cmd = escapeshellcmd($cmd);
雑感
そもそもマルチバイトがコマンドに入るような設計がどうなの。
Safariでは206 Partial Contentに対応していないとVideoタグでmp4が再生できない
S3に保存されたmp4動画をPHPを経由して配信していたのだが、iOS/MacのSafariだと再生できない事が判明した。
原因はVideoタグなどのリソースはRangeヘッダー
を付けてリクエストが投げられるのだが、ここで正しく206 Partial Content
を返してあげないとSafariでは動画を再生できないというものだった。
chromeとかは再生してくれるのにね。流石Safari。すごいぞSafari。滅びればいいのに。
で、以下どうやって対策をとったかという話。
環境
Service | Version |
---|---|
PHP | 7.2.2 |
Laravel | 6.5.0 |
問題のコード
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; class ResourceController extends Controller { public function download() { return Storage::disk('s3')->download('your/file/path/movie.mp4', 'movie.mp4'); } }
対応
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Request; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\StreamedResponse; class ResourceController extends Controller { public function download() { $disk = Storage::disk("s3"); $size = $disk->size('your/file/path/movie.mp4'); $start = 0; $status = 200; $length = $size; $headers = [ 'Content-Type' => 'video/mp4', 'Content-Length' => $size, 'Accept-Ranges' => 'bytes' ]; if ($range = Request::server('HTTP_RANGE', false)) { list($param, $range) = explode('=', $range); if (strtolower(trim($param)) !== 'bytes') { abort(400); } list($from, $to) = explode('-', $range); if ($from === '') { $end = $size - 1; $start = $end - intval($from); } else if ($to === '') { $start = intval($from); $end = $size - 1; } else { $start = intval($from); $end = intval($to); } $length = $end - $start + 1; $headers['Content-Length'] = $length; $headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size); $status = 206; } $response = new StreamedResponse(function() use ($disk, $start, $length) { $stream = $disk->readStream('your/file/path/movie.mp4'); fseek($stream, $start, SEEK_SET); echo fread($stream, $length); fclose($stream); }, $status, $headers); $response->headers->set( 'Content-Disposition', $response->headers->makeDisposition( 'attachment', "movie.mp4", str_replace('%', '', Str::ascii("movie.mp4")) ) ); return $response; } }
上記は実際に書いたコードを何とか一つにまとめたものです。
動作確認はしてないです。(もちろん上記のようにまとめる前の本チャンコードでは動作確認取れてます。)
まあこんな感じ。
めんどくさ。
無停止でCentOS7 on EC2のEBSストレージを拡張する
遅ればせながら今回の案件で初めてEC2を触ることに。
そこでまずはステージング環境を作ろうとごにょごにょしてたら、ディスクを8GiBで作成してしましました。
ステージング環境とはいえもう少し欲しかったのでディスクを拡張しようとしたらAWSではなんと無停止で拡張可能とのこと!再起動も不要!マジ最高!ってなったんですが、やり方が分からず詰まったりしたので備忘録か。
AWSコンソールからEBSボリュームを増やす
AWSコンソールからEC2のボリュームサイズを変更する。
変更後ステータスがin-use-optimizing
が100%になるのを待ちましょう。
いや、待つ必要があるかは分かんないんですが。この辺誰か情報ください。
私は8 -> 40GiBに変更で1時間ほどかかりました。
web上での操作は以上です。
拡張されたボリュームを適用する
拡張が完了したので早速sshログイン後確認してみよう。
$ df -h ファイルシス サイズ 使用 残り 使用% マウント位置 /dev/nvme0n1p1 8.0G 896M 7.2G 11% /
しかし拡張されていない。
おかしい・・・。何かが、あったに・・・違いない・・・。
「そう、誰もパーティショニングを拡張していないのである!」
まずは落ち着いてブロックデバイスを見てみる。
$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT nvme0n1 259:0 0 40G 0 disk └─nvme0n1p1 259:1 0 8G 0 part /
lsblkでブロック確認するとdisk側がpartitionより大きくなって、先ほど指定したサイズに変更されていることが分かる。
というわけでnvme0n1のパーティション、nvme0n1p1をgrowpartで拡張する。
nvme0n1p1をどうやって拡張するんだろうと思ったら、nvme0n1のパーティション1(nvme0n1p1)を拡張するので、/dev/nvme0n1 1
と指定するみたい。
$ sudo growpart /dev/nvme0n1 1 CHANGED: partition=1 start=2048 old: size=16775168 end=16777216 new: size=83883999,end=83886047 $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT nvme0n1 259:0 0 40G 0 disk └─nvme0n1p1 259:1 0 40G 0 part /
これでパーティションがディスクのサイズまで拡張された。
ただこの時点ではファイルシステムにまで影響していないので、最後にファイルシステムを拡張する。
$ sudo xfs_growfs /dev/nvme0n1p1
最後にちゃんと拡張されているか確認して完了だ。
$ df -h ファイルシス サイズ 使用 残り 使用% マウント位置 /dev/nvme0n1p1 40G 897M 40G 3% /
以上ーー!
参考
Laravel6でMinIOを使う
Laravelではドキュメント通りに必要なパッケージをインストールすればS3はすぐに使うことができるようになる。
ただ開発環境はMinIOで代用したい場合などは、少し工夫が必要となる。
というのもバケット指定方法が違うのだ。
ということで config/filesystems.php
にs3
とは別の設定を追加する。
config/filesystems.php
<?php return [ // ~ 'disks' => [ // ~ 'minio' => [ 'driver' => 's3', 'endpoint' => env('MINIO_ENDPOINT', 'http://127.0.0.1:9000'), 'use_path_style_endpoint' => true, 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), ], ], // ~ ];
ここで重要なのが、endpoint
とuse_path_style_endpoint
である。
これを指定することでMinIOに対応してくれる。
ただしdiskの指定をminioにする必要があるため、ファサードを書き換えてしまったり、何か処理を噛ませるといいだろう。そのあたりは各々のプロジェクトで考えてほしい。
おまけ
参考までにファサード書き換えで本番環境と開発環境でs3の向き先を変える方法を記載しておく。
config/app.php
<?php return [ // ~ 'providers' => [ // ~ App\Filesystem\FilesystemServiceProvider::class, // ~ ], 'providers' => [ // ~ // 'Storage' => Illuminate\Support\Facades\Storage::class, 'Storage' => App\Filesystem\Storage::class, // ~ ], // ~ ];
app/Filesystem/FilesystemServiceProvider.php
<?php namespace App\Filesystem; use Illuminate\Support\ServiceProvider; class FilesystemServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { $this->app->bind('filesystem.expansion', function($app) { return new \App\Filesystem\FilesystemManager($app); }); } /** * Bootstrap any application services. * * @return void */ public function boot() { // } }
app/Filesystem/Storage.php
<?php namespace App\Filesystem; use Illuminate\Support\Facades\Storage as OrgStorage; /** * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null) * * @see \Illuminate\Filesystem\FilesystemManager */ class Storage extends OrgStorage { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'filesystem.expansion'; } }
app/Filesystem/FilesystemManager
<?php namespace App\Filesystem; use Illuminate\Filesystem\FilesystemManager as OrgFilesystemManager; /** * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null) * * @see \Illuminate\Filesystem\FilesystemManager */ class FilesystemManager extends OrgFilesystemManager { /** * Get a filesystem instance. * * @param string|null $name * @return \Illuminate\Contracts\Filesystem\Filesystem */ public function disk($name = null) { if (config('app.debug') && ($name === 's3')) { $name = 'minio'; } return parent::disk($name); } }
S3互換のMinIOをCentOS7にインストールする
S3を使ったサービスを開発したいが、開発中は課金が発生してほしくない。
そんなわがままなあなたにMinIOが答えてくれるだろう。
今回はこのMinIOをCentOS7にインストールし、起動スクリプトを書くまでを記す。
ユーザーの追加
$ sudo useradd minio -s /sbin/nologin
インストール
公式サイトからダウンロード
$ cd /usr/local/src $ wget https://dl.min.io/server/minio/release/linux-amd64/minio $ chmod +x minio $ chown minio:minio minio $ mv minio /usr/local/bin
systemd用スクリプトを作成
$ cs /etc/systemd/system/ $ curl -O https://raw.githubusercontent.com/minio/minio-service/master/linux-systemd/minio.service $ sed -i -e 's/minio-user/minio/' minio.service
データディレクトリを作成
$ mkdir /var/lib/minio $ chown mino:minio /var/lib/minio chmod 700 /var/lib/mino
設定ファイルの生成
/etc/default/minio
MINIO_VOLUMES=/var/lib/minio MINIO_ACCESS_KEY={適当なセキュアな文字列} MINIO_SECRET_KEY={適当なセキュアな文字列}
サービスの有効化
$ systemctl start minio $ statemctl status minio $ systemctl enable minio
Can't resolve 'fs'
browser向けjsの環境構築中、環境によって変えたい値を.envで管理したいという事でdotenv-webpackをインストールしたのだが、bundle作成時にエラーが出てつまったので共有をしておく。
環境
Package | Ver |
---|---|
webpack | 4.41.0 |
webpack-cli | 3.3.9 |
webpack-dev-server | 3.8.1 |
dotenv-webpack | 1.7.0 |
経緯
他のプロジェクトでもdotenv-webpack
をブラウザ向けに使っていたこともあり、特に何も考えずにdotenv-webpackをインストールしてビルドした。
$ npm install dotenv-webpack
webpack.config.js
const path = require('path'); const Dotenv = require('dotenv-webpack'); module.exports = { // ... plugins: [ new Dotenv({ systemvars: true }) ], // ... };
package.json
{ // ... "script": { // ... "dev": "webpack-dev-server", // ... }, // ... }
$ npm run dev
すると以下のエラーが発生した。
ERROR in ./node_modules/dotenv/lib/main.js Module not found: Error: Can't resolve 'fs' in '/xxx/xxx/node_modules/dotenv/lib' @ ./node_modules/dotenv/lib/main.js 24:11-24 @ ./src/api/config.ts @ ./src/api/encode.ts @ ./src/api/index.ts @ ./src/index.ts
よくある間違った解法
Can't resolve 'fs'
などでググるとwebpack.config.js
のtargetをnodeに変更しましょうという記事が出てくるが、これはサーバーサイドの時の話である。
ブラウザ向けの場合はこの設定をしてしまうと動かない。ではどうすればいいかというと、fsモジュールを使わないようにするしかない。モジュール内で使われているのであれば代替モジュールを検討するほかなくなる。
しかしそれは非常に困る。だって他のプロジェクトではdotenv-webpack
ちゃんと動いてるし。
解決
css-loaderのissueに同じような問題が上がっていたので、それを参考にwebpack.config.js
にnode: { fs: 'empty'}
を追加した。
webpack.config.js
const path = require('path'); const Dotenv = require('dotenv-webpack'); module.exports = { // ... plugins: [ new Dotenv({ systemvars: true }) ], node: { fs: 'empty' }, // ... };
詳細は公式ドキュメントに記載されているが、要約するとNode用モジュールをポリフィルもしくはモックして、ブラウザーでも実行できるようにする!という事らしい。
そしてempty
は空のオブジェクトを提供するという意味なので、dotenv
内のfs
は何もしない空モジュールという事になる。
ブラウザ用ではfs
なんて使わないので、これで問題無。無事解決。良かった良かった。