Thursday, December 24, 2009

Пару строчек из серии комментарии в коде

В работе с чужим кодом есть свои прелести :)


public static String RUSSIAN_NO_STRING = "НЕТ"; // russion word HET, do not try to correct this string!


/**
*<P>!!!! Pay attention to similiar (the same) method on PrintHandler.
* This method might be not used at all, there is quite mess about that.
*/

Saturday, November 28, 2009

Best Practice: Implementing Telerik RadGrid with Linq(ToSQL)

Telerik RadGrid is a very powerful component that allows you to view, edit, order and filter data.

I'd like to share my experience using this component with linq to sql.

Using this component is very straightforward unless your entity contains references to other entities (in other words if your table have foreign keys to other tables, see DB diagram below). Very often such relations have a kind of reference data.



In this case you may meet the following difficulties:

  1. ability to display data field of referenced entity (except its ID, of course) in grid column;
  2. ability to sort and filter data set of your entities by columns which are references to other entities.


To understand the cause of these difficulties lets look at simple example.

Generated Linq to SQL model for the database schema above will look like the following:



Now, to display a list of Books in RadGrid you should implement databinding. To do this you usually subscribe to RadGrid's OnNeedDataSource event and invoke RadGrid.DataBind() in PageLoad event handler:


protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
RadGrid1.DataBind();
}
}

protected void RadGrid1_NeedDataSource(object source, GridNeedDataSourceEventArgs e)
{
if (!e.IsFromDetailTable)
{
if (e.RebindReason == GridRebindReason.InitialLoad
|| e.RebindReason == GridRebindReason.ExplicitRebind)
{
RadGrid1.VirtualItemCount = Book.GetBooksCount();
}

int skip = RadGrid1.MasterTableView.CurrentPageIndex * RadGrid1.MasterTableView.PageSize;
int take = RadGrid1.MasterTableView.PageSize;

RadGrid1.DataSource = Book.GetBooks(skip, take);
}
}


Here's our data access methods:


public static int GetBooksCount()
{
using (var ctx = new DataClasses1DataContext())
{
return GetBooksQuery(ctx).Count();
}
}

public static List<Book> GetBooks(int? skip, int? take)
{
using (var ctx = new DataClasses1DataContext())
{
var query = GetBooksQuery(ctx);

if (skip.HasValue)
{
query = query.Skip(skip.Value);
}
if (take.HasValue)
{
query = query.Take(take.Value);
}

return query.ToList();
}
}

private static IQueryable<Book> GetBooksQuery(DataClasses1DataContext ctx)
{
var query = (from book in ctx.Books
select book);

return query;
}


part of *.aspx file that holds RadGrid markup:


<form id="form1" runat="server">
<div>
<telerik:RadScriptManager ID="RadScriptManager1" runat="server"/>
<telerik:RadGrid ID="RadGrid1" runat="server"
OnNeedDataSource="RadGrid1_NeedDataSource"
AllowCustomPaging="True"
AllowPaging="True">
<PagerStyle AlwaysVisible="True" Mode="NextPrevAndNumeric" />
</telerik:RadGrid>
</div>
</form>


and the resulting grid:



Notice how we implemented pagind with just few lines of code.

By default RadGrid created columns and bound them to properties of our Book class (AutoGenerateColumns="True"). But we need to display Pubilsher's Name instead of PublisherID.

To do this we need to change AutoGenerateColumns to False and write RadGrid markup by hand.

Here is the markup:

<Columns>
<telerik:GridBoundColumn UniqueName="Title" HeaderText="Title"
DataField="Title" DataType="System.String" />
<telerik:GridBoundColumn UniqueName="Author" HeaderText="Author"
DataField="Author" DataType="System.String" />
<telerik:GridBoundColumn UniqueName="Publisher.Name" HeaderText="Publisher"
DataField="Publisher.Name" DataType="System.String" />
</Columns>

and code behind:

public partial class Book
{

public static int GetBooksCount()
{
using (var ctx = new DataClasses1DataContext())
{
return GetBooksQuery(ctx).Count();
}
}

public static List<Book> GetBooks(int? skip, int? take)
{
using (var ctx = new DataClasses1DataContext())
{

// Preload Book's Publisher field
var loadOptions = new DataLoadOptions();
loadOptions.LoadWith<Book>(b => b.Publisher);
ctx.LoadOptions = loadOptions;

var query = GetBooksQuery(ctx);

if (skip.HasValue)
{
query = query.Skip(skip.Value);
}
if (take.HasValue)
{
query = query.Take(take.Value);
}

return query.ToList();
}
}

private static IQueryable<Book> GetBooksQuery(DataClasses1DataContext ctx)
{
var query = (from book in ctx.Books
select book);

return query;
}
}

Here's what we got at this point:



Now we will add support for filter and sorting capabilities. To do this we should set RadGrid's AllowFilteringByColumn and AllowSorting to True and change our DAL methods to support query filtering and ordering.

RadGrid gives us very good support here, because it can generate linq string that contains part of linq query's where expression. To get this expression we wrote simple RadGridHelper class (see full source code in attachments below).

To use RadGrid expressions we need DynamicQueriable (CSharpSamples.zip\LinqSamples\DynamicQuery\DynamicQuery\Dynamic.cs) to mix dynamic linq expressions with static queries in DAL.

Finishing stroke is to make filtering case insensitive, in order to do this we should set CaseSensitive="False" in RadGrid's GroupingSettings.

Below are the resulting code linstings:

Resulting RadGrid markup:

<telerik:RadGrid ID="RadGrid1" runat="server"
OnNeedDataSource="RadGrid1_NeedDataSource"
AllowCustomPaging="True"
AllowPaging="True"
AllowFilteringByColumn="True"
AllowSorting="True"
AutoGenerateColumns="False" GridLines="None">
<HeaderContextMenu EnableTheming="True">
<CollapseAnimation Type="OutQuint" Duration="200"></CollapseAnimation>
</HeaderContextMenu>
<PagerStyle AlwaysVisible="True" Mode="NextPrevAndNumeric" />
<GroupingSettings CaseSensitive="False" />
<MasterTableView>
<ExpandCollapseColumn>
<HeaderStyle Width="20px"></HeaderStyle>
</ExpandCollapseColumn>
<Columns>
<telerik:GridBoundColumn UniqueName="Title" HeaderText="Title"
DataField="Title" DataType="System.String" SortExpression="Title" />
<telerik:GridBoundColumn UniqueName="Author" HeaderText="Author"
DataField="Author" DataType="System.String" SortExpression="Author" />
<telerik:GridBoundColumn UniqueName="PublisherName" HeaderText="Publisher"
DataField="Publisher.Name" DataType="System.String" SortExpression="Publisher.Name" />
</Columns>
</MasterTableView>
<FilterMenu EnableTheming="True">
<CollapseAnimation Type="OutQuint" Duration="200"></CollapseAnimation>
</FilterMenu>
</telerik:RadGrid>


Resulting code behind:


protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
RadGrid1.DataBind();
}
}

protected void RadGrid1_NeedDataSource(object source, GridNeedDataSourceEventArgs e)
{
if (!e.IsFromDetailTable)
{
string where = RadGridHelper.GetFilterExpression(RadGrid1.MasterTableView,
null);
string orderBy = RadGridHelper.GetOrderBy(RadGrid1.MasterTableView);

if (e.RebindReason == GridRebindReason.InitialLoad
|| e.RebindReason == GridRebindReason.ExplicitRebind)
{
RadGrid1.VirtualItemCount = Book.GetBooksCount(where);
}

int skip = RadGrid1.MasterTableView.CurrentPageIndex * RadGrid1.MasterTableView.PageSize;
int take = RadGrid1.MasterTableView.PageSize;

RadGrid1.DataSource = Book.GetBooks(where, orderBy, skip, take);
}
}


Final Book class:


public partial class Book
{

public static int GetBooksCount(string where)
{
using (var ctx = new DataClasses1DataContext())
{
return GetBooksQuery(ctx, where, null).Count();
}
}

public static List<Book> GetBooks(string where, string orderBy, int? skip, int? take)
{
using (var ctx = new DataClasses1DataContext())
{
var query = GetBooksQuery(ctx, where, orderBy);

if (skip.HasValue)
{
query = query.Skip(skip.Value);
}
if (take.HasValue)
{
query = query.Take(take.Value);
}

return query.ToList();
}
}

private static IQueryable<BookWrapper> GetBooksQuery(DataClasses1DataContext ctx, string where, string orderBy)
{
var query = (from book in ctx.Books
select book);

if (!string.IsNullOrEmpty(where))
{
query = query.Where(where);
}
if (!string.IsNullOrEmpty(orderBy))
{
query = query.OrderBy(orderBy);
}

return query;
}
}


Resulting RadGrid in action:



Attachments


radgridsample-src.zip Full project sources + DB (142 KB)

Thursday, October 29, 2009

Developing Java applications with GAE SDK 1.2.6 and javaagent turned off

Since version 1.2.6 Google AppEngine SDK in development mode requires to run JVM with javaagent.

If you don't do this you'll get the following exception:



java.lang.RuntimeException: Unable to locate the App Engine agent.
Please use dev_appserver, KickStart, or set the jvm flag:
"-javaagent:<sdk_root>/lib/agent/appengine-agent.jar"
at com.google.appengine.tools.development.DevAppServerFactory.testAgentIsInstalled(102)
at com.google.appengine.tools.development.DevAppServerFactory.createDevAppServer(77)
at com.google.appengine.tools.development.DevAppServerFactory.createDevAppServer(38)
at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(153)
at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
at com.google.appengine.tools.development.DevAppServerMain.(DevAppServerMain.java:113)
at com.google.appengine.tools.development.DevAppServerMain.main(89)
Caused by: java.lang.NoClassDefFoundError: com/google/appengine/tools/development/agent/AppEngineDevAgent
at com.google.appengine.tools.development.DevAppServerFactory.testAgentIsInstalled(98)
... 6 more



But if you do you may get very strange behaviour of your app. Here's a few of mines I get developing with Tapestry 5.2.0.0-SNAPSHOT:


java.lang.ClassFormatError: Invalid length 65050 in LocalVariableTable
in class file org/apache/tapestry5/corelib/components/Form
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.lang.ClassLoader.defineClass(Unknown Source)
at javassist.Loader.findClass(Loader.java:379)
at org.apache.tapestry5.internal.services.ComponentInstantiatorSourceImpl$PackageAwareLoader.findClass(94)
...



I've found a discussion where similiar problem was caused by multiple versions of javassist on CLASSPATH. I tried to find them, but I had only one of it.

The other exception is due to previous.



org.apache.tapestry5.ioc.internal.util.TapestryException:
Failure creating embedded component 'sendInvite' of dmitrygusev.ping.pages.Index:
java.lang.ClassNotFoundException: caught an exception while obtaining a class
file for org.apache.tapestry5.corelib.components.Form"
[at context:Index.tml, line 23]
at org.apache.tapestry5.internal.pageload.ComponentAssemblerImpl.createEmbeddedAssembler(316)
at org.apache.tapestry5.internal.pageload.PageLoaderImpl.startComponent(740)

...

Caused by: java.lang.RuntimeException: Class org.apache.tapestry5.corelib.components.Form contains field(s)
(_$bindingSource, _$environment, _$onActionInfo, _$resources, _$type, _$type_0, _$type_1)
that are not private. You should change these fields to private, and add accessor methods if needed.
at org.apache.tapestry5.internal.services.InternalClassTransformationImpl.verifyFields(293)
at org.apache.tapestry5.internal.services.InternalClassTransformationImpl.preloadMemberNames(255)
at org.apache.tapestry5.internal.services.InternalClassTransformationImpl.(151)
at org.apache.tapestry5.internal.services.ComponentClassTransformerImpl.transformComponentClass(163)
at $ComponentClassTransformer_1249f7f3976.transformComponentClass(...)
at org.apache.tapestry5.internal.services.ComponentInstantiatorSourceImpl.onLoad(205)
at javassist.Loader.findClass(Loader.java:340)
... 95 more



This all very strange, because when deployed to Google AppEgine there is no such errors, and this is very similiar to problem with a Security Manager I wrote about in a previous post.

To fix it I had to not specify javaagent on JVM start, but to avoid that "Unable to locate the App Engine agent" exception, I created the following class:


package com.google.appengine.tools.development.agent;

public class AppEngineDevAgent {

public static Object getAgent()
{
return null;
}

}


and putted it to my project:



Now I can run my project without javaagent. Please note, that you may still need to apply patch to Security Manager (its compatible with 1.2.2-6 GAE SDKs) to avoid other potential issues developing with GAE SDK.

Its also okay to deploy your app with this class to GAE, since it won't conflict with GAE environment.

Tuesday, August 25, 2009

Turn Java Security Manager Off In Google App Engine SDK

For some reasons you might want to turn Java Security Management off for Google Appengine Development Environment.

I need this for properly handling of Tapestry5 error page. With Java SecurityManager enabled it produced the following errors:


Could not initialize class org.apache.tapestry5.corelib.components.Loop

java.lang.NoClassDefFoundError: Could not initialize class
org.apache.tapestry5.corelib.components.Loop

java.lang.NoClassDefFoundError:
org/apache/tapestry5/corelib/components/Loop$1


To turn it off, you should replace two classes in SDK, that are responsible to installing Security Manager.

  1. Close all Eclipse instances

  2. Locate appengine-tools-api.jar: %YOUR_ECLIPSE_FOLDER%\plugins\com.google.appengine.eclipse.sdkbundle_1.2.2.v200907291526\appengine-java-sdk-1.2.2\lib\appengine-tools-api.jar

  3. Replace original classes com\google\appengine\tools\development\DevAppServerFactory$CustomSecurityManager.class and com\google\apphosting\utils\security\SecurityManagerInstaller.class with ones provided below (you may want to do a backup first)



Thats all. Now after starting App Engine project in eclipse you should see the following output in console:


Skip install SecurityManager
Create dummy CustomSecurityManager
The server is running at http://localhost:8080/


Just keep in mind that original App Engine cloud still have those Security Managers installed.

FYI here is the sources of those classes:

DevAppServerFactory.java
package com.google.appengine.tools.development;

import java.security.Permission;

public class DevAppServerFactory {

public static class CustomSecurityManager extends SecurityManager {

@Override
public void checkPermission(Permission perm) {
// Do nothing
}

public CustomSecurityManager(DevAppServer devAppServer) {
System.out.println("Create dummy CustomSecurityManager");
}

}

}


SecurityManagerInstaller.java
package com.google.apphosting.utils.security;

import java.net.URL;

public class SecurityManagerInstaller {

public static void install(URL... urls) {
System.out.println("Skip install SecurityManager");
}

}


Download Google App Engine/Java 1.2.2 Security Manager patch here (3 KB)

Friday, August 07, 2009

.Net Collections Lookup Performance



using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace TechNoir.NorCal.BL.Tests
{
[TestFixture]
public class PerformanceTests
{
[Test]
public void CompareLookupSpeed()
{
bool warmUp = true;

for (int j = 1; j <= 10; j++)
{
int n = j * 1000;

Console.WriteLine("\nTest {0}; n = {1}\n", j, n);

List<int> list;
Dictionary<int, object> dictionary;
List<int> sortedList;
IQueryable<int> queryable;

PrepareTestData(n,
out list, out dictionary, out sortedList, out queryable);


DateTime start = DateTime.Now;
foreach (int item in list)
{
Assert.That(dictionary.ContainsKey(item));
}
DateTime end = DateTime.Now;

Console.WriteLine("IDictionary.ContainsKey : "
+ (end - start).TotalMilliseconds);


start = DateTime.Now;
foreach (int item in list)
{
Assert.That(sortedList.BinarySearch(item) != -1);
}
end = DateTime.Now;

Console.WriteLine("List.BinarySearch : "
+ (end - start).TotalMilliseconds);


start = DateTime.Now;
foreach (var item in list)
{
Assert.That(list.Contains(item));
}
end = DateTime.Now;

Console.WriteLine("List.Contains : "
+ (end - start).TotalMilliseconds);


start = DateTime.Now;
if (j <= 3)
{
foreach (int item in list)
{
int localItem = item;
Assert.That(queryable.Any(i => i == localItem));
}
}
end = DateTime.Now;

Console.WriteLine("IQueryable.Any : "
+ (end - start).TotalMilliseconds);


if (warmUp)
{
warmUp = false;
j--;
}

}
}

private static void PrepareTestData(
int n,
out List<int> list,
out Dictionary<int, object> dictionary,
out List<int> sortedList,
out IQueryable<int> queryable)
{
list = new List<int>();

var rand = new Random(42);

for (int i = 0; i < n; i++)
{
int x;
do
{
x = rand.Next(int.MaxValue);
} while (list.Contains(x));

list.Add(x);
}

queryable = list.AsQueryable();

dictionary = new Dictionary<int, object>();

foreach (var item in list)
{
dictionary.Add(item, null);
}


sortedList = new List<int>();
sortedList.AddRange(list);

sortedList.Sort();
}
}
}

Monday, August 03, 2009

Tuesday, July 28, 2009

Fix msysgit install

You may get the following error during commit in git:


0 [main] us 0 init_cheap: VirtualAlloc pointer is null, Win32 error 487 AllocationBase 0x0, BaseAddress 0x30540000, RegionSize 0x240000, State 0x10000 C:\dev\bin\git\bin\sh.exe: *** Couldn't reserve space for cygwin's heap, Win32 error 0


To fix this try the following:


  1. Download http://repo.or.cz/w/msysgit.git?a=blob_plain;f=bin/rebase.exe;hb=full (~500 KB)

  2. Copy this file to the \git\bin folder

  3. Ensure that no Git related programs are open

  4. Open a command prompt and navigate to \git\bin

  5. Execute rebase.exe -b 0x40000000 msys-1.0.dll



This should fix up the problem.

See Issue 180 on msysgit for details.

Friday, July 17, 2009

Есть вещи, которые никогда не меняются

Например, диалог установки шрифтов в Windows, который достался Vista от версии Windows 3.1.



Еще ссылки по теме:

Thursday, July 16, 2009

Host WCF service in Windows service

Hosting WCF service in windows service may cause "Access is denied" error:


Service cannot be started. System.ServiceModel.AddressAccessDeniedException: HTTP could not register URL http://+:8888/docstore/. Your process does not have access rights to this namespace (see http://go.microsoft.com/fwlink/?LinkId=70353 for details). ---> System.Net.HttpListenerException: Access is denied
at System.Net.HttpListener.AddAll()
at System.Net.HttpListener.Start()
at System.ServiceModel.Channels.SharedHttpTransportManager.OnOpen()
--- End of inner exception stack trace ---
at System.ServiceModel.Channels.SharedHttpTransportManager.OnOpen()
at System.ServiceModel.Channels.TransportManager.Open(TransportChannelListener channelListener)
at System.ServiceModel.Channels.TransportManagerContainer.Open(SelectTransportManagersCallback selectTransportManagerCallback)
at System.ServiceModel.Channels.TransportChannelListener.OnOpen(TimeSpan timeout)
at System.ServiceModel.Channels.HttpChannelListener.OnOpen(TimeSpan timeout)
at System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan t...

For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.


MSDN briefs the light on how to configure such service.

For short, on Windows 2003 you have to execute the following command:

httpcfg set urlacl /u {http://URL:Port/ | https://URL:Port/} /aACL


I had several problems with this command:


  1. No httpcfg installed. It shipped with Win2003 server, but may not be installed by default. This link explains how you can obtain it if you're Win2003/XP user.

  2. Value for the /u argument. In my case I just copied the URL I got in stack trace above: http://+:8888/docstore/.

  3. Value for the /a argument. It takes a string that contains an Access Control List (ACL) in the form of a Security Descriptor Definition Language (SDDL) string.



If you look at SDDL string for first time you can harm your brain, here is an example from MSDN:


"O:DAG:DAD:(A;;RPWPCCDCLCRCWOWDSDSW;;;SY)
(A;;RPWPCCDCLCRCWOWDSDSW;;;DA)
(OA;;CCDC;bf967aba-0de6-11d0-a285-00aa003049e2;;AO)
(OA;;CCDC;bf967a9c-0de6-11d0-a285-00aa003049e2;;AO)
(OA;;CCDC;6da8a4ff-0e52-11d0-a286-00aa003049e2;;AO)
(OA;;CCDC;bf967aa8-0de6-11d0-a285-00aa003049e2;;PO)
(A;;RPLCRC;;;AU)S:(AU;SAFA;WDWOSDWPCCDCSW;;;WD)"


I've found a tool by Dominick Baier (HttpCfg ACL Helper) that can help you to build an ACL string. You can found its description and link to sources on Dominick's blog.

If you don't have VS at hand you can download HttpCfg ACL Helper binary here (3 KB).

When you run it it will prompt to add user account under which you will run the windows service and in the output it will build the command for you (thank you Dominick!):



Monday, July 13, 2009

Wednesday, July 08, 2009

Повышение качества изображения при переводе из PDF в JPEG

Если использовать пример из предыдущего поста для перевода PDF в JPEG, можно заметить потерю качества изображения, особенно если оно сильно уменьшается в размерах, например, при создании thumbnails:





Слева результат работы Ghostscript (скачать файл Gordon_Moore_1965_Article.pdf, 820 КБ):

c:\Temp>"c:\Program Files\gs\gs8.64\bin\gswin32c.exe" -dSAFER -dBATCH -dNOPAUSE -sDEVICE=jpeg -r30 -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dMaxStripSize=8192 -sOutputFile=page-%d.jpg Gordon_Moore_1965_Article.pdf

Справа - ImageMagick, набора программ для обработки изображений:

c:\Temp>"c:\Program Files\ImageMagick-6.5.4-Q16\convert.exe" -resize 255x330 Gordon_Moore_1965_Article.pdf output.jpg

Можно заметить, что ImageMagick справился с задачей лучше (на картинке сверху контуры изображения остались более четкими, а сетка на графике точно повторяет исходное расположение как в PDF).

Согласно документации, для перевода PDF файлов в графический формат, ImageMagick использует Ghostscript. Чтобы понять откуда разница в качестве посмотрим, с какими параметрами ImageMagick вызывает Ghostscript (я на время переименовал папку "c:\Program Files\gs", чтобы ImageMagick не смог найти Ghostscript):

c:\Temp>"c:\Program Files\ImageMagick-6.5.4-Q16\convert.exe" -resize 255x330 Gordon_Moore_1965_Article.pdf output.jpg
convert.exe: `%s': %s "C:/Program Files/gs/gs8.64/bin/gswin32c.exe" -q -dQUIET -dPARANOIDSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dEPSCrop -dAlignToPixels=0 -dGridFitTT=0 "-sDEVICE=bmpsep8" -dTextAlphaBits=4 -dGraphicsAlphaBits=4 "-r72x72" -dUseCIEColor "-sOutputFile=C:/Users/DMITRY~1/AppData/Local/Temp/magick-zgPdpP6n" "-fC:/Users/DMITRY~1/AppData/Local/Temp/magick-xyf_86Fy" "-fC:/Users/DMITRY~1/AppData/Local/Temp/magick-ETtA_r9y" @ utility.c/SystemCommand/1880.
convert.exe: Postscript delegate failed `Gordon_Moore_1965_Article.pdf': No such file or directory @ pdf.c/ReadPDFImage/611.
convert.exe: missing an image filename `output.jpg' @ convert.c/ConvertImageCommand/2772.


Помимо прочих параметров, основным отличием является использование специфического устройства вывода (-sDEVICE=bmpsep8). На основании входного файла ImageMagick определяет промежуточный формат, в который он должен перевести содержимое PDF без потери качества. Это может быть RAW-формат (например, pnmraw) или, как в примере, bmpsep8. Дальнейшие трансформации (например, изменение размера) ImageMagick производит на этом временном файле.

Известно, что RAW/BMP файлы имеют большой размер (пропорционально размеру канвы изображения), и даже если на выходе мы хотим получить файл размером 255x300, как в примере, промежуточный файл может быть гигантских размеров. Поэтому нужно быть осторожным при использовании такого метода конвертации, особенно если вы заведомо не знаете о содержимом ваших PDF-файлов.


Так, например, для PDF в 544 КБ (скачать можно здесь), размер промежуточного файла ImageMagick составляет 1,6 ГБ (!). На выходе получается JPEG на 8 КБ, при этом процедура конвертации занимает около 5 минут. Та же самая процедура с использованием Ghostscript + JPEG device занимает меньше секунды при размере выходного файла 4 КБ, но подбирать параметры масштабирования в данном случае приходится вручную. Результат представлен ниже (слева Ghostscript, справа - ImageMagick), о качестве судите сами.


GhostscriptImageMagick



При тестировании использовались Ghostscript 8.64 и ImageMagick 6.5.4-2-Q16.

P.S.
Список поддерживаемых Ghostscript devices можно найти здесь: http://www.gnu.org/software/ghostscript/devices.html.

В одном из следующих постов я расскажу как можно организовать пакетную печать PDF-документов в Windows, используя Ghostscript и устройство mswinpr2.