Self Portrait - November 2014

November 20, 2014

Carrying on the long standing tradition of photographers photographing themselves.


Don't Trust PhotoShop's JavaScript ColorSampler

November 16, 2014

Beware the ColorSampler in PhotoShop's JavaScript (aka ExtendScript) implementation. In CS6, it doesn't always pick the color from the coordinates assigned to it. Problematic since that's the purpose of the tool.

To illustrate, take this image:

It's ten pixels high with alternating rows of red and green. The hex colors are #990000 and #005500, respectively. Now, take this script:

app.preferences.rulerUnits = Units.PIXELS;

for (var row=0; row <=9; row = row + 1) {
  app.activeDocument.colorSamplers.removeAll();
  app.activeDocument.colorSamplers.add([0, row]);
  $.writeln(app.activeDocument.colorSamplers[0].color.rgb.hexValue);
}

It makes sure PhotoShop is using pixels and then samples the first pixel of each row. If the ColorSampler worked as expected, the result would be a list of ten lines alternating between '990000' and '005500'. Instead, running it in PhotoShop CS6 (Version 13.0.6) on a MacBook with OS X 10.10 (14A389) produces this:

990000
005500
990000
005500
990000
005500
990000
990000
990000
005500

Everything looks good until just before the end where '990000' occurs three times in a row. That shows the ColorSampler is pulling the color from either row 7 or 9 even though it was assigned to row 8. It's only one error but in programming that's all it takes to render something useless.

I've found a work around to make the ColorSampler trustworthy again. Instead of using integers (e.g. [10, 0]) for the coordinates, use floats that have an extra tenth of a pixel added on them (e.g. [10.1, 0.1]). In theory, this doesn't make sense because a pixel is supposed to be the smallest possible unit. Regardless, it works. For example, here's an updated version of the script that adds "0.1" to every row except the last one. (If you add it to the last one it pushes off the canvas and PhotoShop throws an error.)

app.preferences.rulerUnits = Units.PIXELS;

for (var row=0; row <=9; row = row + 1) {
  var adjustedRow = (row == app.activeDocument.height) ? row : row + 0.1;
  app.activeDocument.colorSamplers.removeAll();
  app.activeDocument.colorSamplers.add([0, adjustedRow]);
  $.writeln(app.activeDocument.colorSamplers[0].color.rgb.hexValue);
}

Running this on the same test image produces the expected output:

990000
005500
990000
005500
990000
005500
990000
005500
990000
005500

And with that, ColorSampler is trustworthy enough to use again. (Though, I'll certainly be keeping a close eye on it.)

Anecdotal evidence suggest very few folks automate PhotoShop with JavaScript. Seeing a bug like this in mature software is more evidence. Kudos to Adobe for continuing to invest resources in something so few use.


Stay One Undo Away from Green

November 14, 2014

I recently had the good fortune of attending Practical Object-Oriented Design with Sandi Metz and Katrina Owen. It was fantastic. Three days with them did more to improve my object-oriented thinking than months of reading and experimentation. I can't recommended the course highly enough.

Sandi and Katrina provided an elegant refactoring guideline on the first day:

Stay one undo away from green.

It was a key take-away and it's as simple as it sounds. Make one small change at a time during refactoring. If the tests go red, hit undo and immediately get back to green. If you're like me, and easily venture a dozen or more changes away from a passing test suite, this approach is transformative.

To illustrate, they walked the class through their refined, systematic process for extracting a conditional into a method. Two examples of their approach are below. The first lays out the steps used when starting with an 'else' branch. The second starts with an explicit case. The number of steps may appear tedious but each change is tiny. The entire process goes very quickly. As we discovered in class, picking a good name for the method takes longer than typing the code.


Extracting a conditional starting with the 'else' branch

Step 0 - Initial Code

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end end

Step 1 - Identify the things that are most alike.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end end

Step 2 - Identify the smallest difference in the things that are most alike.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..."</span> end end end

Step 3 - Make a method to return the value needed by the 'else' case but don't actually call it. Just make sure it parses.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end def container "bottles" end end

Step 4 - Execute the method without actually using the value.

class Bottles def verse_for(number) container if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end def container "bottles" end end

Step 5 - Used the new method in the 'else' case.

class Bottles def verse_for(number) # container if number == 1 "#{number} bottle of beer on the wall..." else "#{number} #{container} of beer on the wall..." # Replaced 'bottles' end end def container "bottles" end end

Step 6 - Shim on an argument and run the tests to make sure it still passes.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} #{container} of beer on the wall..." end end def container(bottle_count=:FIXME) "bottles" end end

Step 7 - Send an argument to the method even though it's not used.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} #{container(number)} of beer on the wall..." end end def container(bottle_count=:FIXME) "bottles" end end

Step 8 - Remove the default shim for the argument.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} #{container(number)} of beer on the wall..." end end def container(bottle_count) # Removed =:FIXME "bottles" end end

Step 9 - Update the method to have the new functionality without actually calling it in the new location.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} #{container(number)} of beer on the wall..." end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 10 - Call the method in the new location.

class Bottles def verse_for(number) if number == 1 "#{number} #{container(number)} of beer on the wall..." # Replaced 'bottle' else "#{number} #{container(number)} of beer on the wall..." end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 11 - Comment out unused cases.

class Bottles def verse_for(number) # if number == 1 # "#{number} #{container(number)} of beer on the wall..." # else "#{number} #{container(number)} of beer on the wall..." # end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 12 - Remove comments and you're done.

class Bottles def verse_for(number) "#{number} #{container(number)} of beer on the wall..." end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end


Extracting a conditional starting with a specific case

Step 0 - Initial Code

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end end

Step 1 - Identify the things that are most alike.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end end

Step 2 - Identify the smallest difference in the things that are most alike.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..."</span> end end end

Step 3 - Make a method that takes an unused argument and returns the value needed but don't actually call it. Just make sure it parses.

class Bottles def verse_for(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end def container(bottle_count) "bottle" end end

Step 4 - Execute the method without actually using the value.

class Bottles def verse_for(number) container(number) if number == 1 "#{number} bottle of beer on the wall..." else "#{number} bottles of beer on the wall..." end end def container(bottle_count) "bottle" end end

Step 5 - Use the new method

class Bottles def verse_for(number) # container(number) if number == 1 "#{number} #{container(number)} of beer on the wall..." # Replaced 'bottle' else "#{number} bottles of beer on the wall..." end end def container(bottle_count) "bottle" end end

Step 6 - Update the method to have the new functionality without actually calling it in the new location.

class Bottles def verse_for(number) if number == 1 "#{number} #{container(number)} of beer on the wall..." else "#{number} bottles of beer on the wall..." end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 7 - Call the method in the 'else' case.

class Bottles def verse_for(number) if number == 1 "#{number} #{container(number)} of beer on the wall..." else "#{number} #{container(number)} of beer on the wall..." # Replaced 'bottles' end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 8 - Comment out unused case.

class Bottles def verse_for(number) # if number == 1 "#{number} #{container(number)} of beer on the wall..." # else # "#{number} #{container(number)} of beer on the wall..." # end end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Step 9 - Remove comments and you're done.

class Bottles def verse_for(number) "#{number} #{container(number)} of beer on the wall..." end def container(bottle_count) if bottle_count == 1 "bottle" else "bottles" end end end

Don't let the terse nature of the examples fool you. They are simplified versions of a 99 Bottles exercise used throughout the class. The full problem has a lot more going on and the actual extraction provides benefits that aren't evident in this demonstration. If you want to learn more, definitely attend the class. Sandi and Katrina also have a '99 Bottles Of OOP' book coming out soon. Based on the class, I expect it'll be great. Sign up for info about it if you're interested.


Assigning Ruby Variables with a Case Statement

November 08, 2014

Reading Refactoring: Ruby Edition I came across an example of assigning a variable via a case statement. For example:

#!/usr/bin/env ruby

trigger_value = 7

output_string = case trigger_value
  when 1
    "First number"
  when 7
    "Lucky number"
  else
    "Something else"
end

puts output_string   # Outputs: Lucky number

Using the return values from the case statement directly for the assignment is much cleaner than the way I used to do it:

#!/usr/bin/env ruby

trigger_value = 7
output_string = ""

case trigger_value
  when 1
    output_string = "First number"
  when 7
    output_string = "Lucky number"
  else
    output_string = "Something else"
end

puts output_string   # Outputs: Lucky number

I'm learning that most case statements are prime candidates for refactoring. The direct assignment is a nice way to use them until that happens.


32 Terabytes of Photo Storage

September 27, 2014

In 2011, I wired six hard-drives into a case and made an 8 Terabyte server for my digital photos and files. It's been fantastic. The convenience of not having to spread files across half a dozen external drives is worth its weight. The peace of mind the operating system, called FreeNAS1, provides is even better. Its built-in safety features make it the best way to store data2.

After three years of faithful service, Coltrane, which is what I named the server, has run out of space. It's possible to increase the size but I went a different route. A new server built with the knowledge and experience gained working on Coltrane. It's an eleven disk FreeNAS beast named Mingus.

Mingus during construction. Eleven 4 Terabyte drives prepped and ready to go.

And here's the machine with all the drives wired up.

It seems like eleven 4 Terabyte drives should equal 44 Terabytes of storage but that's not how FreeNAS works. Part of the way data is kept safe in a FreeNAS system is by using redundant "parity" drives. The math is interesting3, if you're into that sort of thing, but basically it breaks down like this: For every parity drive in a multi-drive setup, one hard drive from the cluster can die without taking all the data with it. Three of Mingus' eleven drives are parity. So, as long as four drives don't all die at the same time, my files are safe from hard-drive failure4.

Taking the three parity drives out of the equation leaves (8 x 4TB =) 32 Terabytes of space. A little gets chewed up in the way computers deal with division but seeing roughly 32 Terabytes of well protected space is nice.

31.46... 32... Whatever it takes.

There's definitely effort and expense involved in building a FreeNAS server but it's something anyone willing to put in some time can do. The protection and convenience are more than worth the hassle.


Footnotes and Links

  1. FreeNAS is Free, Open-Source, Network Attached Storage software (i.e. the perfect software to use for a server). It relies on a system called ZFS which eliminates several ways data can get corrupted.
  2. After a ton of research, I won't trust my files to any multi-disk storage system that isn't using ZFS under the covers. It just takes a few minutes reading about Data Degradation to freak out anyone who wants to keep files for more than a few years.
  3. ZFS data integrity and Parity Bits for those who are into such things.
  4. There are lots of other ways besides hard drive failure that can kill files. FreeNAS and ZFS do everything they can to protect against them but it's still critical to do backups. If you're files aren't on at least two separate devices it's only a matter of time until they are on none. If you aren't automatically backing up your files sign-up, for Crashplan, Backblaze or some other cloud backup services. Speaking from experience, you'll be a happier person if you get backups going before you need them.
  5. The motherboard I used is an ASRock C2550D4I Mini ITX Server Motherboard FCBGA1283. It comes with an Intel Avoton C2550 Quad-Core Processor and can take up to 64GB of DDR3 1600/1333 Dual-channel RAM. (I didn't max out the RAM. Next time I build one of these I will for a little better performance.) The biggest selling point for the motherboard was the 12 build in SATA ports. That let me write up all the drives without having to use SATA Backplanes. Just one less thing to worry about for compatability.

Finding SATA Cables for SeaSonic Power Supplies

September 17, 2014

(The purpose of this post is to show up in search results so other folks don't have to spend their time tracking down these hard-to-find links.)


This is one of those rare cases of Google not knowing where to find something I'm looking to buy1. Nothing exotic or illicit. Just a power cable.

I'm building a server with a bunch of drives to store my digital life. The power supply is made by a company called SeaSonic. It's perfect, except for one thing. It didn't come with enough cables for all the connections I need. SeaSonic's site lists additional cables as available accessories2 but they don't sell them. Repeated searching for the part number also didn't turn up any sellers. It took an email to support in order to find a supplier3.

The specific cable is the SeaSonic "Serial ATA Power Connector" model: SS-SATA-55-03. The supplier is Build To Order Servers4 and they have it listed as "Seasonic 6pin to three SATA connectors Modular Cable". They also have a 6pin to four SATA connectors version.

Besides the "SS-SATA-55-03" cable, the BTOS folks have a Seasonic 6pin to three Molex connectors Modular Cable which SeaSonic lists as their IDE Power Connector - Model: SS-IDE-55-03.

With luck, searching for those model numbers will bring up this page and folks can skip to the buying part. If you found this page via a search, I hope it helps.

Side note to companies selling things on the web: Include the model number of the parts you sell or you're leaving money on the table.



Footnotes and Links

  1. I'm sure I would have found it eventually with enough Google triangulation and a trip through the wasteland that is pages 2 through ∞ of the search results. There was one hopeful looking link in the first page of results. It didn't work out. Instead of actually selling the cable the only option was to fill in a "Send Enquiry" (sp) form that asks for an email. All the other links were to forums where others are looking for the same cable.
  2. SeaSonic's Accessories page where the model numbers look like links but aren't. To help the search engines, the other items on that page that might also be available on the supplier site are the 20/24 Pin M/B Extension Cable (Model: SS-MB-20), the 8/6 Pin PCI Express Power Connector (Model: SS-PCI-E-60) and the EPS 12V 8P Extension Cable (Model: SS-CPU-30).
  3. There should be a name for the little dollop of dread that comes from finally breaking down and deciding to use the "Contact Us" form/email. It wasn't warranted in this case. The SeaSonic's folks got back to me quickly with the links I needed.
  4. Build To Order Servers, officially BTOS Integration, Inc. They'll also customize cables for you to adjust the length or add more connectors.

Discarding Websites - An Effort to Focus on Fewer Things

September 13, 2014

Ideas? Ideas are easy. Coming up with hundreds a year is no problem. Most dissipate, but the raw tonnage usually generates a few worth pursuing.

When thrashing out an idea, I look for good website names to accompany it. This is where things get a little out of hand. It's too easy to spot a good one and buy it for fear that cyber-squatters1 are close behind. It doesn't take long before I've stocked a stable full of unused websites2. My current count: 46.

If I never have another new idea, I wouldn't be able to tackle half my current list. This never used to bother me. I get a rush out of the thought experiments that go along with new ideas, but I've come to appreciate that they have a insidious opportunity cost. If I don't make an explicit decision to not pursue an idea, part of my brain keeps chewing on it. Getting Things Done3 describes this as an "open loop". Each one consumes a little mental energy. Individually no big deal, but combining several really saps the energy available to focus on anything else.

So, I'm releasing sites back into the wild to jettison some mental baggage. Here's a partial list of those that are either going or are gone:

  • AmbientForge.com - Ambient Forge was a potential name for a software company.
  • AmbientFoundry.com - As was Ambient Foundry.
  • AmbientWares.com - And Ambient Wares. (In case you haven't noticed, I really dig the word "ambient".)
  • AnotherLittle.com - The site you're reading (alanwsmith.com) started life with that name. I grew paranoid having my name so closely associated with my web presence and decided to move it. AnotherLittle.com was the first of those moves.
  • awsr.me - My personal link shortener (like bit.ly) for use on Twitter. I wanted control of my links. Partly so I could make sure they wouldn't disappear if the third-party service went away. Ironic, right?
  • HoundstoothGirls.com - This site was going to be a photo site with models donning various pieces of houndstooth clothing. Prints, calendars and other image products would also be sold to Bama fans. (For those unfamiliar with Alabama football, houndstooth is effectively the official team pattern.)
  • LessBadNews.com - I've tapered off from being an avid news consumer to actively avoiding it. It's too depressing. LessBadNews.com was going to focus on positive stories. The original idea was to grab public news feeds and remove anything that mentioned war, death, politics or celebrity bullshit.
  • MockupDepot.com - The idea was to set up a service that makes it easier for designers to deliver, and get feedback on, mock-ups.
  • Mp3Recommender.com - Think Spotify/Pandora type recommendations. MP3Recommender.com would have pointed to songs you might like and earned me a few pennies for every sale.
  • PhotoHostReview.com - A comparison engine for photographers looking for web hosting options.
  • PortfolioPresenter.com - I considered building an iPhone/iPad portfolio application. This would have been its marketing site.
  • RovingPortraitProject.com - The original name for my Million Portrait Project. Not a bad name, but The Million Portrait Project is better.
  • SomeRandomPhotos.com - Where I used to post my photos when my name paranoia was at its peak (see anotherlittle.com above).
  • StimulatingPixels.com - When I wanted something with a little more personality than anotherlittle.com for my site. (Yes, the site went alanwsmith.com -> anotherlittle.com -> stimulatingpixels.com -> alanwsmith.com).
  • TackSharpTech.com, TackSharpTechnologies.com and TackSharpTechnology.com - All possible business domains for selling software and web services to photographers.

I'd enjoy doing any of those projects. After realizing how much keeping them around without working on them was making me less productive, the decision to let them go became easier. The hard part will be keeping myself from buying more to replace them.


Footnotes

  1. Cyber-squatters are people who buy cool website names but instead of building something they just try to re-sell them for a profit.
  2. Some websites I'm hanging on to are for friends and family. They are better described as hibernating. Even taking those out, the total I own teeters on the ridiculous.
  3. Of the hundreds of books I've read, Getting Things Done by David Allen is in the top five list of ones that improved my life. Its guidelines for getting all the thoughts, ideas, todos out of your head and into a trusted system are worth their weight in gold. Doing so lets me focus on actually doing things instead of just trying to keep up with all the things I need to do. It took a few false starts and can still be a struggle, but it is by far the best way I've found to deal with information overload.

Deceased Dragonfly

August 02, 2014

Here lies D. the Dragonfly. As beautiful in death as in life.


Go To Index Page: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107


© Alan W. Smith
RSS Feed