BookNotesSlidesP-sets Syllabusdraft · v0.2.49.1
◀▲⌂▶
💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.

45.33 Photoshop 1.0, running⧉

Many operations in the image-processing chapters of this book was, at some point, a menu item that somebody shipped. Photoshop 1.0 went out in February 1990: one application, written by Thomas Knoll, pushed toward being a product by his brother John, and licensed to Adobe. It is running a few lines below, on a Macintosh II that exists only as WebAssembly.

fig-photoshop-1990
Figure 45.33.1. Adobe Photoshop 1.0 running on an emulated Macintosh II under System 6.0.8. Nothing downloads until you press Boot; the machine then reaches a System 6 desktop in about five seconds, and the Open Photoshop button drives the emulated mouse through the Finder to launch the application. The disk of book photographs, offered below the screen, mounts when you drag it onto the Mac.

45.33.1 What is on the menu bar⧉

File, Edit, Mode, Image, Select, Window. Six menus, and the whole program is in them.

Mode is the list of representations a picture is allowed to be in: Bitmap, Gray Scale, Indexed Color, RGB Color, CMYK Color, HSL Color, HSB Color, Multichannel. Two of those did not survive. You could hold a photograph in HSL or HSB, rather than reach for a hue slider that converts on the way in and back out, which makes the point of Chapter 4.1, Color space 101 more forcefully than a modern editor can: a color space is a choice of coordinates, and in 1990 you chose it. Converting to Indexed Color makes you pick a bit depth, a palette and a dithering method, because the Mac II showed 256 colors at a time and a photograph has far more.

Image is where the pixels change, and it is organized by how much of the image each operation looks at. Map holds the ones that need a single pixel: Invert, Equalize, Threshold, Posterize, and Arbitrary, which gives you a box to draw a transfer function in with the mouse. There is no Curves in 1.0. Arbitrary is its ancestor, and it is exactly the lookup table of Chapter 8.3, Tone point operations with a pencil for an interface. Adjust holds the parameterized versions of the same idea: Levels, Brightness/Contrast, Color Balance, Hue/Saturation. Filter collects neighborhood operations and effects such as Add Noise, twenty-two of them, and the list is a fair map of what counted as image processing that year: Blur, Blur More, Gaussian Blur, Sharpen, Sharpen More, Sharpen Edges, Unsharp Mask, High Pass, Median, Maximum, Minimum, Despeckle, Add Noise, Find Edges, Trace Contour, Motion Blur, Offset, Mosaic, Facet, Fragment, Diffuse, and Custom. Convolution (Chapter 6.2, Convolution), morphology (Maximum and Minimum), and order statistics (Median) were all already menu items.

Calculate does arithmetic between whole channels, which is how you composited before there were layers.

Because there are no layers. Layers arrive in version 3.0, in 1994. In 1.0 an edit lands on the pixels and Edit ▸ Undo gets you back exactly one step. Everything a modern editor treats as a reversible parameter is here a destructive operation on a single buffer, which is worth feeling once: the non-destructive pipeline of Chapter 8.12, Recap ISP, non-destructive editing was invented because of this.

45.33.2 Running it in a browser⧉

The emulator is Infinite Mac, Mihai Parparita's WebAssembly builds of Mini vMac, Basilisk II and several others, embedded through its documented iframe endpoint. The machine is a Macintosh II under System 6.0.8, which is what the application shipped for, with 8 MB of memory and a 640 by 480 screen at 8 bits per pixel. Adobe Photoshop 1.0 sits on the emulator's software library volume, in Graphics.

Two details of the embedding matter. First, SharedArrayBuffer is unavailable to a frame whose host page is not cross-origin isolated, and the book's pages are not, so the emulator falls back to a slower scheduling loop. A 16 MHz 68020 does not notice. Second, a fresh boot puts the Finder's windows and icons in the same places every time, so the Open Photoshop button can drive the emulated mouse blind, sending absolute positions and clicks to the frame and letting the Finder do the rest.

Getting a picture in is the awkward part, and the reason is chronological: Photoshop 1.0 shipped two years before JPEG was standardized, so nothing off a phone will open in it. The page therefore offers a one-megabyte disk image holding two of this book's photographs as TIFFs, one gray and one color. Download it, drag it onto the Mac, and it mounts as a volume called Book Photos.

45.33.3 What to try⧉

Open the gray photograph, a carved stucco panel with detail at every scale, and go to Image ▸ Filter ▸ Custom. What comes up is a 5 by 5 grid of empty boxes, a Scale field and an Offset field, pre-filled with a center 5 and four neighboring -1s. That is a convolution kernel, typed in by hand, with a divisor and a bias, and the default is the discrete Laplacian sharpener of Chapter 6.2, Convolution. Change the numbers and the photograph responds. It is Problem Set 2, in 1990, with a worse text editor.

Then Unsharp Mask on the same image. The dialog asks for Amount in percent, Radius in pixels, and Threshold in levels, which are the three fields Photoshop still asks for; push Amount well past what looks reasonable and you get the halos of Chapter 8.8, Sharpening. Compare Blur with Gaussian Blur while you are there: both exist because the fixed small kernel was cheap on a 68020 and a general Gaussian was not, which is the argument of Part 29, Making computational photography code fast showing up as two menu items.

Then open the color photograph, dyed wool at a market, and take it through Mode ▸ Indexed Color. The dialog offers a resolution from 8 bits per pixel down to 3, a palette (Exact, System, or Adaptive), and a dither (None, Pattern, or Diffusion), which is the whole quantization problem laid out as three radio-button groups. Run it at 8 bits, then 5, then 3, with Dither set to None and then to Diffusion. A saturated photograph crushed onto 8 colors makes the case for dithering faster than a figure does.

45.33.4 The source code is public, and that is not the same as buildable⧉

In February 2013 the Computer History Museum published the source to version 1.0.1, with Adobe's permission: 179 files, roughly 128,000 lines, about three quarters Object Pascal and one sixth 68000 assembly, licensed for non-commercial use and not for redistribution. It is a rare thing, a finished and famous program you can read.

Reading it is not running it, and the gap is wider than it looks. The release deliberately omits MacApp, Apple's application framework, which Adobe had no right to hand out; the version required is MacApp 1, distributed on two floppies in 1987 and superseded in 1990. Knoll had also modified his own copy of it. The Photoshop sources override three MacApp methods that MacApp 1 does not contain, one of them TrackMouseUp, which is what makes the option key turn a lasso into straight segments. Anyone rebuilding the application has to reinvent those, with no comments to go on.

One person has publicly reported getting it to compile, in 2013. They bought MacApp 1.1.1 on eBay and found the diskettes mostly readable, built under MPW 3.0 on System 6, stripped the Windows line endings the archive had introduced into every file, restored the MPW metacharacters (∂ and ƒ) that had been replaced with a backslash and a colon, patched a QuickDraw header rename, wrote the three missing methods themselves, and stubbed out a copy-protection object file that the linker demands and the program never calls. A full build took fourteen minutes on a Mac IIci. The debug build's registration resource identifies the user as Thomas Knoll, of Knoll Software.

45.33.5 How the program is put together⧉

Photoshop 1.0 is one programmer's code, and it is organized the way a careful programmer organizes 128,000 lines: a thin Macintosh shell, a Pascal unit per feature, and a hot core in assembly, with the boundaries drawn where the costs change.

The shell is MacApp, Apple's Object Pascal application framework. Windows, the event loop, and undo come from framework classes the program subclasses; the main program file declares TPhotoshopApplication and little else. Everything the user sees is data rather than code: the menu bar is a set of cmnu resources, every dialog is a DITL resource listing its controls one by one, and each filter is a FILT resource declaring its parameters, ranges, and defaults. The Filter menu builds itself at launch from whatever FILT resources it finds. That one decision is why plug-ins exist: a third-party filter is just another file of resources, found the same way. FilterInterface.p, a header reading "Copyright 1990 by Thomas Knoll" and already at "version 3" of the interface, defines the protocol as six calls (About, Parameters, Prepare, Start, Continue, Finish) plus a callback the plug-in polls so the user can abort. The plug-in architecture that later carried an industry is fully formed in version 1.

Below the shell there is a unit per concern: UDraw for the painting tools, USelect for selections, UAdjust for Levels and its siblings, UConvert for mode changes, and one unit per file format, a dozen of them. Underneath everything sits the unit that made the program possible on a 1990 Macintosh: UVMemory, Photoshop's own virtual memory. An image did not fit in RAM, so TVMArray slices each image plane into 30 KB pages, keeps a page table, and spools pages to a scratch file. Code never touches pixels directly; it checks rows out with NeedPtr and hands them back with DoneWithPtr, and the pager decides what stays resident. This is the scratch disk Photoshop is still famous for, present in its first release, and it explains a structural fact about every filter in the program: each one consumes the image a row at a time, because a row is what the pager can promise.

The pattern that matters most is the twinning. UFilters.p declares, UFilters.inc1.p implements, and UFilters.a is the same unit's assembly half; most units with an inner loop have such a twin. Pascal owns structure, dialogs, paging, and edge cases; assembly owns the loop over pixels. Twenty assembly files export 179 procedures, about 20,000 lines of 68000 that do nearly all of the per-pixel work.

45.33.6 What the assembly relies on⧉

The machine is a 16 MHz 68020 with memory measured in single megabytes and no floating point worth assuming (the 68881 coprocessor was an option). At 640 by 480, a filter that spends fifty instructions per pixel takes seconds. The assembly's job is to get that number down to a handful, and it leans on four ideas.

Integers only, in fixed point. The Gaussian's kernel weights are 16-bit fixed-point fractions. DoWeightedFilter multiplies each tap with an unsigned multiply, accumulates into a 32-bit register preloaded with $8000 (one half in this representation, so the result rounds instead of truncating), and takes the high word with a single SWAP. The divide by 65536 that converts back to pixel units costs nothing: it is the register swap.

Tables instead of arithmetic. Find Edges needs the magnitude of the Sobel gradient, a square root per pixel; the code ships a 257-entry table of precomputed hypotenuses and indexes into it, loading the table's address into a register before the loop starts. Every Map and Adjust command, Levels included, compiles the user's settings into one 256-byte lookup table, so the per-pixel work of Levels is one indexed byte move.

Restructure until the inner loop's cost stops depending on the parameters. The box blur does not sum a window per pixel. One routine maintains running column totals, adding the arriving row and subtracting the departing one; another slides along the row, adds the leading total, does the one unavoidable divide, and subtracts the trailing total. The cost per pixel is the same at radius 2 and radius 40. The next appendix shows what this buys: a large-radius Gaussian approximated by several box passes. The same instinct governs the borders: rather than test for the edge inside the loop, the code folds the first and last pixel into the totals radius times up front, so the interior loop has no branches at all.

The small idioms fill in the rest. Loops run on DBF, the 68000's decrement-and-branch, one instruction of loop overhead per pixel. Pointers walk with post-increment addressing, so advancing through a row is free. Where a kernel weight is a power of two, the convolver shifts instead of multiplying; the comment in the source says "see if we can do a bit shift", and the built-in Blur and Sharpen kernels are chosen so that it can.

None of this is exotic, and that is what makes it worth reading. Pick an integer representation, precompute what repeats, make the loop independent of the parameters, keep branches out of the interior: the same list, under the names it has today, is Chapter 29.1, Algorithmic speedups and Chapter 29.9, 8-bit and fixed-point arithmetic. The costs have moved since, and sometimes reversed: on this machine multiplication was relatively expensive and a small lookup table could save time, while on a modern core multiplication may cost less than a lookup that misses cache, which is the subject of Modern CPUs. The discipline transfers even where the constants flipped. In 1990 it was mandatory; today the hardware just stopped forcing you to learn it.

So this appendix runs the binary. The half of the source code that concerns us, the image processing, barely touches the Macintosh at all: it is arithmetic over a buffer, in Pascal, and it can be lifted out and run on a modern image. That is Chapter 45.34, Photoshop 1.0 ported to JavaScript, and it is a better way to read 128,000 lines than scrolling through them.


No new notation is introduced in this chapter.