I've been working on some code in C# to parse PDF files and check for security restrictions such as the existence of user and owner passwords and settings such as printing disabled and thought I should share some notes.
PDF File Format
The PDF file format is extremely flexible which is great in some aspects but when writing a parser that supports such flexibility, it can become a challenge. For example newline sequences can vary depending on the OS or software used to create the PDF file. Different versions of the PDF specification do things in different ways and the contents of files may be encrypted or encoded in different ways making writing a complete parser a challenging prospect.
Apart from a couple of elements, PDF files are largely made up of objects. These objects can described where to find other objects in the file, text or images that appear on the page, fonts and descriptions of which objects appear on which page.
PDF files have two passwords. The owner password is required to change the document and the user password is require to open the document. If a file has restrictions such as No Printing but doesn't prompt you to enter a password when you open it in Adobe Reader, you will find that the document is encrypted using a default user password. You can find more information about this in the specification document.
Using C#
I'll include more details of how I approached implementing a PDF parser in C# in a future post but here are some general thoughts about it.
It should go without saying, but write as generic code as possible. Most elements of PDF files are Objects each of which usually has a Dictionary and a content Stream.
I ended up using FileStream to read files. I initially used a StreamReader but came across several problems. When reading a PDF file, you will need to jump around the file to various points and I found that StreamReader buffers its reads so StreamReader.BaseStream.Position and StreamReader.BaseStream.Seek may refer to one location in the file but the StreamReader read methods may read data from a different point in the file.
It is common for object streams (the content of an object) to be compressed using FlateDecode compression. This can be decompressed using the System.IO.Compression.DeflateStream class, although I found that I needed to skip the first two bytes when deflating object streams.
Checking if the file has a user password requires a combination of creating MD5 hashes and encrypting using RC4. An MD5 class is provided by .NET which works well and I was able to use an RC4 encryption/decryption class I had previously written for encryption purposes.
Resources
The best resource for information about the PDF file format is the specification document which is available on the Adobe site.
Adobe also have a very useful forum where you will most likely find that someone has already had the same problem as you.
Wednesday, 18 July 2012
Sunday, 6 November 2011
SPGridView Part 2 - Custom Filters
The SharePoint 2007 SPGridView control allows easy filtering by setting the AllowFiltering property to true, but it generates the list of possible filter options by selecting all of the distinct values for the column when the column header is clicked. In a DB table I'm using with an SPGridView I store integer values of 0 to represent unset, 1 for low, 2 for medium and 3 for high and so the available filter options are 0, 1, 2 or 3.
To allow filtering by the associated text value, I check for a callback in my WebPart's override of CreateChildControls and if the column header in question has been clicked, bind the SPGridView control to a temporary data table which has a column with the same name as the one clicked containing all of the required values:
Also in CreateChildControls, if it isn't a callback to get the available filter options, I check for an actual filter call or a clear filter call and flip the text value back to the associated id if required (storing the filter in the ViewState) using a call to the following:
Finally, override OnPreRender and set the FilterExpression of the SqlDataSource control associated with the SPGridView to the query stored in the ViewState:
To allow filtering by the associated text value, I check for a callback in my WebPart's override of CreateChildControls and if the column header in question has been clicked, bind the SPGridView control to a temporary data table which has a column with the same name as the one clicked containing all of the required values:
if (Page.IsCallback && !string.IsNullOrEmpty(Page.Request.Form["__CALLBACKPARAM"])) { string[] param = Page.Request.Form["__CALLBACKPARAM"].Split(';'); if (param[1] == myColumn) { DataTable dt = new DataTable(); dt.Columns.Add(param[1]); dt.Rows.Add("Unset"); dt.Rows.Add("Low"); dt.Rows.Add("Medium"); dt.Rows.Add("High"); m_SpGridView.DataSourceID = null; m_SpGridView.DataSource = dt; m_SpGridView.DataBind(); } }
Also in CreateChildControls, if it isn't a callback to get the available filter options, I check for an actual filter call or a clear filter call and flip the text value back to the associated id if required (storing the filter in the ViewState) using a call to the following:
protected virtual void CheckFilter() { // If there is a call back with an event argument and event target and the target is our gridview... if (Context.Request.Form["__EVENTARGUMENT"] != null && Context.Request.Form["__EVENTTARGET"] != null && Context.Request.Form["__EVENTTARGET"].EndsWith(m_SpGridView.ID)) { string search = "__SPGridView__;__Filter__;"; if (Context.Request.Form["__EVENTARGUMENT"].Equals("__SPGridView__;__Filter__;__ClearFilter__")) { ViewState.Remove("FilterExpression"); } else if (Context.Request.Form["__EVENTARGUMENT"].StartsWith(search)) { string[] newFilter = Context.Request.Form["__EVENTARGUMENT"].Replace(search, "").Split(';'); // If this is a custom filter field, switch the selected value for the actual value if (newFilter[0] == myColumn) { if (newFilter[1] == "Unset") newFilter[1] = "0"; else if (newFilter[1] == "Low") newFilter[1] = "1"; else if (newFilter[1] == "Medium") newFilter[1] = "2"; else if (newFilter[1] == "High") newFilter[1] = "3"; } // Set the filter in the viewstate ViewState["FilterExpression"] = string.Format("{0}='{1}'", newFilter[0], newFilter[1]); } } }
Finally, override OnPreRender and set the FilterExpression of the SqlDataSource control associated with the SPGridView to the query stored in the ViewState:
protected override void OnPreRender(EventArgs e) { m_SqlDataSource.FilterExpression = string.Empty; if (ViewState["FilterExpression"] != null) m_SqlDataSource.FilterExpression = ViewState["FilterExpression"].ToString(); base.OnPreRender(e); }
Saturday, 5 November 2011
SPGridView Part 1 - Multiple SPGridViews
There appears to be a bug clearing filters when using multiple instances of the SharePoint 2007 GridView (SPGridView) control on the same page. In my case I had two very similar custom WebParts on the same page using SPGridView controls to display data pulled from a database.
If filtering is enabled on the controls, when the user clears the filter on the second SPGridView on the screen, the filter on the first SPGridView is cleared instead.
After some searching, I found a nice solution to this problem here.
I copied the suggested solution into a class that inherits from SPGridView and used that in my WebParts to resolve the problem:
If filtering is enabled on the controls, when the user clears the filter on the second SPGridView on the screen, the filter on the first SPGridView is cleared instead.
After some searching, I found a nice solution to this problem here.
I copied the suggested solution into a class that inherits from SPGridView and used that in my WebParts to resolve the problem:
public class MySPGridView : SPGridView { // Override the OnPreRender to databind and then fix each headerrow control protected override void OnPreRender(EventArgs e) { DataBind(); if (this.HeaderRow != null) { foreach (WebControl control in this.HeaderRow.Controls) { UpdateTemplateClientID(control); } } base.OnPreRender(e); } // Fix the ClientOnClickPreMenuOpen property of a menu control private void UpdateTemplateClientID(Control control) { if (control is Microsoft.SharePoint.WebControls.Menu) { Microsoft.SharePoint.WebControls.Menu menuControl = control as Microsoft.SharePoint.WebControls.Menu; string jsFunctionCall = menuControl.ClientOnClickPreMenuOpen; menuControl.ClientOnClickPreMenuOpen = jsFunctionCall.Replace("%TEMPLATECLIENTID%", this.ClientID + "_SPGridViewFilterMenuTemplate"); } else if (control.HasControls()) { foreach (WebControl c in control.Controls) UpdateTemplateClientID(c); } } }
Friday, 26 August 2011
XBMC on the Apple TV 2
As previously mentioned, I've been playing with XBMC on the Apple TV 2.
Its worth noting that you need to finish running through the Apple TV setup (selecting language and location etc.) before you can access the device. This needs to be done after updating the firmware.
After installing the latest official build using these instructions I wanted to try the nightly builds. When attempting to update XBMC to a nightly version using the instructions on the same page, dpkg would halt on:
It turns out that because my Apple TV is running iOS 4.3, rm -rf which is used to remove the previous version when upgrading, is broken. I therefore needed to manually remove the previous version before updating using the following:
Skins can be added manually to the Apple TV 2 build of XBMC by SSH'ing to it and downloading them with wget to the addons directory and unzipping:
If using an advancedsettings.xml for custom settings, this needs to go here on the Apple TV 2:
Its worth noting that you need to finish running through the Apple TV setup (selecting language and location etc.) before you can access the device. This needs to be done after updating the firmware.
After installing the latest official build using these instructions I wanted to try the nightly builds. When attempting to update XBMC to a nightly version using the instructions on the same page, dpkg would halt on:
Preparing to replace org.xbmc.xbmc-atv2 10.0-9 ...
It turns out that because my Apple TV is running iOS 4.3, rm -rf which is used to remove the previous version when upgrading, is broken. I therefore needed to manually remove the previous version before updating using the following:
rm -r /Applications/XBMC.frappliance
Skins can be added manually to the Apple TV 2 build of XBMC by SSH'ing to it and downloading them with wget to the addons directory and unzipping:
cd /private/var/mobile/Library/Preferences/XBMC/addons/The skin can then be selected in XBMC (I'm using 'Night' at the moment).
wget <url of skin.zip>
unzip <skin zip file >
rm <skin zip file >
If using an advancedsettings.xml for custom settings, this needs to go here on the Apple TV 2:
/private/var/mobile/Library/Preferences/XBMC/userdataAnd the ever useful log file is at:
/private/var/mobile/Library/Preferences/xbmc.log
A Shared MySQL Database for XBMC
I want to use XBMC to stream media from my home server to various devices on my network. XBMC supports using a central MySQL database to store details about media such as the cast of a movie which can be accessed by any device on the network. This means that all devices are in sync, media only has to be scanned once rather than on each device and if you stop playback on one device you can resume from the same place on another device.
I followed this guide to set up the database.
I had to open port 3306 for TCP in the Vista firewall to allow devices to access to the database.
XBMC threw up some errors and I found that I had to scrap the databases I had created (xbmc_video and xbmc_music) and build them again with the following commands:
After getting it working with a XBMC 10.01 for Windows, I installed a nightly build of XBMC on an Apple TV 2 and found I had problems with the Apple TV build expecting a different database version. It looks like you need to be careful about using builds that that expect the same database version. XBMC creates the required tables automatically when scanning the library provided it can get to the DB and has access, so to rebuild the DB to a different version you just need to drop the tables (or drop and recreate the databases) and re-scan the library from XBMC.
The advancedsettings.xml file used by XBMC to enable the MySQL DB needed to go in the following locations:
The XBMC log files are really helpful in diagnosing issues like these and are in the following locations:
I followed this guide to set up the database.
I had to open port 3306 for TCP in the Vista firewall to allow devices to access to the database.
XBMC threw up some errors and I found that I had to scrap the databases I had created (xbmc_video and xbmc_music) and build them again with the following commands:
CREATE DATABASE xbmc_video CHARACTER SET latin1 COLLATE latin1_general_ci;
CREATE DATABASE xbmc_music CHARACTER SET latin1 COLLATE latin1_general_ci;
After getting it working with a XBMC 10.01 for Windows, I installed a nightly build of XBMC on an Apple TV 2 and found I had problems with the Apple TV build expecting a different database version. It looks like you need to be careful about using builds that that expect the same database version. XBMC creates the required tables automatically when scanning the library provided it can get to the DB and has access, so to rebuild the DB to a different version you just need to drop the tables (or drop and recreate the databases) and re-scan the library from XBMC.
The advancedsettings.xml file used by XBMC to enable the MySQL DB needed to go in the following locations:
Windows - C:\Users\<USERNAME>\AppData\Roaming\XBMC\userdata
Apple TV 2 - /private/var/mobile/Library/Preferences/XBMC/userdata
The XBMC log files are really helpful in diagnosing issues like these and are in the following locations:
Windows - C:\Users\<USERNAME>\AppData\Roaming\XBMC\xbmc.log
Apple TV 2 - /private/var/mobile/Library/Preferences/xbmc.log
Vista on the HP Proliant Microserver
I recently got an HP Proliant Microserver (which currently has a £100 cash back offer from HP) to store media and backups on. This is a little server box with 4 drive bays sporting a low power dual core AMD processor. I've added 4GB of DDR3 ECC RAM to give 5GB and as well as the 250GB HDD that comes with it, added a 2TB drive for storage. I had a spare copy of Vista so its currently running that while I wait for Windows Home Server 2011 to arrive.
I had a couple of issues installing Vista on it. First of all I had to change the SATA controller to IDE mode in the bios to get the installer to detect the hard drive.
Vista didn't find an ethernet driver. The machine has a Broadcom NetXtreme BCM5723 ethernet controller so I was able to use a driver from the Broadcom site.
Finally, rather than use the standard VGA driver, I used ATI Catalyst Mobility utility to pull down the appropriate graphics driver.
Vista runs a little sluggish on this machine but it should be fine for a little share sharing until WHS 2011 arrives.
I had a couple of issues installing Vista on it. First of all I had to change the SATA controller to IDE mode in the bios to get the installer to detect the hard drive.
Vista didn't find an ethernet driver. The machine has a Broadcom NetXtreme BCM5723 ethernet controller so I was able to use a driver from the Broadcom site.
Finally, rather than use the standard VGA driver, I used ATI Catalyst Mobility utility to pull down the appropriate graphics driver.
Vista runs a little sluggish on this machine but it should be fine for a little share sharing until WHS 2011 arrives.
Subscribe to:
Posts (Atom)
