AppleWin/AppleWin

Ancient Printer Emulator v1

tomcw opened this issue · 16 comments

tomcw commented

From emails with Nick (@sicklittlemonkey), Aug 2017:


...after the discussion about printing a while back I became embarrassed about the limited output from AppleWin.

So after a bit of messing around (often with a 2-year-old on my knee) I've got the attached AppleWriter command-line prototype working. It's built with VS 2017. I considered using a PDF library, but they are quite large and complicated, so decided to stick with GDI printing for now, since it's easy to print to PDF. The printer side and output side can easily be extended or replaced, so an Epson FX-80 emulation could be written to use the same output class, or PDF or HTML (etc) output classes could be written.

Just run from the command line passing one of the attached "raw" filenames, and choose Windows' PDF or XPS printer. It's very rudimentary for now, only emulating just enough for these simple examples, but more features can easily be added further down the road as samples become available.

Still better than the current "anything you like, as long as it's text-only!" There's still a bit more to tidy up in the code structure, and then I'll hand it over if you want it. Will be trivial to integrate.


Ok, well for such a small project it's taken a long time, still has a lot of holes and TODOs, but ... it basically works.

Attached is the "Ancient Printer Emulator" program, which uses the "Ancient Printer Emulation Library" (APEL, heh) which supports the simplest features of only one Printer (the AppleWriter) and two Writers (Text File and GDI Printer). Also included are some sample raw print streams and renderings of those samples using one or both of the two writers.

And "trivial to integrate" sound right up my street!

Well, it's as simple to use as:
AncientPrinterEmulationLibrary::TextFileWriter output(output_file);
or
AncientPrinterEmulationLibrary::GdiPrinterWriter output("AppleWin Printer", "Courier New");

Then:
AncientPrinterEmulationLibrary::AppleWriterPrinter printer(output);
while (input_file.get(c))
printer.Send(c);

AncientPrinterEmulatorV1.zip


Oh, and I wanted to start mentioning a few things about the code. It's late, so here are the main things.

There are lots of TODO comments in the code. The class design is mostly done, but there are a few things I'm still not happy with.

  • Choosing a paper size. (Right now it defaults to Letter and doesn't scale or anything if it's changed. Just like a real ancient printer. ; - )
  • Communication of paper metrics (dimensions and margins etc) between the classes doesn't do anything meaningful yet.
  • Most other TODO items are just more commands or options that should be handled or added.

But this is the important one I ran into and spent a couple of weeks playing with in GdiPrinterWriter:
// TODO: Using FillRect to simulate adjacent plots results in gaps needing fudge factors above.
// It might be a scaling error in GDI printing, since both PDF and XPS virtual printers show it.
// Plotting to a bitmap then rendering that to the printer page might look better.

Search for Fudge Factor and try changing the values (e.g. to zero first) to see the problems I encountered.
There seems to be some scaling error in GDI. I'm pretty sure it's not in my code (e.g. see the PITCHTYPE macro).
I would like to port APEL to C# and see if WPF/GDI+ performs better. Also it would be easier to try the bitmap in C#.

After getting to this stage I went and had a look at GSport to see the AppleWriter II emulation there.
Though the code is horrible, it seems to use a bitmap. (I haven't tried to test it yet.)

One reason I wanted to avoid a bitmap was to draw text with GDI fonts for better quality and PDF text selection.
I suppose there are ways it could be achieved.

  • Text could still be GDI based and the graphics bitmap drawn with transparency over or under it.
  • Text could be bitmap based - this could offer better retro-fidelity and use the actual font data from the printer.
  • For bitmap text, the GDI text drawing could be done transparently so PDF text selection is still possible.

Just a quick comment / question. Which printer model is meant by "AppleWriter"? I am not aware of an Apple printer by this name. Perhaps @tomcw means "ImageWriter"?

-Matthew

tomcw commented

I think "AppleWriter" is "Apple ImageWriter I", as the comments in Nick's code (in AppleWriterPrinter.cpp) reference a manual where the pages seem to correspond to the 'Apple ImageWriter I Reference Manual' (here, albeit this is in French!).

Nick / @sicklittlemonkey - can you confirm, and link to the manual your code comments reference?

Hi guys.

Sorry about that. I dashed this code out in a week, and then spent another week agonizing over the problems with GDI.

It is indeed the Apple ImageWriter (I, not II). Five years ago, so not sure what was going on in my head: from the email above it looks like I was just confused about the name, but I might have decide Image was confusing as part of the type name. (I've never owned an ImageWriter.)

The manual I was referring to is the English version of that French user's manual, here: http://vtda.org/docs/computing/Apple/Printers/Imagewriter_UsersManual.pdf

It's exactly that PDF as it has the hand-written "Support # ..." written on page 2, but I can't find it in any of the usual places like asimov or the Apple II documentation project.

Cheers,
Nick.

tomcw commented

Hmm... Malwarebytes is blocking my access to vtda.org as it may contain a trojan!

I found a mirror on Internet Archive here:
https://archive.org/download/2020.11.vtda.org.mirror/2020.11.vtda.org.mirror.zip/vtda.org%2Fdocs%2Fcomputing%2FApple%2FPrinters%2FImagewriter_UsersManual.pdf

I've read through most of Nick's APEL code, and concur with the basic architectural choices. There should indeed be a Printer layer that handles the printer-specific commands, and a Writer layer that outputs a PDF, image, or even on-screen UI. (The name "Writer" seems to understate what it does, though. I was thinking "Renderer" or "Rasterizer" maybe?)

I think the existing ParallelPrinter.cpp needs to be refactored thusly:

  • Move the g_bDumpToPrinter logic to a new pass-through Printer-Writer pair. Seems like BasePrinter and TextFileWriter both do too much already.
  • Move the g_bConvertEncoding, g_bFilterUnprintable, and g_bPrinterAppend logic to TextFileWriter.

ParallelPrinter.cpp would then essentially become just the implementation of the Parallel Interface Card, oblivious to what printer it's connected to. This means the configuration would become more complex than just calling PrintLoadRom because we'd also want to configure which printer-writer pair to use. To match the existing behavior we'll need something like this:

#pragma once
#include "Printer.h"

namespace AncientPrinterEmulationLibrary
{
	class GenericPrinter : public Printer
	{
	public:
		GenericPrinter(Writer& output) : Printer(output) {};
		virtual int Send(unsigned char byte) { return m_Output.WriteCharacter(0, 0, byte); };
	};
}

to go with TextFileWriter because BasePrinter is abstract.

AppleWriterPrinter should probably be renamed something like AppleImageWriter1Printer as medasaro pointed out above. An Epson FX/MX/LX-80 printer implementation would also not be hard from that point.

Linux and macOS would need to provide their own Writer classes. I was thinking on-screen UI for macOS, because it's allegedly easy to generate a PDF from an AppKit view.

Unfortunately, this is way more than I'd be comfortable submitting a PR for because I don't have a Windows development set-up. For now I'm "forking" ParallelPrinter until somebody can refactor and test this in Windows.

Thoughts?

Naming is the hardest thing. I like "Renderer", and AppleImageWriter1Printer.

Glad to see someone having a hack at it! : - D

tomcw commented

@sh95014 - your description of the basic architecture sounds sensible (and I'm sure Nick would point out anything wrong or if he disagrees with anything).

Yes, it sounds like it will turn into a big PR... so as you suggest: forking is probably best. Then we can figure out how to merge it back into the AppleWin main-line code at a later time.

btw, one small piece of work is to convert the current ParallelPrinter.cpp/h into a C++ class, so that the card can be instantiated into any (or multiple) slots. Similar work is progressing with other outstanding cards. You may want to wait until I have done this (I hope to do it within the next week).

WAIT?! :)

Just kidding. All I did was to copy ParallelPrinter to a new .cpp/.h pair called ParallelInterface and then deleted most of the old code except PrintLoadRom, PrintStatus, PrintTransmit, and some stub functions. I then added:

void Printer_SetPrinter(AncientPrinterEmulationLibrary::Printer & printer)
{
	g_printer = &printer;
}

and so PrintTransmit basically calls g_printer->Send(c);. I don't expect any real issues adopting your card instance refactoring when that's ready.

But as I mentioned above, the refactor would be a bit more complicated if you want to keep supporting the following:

  • bool g_bDumpToPrinter = false;
  • bool g_bConvertEncoding = true;
  • bool g_bFilterUnprintable = true;
  • bool g_bPrinterAppend = false;
  • bool g_bEnableDumpToRealPrinter = false;
    To be honest, I'm not sure any of them are all that useful.

Anyway, here are a couple of screenshots of a prototype printer output window:

Screen Shot 2022-03-15 at 10 49 29 PM

Screen Shot 2022-03-15 at 10 59 07 PM

MacOS lets you use approximately the same code to generate PDFs as well, so trivially:

copya.pdf
hello.pdf

(If you noticed the dot matrix font, that's courtesy of http://const-iterator.de/fxmatrix/)

Issues:

  1. I'm fairly sure AddX is not supposed to automatically WrapX, and the app is expected to issue its own LF. If I don't remove the WrapX call there would be large gaps in the PrintShop output. @sicklittlemonkey would appreciate if you could confirm.
  2. Modern printers can't really reach the very top and very bottom of pages like the dot matrix printers can with continuous forms. On MacOS, it looks like the PDF generation code has a bug and has coordinate shifting and clipping problems. Anyway, each Writer is gonna have to figure out the actual printable area and squeeze into it.*
  3. Dazzle Draw and Newsroom, when configured for Imagewriter/Parallel/Slot1 seems to expect all 8 pins to fire, unlike PrintShop which knows that only 7 bits go out the PIC. So our & 0x7F code in ParallelPrinter ends up blanking out a whole row. @sicklittlemonkey I saw you included in your zip file a DazzleDraw.pdf. Was that actually generated by Dazzle Draw?
  4. When I print a third time in Dazzle Draw or a banner in PrintShop, nothing shows up beyond the first page. I think BasePrinter might not realize that the page has ended when printing graphics, but I'm not sure yet, will investigate.

Oh, and all the code lives in https://github.com/sh95014/AppleWin/tree/printer-support for now if anybody's curious.

* This is actually a bit tricky. Text will for sure need to be squeezed into the printable area so it doesn't get clipped, but graphics programs generally know to leave their own margins and the squeezing will introduce a redundant margin. And it's theoretically possible to mix graphics and text (for example to print a custom character as a graphic) so they need to both get squeezed or both not... I'm leaning towards not trying to respect the original print size in favor of avoiding clipping.

@sh95014 It looks great!

  1. I don't recall. Hopefully the manual would specify. Maybe it was an option. IIRC the FX-80 had DIP switches.
  2. Paper size and margins were left as a big TODO.
  3. I notice in the code I commented that MousePaint sent 8-bit ESC characters. The PDFs in the archive were generated by putting the respective .raw files (captured printing from AppleWin) through the APEL exe and printing via GDI to Windows' save to PDF virtual printer.

Cheers,
Nick.

  1. The sample code in page 73 of the Imagewriter manual shows a WRITELN (PRINTER) after each row of graphics, so I think the WrapX should indeed not be called.

  2. I can imagine a crafty implementation that tries to let the Apple software control its own margins, as long as it's within the printable bounds of the real output device, but that really seems to be overthinking it. Different paper sizes would certainly be more interesting to support than margins.

  3. Found it in the Epson LX80 User's Manual Appendix F-12:

There are two types of problems that you who own Apple II computers will need to address. The first is that the Apple II is an 8-bit computer, but its printer interface only handles seven bits. [...]

The printer interface card furnished with the Apple II computer only passes seven bits to the LX-80, which means that you have a 7-bit system. Should you need an 8-bit system, the simplest solution is to purchase a new printer interface card from your computer dealers. Such a card is available for the Apple II.

However, the same sample code in page 73 of the Imagewriter manual also tries to print CHR(129) so it's clearly expect eight bits to pass through the controller. I don't see anything in the Parallel Interface Card Operating Manual that says the 8th bit is suppressed.

So confused.

However, the same sample code in page 73 of the Imagewriter manual also tries to print CHR(129) so it's clearly expect eight bits to pass through the controller. I don't see anything in the Parallel Interface Card Operating Manual that says the 8th bit is suppressed.

Indeed. And on page 71:

Each of the data bytes defines a vertical column of eight dots
printed by the type head. A dot is printed for each bit set to 1 in the
data byte. Bit 7 causes the dot at the bottom of the column to be
printed and bit O causes the dot at the top to be printed.

But maybe I can help with the following:

So confused.

The ImageWriter is a serial printer, not a parallel one! : - D

The ImageWriter is a serial printer, not a parallel one! : - D

D'oh! :)

...

Also figured out problem 4 above, it was a bug in my implementation of Writer. However, I did find an issue with BasePrinter if the final row of a page is graphics and it straddles page boundaries. My fix goes something like this:

  • The Writer allocates an overflow strip at the bottom of each page that it can print to.
  • If the overflow area is actually used, it's peeled off and given to the new page as its top strip.
  • BasePrinter, however, can't just start the new page at PageTopMarginTwips. It has to know how far into the new page the final LF took it.

I'm attaching a diff to APEL here that does a few things:

  • Pass to the Writer whether the current character is a "continuation" of the previous. This allows the Writer to collate the characters into a string, which should be much more efficient than literally a page full of single characters at specific xy coordinates. This is handled by the CharacterIsAdjacent bits.
  • The LF straddling page boundaries issue above. This is handled by the new parameter to NewPage.
  • The removal of WrapX from AddX mentioned above.
  • Flush the output to Writer after every line. (The code that allows \n to get passed through might be vestigial now for me, but not quite sure.)
  • Give the Printer a human-readable name for display.

APEL.patch.zip

And here's a PDF showing off multi-page graphics printing just for giggles. You can see how one row of the rectangles in the "C" in "QUICK" straddles two pages.

brown.pdf

Anyway, I think I've taken this about as far as I could, except for some UI niceties. My motivation to build an FX-80 emulator is much lower now because APEL works just fine. Gonna need somebody to pick it up on the Windows side now.

Okay, I lied, here's a first stab at Epson FX-80. Not able to test it against the GDI writer, but supports PrintShop (some aliasing possibly related to the macOS writer being wedded to 72dpi), Newsroom (vertical gaps, probably because it's printing at 60 dpi to a 72 dpi writer), and Dazzle Draw (horizontal gaps, not sure why).

Would appreciate if somebody has time to try it out with the GDI writer. Latest version would be at https://github.com/sh95014/AppleWin/tree/printer-support/source/Printers but dumped here for convenience:

EpsonFX80Printer.zip

tomcw commented

Hi @sh95014 -

Anyway, I think I've taken this about as far as I could, except for some UI niceties. My motivation to build an FX-80 emulator is much lower now because APEL works just fine. Gonna need somebody to pick it up on the Windows side now.

I don't have time at the moment to pick this up (but maybe someone else does?), but for future reference could you list the high-level things that need doing "on the Windows side" ?

Thanks.

  • Add the APEL code (including the APEL.patch.zip and EpsonFX80Printer.zip) to the project and get it to build. I put them in source/Printers downstream but can of course adjust as necessary.
  • Optionally rename AppleWriterPrinter to more correctly reference the Apple Imagewriter.
  • Figure out the right place to initialize the APEL Printer-Writer pair.
  • Split up ParallelPrinter into a parallel interface (loads ROM and handles I/O) and just pass the bytes to the APEL Printer. Audit the methods like CheckPrint, Update, and others to see if they're actually still necessary. The new parallel interface card code probably needs a SetPrinter method to tell it who to send the bytes to.
  • Move any existing flags (m_bDumpToPrinter and friends) that we actually want to keep to either BasePrinter or the Writer as appropriate. I don't think any of them logically belong with the parallel interface, and things like m_bFilterUnprintable actively break things downstream.
  • Decide what to do with printer output on Windows and Linux. On macOS I pop a new window like in the screenshots above, but there are other options. Note that "dump to file" might seem easy but you'll have to handle multiple pages, so if you choose BMP as output format you might then want a BMP file (so maybe named AppleWinPrinter-[yyyymmdd]-[hhmmss].BMP) for each page. If you choose PDF then you might need to append to the same PDF somehow, which I'm guessing is complicated.
  • Figure out what to do with the Imagewriter. Right now it's a serial printer that we're allowing to be connected to a parallel port. The right thing to do is probably to add an interface type to each APEL printer class, and teach the AppleWin serial card to also talk to printers.
  • Think through what state restoration means for a printer. I'm not familiar enough with the philosophy to suggest the lengths to go to, and on macOS I just didn't implement it at all.
  • command line or UI to configure the interface-printer pair to use.
tomcw commented

Thank you. That's a very nice list of steps in order for someone to take your work and get it working on Windows.