午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPFでPDFをjpeg画像に変換するアプリ作ってみた

PDFファイルをjpegの画像ファイルに変換するアプリ作ってみた

f:id:gogowaten:20191231184140p:plain

ダウンロード

PDFtoJPEG.zip

ギットハブ

github.com

 

 

農林水産省 トマトの種類と見分け方

http://www.maff.go.jp/j/pr/aff/1708/pdf/1708_04.pdf

 

ここからダウンロードしたPDFファイルをアプリにドラッグアンドドロップ

 

f:id:gogowaten:20191231184056p:plain

最初のページだけが表示される

保存ボタンを押すと全てのページがjpegファイルとして保存される、場所は元のPDFファイルと同じフォルダ

 

f:id:gogowaten:20191231184505p:plain

保存処理完了したところ

 

 

2ページのPDFだったので、できあがったjpegファイルは2つ

f:id:gogowaten:20191231184616j:plain

1708_04_1.jpg



f:id:gogowaten:20191231184631j:plain

1708_04_2.jpg

f:id:gogowaten:20191231214602p:plain

作成されたjpegファイル2つ

作成されるファイル名

ファイル名は元のPDFファイル名+_+連番になる。連番は総ページ数の桁を0埋め

1708_04.pdfが元のPDFファイル名でページ総数が2なら作成されるのは

1708_04_1.jpg
1708_04_2.jpg

 

元のPDFのページ総数が30なら2桁なので

元のPDFファイル名_01.jpg

元のPDFファイル名_02.jpg

元のPDFファイル名_30.jpg

になる

 

 

dpiの指定

といっても画像のサイズの指定に使うだけで、保存されるjpg画像のdpiはWindows基本の96dpi

f:id:gogowaten:20191231190049p:plain

DPI

数字のボタンで切り替わる。jpeg変換はこの表示状態でされる。PDFファイルによるけど、だいたい縦ピクセルが2000前後になるDPIが良さそう

文字はきれいに拡大されるけど画像の場合

f:id:gogowaten:20191231191039p:plain

PDFに埋め込まれている画像の解像度が低いとガタガタになる

 

変換処理時間

環境は

OS:Windows 10

CPU:Ryzen 5 2400G

メモリ:DDR4 2666

 

dpiを高くして画像が大きくなるほど時間がかかる

高dpi > 低dpi

 

 

別のアプリ

f:id:gogowaten:20191231193327p:plain

PDF-XChange Viewer

色々細かい設定ができる素晴らしいアプリ

今まではこのアプリでjpgに変換していた

 

2ページのPDFファイルの変換時間

f:id:gogowaten:20191231193707p:plain

300dpiでの変換時間は4秒、600dpiでの変換時間は12秒

今回作ったアプリでは

300dpiでの変換時間は2秒、600dpiでの変換時間は6秒

約半分の処理時間にできた

 

仕様

  • jpegの品質は85固定
  • 保存されるjpegのdpiは96固定
  • 表示できるのは最初のページだけ
  • 処理の途中で中止はできない、終わるまでひたすら待つ
  • 処理の進捗プログレスバーなし、終わるまでひたすら待つ
  • シングルスレッド
  • 保存先に同名ファイルがあっても問答無用で上書き

 

 

f:id:gogowaten:20191231195351p:plain

 

<Window x:Class="PDFtoJPEG.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PDFtoJPEG"
        mc:Ignorable="d"
        Title="MainWindow" Height="800" Width="600">
  <Window.Resources>
    <Style TargetType="Button">
      <Setter Property="Margin" Value="10,5,0,5"/>
    </Style>
    <Style TargetType="TextBlock">
      <Setter Property="Margin" Value="10,5,0,5"/>
      <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal">
      <Button x:Name="ButtonSave" Content="保存" Click="ButtonSave_Click"/>
      <Button Name="ButtonDpi96" Content="96" Click="ButtonDpi96_Click"/>
      <Button Name="ButtonDpi150" Content="150" Click="ButtonDpi150_Click"/>
      <Button Name="ButtonDpi300" Content="300" Click="ButtonDpi300_Click"/>
      <Button Name="ButtonDpi600" Content="600" Click="ButtonDpi600_Click"/>
      <TextBlock Name="tbDpi" Text="今のdpi"/>
      <TextBlock Name="tbHeight" Text="縦ピクセル"/>
      <TextBlock Name="tbPageCount" Text="ページ総数"/>
    </StackPanel>
    <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
      <Image Name="MyImage" Stretch="None" UseLayoutRounding="True"/>
    </ScrollViewer>
  </Grid>
</Window>

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.IO;//必須
using Windows.Data.Pdf;

//下の2つを参照に追加する必要がある
//"C:\Program Files (x86)\Windows Kits\8.1\References\CommonConfiguration\Neutral\Annotated\Windows.winmd"
//"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll"

//参照したところ
//WPFアプリにPDFを表示する — 某エンジニアのお仕事以外のメモ(分冊)
//https://water2litter.net/rum/post/cs_pdf_wpf/

//    [UWP][PDF] PDFファイルを表示する | HIROs.NET Blog
//http://blog.hiros-dot.net/?p=7346

//C# Taskの待ちかた集 - Qiita
//https://qiita.com/takutoy/items/d45aa736ced25a8158b3
//農林水産省 トマトの種類と見分け方 //http://www.maff.go.jp/j/pr/aff/1708/pdf/1708_04.pdf namespace PDFtoJPEG { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { private PdfDocument MyPdfDocument;//PDFファイルを読み込んだもの //private string MyPdfPath;//読み込んだPDFファイルのフルパス private string MyPdfDirectory;//読み込んだPDFファイルのフォルダ private string MyPdfName;//読み込んだPDFファイル名 private double MyDpi;//PDFを画像に変換する時のDPI public MainWindow() { InitializeComponent(); this.Title = "PDFtoJPEG"; this.AllowDrop = true; this.Drop += MainWindow_Drop; } //ファイルがドロップされたとき private void MainWindow_Drop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; } string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop); LoadPdf(filePath[0]); } //PDFファイルを読み込んで最初のページを表示 private async void LoadPdf(string filePath) { var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(filePath); try { MyPdfDocument = await PdfDocument.LoadFromFileAsync(file); MyPdfDirectory = System.IO.Path.GetDirectoryName(filePath); MyPdfName = System.IO.Path.GetFileNameWithoutExtension(filePath); MyDpi = 96; DisplayImage(0, 96); tbPageCount.Text = $"ページ数 : {MyPdfDocument.PageCount.ToString()}"; } catch (Exception) { } } //PDFファイルを画像に変換して表示 private async void DisplayImage(int pageIndex, double dpi) { if (MyPdfDocument == null) { return; } MyDpi = dpi; using (PdfPage page = MyPdfDocument.GetPage((uint)pageIndex)) { double h = page.Size.Height; var options = new PdfPageRenderOptions(); options.DestinationHeight = (uint)Math.Round(page.Size.Height * (dpi / 96.0), MidpointRounding.AwayFromZero); tbDpi.Text = $"dpi : {dpi.ToString()}"; tbHeight.Text = $"縦ピクセル : {options.DestinationHeight.ToString()}"; using (var stream = new Windows.Storage.Streams.InMemoryRandomAccessStream()) { await page.RenderToStreamAsync(stream, options); BitmapImage image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.StreamSource = stream.AsStream();//using System.IOがないとエラーになる image.EndInit(); MyImage.Source = image; MyImage.Width = image.PixelWidth; MyImage.Height = image.PixelHeight; } } } //DPI指定ボタンクリック時 private void ButtonDpi96_Click(object sender, RoutedEventArgs e) { DisplayImage(0, 96); } private void ButtonDpi150_Click(object sender, RoutedEventArgs e) { DisplayImage(0, 150); } private void ButtonDpi300_Click(object sender, RoutedEventArgs e) { DisplayImage(0, 300); } private void ButtonDpi600_Click(object sender, RoutedEventArgs e) { DisplayImage(0, 600); } /// <summary> /// jpeg画像で保存 /// </summary> /// <param name="pdfDocument">読み込んだPDF、Windows.Data.Pdf.PdfDocument</param> /// <param name="dpi">PDFファイルを読み込む時のDPI</param> /// <param name="directory">保存フォルダ</param> /// <param name="fileName">保存名</param> /// <param name="pageIndex">保存するPDFのページ</param> /// <param name="quality">jpegの品質min0、max100</param> /// <param name="keta">保存名につける連番0埋めの桁数</param> /// <returns></returns> private async Task SaveSub2(PdfDocument pdfDocument, double dpi, string directory, string fileName, int pageIndex, int quality, int keta) { using (PdfPage page = pdfDocument.GetPage((uint)pageIndex)) { //指定されたdpiを元に画像サイズ指定、四捨五入 var options = new PdfPageRenderOptions(); options.DestinationHeight = (uint)Math.Round(page.Size.Height * (dpi / 96.0), MidpointRounding.AwayFromZero); using (var stream = new Windows.Storage.Streams.InMemoryRandomAccessStream()) { await page.RenderToStreamAsync(stream, options); BitmapImage image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.StreamSource = stream.AsStream(); image.EndInit(); JpegBitmapEncoder encoder = new JpegBitmapEncoder(); encoder.QualityLevel = quality; encoder.Frames.Add(BitmapFrame.Create(image)); pageIndex++; string renban = pageIndex.ToString("d" + keta); using (var fileStream = new FileStream( System.IO.Path.Combine(directory, fileName) + "_" + renban + ".jpg", FileMode.Create, FileAccess.Write)) { encoder.Save(fileStream); } } } } private async void ButtonSave_Click(object sender, RoutedEventArgs e) { if (MyPdfDocument == null) { return; } this.IsEnabled = false; try { int keta = MyPdfDocument.PageCount.ToString().Length;//0埋め連番の桁数 //各ページの保存処理のリスト作成 var MyTasks = new List<Task>(); for (int i = 0; i < MyPdfDocument.PageCount; i++) { MyTasks.Add(SaveSub2(MyPdfDocument, MyDpi, MyPdfDirectory, MyPdfName, i, 85, keta)); } //各タスク実行 for (int i = 0; i < MyTasks.Count; i++) { await MyTasks[i]; } MessageBox.Show("処理完了"); } catch (Exception ex) { MessageBox.Show($"なんかエラー出たわ \n {ex.Message} \n {ex.ToString()}"); } finally { this.IsEnabled = true; } } } }

なんとか書いてみたけど例によってあんまりわかっていないし、残念なのはシングルスレッドの処理になってしまったこと

各タスク実行のところのforをParallel.forに書き換えてみたけど変化なし

awaitとかTaskとかがまだわからん、マルチスレッドに対応できれば最低でも今の4倍速くなるんだけどねえ

そもそもPDF-XChange Viewerでの変換をもっと速くできないかなあと思って、今使っているのが4年前のバージョンなので、新しいのならマルチスレッドに対応しているかもと思ってダウンロードしたら、めんどくさいインストール仕様に変更されていたのでWPFでできないものかとググったのが始まりだっただけに残念、それでも2倍速くなったから、そこは満足

 

今気づいたのが、セキュリティとかで保護されているPDFファイルも問答無用で画像に変換できる

たとえば

www.asus.com

より引用、ダウンロードした日本語マニュアルPDF

f:id:gogowaten:20191231212905p:plain

パスワードセキュリティのあるPDFファイル


これだと

f:id:gogowaten:20191231212954p:plain

PDF-XChange Viewerではエクスポートの項目が、グレーアウトで画像に変換できないけど、今回のアプリなら

 

f:id:gogowaten:20191231213039j:plain

うれしい誤算、個人で使う分には問題ないと思う

 

 

参照したところ

water2litter.net

 

blog.hiros-dot.net

 

qiita.com

 

www.atmarkit.co.jp

 

 

関連記事

続きは2日後の

gogowaten.hatenablog.com