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.

Tuesday, June 02, 2009

Установка поискового плагина Яндекс.Lingvo в Firefox

В предыдущих версиях Firefox было такое поисковое дополнение, которое позволяло искать перевод слов в словарях Яндекс.Lingvo.

Никак не мог найти его для версии Firefox 3.0 и сегодня случайно наткнулся.

Чтобы установить этот плагин нужно зайти на страницу "Яндекс.Словари" и щелкнуть на выпадающий список поисковых плагинов:



В этом списке должно появиться дополнение "Добавить Яндекс.Словари. Перевод".

Добавляйте и пользуйтесь.


Эти пункты добавились из-за того, что в HTML-коде страницы есть такие строчки:


<link rel="search" href="/opensearch-meaning.xml" 
title="Яндекс.Словари. Энциклопедии"
type="application/opensearchdescription+xml">

<link rel="search" href="/opensearch-spelling.xml"
title="Яндекс.Словари. Русский язык"
type="application/opensearchdescription+xml">

<link rel="search" href="/opensearch-translation.xml"
title="Яндекс.Словари. Перевод"
type="application/opensearchdescription+xml">


Оказывается есть такая спецификация (OpenSearch description document), которая позволяет описывать поисковые плагины и которую умеют распознавать браузеры.

Например, поиск перевода на Яндекс.Lingvo в формате OpenSearch выглядит следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Яндекс.Словари. Перевод</ShortName>
<Description>Поиск по словарям русского языка: этимология, синонимы, ударения, орфография.</Description>
<Image width="16" height="16" type="image/x-icon">http://slovari.yandex.ru/favicon.ico</Image>
<Url type="text/html" template="http://lingvo.yandex.ru/search.xml?text={searchTerms}&amp;st_translate=on"/>
<InputEncoding>UTF-8</InputEncoding>
</OpenSearchDescription>



Достаточно просто.

Так же можно реализовать поисковый плагин для своего сайта, и я думаю Юра прикрутит такой к планете? :)

Thursday, May 14, 2009

ИТ-Весна 2009


Не знаю что меня завлекло на этот форум. Наверное то, что в программе были заявлены разговоры об эффективности ИТ и о развитии Open Source в России.

Основная цель конгресса, учитывая сложную экономическую ситуацию, сложившуюся сейчас в мире, - рассказать делегатам о возможностях оптимизации и снижения затрат их компаний с помощью использования новейших информационных технологий.


Дело даже не в том, что я целый час искал нужный зал, потому что единственный указатель (нужно было очень постараться, чтобы его найти) указывал не туда. Поэтому, кстати, я пропустил доклад Криса ДиБона из Google.

Уже традиционно организаторы пригласили выступить на Форуме мировых гуру в области Open Source. Сейчас предложение представить ключевые доклады в рамках программы Форума рассматривают Брайен Фицжеральд, автор концепции OSS 2.0, и Мигель де Иказа, основатель проектов GNOME и Mono.


Возможно я не туда попал, но ни в программе конференции, ни очно, этих людей не было. А жаль, 50% из-за них я поехал.

Зато пока искал Синий зал второго павильона, по ходу посмотрел выставки литейщиков и связистов. Это единственное положительное впечатление. Нашел там вот такой вот экспонат:



По поводу участников. Не знаю как можно было приглашать за один круглый стол кодеров Open Source'овиков, евангелистов/менеджеров из Microsoft, депутата госдумы и замдиректора департамента по стратегии построения информационного общества министерства связям и массовым коммуникациям РФ... Каждый из них преследует свои цели, может быть поэтому конструктивного разговора не получилось.

Забегая вперед отмечу, что единственным адекватным человеком, на мой взгляд, оказался Дмитрий Комиссаров (советник гендиректора компании АйТи и гендиректор PingWin Software), который более менее реально смотрел на вещи и отдавал себе отчет в происходящем.

Отдельно порадовал Григорий Лабзовский из Sun Microsystems, который вроде как был под кайфом?.. или просто так сильно волновался, что постоянно всему удивлялся, потом сразу же улыбался самому себе, и мгновенно приходил в себя :) Позитивный молодой человек.

Было две больших темы. Первая - Open Source в России, который почему-то (по введенной нашими властями терминологии) называли СПО (свободное программное обеспечение). Майкрософт (генеральный спонсор форума), как обычно, не упустил возможности попиариться.

По вопросу Open Source мне понравилось только одно высказывание единственного в числе участников CIO - Бориса Славина (Союз ИТ-Директоров): "Для меня, как ИТ-директора, нет разницы - являются ли решения, Open Source или проприетарными. В любом случае придется платить. Главное, - предоставить возможность выбора. И чем разнообразнее он будет - тем лучше."

Вторая тема - о создании национальной программной платформы. Эта тема горячо обсуждалась около полугода назад в ЖЖ и на it-blogs.

Ключевой доклад представлял Илья Пономарев (депутат гос думы). Выступая с предложением о создании национальной ОС, он начал свое выступление с заметки, что во время войны в Абхазии из Google Maps исчезли карты региона военных действий, и что это не единственный пример. Например, в других войнах переставали работать GPS... Видимо это его испугало, или стало обидно, что Google может выключить свои карты, когда захочет, а нам даже выключить нечего. Или что, наши военные ориентируются на карты гугл в военных действиях? Не знаю к чему он это, но мысль о создании собственной российской ОС он представил именно с этих позиций.

Если честно я даже не понял, почему от идеи национальной программной платформы перешли к разговору об ОС. Насколько я понял, суть создания национальной программной платформы, со слов Пономарева и Иванова, была в том, чтобы поддержать российскую ИТ-индустрию. И проект с ОС должен, по мнению Пономарева, дать толчок в этом направлении. Не понятно только почему именно ОС нужно создавать?

Хотя у меня есть одно соображение по этому поводу. Могло случиться так, что возникла путаница в использовании термина СПО, который у ИТ-специалистов расшифровывается как Системное Программное Обеспечение (то есть как раз-таки Операционная система и сопутствующие компоненты), но в высших кругах в правительстве при обсуждении этой темы под СПО понимают Свободное Программное Обеспечение (см. выше), основным представителем которого они видят Linux'ы.

Сам Пономарев отмечал, что не столько принципиально то, _что_ мы будем разрабатывать (и будем ли разрабатывать с нуля или возьмем какой-нибудь дистрибутив линукса за основу), сколько сам факт того, что _мы_ будем что-то разрабатывать, поднимая тем самым культуру разработки ПО в России. С другой стороны, он сказал, что создаваемым решением хочет конкурировать на рынке ОС в России с Майкрософт...

Вообще в речи Пономарева и особенно Сергея Иванова (зам из министерства) было много противоречий. Сложилось впечатление, что Иванов хорошо разбирается в гос. управлении, но в ИТ понимает очень слабо.

Обсуждая тему национальной ОС, несколько раз поднимали вопрос о целесообразности ее создания. Положительного ответа так и не прозвучало, если не считать ремарку Пономарева о Google Maps и гос. безопасности... смех. Разве карты гугл выключили потому что гугл работает на Windows?

Разве у нас в стране вообще есть проблемы с операционными системами? Взять хотя бы гос. структуры: разве был хоть один гос. тендер о поставке операционных систем? Зато вот тендеры на создание прикладных ИТ-решений - обычное дело. Так может быть лучше в этом направлении искать пути развития ИТ-индустрии и создании национальной программной платформы?

Посмотрите, например, как обстоит дело с бухгалтерией. Это единственная сфера деятельности любой компании, в которой можно наблюдать высокий уровень использования ИТ. По этому поводу у нас даже есть своя национальная платформа - 1С. И, кстати, она прекрасно работает на разных ОС (Windows/Linux) и с разными СУБД (MS SQL Server/Postgresql/...).

Я думаю, если поощрять (или даже навязывать) использование ИТ (электронных документов, и EDI вообще) хотя бы для начала в госструктурах, то начнут появляться и национальные решения a-la 1С, которые будут решать поставленные задачи. (Хотя, надо отметить, что такие движения уже есть, например, связанные с электронным рынком торговли электроэнергией.)

Важно отметить, что решения западных вендоров здесь не будут конкурентами, потому что они и так слабо вписываются в специфику деятельности отечественных компаний.

У меня так и не получилось задать этот вопрос Пономареву - закончилось время форума - но очень хотелось услышать его мнение по поводу создания национальной программной платформы, начиная не с ОС, а со специфических решений для госсектора.

P.S.
Кстати, только что узнал, что Пономарев ведет ЖЖ (http://ilya-ponomarev.livejournal.com), может быть он ответит здесь или напишет свои мысли у себя в журнале.

Wednesday, April 29, 2009

Использование Ghostscript для перевода PDF в JPEG

В одном из последних проектов я занимался пакетной обработкой pdf-документов: слияние (merge) документов и распознание 1D штрих-кодов (barcode recognition).

Хочу поделиться интересными, на мой взгляд, особенностями и ссылками на Open Source-проекты.

С первой задачей (merge документов) отлично справился PDFsharp - Open Source-проект со свободной лицензией и с достаточно простым и удобным API (.Net).

Со штрих-кодами оказалось немного сложнее. Дело в том, что основной формат документов для "читалок" - файлы изображений (jpeg, png, tiff и т.д.), а перевести PDF в картинку оказалось не так просто...

Мы решили эту задачу с помощью Ghostscript - Open Source (GPL License) интерпретатор языка PostScript и пакет для работы с PDF.

Вообще говоря, перевести PDF в картинку, используя Ghostscript, можно одной командой, примерно так:


c:\Temp>c:\dev\bin\gs\gs8.64\bin\gswin32c.exe -q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=jpeg -r150 -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dMaxStripSize=8192 -sOutputFile=page-%d.jpg mydocument.pdf


На выходе должно быть несколько файлов (по одному на страницу): в данном случае page-1.jpg, page-2.jpg и т.д.

Проблемы начинаются, если PDF-документ содержит экзотические шрифты (например, IDAutomationHC39M.ttf - шрифт штрих-кода Code39), или если документ был создан при помощи MS Reporting Services или другой программы, которая по тем или иным причинам не включает используемые шрифты в выходной PDF-файл (PDF fonts embedding).

Наш PDF был именно такой:



В результате во время рендеринга у меня возникла такая ошибка:


Error: /undefined in findresource
Operand stack:
--nostringval-- --dict:7/16(L)-- F14 10.0 --dict:5/5(L)-- --dict:5/5(L)-- TimesNewRoman,Bold --dict:11/12(ro)(G)-- --nostringval-- CIDFontObject --dict:6/6(L)-- --dict:6/6(L)-- Adobe-Identity
Execution stack:
%interp_exit .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- --nostringval-- --nostringval-- false 1 %stopped_push 1862 1 3 %oparray_pop 1861 1 3 %oparray_pop 1845 1 3 %oparray_pop --nostringval-- --nostringval-- 3 1 2 --nostringval-- %for_pos_int_continue --nostringval-- --nostringval-- --nostringval-- --nostringval-- %array_continue --nostringval-- false 1 %stopped_push --nostringval-- %loop_continue --nostringval-- --nostringval-- --nostringval-- --nostringval-- --nostringval-- --nostringval-- %array_continue --nostringval-- --nostringval-- --nostringval-- --nostringval-- --nostringval-- %loop_continue --nostringval-- --nostringval-- --nostringval-- %loop_continue
Dictionary stack:
--dict:1156/1684(ro)(G)-- --dict:1/20(G)-- --dict:74/200(L)-- --dict:74/200(L)-- --dict:106/127(ro)(G)-- --dict:278/300(ro)(G)-- --dict:22/25(L)-- --dict:4/6(L)-- --dict:21/40(L)-- --dict:10/13(L)--
Current allocation mode is local
Last OS error: No such file or directory
GPL Ghostscript 8.64: Unrecoverable error, exit code 1


Ошибка связана с тем, что у меня на компьютере нет шрифта TimesNewRoman,Bold.

В Интернете все упоминания о подобных ошибках связаны с использованием японского или корейского шрифтов, но у меня в документе находится обычный английский текст.

Обратите также внимание на кодировку шрифта: Identity-H. Из того, что мне удалось выяснить, эта кодировка говорит о том, что используемый шрифт, вообще говоря, не существует, и в документе используются только коды символов. Для отображения символов, соответствующих этим кодам Ghostscript'у нужно "подсунуть" шрифт с аналогичной таблицей символов. Как это сделать описано в документации Ghostscript (см. CID Font Substitution).

В кратце, в файл c:\dev\bin\gs\gs.8.64\lib\cidfmap (этот файл не будет создан, если при установке вы не отметили галочку "Use Windows TrueType fonts for Chinese, Japanese and Korean"; в этом случае создайте его сами) нужно добавить инструкцию, которая будет указывать, какой шрифт использовать на самом деле вместо того, который указан в PDF.

У меня заняло некоторое время, чтобы подобрать параметры для этой инструкции. Дело в том, что нельзя заменить шрифт на любой другой (например, я не могу заменить мой английский шрифт на японский или корейский). Кроме как в исходниках я нигде больше не смог найти, что нужно подставить на место /CSI Ordering. В итоге получилась такая строчка:


/TimesNewRoman,Bold << /FileType /TrueType /Path (timesbd.ttf) /SubfontID 0 /CSI [(Unicode) 0] >> ;


Файл timesbd.ttf я взял из папки C:\Windows\fonts. Есть некоторые особенности использования абсолютных и относительных путей файла с использованием опции -dSAFER. Если указана эта опция, то путь к имени файла шрифта должен быть относительным. Поэтому я скопировал его в папку c:\dev\bin\gs\fonts (в моем случае именно она используется для поиска шрифтов по умолчанию).

Чтобы посмотреть детальные логи работы Ghostscript, нужно убрать опцию -q, и вот что мы увидим:


GPL Ghostscript 8.64 (2009-02-03)
Copyright (C) 2009 Artifex Software, Inc. All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
Processing pages 1 through 2.
Page 1
Substituting font Helvetica for IDAutomationHC39M.
Loading NimbusSanL-Regu font from %rom%Resource/Font/NimbusSanL-Regu... 2875432 1123207 25112624 23807796 3 done.
Loading NimbusRomNo9L-Regu font from %rom%Resource/Font/NimbusRomNo9L-Regu... 2912216 1253685 25112624 23819376 3 done.
Loading NimbusRomNo9L-Medi font from %rom%Resource/Font/NimbusRomNo9L-Medi... 3069576 1402046 25132720 23832802 3 done.
Page 2
Substituting font Helvetica for IDAutomationHC39M.
Loading NimbusSanL-Bold font from %rom%Resource/Font/NimbusSanL-Bold... 3166648 1539642 25152816 23854836 3 done.
Loading a TT font from C:\dev\bin\gs\fonts/timesbd.ttf to emulate a CID font TimesNewRoman,Bold ... Done.


Ошибка пропала, но при рендеринге штрих-кода вместо "правильного" шрифта использовался Helvetica. Это связано с тем, что Ghostscript не может использовать системные шрифты по-умолчанию. Эту проблему я решил копированием файлов шрифтов (IDAutomationHC39M.ttf и IDAutomationHC39M_0.ttf) в папку C:\dev\bin\gs\fonts и указанием ключика -sFONTPATH="C:\dev\bin\gs\fonts".

В результате получаем:


c:\Temp>c:\dev\bin\gs\gs8.64\bin\gswin32c.exe -sFONTPATH="C:\dev\bin\gs\fonts" -dSAFER -dBATCH -dNOPAUSE -sDEVICE=jpeg -r150 -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dMaxStripSize=8192 -sOutputFile=page-%d.jpg mydocument.pdf

GPL Ghostscript 8.64 (2009-02-03)
Copyright (C) 2009 Artifex Software, Inc. All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
Processing pages 1 through 2.
Page 1
Scanning c:\dev\bin\gs\fonts for fonts... 3 files, 3 scanned, 2 new fonts.
Loading IDAutomationHC39M font from c:\dev\bin\gs\fonts/IDAutomationHC39M.ttf... 2868992 1068452 25189416 23882410 3 done.

Loading NimbusRomNo9L-Regu font from %rom%Resource/Font/NimbusRomNo9L-Regu... 2885680 1201118 25209512 23896006 3 done.
Loading NimbusRomNo9L-Medi font from %rom%Resource/Font/NimbusRomNo9L-Medi... 3022944 1346357 25209512 23905928 3 done.
Loading NimbusSanL-Regu font from %rom%Resource/Font/NimbusSanL-Regu... 3120016 1438677 25531048 24170429 3 done.
Page 2
Loading IDAutomationHC39M font from c:\dev\bin\gs\fonts/IDAutomationHC39M.ttf... 3120016 1443415 25189416 23883485 3 done.
Loading NimbusSanL-Bold font from %rom%Resource/Font/NimbusSanL-Bold... 3217088 1539692 25229608 23926657 3 done.
Loading a TT font from C:\dev\bin\gs\fonts/timesbd.ttf to emulate a CID font TimesNewRoman,Bold ... Done.


Хочется отметить еще одну полезную опцию Ghostscript: -r150. Если этот параметр не указывать, то рендеринг будет происходить в мастшабе по умолчанию, в результате чего из-за маленьких размеров не распознавался штрих-код. Увеличение размера до 150 пикселей на дюйм (-r150) это исправило.

Аналогично можно также уменьшать размер картинки, например, чтобы сделать thumbnails (см. заметки по качеству перевода PDF в JPEG).