Saturday, May 23, 2020

The given CanonicalName is not valid ShellObject.Properties.GetProperty Microsoft Windows API Code Pack


If you are getting "The given CanonicalName is not valid" while using Microsoft Windows API Code Pack to get metadata about a image. Look no further, I have been racking my brains, over how to get this to work.


You probably have tried then following attempts that yielded the error. In this example, it's just trying to extract MIMEType, a field always populated and chosen for testing purposes.

This first attempt to get MimeType1 method uses the proper string signature to extract the MIMEType property, but you get the "The given CanonicalName is not valid" error. The second attempt also fails.

var MimeType1 = picture.Properties.GetProperty("SystemProperties.System.MIMEType").ToString(); //properly specified
var MimeType2 = picture.Properties.GetProperty("MIMEType"); //random attempt

So what is going on here? 

This method which uses the PropertyKey type and works, and you think you could just get the string of this.


var MimeType1 = picture.Properties.GetProperty(SystemProperties.System.MIMEType).ToString(); //properly specified

But SystemProperties.System.MIMEType has no .CanonicalName property to use, or any named property and toString() will not work.

But it turns out that the documentation for ShellObject.Properties.GetProperty() is very hard to find! I can get valid types here

So, you have to look at the source code of the Microsoft Windows API Code Pack

And from there, you get that GetProperty is really a native win32 call to propsys.dll


        //PS refer to Propery Store - http://www.pinvoke.net/default.aspx/Interfaces/IPropertyStore.html
        [DllImport("propsys.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int PSGetPropertyKeyFromName(
            [In, MarshalAs(UnmanagedType.LPWStr)] string pszCanonicalName,
            out PropertyKey propkey
        );


And at leas there some documentation of this call - https://docs.microsoft.com/en-us/windows/win32/api/propsys/nf-propsys-psgetnamefrompropertykey

From here it gives you a very vague idea of what to do.


Solution:

It turns out that you cannot use what you would naturally think;


"SystemProperties.System.MIMEType" must be "System.MIMEType"
"SystemProperties.System.Photo.CameraModel" must be "System.Photo.CameraModel"

The metadata image, video, music properties are all listed here to extract

https://docs.microsoft.com/en-us/windows/win32/properties/props

You may ask why use the string name, this is the only way to iterate over the collection, see my post here.



Wednesday, May 20, 2020

CSharp - Human readable video bit rate (bits per second) format


Format video bit rate measured in 
bits per second into "human readable" format, meaning that number is formatted as specified by metric SI convention using E,P,T,G,M,k prefix units.

"12344".ToBPSReadableFormat() outputs 12.3kbps

Quoting https://en.wikipedia.org/wiki/Metric_prefix for SI units; 
exaE 10181000000000000000000 quintillion
petaP 10151000000000000000 quadrillion
teraT 10121000000000000 trillion
gigaG 1091000000000 billion
megaM 1061000000 million
kilok 1031000 thousand
This string extension method will format to bps to 1 decimal place. 

Update June 1, 2020:  You can combine this with fast integer extraction 
https://metadataconsulting.blogspot.com/2020/06/CSharp-Get-a-number-or-integer-from-a-string-fast-a-speed-comparison.html


        /// <summary>
        /// Return bits per second formatted w/ metric SI units & to 1 decimal place - https://en.wikipedia.org/wiki/Metric_prefix
        /// </summary>
        /// <param name="numlong"></param>
        /// <returns></returns>
        public static string ToBPSReadableFormat(this string numlong)
        {
            string unit;
            double d;

            long l = 0; //reqr'd -> cannot do a bitwise shift on a double
            long.TryParse(numlong, out l);
            
            if (l==0) return "0bps";

            long abs = (l < 0 ? -l : l);   //absolute value, we could use ulong if you know you will not get negative values
            
            if (abs >= 0x1000000000000000) // Exabyte
            {
                unit = "Ebps";
                d = (l >> 50);             
                d = (d / 1000); 
            }
            else if (abs >= 0x4000000000000) // Petabyte
            {
                unit = "Pbps";
                d = (l >> 40);
                d = (d / 1000); // Divide by 1000 to get fractional value
            }
            else if (abs >= 0x10000000000) // Terabyte
            {
                unit = "Tbps";
                d = (l >> 30);
                d = (d / 1000);
            }
            else if (abs >= 0x40000000) // Gigabyte
            {
                unit = "Gbps";
                d = (l >> 20);
                d = (d / 1000);
            }
            else if (abs >= 0x100000) // Megabyte
            {
                unit = "Mbps";
                d = (l >> 10);
                d = (d / 1000);
            }
            else if (abs >= 0x400) // Kilobyte
            {
                unit = "kbps";
                d = l;
                d = (d / 1000);
            }
            else
            {
                unit = "bps";
                d = l;
            }
          
            return string.Concat(d.ToString("0.#"),unit);
        }

C# Get the number of pages in a PDF document from metadata without load entire file


A popular solution is to use pdfinfo  and pump it through a command line to get the number of pages in a PDF. However, if you examine the source code, https://dl.xpdfreader.com/xpdf-4.02.tar.gz you'll see that this reads the entire PDF to determine the page count. Therefore, the load time will be indeterminate, esp. slow for large files.


Here's a way to read the page count of a PDF using the metadata in a fixed amount of time.
BUT BE ADVISED, this only works for some versions of PDF encoding versions. It does not guarantee a returned result. For that you need to read entire file and iterate over this regex - "/\/Count\s+(\d+)/".

However, this works in a preset amount of time 10ms, since it reads only the first 32,767 bytes of the file. It then matches some possible patterns for the page count. Most common are matched first. 

Please add more in the comments section. You can find patterns by inspect the PDF file for patterns.

You can inspect PDF files using 
Frhed a free hex/binary editor for Windows, that will open a PDF file, and you can view the metadata for youself. Or test drive my Clipboard Plaintext Power Tool which has a custom version of Frhed built in.

const string strRegexNT = @"\/N\s*(\d*)\s*\/[T|O]\s"; //Seems to be most common found and reliable
private static readonly Regex rgxNT = new Regex(strRegexNT, RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

const string strRegexPRT = @"\/Pages\s*(\d*).*R.*\/T";
private static readonly Regex rgxPRT = new Regex(strRegexPRT, RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); 

const string strRegexTPC = @"\<\<\/Type\/Pages\/Count\s*(\d*)\s*\/Kids";
private static readonly Regex rgxTPC = new Regex(strRegexTPC, RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

const string strRegexEndObj = @"endobj\s*7\s*0\s*obj\s*\<\<\s*\/Count\s*(\d*)\s*\/";
private static readonly Regex rgxEndObj = new Regex(strRegexEndObj, RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);

/// <summary>
/// Gets number of PDF pages reading only 1st 32767 bytes in 10ms, should cover most cases
/// https://metadataconsulting.blogspot.com/2020/05/C-Get-the-number-of-pages-in-a-PDF-document-from-metadata-without-load-entire-file.html
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string GetNumofPdfPages(string fileName)
{
    string o = string.Empty;
    string head = string.Empty;
    Match m;

    try //for Openread
    {
        using (BinaryReader br = new BinaryReader(File.OpenRead(fileName)))
        {
            head = Encoding.UTF8.GetString(br.ReadBytes(Int16.MaxValue)); //32767
        }
    }
    catch { }
    
    if (!string.IsNullOrEmpty(head))
    {

        m = rgxNT.Match(head);
        if (m.Groups.Count == 2)
            o = m.Groups[1].Value;

        if (string.IsNullOrEmpty(o))
        {

            m = rgxPRT.Match(head);
            if (m.Groups.Count == 2)
                o = m.Groups[1].Value;

        }

        if (string.IsNullOrEmpty(o))
        {

            m = rgxTPC.Match(head);
            if (m.Groups.Count == 2)
                o = m.Groups[1].Value;

        }
       

        if (string.IsNullOrEmpty(o)) {

            m = rgxEndObj.Match(head);
            if (m.Groups.Count == 2)
                o = m.Groups[1].Value;

        }
    
    }
    return o;
}