Blog

Posts written in August 2019.

  • Making the transition in moving photos from physical to digital form can be quite an undertaking depending on the volume of photos you have to work with. Traditional flat-bed scanning and Photoshop combinations aren’t really up to the task if you want a process that requires minimal manual intervention. It can all be quite cumbersome, from placing the photo correctly on the scanner to then carrying out any photo enhancements, cropping and exporting. Yes, you get a fantastic digital print but it comes at a cost - time.

    If you are really serious in digitising a bulk load of photos, there are a couple viable options:

    1. Photo-scanning service where you post all the photos you wish to digitise. The costs can be relatively low (around 1p per photo) and is good if you have a specific number of photo’s to digitise.
    2. Purchase a photo scanner where photos are scanned manually in a document feeding process, which makes for a less intensive job.

    Due to the large number of photos that have accumulated over the years, I preferred to purchase a photo scanner. Sending off photos to a photo-scanning service didn’t seem viable and could prove quite costly. I also had the fear of sending over photos via post where I do not have the original negatives. They could be lost in transit or handled incorrectly by the photo-scanning service. Not a risk I was willing to take. Photos are precious memories - a snapshot of history.

    The most ideal photo scanner for a job of this undertaking needs to be sheet-fed, where the photos are fed through a scanning mechanism. There are quite a number of these type of scanners, mostly being document scanners, which isn’t the type of scanner you want. From personal experience I found document scanners lack the resolution required and the feeding mechanism can be quite rough on photos.

    I decided to go for the Plustek ePhoto Z300 as it seems to fit the bill at a really good price (at time to writing £170).

    Initial Impressions

    The Plustek scanner doesn’t look like a scanner you’ve ever seen and almost looks other worldly. Due to its upright position, it requires very little real-estate on your desk when compared to a flat-bed scanner.

    All functions are performed from the software you can download from the Plustek site or via the CD provided in the box. Once the software is installed and scanner calibrated you’re good to go.

    Software

    I’m generally very reluctant to install software provided directly by hardware manufacturers as they encompass some form of bloatware and prefer a minimum install of just the drivers. The software provided by Plustek is very minimal and does exactly what it says on the tin - no thrills!

    Just to be sure you’re running the most up-to-date software, head over to the Plustek site.

    When your photos are scanned you’ll be presented with thumbnails in the interface where you can export a single or group selection of images to the following formats:

    • JPG
    • PDF
    • PNG
    • TIFF
    • Bitmap

    I exported all my scans to JPEG in high quality.

    There is a slight bug-bare with the Mac OS version of the software as it doesn't seem to be as stable as its Windows counterpart. This only became apparent after installing the software on my Dad’s computer running on Windows. I noticed when you have collected quite a few scans, the Mac OS version seems to lag and crash randomly, something that doesn’t seem to occur on a Windows machine. This is very annoying after you’ve been scanning over a 100 photos.

    The hardware specifications on both machines are high running on i7 processors and 16GB of RAM, so the only anomaly is the software itself. A more stable Mac OS version of the scanning software would be welcome. In the meantime, I would recommend Mac users to regularly save small batches of their scans.

    The Scanning Process

    The speed of scanning varies depending on the resolution set from within the software, where you have either 300 or 600 dpi to choose from. I scanned all my prints at 600 dpi, which taken around 15 seconds to scan each 4x6 photo, whereas 300 dpi was done in a matter of seconds. I wanted to get to the best resolution for my digitised photos and thought it was worth the extra scanning time opting for 600 dpi.

    Even though Plustek ePhoto Z300 is a manually fed scanner, I was concerned that I would have to carry out some form of post-editing in the software. By enabling "Auto crop and auto deskew” and “Apply quick fix” within the scan settings, all my photos were auto-corrected very well even when accidentally feeding a photo a that wasn’t quite level.

    To save time in correcting the rotation of your images post-scan, just always ensure you feed the photos top first.

    Conclusion

    The Plustek Scanner performs very well both on price and performance. I have been pretty happy with the quality when scanning photos in either black and white or colour.

    The only thing that didn’t come to mind at time of purchase is scanning is a very manual process, especially when churning through hundreds of photos. It would be great if Plustek had another version of the Z300 that encompassed an automatic feeding mechanism. There were times when I would feed in the next photo before the currently scanned photo had finished, resulting in two photos scanned into one. This didn’t become a regular occurrence once you have got into the flow of the scanning process.

    Not having an automatic feeding mechanism is not at all a deal breaker at this price. You get a more than adequate photo scanner that makes the tedious job of digitising batches of photos somewhat surmountable.

  • Published on
    -
    5 min read

    Generate Code Name For Tags In Kentico

    With every Kentico release that goes by, I am always hopeful that they will somehow add code name support to Tags where a unique text-based identifier is created, just like Categories (via CategoryName field). I find the inclusion of code names very useful when used in URL as wildcards to filter a list of records, such as blog posts.

    In a blog listing page, you'll normally have the ability to filter by both category or tag and to make things nice for SEO, we include them in our URLs, for example:

    • /Blog/Category/Kentico
    • /Blog/Tag/Kentico-Cloud

    This is easy to carry out when dealing with categories as every category you create has "CategoryName" field, which strips out any special characters and is unique, fit to use in slug form within a URL! We're not so lucky when it comes to dealing with Tags. In the past, to allow the user to filter my blog posts by tag, the URL was formatted to look something like this: /Blog/Tag/185-Kentico-Cloud, where the number denotes the Tag ID to be parsed into my code for querying.

    Not the nicest form.

    The only way to get around this was to customise how Kentico stores its tags on creation and update, without impacting its out-of-the-box functionality. This could be done by creating a new table that would store newly created tags in code name form and link back to Kentico's CMS_Tag table.

    Tag Code Name Table

    The approach on how you'd create your table is up to you. It could be something created directly in the database, a custom table or module. I opted to create a new class name under one of my existing custom modules that groups all site-wide functionality. I called the table: SurinderBhomra_SiteTag.

    The SurinderBhomra_SiteTag consists of the following columns:

    • SiteTagID (int)
    • SiteTagGuid (uniqueidentifier)
    • SiteTagLastModified (datetime)
    • TagID (int)
    • TagCodeName (nvarchar(200))

    If you create your table through Kentico, the first four columns will automatically be generated. The "TagID" column is our link back to the CMS_Tag table.

    Object and Document Events

    Whenever a tag is inserted or updated, we want to populate our new SiteTag table with this information. This can be done through ObjectEvents.

    public class ObjectGlobalEvents : Module
    {
        // Module class constructor, the system registers the module under the name "ObjectGlobalEvents"
        public ObjectGlobalEvents() : base("ObjectGlobalEvents")
        {
        }
    
        // Contains initialization code that is executed when the application starts
        protected override void OnInit()
        {
          base.OnInit();
    
          // Assigns custom handlers to events
          ObjectEvents.Insert.After += ObjectEvents_Insert_After;
          ObjectEvents.Update.After += ObjectEvents_Update_After;
        }
    
        private void ObjectEvents_Insert_After(object sender, ObjectEventArgs e)
        {
          if (e.Object.TypeInfo.ObjectClassName.ClassNameEqualTo("cms.tag"))
          {
            SetSiteTag(e.Object.GetIntegerValue("TagID", 0), e.Object.GetStringValue("TagName", string.Empty));
          }
        }
    
        private void ObjectEvents_Update_After(object sender, ObjectEventArgs e)
        {
          if (e.Object.TypeInfo.ObjectClassName.ClassNameEqualTo("cms.tag"))
          {
            SetSiteTag(e.Object.GetIntegerValue("TagID", 0), e.Object.GetStringValue("TagName", string.Empty));
          }
        }
    
        /// <summary>
        /// Adds a new site tag, if it doesn't exist already.
        /// </summary>
        /// <param name="tagId"></param>
        /// <param name="tagName"></param>
        private static void SetSiteTag(int tagId, string tagName)
        {
          SiteTagInfo siteTag = SiteTagInfoProvider.GetSiteTags()
                                .WhereEquals("TagID", tagId)
                                .TopN(1)
                                .FirstOrDefault();
    
          if (siteTag == null)
          {
            siteTag = new SiteTagInfo
            {
              TagID = tagId,
              TagCodeName = tagName.ToSlug(), // The .ToSlug() is an extenstion method that strips out all special characters via regex.
            };
    
            SiteTagInfoProvider.SetSiteTagInfo(siteTag);
          }
        }
    }
    

    We also need to take into consideration when a document is deleted and carry out some cleanup to ensure tags no longer assigned to any document are deleted from our new table:

    public class DocumentGlobalEvents : Module
    {
        // Module class constructor, the system registers the module under the name "DocumentGlobalEvents"
        public DocumentGlobalEvents() : base("DocumentGlobalEvents")
        {
        }
    
        // Contains initialization code that is executed when the application starts
        protected override void OnInit()
        {
          base.OnInit();
    
          // Assigns custom handlers to events
          DocumentEvents.Delete.After += Document_Delete_After;
        }
    
        private void Document_Delete_After(object sender, DocumentEventArgs e)
        {
          TreeNode doc = e.Node;
          TreeProvider tp = e.TreeProvider;
    
          GlobalEventFunctions.DeleteSiteTags(doc);
        }
    
        /// <summary>
        /// Deletes Site Tags linked to CMS_Tag.
        /// </summary>
        /// <param name="tnDoc"></param>
        private static void DeleteSiteTags(TreeNode tnDoc)
        {
          string docTag = tnDoc.GetStringValue("DocumentTags", string.Empty);
    
          if (!string.IsNullOrEmpty(docTag))
          {
            foreach (string tag in docTag.Split(','))
            {
              TagInfo cmsTag = TagInfoProvider.GetTags()
                               .WhereEquals("TagName", tag)
                               .Column("TagCount")
                               .FirstOrDefault();
    
              // If the the tag is no longer stored, we can delete from SiteTag table.
              if (cmsTag?.TagCount == null)
              {
                List<SiteTagInfo> siteTags = SiteTagInfoProvider.GetSiteTags()
                                     .WhereEquals("TagCodeName", tag.ToSlug())
                                     .TypedResult
                                     .ToList();
                if (siteTags?.Count > 0)
                {
                  foreach (SiteTagInfo siteTag in siteTags)
                    SiteTagInfoProvider.DeleteSiteTagInfo(siteTag);
                }
              }
            }
          }
        }
    }
    

    Displaying Tags In Page

    To return all tags linked to a page by its "DocumentID", a few of SQL joins need to be used to start our journey across the following tables:

    1. CMS_DocumentTag
    2. CMS_Tag
    3. SurinderBhomra_SiteTag

    Nothing Kentico's Object Query API can't handle.

    /// <summary>
    /// Gets all tags for a document.
    /// </summary>
    /// <param name="documentId"></param>
    /// <returns></returns>
    public static DataSet GetDocumentTags(int documentId)
    {
      DataSet tags = DocumentTagInfoProvider.GetDocumentTags()
                        .WhereID("DocumentID", documentId)
                        .Source(src => src.Join<TagInfo>("CMS_Tag.TagID", "CMS_DocumentTag.TagID"))
                        .Source(src => src.Join<SiteTagInfo>("SurinderBhomra_SiteTag.TagID", "CMS_DocumentTag.TagID"))
                        .Columns("TagName", "TagCodeName")
                        .Result;
    
      if (!DataHelper.DataSourceIsEmpty(tags))
        return tags;
    
      return null;
    }
    

    Conclusion

    We now have our tags working much like categories, where we have a display name field (CMS_Tag.TagName) and a code name (SurinderBhomra_SiteTag.TagCodeName). Going forward, any new tags that contain spaces or special characters will be sanitised and nicely presented when used in a URL. My blog demonstrates the use of this functionality.