文字列の途中の不要な文字列を正規表現で検知して消してみる

こんにちは。くろはです。 今回は意外と使う場面のありそう(個人的な感想)な”文字列途中の可変な不要箇所を正規表現を用いて消す”ということについて書こうかと思います。

背景


私はDBを操作する際のGUIアプリケーションとしてHeidiSQLというアプリケーションを使用しています。
HeidiSQLは単一テーブルから抽出したレコードをInsert文の形式でエクスポートできる機能があるため、
時々この機能を利用して開発環境からマスタデータをローカル環境に移してきて開発を行っています。
ただしここでそのままInsert文を実行してしまうと主キーとなっているID列が開発環境の状態のままテーブルに格納されてしまいます。
たいていは気にしなくてもよいのですがローカル環境も元々はいつぞやの時点の本番環境をコピーしたものなので開発環境のIDをそのまま使うと
自分の行っている改修とは無関係な動作を引き起こしかねない(=正しい結果が得られない)ことにつながるパターンがあったりします。
そこでid列を指定せずにレコードを挿入するとテーブル定義上は連番でIDを振ってくれるのでinsert文からID列と値の指定箇所を消してから挿入、といった作業を時々しています。
10行くらいなら「まぁ手作業でもよいか…」となりますがさすがにそれ以上は労力が見合ってくれません。 というわけで表題の正規表現で一気に指定箇所を置換してしまおうと思った次第です。

結論

このようなSQLが複数あったとしてID列とその値を1度に消します。
INSERT INTO `テスト` (`id`, `○○`, `△△`,`□□□`) VALUES (4401241, 'hoge', 'huga', 'hogehoge');
置換前と置換後の正規表現です。 置換前 (.+)id,(.+) VALUES ([0-9]*,(.+) 置換後 $1$2 VALUES ($3 置換した後のSQLは次のように変更されます。 このようなSQLが複数あったとしてID列とその値を1度に消します。
INSERT INTO `テスト` (`id`, `○○`, `△△`,`□□□`) VALUES (4401241, 'hoge', 'huga', 'hogehoge');
置換前と置換後の正規表現です。 置換前 (.+)id,(.+) VALUES ([0-9]*,(.+) 置換後 $1$2 VALUES ($3 置換した後のSQLは次のように変更されます。  
INSERT INTO `テスト` (`○○`, `△△`,`□□□`) VALUES ('hoge', 'huga', 'hogehoge');
無事ID列と値だけが消えました。  

考え方

正規表現における記号の意味等は割愛しますが置換前のパターン構造を文字で解説すると、 【文頭からidまでの任意の文字の繰り返し】 + 【`id`,】 + 【id,からVALUESまでの任意の文字の繰り返し】+【VALUES ( 【0~9の任意の数字の繰り返し+「,」】】+ 【以降文末までの任意の文字の繰り返し】 という感じです(逆に複雑か….?) 正規表現では”()”でくくると1つのグループとして評価されます。(=文字列とは判定されない) そのため「VALUES ( 」 として「 ( 」をエスケープしてあげないと正しくパターンを判定できません。 置換後の正規表現は結構シンプルで 【1つ目のグループに含まれる文字列】+【2つ目のグループに含まれる文字列】+【VALUES (】+ 【3つ目のグループに含まれる文字列】 というパターン構造になっています。 置換前のパターンの箇所で「”()”でくくるとグループとして評価されます」と書きましたが”()”にはもう一つ、後方参照という役割があります。 後方参照とはざっくりというとカッコ内のパターンに合致する文字列を記憶し、パターン内の変数の値として該当する文字列を返してくれます。 置換後の正規表現パターンの$1~$3がその変数にあたります。 これにより変更が不要な文字列をそのまま元の文字列から持ってくることができます。 青線が今回削除した部分、赤線がかっこでくくったり文字列をそのまま指定して新しく作る文字列として指定している部分です。こうやった方が分かりやすかったかも。 今回はIDの値にあたる部分がVALUES ( の後に来る文字列だったためVALUES ( をわざわざべた書きでパターンとしたけどもっと簡潔に書けたかもしれないと思えてきました。話が脱線しそうなので今回はここまでです。

終わりに

改めて見返すと正規表現のパターンの書き方って難しいけどどうパターンを組むかというのがパズルのようで楽しかったりするんですよね。ただカッコでネストしまくるとあっという間に迷宮の出来上がりなのでできるだけ簡潔に書けるように普段から意識していきたいですね。

PostmanのPre-requestって便利だなあ

こんにちわわ🐾。たにすぎです。先日APIのテストをする機会があってPostmanを利用したんですが、改めてPre-requestの機能が便利だな、と感じたので紹介します。

Postmanとは

https://www.postman.com/
Postman社が提供するAPIの開発用プラットフォームでリクエストや認証のテスト等を行えます。
Code snippetを使えばcURLやHTMLのコードにも変換してくれるので、Postmanのチームを共有していなくても他の人にリクエスト例を共有できるのでなにかと便利です。

Pre-requestって?

リクエストの前に決まった処理をしてくれる機能です。
APIを試すぞ、って時にパラメタにユニークなIDを設定しないといけなかったり、AのAPIの結果をBのAPIをリクエストに使用したい時など、手作業でパラメタ設定すると面倒な時にこのPre-requestが便利です。

ざっくり試し方

基本的なリクエストの方法は割愛します。
今回はあるAPIのリクエスト結果をPre-requestで取得して、その結果をAPIのリクエストに使います。

とりあえずENVを設定する

右上の目が付いた表みたいなアイコンを押してEnvironmentのAddをクリック
適当に変数セットの名前を入力して、Variablesに変数名を入れる

今回は環境で変わる(であろう)URLのメインの部分と1つ目のAPIの結果を入れるなんちゃらIdsを設定しています。

Pre-requestにリクエスト前の処理を書く

今回はAの結果でIDのリストのみが返ってくるのでこんな感じになりました。

const server = pm.environment.get("server"); //envからサーバー名を取得 
const xxxIdsUrl = "https://"+server+"/?limit=40";

pm.sendRequest(xxxIdsUrl, function (err, response) {
    pm.environment.set("xxxIds", response.json()); // なんちゃらIdsに結果をセット
});

envの値の取得方法が良くわからん、て場合もよく使う例が右側にSnippetsがあるので割と楽に書けました。

結果

リクエストURL横のSendを押してリクエストしてみます。

無事結果が取得できました。envのCurrent valueも更新されてます。

まとめ

使ってる方も多いかなと思いつつPostmanのPre-requestの機能を紹介しました。
Postmanで取得データのテストも書けたり、チームでコレクションを共有したりも出来るので、上手く使えば更に効率よく開発ができるんだろなと思います。
みんなで知見を共有しつつもっと活用したいですね……!

「R」を使ってみた

こんにちは、イメージマジック三浦です。最近はChatGPTで大騒ぎですが、今回は「R」を紹介します。

「R」とは

「R」は統計解析向けのプログラム言語・その開発実行環境を併せた総称です。統計学的な操作を行うためのメソッドが充実しています。特に計算式をプログラムに落とし込むような手間がなく、統計的な要件に対しては、とても使いやすいツールです。
プログラム言語と言いつつも、専門の書籍は数学関係の場所に置いてあることが多いです。

インストール

Rの公式サイトから「Download」の下にある「CRAN」をクリックすると、各国用のミラーサイトに遷移できます。遷移先のサイトから、Windows, macOS, Linux用のインストーラをダウンロードできるようになっています。 Rだけでも十分使えますが、RStudioを使うと便利です。画面表示は全部英語ですが、日本語を扱うことに問題はなく十分に使いやすいツールです。

サンプルプログラム

詳しい説明は省略しますが、参考として単回帰分析を行うコード例を出します。
data<-read.csv(
    "./_data/sample_data.csv",
    encoding = 'UTF-8',
    stringsAsFactors = F,
    header=T
)
x <- data$dataX
y <- data$dataY
result <- lm(y~x)
summary(result)
分析用のデータをきちんと準備すれば、これだけのコード量で単回帰分析を実行できます。仮に単回帰分析の導出式をプログラム言語で実装しようとすれば、サンプルコードの数倍くらい必要になるかもしれません。

計算の前後が大事

Rは、分析用データを準備すれば計算を実行して結果を返してくれますが、結果の品質はデータの準備度合いに依存します。例えば分析用データに以下のようなものが混ざっていれば、結果の精度が落ちてしまいます。
  • 全体的な傾向から逸脱しているデータがある
  • 分析に使うデータ量やパラメータが適切でない
  • データに抜けがある
このようなデータを除くために「前処理」という工程が存在し、「前処理」だけで1冊専門書が出るくらいの内容があります。 計算後にしても、予想通りの結果が得られたのか予想と異なる結果が得られたかを検証し、状況次第では分析の見直し等が必要になってきます。

まとめ

強力な機能を備えるRですが、出てきた結果を鵜呑みにせず、結果が本当に正しいかを吟味することが必要だと思います。そうできるように、これからも精進あるのみです。

ImageMagick脆弱性対応(パッチ)

こんにちは岡野です。
先日行った、画像処理ライブラリImageMagickの脆弱性対応を共有します。当社ではオリジナルプリントやMEET MY GOODSといったサービスで処理高速化のためリコンパイルしたImageMagickを使用しています。

脆弱性の概要

脆弱性(Metabase Qの報告
CVE-2022-44267:DoS攻撃
CVE-2022-44268:任意のファイル参照
ですが、不正なテキスト情報を埋め込んだPNGファイルをImageMagickが処理する際に発生する物でした。

脆弱性の対応案

・ImageMagickのバージョンアップ
ImageMagickを最新版へバージョンアップすれば解決するとのことでしたが、テストに時間が掛かるため他の方法を検討しました。

・画像データの修正
脆弱性の原因は前述の通り不正なテキスト情報ですので、ImageMagickへ画像ファイルを渡す前に不正なテキスト情報を除去する案です。PNG最適化ツールpngcrushで除去する方法をネットで見つけ試してみましたが除去処理に時間が掛かるためあきらめました(バージョンにより違うのかもしれませんが大きめの画像では10秒以上掛かることがありました)。

・ImageMagickのパッチ適用
脆弱性を修正するパッチをImageMagickのソースコードへ適用する案です。今回はこの案で対応しました。

パッチ適用の手順

今回はUbuntu用のパッチを流用しました。
https://launchpad.net/ubuntu/+source/imagemagick/8:6.9.7.4+dfsg-16ubuntu6.15
の「Available diffs」の箇所です。
但しとあるツイートに存在したPNGファイル
echo -en "P4\n1 1\nX" | pnmtopng -text <(echo 'fx:while(1,debug(hui=3)) 0') > payload.png
の脆弱性を防げなかったためパッチのif文
if ((LocaleCompare(key,"version") == 0) ||
    (LocaleCompare(key,"profile") == 0) ||
    (LocaleCompare(key,"width") == 0))
へ「fx:」を無効化する
    (LocaleNCompare(key,"fx:",3) == 0) ||
を追加しました(ImageMagickのバージョンによっては再現しなさそうでした、おそらくこのissue)。

まとめ

残念ながらImageMagickの脆弱性は今後も発見されると思いますので、バージョンアップを容易にしておく必要性を感じました。今後はバージョンアップ用の自動テストケースを準備しようと思います。

PHPのgetRealPathでファイルパスの代わりにプロジェクトルートが取れてしまった話

スズキです。 Webアプリの画面からファイル添付してサーバに送ることってありますよね。今回は、サーバ側のPHPプログラムでリクエストから添付ファイルを取り出そうとしたら、思ってたのと全然違う挙動になってしまった話です。

やろうとしたこと

やろうとしたことは超シンプルです。
  • HTTPのmultipart/form-dataで送られてきたファイルをリネームして保存する
  • ファイルが無ければエラーにする

サンプルコード(修正前)

PHP+Symfonyのコードです。例外処理などはごっそり省略。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getRealPath();
    
    // ③ファイル存在チェック
    if (!$tmpFilePath) {
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}

起きたこと

②でファイルパスを取得したとき、通常なら、 // $tmpFilePath : ‘/tmp/phpucYAwE’ といった一時ファイルのパスが取得できるのですが、PHPのアップロードサイズ上限値(php.iniで設定する値)を超えたファイルを送ってみると、
// $tmpFilePath : '/opt/myWebApp/public'
といったように、なぜかWebアプリのルートディレクトリパスが取得されてしまいました。そして、③のファイル存在チェックでもエラー判定できず、そのまま④でアプリのルートディレクトリがリネームされてアプリが動かなくなる事態に…

対処

②でのファイルパス取得にて「getRealPath」を「getPathname」に変えたところ、アップロードサイズ上限を超えたファイルの場合に、期待通りに空文字が取得できるようになりました。
ファイルサイズが大きくない場合は、「getRealPath」と「getPathname」は同じパスを返してくれたので、問題なさそうです。
// $tmpFilePath : '' ←ファイルサイズ上限オーバーのときは空文字が返ってくる(期待通り)

サンプルコード(修正後)

ディレクトリパスが取得されてしまうと悲惨なので、②のパス取得部分だけでなく、安全のため③のファイル存在チェックの条件も増やしています。
/**
 * ファイル保存
 */
public function uploadFile(Request $request)
{
    // ①リクエストからファイルを取り出す
    $file = $request->files->get('file');
    
    // ②ファイルが一時的に保存されているパスを取得
    $tmpFilePath = $file->getPathname(); // getRealPath ではなく getPathname を使用
    
    // ③ファイル存在チェック
    if (!$tmpFilePath || is_dir($tmpFilePath)) { // パスがディレクトリだった場合もエラーにしておく
        return 'ファイルが見つかりません';
    }
    
    // ④ファイル名をリネーム
    $newFilePath = $this->makeNewPath($tmpFilePath);
    rename($tmpFilePath, $newFilePath);

    // ⑤ファイル保存
    $this->saveFile($tmpFilePath);

    return '成功しました';
}
 

原因推測

「getPathname」と「getRealPath」はどちらも似た説明になっていますが、「getRealPath」は絶対パスを取得するようです。 「getRealPath」が絶対パスに変換する処理の中で、空文字のパスがプロジェクトルートと解釈されたのかな…と推測しています。 リクエストに添付ファイル自体が無ければ、①の $file = $request->files->get('file'); のところでnullが取得されて特に被害は発生しないのですが、サイズ上限オーバーの場合は中途半端にファイルが存在する感じになっていそうです。 今回は事例紹介まで。

Integer型の変数がNullかどうかチェックするのはなぜ?

クロハです。 前回のPHPの初歩的な部分に躓いた話に引き続いて、たぶん今回もプログラム経験の浅い人向けの内容です。

事の発端

唐突なのですがJavaのソースの改修をしているときに次のような条件分岐に改修する必要な場面に遭遇しました。

// Hogehoge.hogeId はInteger型の変数 private boolean IsHoge(Hogehoge hogeEntity) { Integer hogeId = 0; if(hogeEntity.hogeId != null) { hogeId = hogeEntity.hogeId; } /** 以下割愛 **/ }

内容としてはhogeId に0を代入しておいて引数のhogeEntityクラスのhogeIdがNullじゃなければhogeIdに代入するというものです。他のプログラムの書き方を見ていて思いついた処理ですね。 ちなみにこの割愛している部分の処理でDBに接続してレコードを検索するのですがフレームワークの都合でhogeIdがNullだとwhere条件句からhogeIdの指定条件が消えてしまうのでhogeIdが異なっても他の条件に一致していればレコードがヒットしてしまうという事情があり上記の条件分岐と代入処理が必要でした。 この時「int」と「Integer」の違いを知らなかった私は「Integerってつまりintでしょ?nullにしようにも0しか入らなくない?」と最初は思っていました。
その後、「そもそもintにnull入れようとするとNullPointerExceptionが出るから0にすらならない..?」とか色々自分の中でも矛盾が発生したので気になって調べた、というのが今回の事の発端です。

「int」と「Integer」の違いとは

ざっくりと書くと
  • int → プリミティブ型
  • Integer → 参照型(intのラッパークラス)
ここで上記の条件の理由が分かった人はプログラミングを基礎からちゃんと理解できてる人だと思います。(というか「int null なぜ」とかググったりしないですよね..) 話を戻します。 まずラッパークラスについてですがこれも言い換えれば「基本のデータ型やオブジェクトを使いやすくするためにメソッドなどを追加したクラス」です。 あまりいい例が思いつかなかったのですが洗濯機を例にします。 洗濯の工程として 対象を投入する→洗う→干す(乾燥する)→畳む を想定すると 普通の洗濯機(基本のデータ型)では投入された対象を洗う機能しかないとなると乾燥以降の手順を自前で行う必要があります。 しかし乾燥機能付きの洗濯機(洗濯機のラッパークラスとする)であれば洗う→乾燥までの工程を洗濯機側でやってくれるので人間は対象の投入と畳む工程をやればよい、という感じですね。文明の利器バンザイ。 次にプリミティブ型と参照型についてですが、 プリミティブ型→値を持つ 参照型→メモリ上の値が格納されているアドレスを持つ という違いがあります。 こちらの記事 に良い具体例があったので引用させていただくと
int a = 1;
int b = a; // bにはaの値:1が格納される
a = 2;

Systemout.println(a); // 2 が出力される
Systemout.println(b); // 1 が出力される

int[] a = {1, 2, 3};
int[] b = a;
a[0] = 0;

Systemout.println(a[0]); // 0 が出力される
Systemout.println(b[0]); // 0 が出力される
int[] b = a の箇所ではint配列aと同じアドレスを見ているためaの値の変更が反映されていますね。 雑な結論ではありますが値が格納されているアドレスを見ているので「値の入っている場所(アドレス)が指定されていない」という状態が参照型におけるNullなのだと個人的に解釈しました。

終わりに

さて、そろそろ話を締めます。 今回学んだことを踏まえて発端となった条件分岐を考えると 引数のHogehogeエンティティのhogeIdがNullの場合は Hogehoge.hogeId に入る値のアドレスがない=アドレスが指定されていない=Null となることがあり得るということでした。 自分の備忘録的な部分がメインですが同じような疑問を持っていた方の助けになればと思います。

目当てのIPアドレスを探せ

ある日、ネットワーク内につないだ機器のIPアドレスが現地で分からなくなったので、調べてほしいと依頼がありました。当日は力技で何とかしましたが、もっと楽にできることあったので、振り返ってみます。

前提条件

幸いなことに、今回問題のネットワークでは、以下の点が分かっていました。
  • 現地の機器をつないでいるネットワークへアクセスできるサーバーに、本社オフィスからアクセスできた。
  • 探すべきIPアドレス帯は第3オクテットまで固定、かつ第4オクテットの範囲が決まっている。
  • IPアドレスを指定してwgetが成功することで、そのIPが当たりと言える。
これなら何とかなりそうです。

当日の手段:力業

当日は仕方なかったので、こんなシェルスクリプトを書いて、見つかったらCtrl+Cで対応しました。
wget -t 1 -T 3 --spider http://X.X.X.1/hogehoge;
wget -t 1 -T 3 --spider http://X.X.X.2/hogehoge;
・・・
wget -t 1 -T 3 --spider http://X.X.X.99/hogehoge;
wget -t 1 -T 3 --spider http://X.X.X.100/hogehoge;
IP1個につき1回wgetし、3秒待機するコマンドを第4オクテットとして考えられる範囲分(上の例なら100行分)を書いています。URLの存在チェックができれば十分だったので、–spiderを付けました。最初から1行ずつ実行していくだけのコマンドです。 コマンド自体は表計算ソフトで量産できますが、都度作るのは煩雑です。一発でできるようにしたいです。

次回からはこれを使う:ワンライナー

xargsを使うことで、ワンライナーで実行できます。
$ seq 1 100 | xargs -I@ wget -t 1 -T 3 --spider http://X.X.X.@/hogehoge;
seqコマンドで生成された数を、xargs以下に渡しています。X.X.X.@ のアットマーク分がxargsの引数で変化し順次実行していきます。これで目標とした検索が実現できます。 1から順に確認して見つけたらCtrl+Cで止めるのは、力業と同じです。

Ctrl+Cを使わない方法はあるか

Ctrl+Cで止めない方法も考えたいと思って、for文でコマンドを試作してみました。
## 1から順番に数を調べて、3の倍数になったらループを抜ける例。
for v in `seq 1 10`; do if [ $((v % 3)) -eq 0 ]; then echo fuga; break; else echo $v; fi; done
実際書いてみると、単語が込み入って見づらいと感じました。これなら、素直にシェルスクリプトファイルにした方が見やすそうです。よりよい方法があるかもしれませんが、今回はこれ以上の調査を見送ります。

余談

「ワンライナー」という言葉を使いましたが、似た感じの言葉で「リニア」があります。「ライナー」と「リニア」の違いが気になって調べてみました。
  • ライナー(liner)
    • 定期運行する交通機関
    • 「京成スカイライナー」「おはようライナー」の「ライナー」はこちらの意味
  • リニア(linear)
    • 一直線に伸びた感じの意味
    • リニアモーターカー linear motor car
    • 線形代数学 linear algebra

C#でTesseract 5

矢野です。
社内向けに作成していたC#アプリケーションで利用するTesseractのバージョンを4系から5系に更新した概要を簡単にメモしておきます。Tesseractに限らずC#での画像認識に関する情報って、Pythonなどに比べると日本語でも英語でもかなり少ないですよね。ビジネスロジックをつくっていく段階であれば、もちろん他のプログラミング言語向けの情報でも大いに参考になるのですけれど、最初の一歩して「とりあえず動くようにする」ところがその言語なり、ライブラリなりの固有の手順が必要で、そういうところで引っ掛かりがちです。 .NET Framework, .NET向けに作成されたTesseractのラッパーライブラリとしてよく使われているのは恐らくcharlesw/tesseractだと思います。私もこれまでお世話になってきていたのですが、残念ながらTesseract 4.1までしかサポートされていません。今すぐ5系が必要な状況というわけではないですが、余力のある時に乗り換えておいた方が良いこともあるだろうと思って、対応したライブラリを探してみました。 上記のcharleswさんのところから派生したSicos1977/TesseractOCRを導入することで、ほんの少しのコードの修正でTesseract 4系から5系へ移行することができました。
NuGetパッケージマネージャーからTesseractOCRをインストール
Visual Studioからであれば、NuGetパッケージとして簡単にインストールできます。 以降前のcharlesw/tesseract向けのコードは、非常に簡素に書くとこんな感じ。

using Tesseract;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new TesseractEngine("tessdataフォルダのパス", "eng");
using (var pix = Pix.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.GetText();
}


これを、Sicos1977/TesseractOCR向けに修正すると、

using TesseractOCR;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new Engine("tessdataフォルダのパス", "eng");
using (var pix = TesseractOCR.Pix.Image.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.Text;
 }


名前空間とクラス名、オブジェクトの構成を少し修正するだけで、ほぼそのまま使えました。
分かりにくいけど上が認識対象の画像、下がTesseract 5.2での認識結果です。
えっ? たったこれだけ? そうなんです、たったこれだけでした。その先は、これまでのTesseract 4系向けにつくっていたのと同じようにビジネスロジックをつくり込んでいくことができました(同時期にEAST text detectorを組み込んでみたりもしたけど、それは別の話)。これまで未経験の技術とか、「たかが」ライブラリのアップデートとか、どうしても気後れしちゃうこともありますが、一歩踏み出してしまえば「たったこれだけ」で進めちゃうこともよくあるよね、というお話でした。

illustratorからSVGを出力してHTMLに表示する

株式会社イメージ・マジックの技術ブログ、今回の担当のsoenoです。
最近SVGを触ることがあり、いろいろできるんだなあという印象です。
手軽に使えるということでボタンをillustratorからSVGを書き出して実際に使うまでの流れを紹介します。

illustratorからSVGを書き出す

  1. illustratorでボタンを作成し、ボタンの要素をグループ化します。(要素を選択し右クリック)



  2. グループ化した要素をもう一度右クリックし、書き出し用に追加(単一のアセット)します。



  3. アセットの書き出しタブの中の書き出し設定のセレクトボックスでSVGを指定します。



  4. アセットの書き出しパネルの右下のボタンから書き出しを選択。


  5. 書き出したい場所を選んで保存。

HTMLに埋め込む

HTMLでイメージの参照先に保存したSVGを指定する(下のパスは同一階層にある場合の指定)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>svg追加</title>
</head>
<body>
    <img src="btn.svg" width="79" height="35">
</body>
</html> 


上のコードをブラウザで表示させた時

最後に

上の例ではillustratorから書き出したSVGをHTMLに組み込む場合の流れを書きました。
illustratorからの書き出し方にもSVGの埋め込み方にも他の方法があります。
表示方法でできることが変わったりもします。
(SVGをHTMLに埋め込めばjavascriptから見た目を変更できる等)
また、SVG自体も使うプロパティーでいろいろな表現ができます。
プロパティーはブラウザの対応状況にばらつきがあるので使用時には少し注意がいりそうです。
自サイトのブラウザの対応範囲と、代替え手法が用意できるかなど、
様子見しながら使う感じになるのではないかと思います。

IllustratorのVer UPによるPNG読込挙動について

はじめに

こんにちは、イメージ・マジックのもあいです。
弊社では印刷用データの自動処理でPhotoshopやIllustratorのスクリプトを使用する事があるのですが、最近でIllustratorのバージョンアップを行ったら正常に動作しなくなったと言うことがありました。
その事象について備忘録として記事を残しておくことにします。

事象

具体的にどうなったのかというと、Illustratorに配置している画像がずれていてかつ大きさが小さくなるという事象でした。 報告を受けて使用しているIllustratorのバージョンを報告者に確認したところ26.1.0/26.2.1は問題が発生しないが、26.5だと発生するとのことでした。こういう事象が発生したときにバージョンを確認するとよくあるのが「最新版」という報告ですが、それが無かっただけでも良かったと思っています。
ただ、それでも不明なバージョンもあったので、Adobe Creative Cloudでバージョンダウングレードテストも行って、結果としては26.2まではOKで26.3/26.4/26.5はNGでした。

原因

Illustrator上で画像が小さくなったとのことでしたので、最初から解像度の問題であることを疑いましたし、それが正解でした。サーバ上で解像度(DPI値)のみを埋め込んでいたのですが、単位までは埋め込んでいなかったのが原因で、それによってIllustratorのバージョンによって解釈が異なっていることがわかりました。 Illustratorは解像度を埋め込んでいない画像については72dpiとして扱います。26.2までは解像度のみを埋め込んでも72dpi扱いで、単位を埋め込んで初めて認識します。26.3からは解像度のみを埋め込むと単位をdpiで認識していました。これが原因して今まで動いていたものが動かなくなりました。

検証

5種類の画像を用意してIllustratorの26.2と26.5に読み込ませたらどうなるかを検証しました。
下記のような5種類の画像を用意しました。
No 解像度 単位 大きさ(px) 備考
1 なし なし 800 x 80 何も埋め込んでいないバーコード画像
2 なし なし 610 x 88 1に余白をつけたバーコード画像
3 200 なし 610 x 88 1に余白をつけて解像度を付与
4 200 PixelPerInch 610 x 88 1に余白をつけて解像度/単位を付与
5 72 PixelPerInch 610 x 88 1に余白をつけて解像度/単位を付与
下記の画像ですが上記の表の画像を上から順にIllustrator 26.2.1に読み込ませました。
No4は200dpiとして認識しているので小さく表示されていますが、それ以外は一緒です。   次にIllustrator26.5で読み込ませた結果です。
No4はバージョンが異なっても同じですが、No3の解像度のみ指定している画像も200dpi扱いになって小さく表示されます。 ※上記2つの画像は同じ拡大率で表示してるものを縦に並べてスクリーンショットをとっています。

結論

画像の解像度を埋め込む場合は、解像度とともに単位も設定する。片方だけだと誤動作するので厳密に値を設定するのがよい。