Dan, or Brian, or whoever is in the Spruce driver’s seat:

I have made a number of changes to Spruce at CMU, some of which I think are important. This is my attempt to pass back to you in the most succint and useful form possible the changes I have made.

I have tried to sort the items in order of priority (top priority first). Some things were clearly "bugs", probably having to do with having started my work from an inconsistent set of sources. These have almost surely been fixed, but might bear checking.

1. [FEATURE] (a) We found it extremely useful to include on the break page the date of printing. This is actually the date and time at which the Spruce server printed the file. We use this to periodically thin out our paper bins, discarding listings that are too old. Note that the date of document creation is not sufficient for this purpose.

(b) People insisted that I put a version of the printed-by information at the top of the page so that when output was inserted in vertical bins, people could see the names of people who printed files.

(c) We have also removed the Xerox logo from our output. I guess that’s now an installation option.

I include below the relevant section of SpruceScan for all this, as amended (known amendments are commented //%%):

and ScanBreakPage(pDoc, page, pass) be
[
manifest [
textLeft=30
textFirst=70
textSpace=4 //%% to fit more entries on break page

commentLeft = 15
commentFirst = textFirst - 8*textSpace //%% change 7 to 8
commentSpace = 2
]
let nBreaks = ((Capabilities & mPimFiles) eq 0)? 1,((Capabilities & mBlackHousing) ne 0)? 4,3
let blackPass, magentaPass, yellowPass, cyanPass = true, true, true, true
test nBreaks eq 1 ifso
[
if pass ne 1 return
]
ifnot
[
magentaPass = pass eq 1
yellowPass = pass eq 2
cyanPass = pass eq 3
if nBreaks eq 4 do blackPass = pass eq 4 // for threecolor all passes are black
]
let f = commentFree// Temporarily disable comment entries
DisableComments()

ShowCharSetSpace(0)
ShowCharSet(64)// Font set reserved for break page
ShowCharFont(0)// Normal printing
if blackPass do
[
BreakString("Printer ", textLeft, textFirst)
BreakString(printerName)
BreakString("Spruce version ", textLeft, textFirst-textSpace)
BreakNumber(Version)
BreakString(".")
BreakNumber(MinorVersion)
BreakString(" -- spooler version ")
BreakNumber(SpruceVersion)
BreakString(".")
BreakNumber(SpruceMinorVersion)
]
if blackPass % cyanPass do BreakString("File: ", textLeft, textFirst-3*textSpace)
if cyanPass do
BreakString(lv pDoc>>DocG.FileStr)
if blackPass do
[
BreakString("Creation date: ", textLeft, textFirst-4*textSpace)
BreakString(lv pDoc>>DocG.DateStr)
BreakString("Printing date: ", textLeft, textFirst-5*textSpace)//%%
let s=vec 100//%%
CONVUDT(s,0,true)//%%
BreakString(s)//%%
]
if blackPass % magentaPass % yellowPass do BreakString("For: ", textLeft, textFirst-6*textSpace)
//%% change 5*textSpace to 6*...
if magentaPass % yellowPass do
BreakString(lv pDoc>>DocG.CreatStr)
if blackPass do
[
if (lv pDoc>>DocG.ByStr)>>STR.length ne 0 do
[
BreakString(" By: ")
BreakString(lv pDoc>>DocG.ByStr)
]
BreakString("", textLeft, textFirst-7*textSpace) //%% change 6*... to 7*...
let p=(pDoc>>DocG.nPages-nBreaks)/nBreaks
let c=pDoc>>DocG.nCopies
let s = p
if ((Capabilities&mTSPrint) ne 0) & (pDoc>>DocG.simplex eq 0) then s= (p + 1)/2
BreakNumber(s*c+1)
BreakString(" total sheets = ")
BreakNumber(p)
BreakString((p eq s? ( p eq 1? " page, ", " pages, "), (p eq 1? " side, ", " sides, ")))
BreakNumber(c)
BreakString((c eq 1? " copy.", " copies."))

if numComments then for i = 0 to numComments do
[
let comment = comments!i
unless comment loop
BreakString(comments+comment, commentLeft, commentFirst-i*commentSpace)
]
//%% Here to next //%% is for top of break page
// Sprinkle top of break page with first letters of "For" name
let p=(lv pDoc>>DocG.CreatStr)
let s=vec 2
s>>STR.length=2
let c=p>>STR.char↑1
s>>STR.char↑1=(c ge $a)? c-$a+$A,c
s>>STR.char↑2=$*s
BreakString(s,12,100)
for i=0 to 5 do BreakString(s)
BreakString(p,42,100)
//%%
]
if (Capabilities & mMailbox) do
[
ShowOnCopy(1001)
BreakString("MORE IN OVERFLOW BIN", textLeft, commentFirst-(numComments + 1)*commentSpace)
ShowOnCopy(1002)
BreakString("MORE IN MAILBOX", textLeft, commentFirst-(numComments + 1)*commentSpace)
ShowOnCopy(0)
]
if (Capabilities & mTSPrint) do
[
ShowOnCopy(1003)
BreakString("Probable two-sided print problem", textLeft, commentFirst-(numComments + 1)*commentSpace)
ShowOnCopy(0)
]
// Now pepper the logo around...
ShowCharFont(1)
//if cyanPass do for x=0 to 1 do for y=0 to 1 do
//
BreakString("XEROX", logoLeft+x*(logoRight-logoLeft),
//
logoBottom+y*(logoTop-logoBottom))

// And print some strong vertical lines for easy identification:
let x=5
// Red
if magentaPass % yellowPass do for i=0 to 1 do
[
for j=0 to 5 do
[
ShowXY((x+j)*254, 10*254)// x=.5",.6",,7" ; y=1.0"
// 10.0 " takes too much time on Alto II XM at 384 7-31-78
ShowRectangle(254/2, 90*254)// .05" by 9.0" -- penguin 1/29/79
]
x=75
]
commentFree = f// reenable comments if were enabled before
]


I also had to change the load command to include timer stuff; here’s an exerpt:

OVInterpret/B SpruceBand SpruceBandMl SpruceDir SpruceFontMake SpruceInterpret ↑
SpruceParts SpruceScan SpruceShow TimeIO TimeConvA TimeConvB calendar ↑


2. [BUG] Spruce would occasionally crash and complain of an "alignment erro" in its file package. This occurred because the error machinery was trying to read Spruce.Errors and Spruce.Errors had an odd number of bytes in it. There is a check deep in the code (<sprucestreams>setupwindowstream) to insure that a buffer has an even number of bytes in it. Of course, usually, the error machine does not "run off the end" of Spruce.Errors, because it finds the error message before hitting end of file. It’s not clear why it wasn’t finding error messages; I never tracked that down.

3. [FEATURE] I put in some of the EFTP abort codes described in the protocol document because the tranmission from our PDP-10 seemed to demand it. I’m not sure I did it right, and you probably have this done properly now, but I thought I’d include the snippet from sprucespool.bcpl that I used:

and SpoolAFile() = valof // Someone is known to be knocking
// ------------------------------------------------------
//
Manages the EFTP operation, the queueing operation via SpruceQueue functions, and the
// reporting of results to the SpruceUser context via the Post() function. FillInNames(), properly a
// Sprint operation, examines the Press directory for Press file information: name, creator, date,
// approximate size, etc. This is used in reports at the user terminal and in spooler status packets.
//
Returns true only if another spool request arrived while spooling (someone else knocked).
[
PostStr = spooling?
"Sorry, Spruce Spooling Facility Full--Please Try Again.",
"Sorry, Spruce Service not Available -- Please Try Again Later."
let doc, spoolResult = 0, ECSpoolTerminate
PostStr = "" // No information to relate
test spooling¬ FullQueue() then [ CursorChar($E); doc =
FetchAFile()]
or [ numMustPrint = 3;
//%% here to next //%%
PostStr = spooling?
"Sorry, Spruce Spooling Facility Full--Please Try Again.",
"Sorry, Spruce Service not Available -- Please Try Again Later."
let PostCode = ExternalReceiverAbort
if FullQueue() then PostCode = 7
if not spooling then PostCode = 6
SendEFTPAbort(spoolSocket, PostCode, PostStr)
]
//%%
CloseEFTPSoc(spoolSocket)
let someoneElseKnocked = spoolSocket>>EFTPSoc.SomeoneElseWaiting
if doc then
[
let spooledFile = doc>>DocG.PressFile
CleanupQueue(spooledFile>>SPruceFile.numPages, false) // Eliminate obliterated entries
if doc>>DocG.reportCode eq 0 then
[
pressFileIndex = pressFileIndex+1 repeatuntil (pressFileIndex&FILEPressMask) ne 0
spooledFile>>SPruceFile.fileCode = FILEPress + (pressFileIndex&FILEPressMask)
InitSpruceFile(spooledFile, 1, 3); FillInNames(0, doc, spooledFile) // fill in stuff from Press dir.
ResetSpruceFile(spooledFile)
AddToQueue(doc)
if numFilesSpooled ge maxQueued-(maxQueued rshift 2) % // check queue near-full
freeFile>>SPruceFile.numPages le (SpoolFile>>SPruceFile.numPages rshift 2) then
numMustPrint = 1
PostStr, spoolResult = "Received from remote host", 0
]
] // doc
Post(doc, spoolResult, PostStr)
PrepareToSpool(someoneElseKnocked? $I, $R) // Ready, denote whether others at the door
resultis someoneElseKnocked
]


4. [FEATURE] People asked me to lengthen the queue, but upon looking into it, I decided it would be hopeless. It seems too tightly tied to an in-core data structure that, if increased, would squeeze out too many other things. Increasing queue size would be a good idea, I think.

5. [FEATURE] People wanted the user console screen to flash if operator intervention was required. This was mostly because when hopper became full, Spruce stopped printing without enough fanfare to tell people to fix it. I put some code in to make screen flash, but I don’t think it works. I also removed entirely the full hopper thing. I understand the new version also allows installer to defeat full hopper check.

6. [MINOR FEATURE] Someone suggested that the font selection should be symmetric about rotational differences, (i.e., treat + and - differences properly), and should also be "analog" about the degree of mismatch. So, here’s a bit from sprucedir:

and Match(ix, fn) = valof
[
// Compute "point size" of char in scan-lines.
// Maximum difference contribution = 100
let rx = ix - (ix>>IXH.Type eq IXTypeMultiChars? (offset IX.resolutionx-offset IXM.resolutionx)/16, 0)
let fontSiz=MulDiv(rx>>IX.resolutionx, ix>>IX.siz, 25400)
let reqSiz=MulDiv(ResolutionS, fn>>FN.siz, 25400)
let dif=(fontSiz-reqSiz)
if dif ls 0 then dif=-dif
if dif gr 100 then dif=100

//%% from here to next //%%
// Rotation: contribution = 400
let rd = ix>>IX.rotation-fn>>FN.rotation
if rd ls 0 then rd=-rd
while rd gr (180*60) do rd=rd-(360*60)
if rd ls 0 then rd=-rd
dif=dif+MulDiv(rd,400,180*60)
//%%
// Face: contribution = 200
if ix>>IX.face ne fn>>FN.face then dif=dif+200
// Family: contribution = 200
if ix>>IX.fam ne fn>>FN.fam then dif=dif+200
resultis 1000-dif
]