ImageFileConvert

Four of the Convert... modules had been created which had identical APIs and which were likely to be used for rendering through the ImageFileRender interface. This duplication was a frustration; it went against the aim of reducing duplication in the components. All these conversion modules had a common interface, and the conversion function in itself was very useful, so I decided that it should be a separate component in its own right.

My original goals for conversion APIs were that there be different types of conversion - those for editing, presentation, printing, or other uses. In the interests of simplifying the implementation and API, I abandoned these distinctions early on. I wasn't too sad to lose them, though, because the conversion system had been so long coming, that getting anything working was going to be both fun and incredibly useful.

The ImageFileConvert has a very similar API to that which had been created for the four Convert... modules. Instead of providing the conversions and rendering themselves, ImageFileConvert provided the API, and the modules registered themselves with it, declaring what filetypes they could convert. Essentially, ImageFileConvert sat between the conversion functions that had been written and the ImageFileRender interface, replacing all the duplicated code that dealt with creating different types file from the one that was supplied before rendering.

The code that had been written for ConvertPNG to perform the ImageFileRender support was moved out into the ImageFileConvert module. ImageFileConvert would register renders with ImageFileRender on behalf of the conversion functions. In order to do this, ImageFileConvert examines the filetypes which can be rendered and matches those with the types which can be converted to.

For example, ConvertPNG would declare to ImageFileConvert that it can convert from PNG (&B60) to Sprite (&FF9). ImageFileConvert queries ImageFileRender and discovers that it can render type &FF9, so registers a renderer for &B60 (PNG).

When the user requests that ImageFileRender render a PNG, it calls the ImageFileConvert registered interface. This then calls down to ConvertPNG to create the sprite in a temporary area. Once the sprite has been created, ImageFileConvert then calls the render function in order to render this newly created sprite. This process can be applied for other types than sprite. For example, the ConvertWMF module would create DrawFiles, so if you requested a render of a WMF, the ImageFileConvert module would create an intermediate DrawFile and then call the DrawFile renderer to perform the actual rendering.

This was pretty funky - I was quite pleased when the process worked and we could render things in a variety of ways, using the features provided by the ImageFileRender interface. Want to render a PNG at 45 degrees to fit a box, using a gamma correction table? Dead easy.

Here's what happens when you list the renderers which are present in the system:

*ImageFileRenderers
Type Magic    Converter         Ver.  Author
---- -----    ---------         ----  ------
&132 00000000 ConvertICO        0.11  Justin Fletcher, Graeme Gill (via IFC)
&690 00000000 ConvertClear      0.07  RISCOS Ltd (via IFC)
&695 00000000 ConvertGIF        0.17  Justin Fletcher, ANT Ltd (via IFC)
&697 00000000 ConvertPCX        0.05  Justin Fletcher, Tim Northrup (via IFC)
&69c 00000000 ConvertBMP        0.16  Justin Fletcher, Graeme Gill (via IFC)
&69e 00000000 ConvertPNM        0.08  Justin Fletcher, Jim Frost (via IFC)
&aff 00000000 ImageFileRender   0.36  Justin Fletcher
&b60 00000000 ConvertPNG        0.39  Justin Fletcher (via IFC)
&b61 00000000 ConvertXBM        0.09  Justin Fletcher, ANT Ltd (via IFC)
&bcf 00000000 ImageFileRender   0.36  Justin Fletcher
&c85 00000000 ImageFileRender   0.36  Justin Fletcher
&cc3 00000000 ImageFileRender   0.36  Justin Fletcher
&d91 00000000 ImageFileRender   0.36  Justin Fletcher
&d94 00000000 IFR Artworks      0.18  Justin Fletcher
&fc9 00000000 ConvertSun        0.10  Justin Fletcher, Jim Frost (via IFC)
&ff9 66990101 ImageFileRender   0.36  Justin Fletcher

The types which have no direct renderers are proxied by the ImageFileConvert module, and are labelled 'via IFC'.

I wasn't content with this, though. It's very flexible but I don't want to have to work out what sequence of conversions I need to do to get the results I need. The original plans for conversion APIs included inferred conversions, and it was easy to see how these could be fitted in. If ConvertPNG could make PNGs into Sprites, and ConvertBMP could make Sprites into BMPs, it's obvious that you can convert a PNG to a BMP.

I added support for 'implicit' conversions which the ImageFileConvert module could perform on behalf of the requester. When ImageFileConvert is considering what types it can convert, it generates an extended list of conversions which are implied by the existing converters. Each of these implicit converters is a lower priority converter that will be removed if a dedicated converter is registered. In principle, a dedicated conversion should be better than converting via another format.

The addition of the implicit conversion meant that from simple conversions there was much greater numbers of conversions possible which were previously more tedious to perform. Any conversion from &FF9 (Sprite) to another format would cause all the other conversions to become available as well. The ConvertICO module was extended to add conversion from Sprite to .ico (&132), which made all the other type conversions possible to .ico.

You want a .ico converting to a PNG ? Yup, that works. <smile>

Pleased though I may have been, that's not all we can do! No! - we also throw in this handy carrot dicer! Hmm... no, that's a different ramble <smile>. The implicit conversion only applied to the formats that were registered with ImageFileConvert. What about those types that weren't convertable directly but could be converted via a render?

The ConvertSprite module was (partially) created to handle this - it could create a sprite from any of the renderable formats. The ConvertSprite module would query the ImageFileRender module for renderable types, and check with ImageFileConvert whether it is already possible to convert them to a sprite. If it it isn't possible to convert to a sprite, a handler is registered. When a conversion request is made, a sprite is created in the workspace requested and the file rendered into it. The information from the SWI ImageFileRender_Info call is used to determine what depth of sprite to use and whether a mask should be generated.

The mask generation is not very complicated, and is based on rendering multiple times with different backgrounds and comparing the differences. It does not work if there is alpha channel present - it will effectively make the mask binary. But that's a reasonable compromise for an automated conversion between renderable formats.

This means that there are even more conversion paths available. DrawFile (or !ArtWorks) to Sprite are simple conversions, but also the implicit conversion code kicks in as well, which means that any of these conversions can become an implicit conversion to other types. DrawFile to PNG or Artworks to JPEG are implicitly created conversions, and are just couple of examples of the large list of types that can be created.

The list of converters that were supplied got rather silly when all the implicit conversions were included:

*ImageFileConverters
Conversion    Converter         Ver.  Author
----------    ---------         ----  ------
&132 -> &69c  Implicit          -     Through &ff9
&132 -> &b60  Implicit          -     Through &ff9
&132 -> &c85  Implicit          -     Through &ff9
&132 -> &ff9  ConvertICO        0.11  Justin Fletcher, Graeme Gill
&690 -> &132  Implicit          -     Through &ff9
&690 -> &69c  Implicit          -     Through &ff9
&690 -> &b60  Implicit          -     Through &ff9
&690 -> &c85  Implicit          -     Through &ff9
&690 -> &ff9  ConvertClear      0.07  RISCOS Ltd
&695 -> &132  Implicit          -     Through &ff9
&695 -> &69c  Implicit          -     Through &ff9
&695 -> &b60  Implicit          -     Through &ff9
&695 -> &c85  Implicit          -     Through &ff9
&695 -> &ff9  ConvertGIF        0.17  Justin Fletcher, ANT Ltd
&697 -> &132  Implicit          -     Through &ff9
&697 -> &69c  Implicit          -     Through &ff9
&697 -> &b60  Implicit          -     Through &ff9
&697 -> &c85  Implicit          -     Through &ff9
&697 -> &ff9  ConvertPCX        0.05  Justin Fletcher, Tim Northrup
&69c -> &132  Implicit          -     Through &ff9
&69c -> &b60  Implicit          -     Through &ff9
&69c -> &c85  Implicit          -     Through &ff9
&69c -> &ff9  ConvertBMP        0.16  Justin Fletcher, Graeme Gill
&69e -> &132  Implicit          -     Through &ff9
&69e -> &69c  Implicit          -     Through &ff9
&69e -> &b60  Implicit          -     Through &ff9
&69e -> &c85  Implicit          -     Through &ff9
&69e -> &ff9  ConvertPNM        0.08  Justin Fletcher, Jim Frost
&aff -> &132  Implicit          -     Through &ff9
&aff -> &69c  Implicit          -     Through &ff9
&aff -> &aad  DrawFile          1.65  Justin Fletcher
&aff -> &b60  Implicit          -     Through &ff9
&aff -> &c85  Implicit          -     Through &ff9
&aff -> &ff9  ImageFileRender   0.36  Justin Fletcher (via ConvertSprite)
&b60 -> &132  Implicit          -     Through &ff9
&b60 -> &69c  Implicit          -     Through &ff9
&b60 -> &c85  Implicit          -     Through &ff9
&b60 -> &ff9  ConvertPNG        0.39  Justin Fletcher
&b61 -> &132  Implicit          -     Through &ff9
&b61 -> &69c  Implicit          -     Through &ff9
&b61 -> &b60  Implicit          -     Through &ff9
&b61 -> &c85  Implicit          -     Through &ff9
&b61 -> &ff9  ConvertXBM        0.09  Justin Fletcher, ANT Ltd
&bcf -> &132  Implicit          -     Through &ff9
&bcf -> &69c  Implicit          -     Through &ff9
&bcf -> &b60  Implicit          -     Through &ff9
&bcf -> &c85  Implicit          -     Through &ff9
&bcf -> &ff9  ImageFileRender   0.36  Justin Fletcher (via ConvertSprite)
&c85 -> &132  Implicit          -     Through &ff9
&c85 -> &695  CompressJPEG      0.30  Justin Fletcher, IJG
&c85 -> &69c  CompressJPEG      0.30  Justin Fletcher, IJG
&c85 -> &69d  CompressJPEG      0.30  Justin Fletcher, IJG
&c85 -> &b60  Implicit          -     Through &ff9
&c85 -> &ff9  ImageFileRender   0.36  Justin Fletcher (via ConvertSprite)
&c85 -> &ff9  CompressJPEG      0.30  Justin Fletcher, IJG
&cc3 -> &132  Implicit          -     Through &ff9
&cc3 -> &69c  Implicit          -     Through &ff9
&cc3 -> &b60  Implicit          -     Through &ff9
&cc3 -> &c85  Implicit          -     Through &ff9
&cc3 -> &ff9  ImageFileRender   0.36  Justin Fletcher (via ConvertSprite)
&d91 -> &132  Implicit          -     Through &ff9
&d91 -> &69c  Implicit          -     Through &ff9
&d91 -> &b60  Implicit          -     Through &ff9
&d91 -> &c85  Implicit          -     Through &ff9
&d91 -> &ff9  ImageFileRender   0.36  Justin Fletcher (via ConvertSprite)
&d94 -> &132  Implicit          -     Through &ff9
&d94 -> &69c  Implicit          -     Through &ff9
&d94 -> &b60  Implicit          -     Through &ff9
&d94 -> &c85  Implicit          -     Through &ff9
&d94 -> &ff9  IFR Artworks      0.18  Justin Fletcher (via ConvertSprite)
&fc9 -> &132  Implicit          -     Through &ff9
&fc9 -> &69c  Implicit          -     Through &ff9
&fc9 -> &b60  Implicit          -     Through &ff9
&fc9 -> &c85  Implicit          -     Through &ff9
&fc9 -> &ff9  ConvertSun        0.10  Justin Fletcher, Jim Frost
&ff9 -> &132  ConvertICO        0.11  Justin Fletcher, Graeme Gill
&ff9 -> &695  Implicit          -     Through &c85
&ff9 -> &69c  ConvertBMP        0.16  Justin Fletcher, Graeme Gill
&ff9 -> &69d  Implicit          -     Through &c85
&ff9 -> &b60  ConvertPNG        0.39  Justin Fletcher
&ff9 -> &c85  CompressJPEG      0.30  Justin Fletcher, IJG

The implicit conversions which ImageFileConvert has inferred and handles are marked as 'Implicit', together with the type that they will convert through. The conversion of the vector renderable formats are indicated with a suffix of 'via ConvertSprite'.

In use, these intermediate stages were completely transparent to the developer. In conversion code they only need to say what they are giving as the input format, and what they want out.

REM >IFCExample
REM Convert between formats
:
f$="MoreThanMeetsTheEye":REM DrawFile
outtype%=&b60:REM PNG
bg%=-1:REM No background colour
:
REM Get file details and load
SYS "OS_File",20,f$ TO obj%,,,,inlen%,,type%
IF obj%<>1 THENSYS "OS_File",19,f$,obj%
DIM inbuf% inlen%
SYS "OS_File",255,f$,inbuf%,0
:
REM Calculating size of output image
SYS "ImageFileConvert_Convert",0,(type%<<16) OR outtype%,inbuf%,inlen%,0,0,bg% TO ,,,,,outlen%
IF outlen%=-1 THENoutlen%=inlen%*2:REM Unknown length; guess.
:
DIM outbuf% outlen%
SYS "ImageFileConvert_Convert",0,(type%<<16) OR outtype%,inbuf%,inlen%,outbuf%,outlen%,bg% TO ,,,,,outlen%
:
REM Save out result
SYS "OS_File",10,"Output",outtype%,,outbuf%,outbuf%+outlen%
Converting a DrawFile to a PNG, implicitly converted through Sprite.

The resulting image
More Than Meets The Eye album cover. This rendering by Justin, Julian and Simon Fletcher.
(Larger version (86K))

All of which makes my head hurt really:

  • ImageFileConvert proxies ImageFileRender requests for its converters.
  • ImageFileConvert allows implicit conversions for its converters through an intermediary.
  • ConvertSprite proxies ImageFileConvert requests for ImageFileRender handlers, via sprites.

There's something of a storm of services issued as any part of the stack changes, which is mitigated very slightly on system start up. There's still quite a lot of processing that goes on there, and debugging it without remembering each of the components which does things can be a little confusing. Usually removing ConvertSprite and/or ImageFileRender helps greatly in reducing the complexity whilst working out what's going on.

I was really pleased with the way that the ImageFile stack works together, and the facilities it provides across the system. There are always going to be criticisms to level at it, and I would probably say that my biggest frustration (if you want to call it that - as it's not huge) is that it relies on the data to be converted being entirely within memory space. This restricts it to having only smaller files, but then it was never intended to handle (for example) video conversion.

A more general criticism is the amount of work being performed in SVC mode. This meant that all of the heavy lifting of these (essentially) simple library operations was being performed in the most privileged mode. As modules provided the way to share libraries, this made the most sense, but it does have those drawbacks.

As mentioned previously, there had been work towards the handling of USR mode SWIs, but this was pretty much stillborn because of the issues with interacting with USR mode applications which didn't adhere to APCS, or assumed that USR mode meant that their environment was present. The primary place where this would be an issue would be any compiled code that didn't use APCS and expected different conventions, and which you wanted to use the SWIs in.

That really meant any programming language that wasn't C-based (or C-based using an old standard), such as BASIC. Obviously any assembler programmers got everything they deserved if they did things differently - my views on writing anything significant in assembler can be summed up in one line: "Don't do it, kids - it's not big and it's not clever" - but the existence of such things meant that assumptions about USR mode cannot be made. At least, not within the current environment organisation. But more about that in another ramble.

ConvertPNG again

... and we're back to the ConvertPNG module. With the ImageFileConvert module stable and able to be used by other components, the ConvertPNG module's own implementation could be enhanced. As will be discussed later (see the 'Sprites' ramble), the sprite system was updated to support true alpha-channel - partly in order to provide full support for conversion of PNGs.

Initially the conversion started out by only supporting PNG conversion to 24bit sprites, as this form of transparency is trivial to process. Once this was working happily, I added support for the lower depth with the 'tRNS' chunk. This encodes a palette entry which is both colour and alpha mask value, so results in a post-processing phase where the mask is generated from the palette entry alpha component. I had considered using the same method for low depth sprites, but the limitation of having a palette entry contain both alpha and colour (compared to having alpha completely independent in the mask), and the fact that the entire graphics system would need to be reworked to understand that a mask was present in a palette entry (and in some cases the space for alpha had been reserved), meant that I abandoned the idea pretty sharply.

There were quite a few fixes and minor enhancements as the PNG to sprite conversion was ironed out. In general there weren't too many problems, but getting sensible conversions was important - incorrectly widening images (for example a 24bit sprite when a 256 colour paletted sprite would suffice) would mean that the sprites produced were larger than they needed to be, which might be wasteful if the images were being used regularly (such as might be the case if themes were stored as PNGs).

The reverse conversion - that of sprite to PNG was equally fun. Some sprites could be converted efficiently to PNGs (16 colour and most 256 colour sprites). However where all 256 colours were used in a 256 colour sprite and a mask was present, it isn't possible to retain the same data - it has to be converted to 24 bit-per-pixel plus alpha channel.

To create a suitable 'tRNS' entry for a paletted sprite, when there is a binary mask present the conversion searches the image for any unused palette entry. If it finds one, that entry becomes the mask colour in the PNG. For a binary mask on a deeper sprite, it's more fun. The mask colour can be any representable colour within the colour space which isn't used. However, keeping track of 24 bits worth of colour in order to find a suitable colour that is not used would be tricky. Instead of searching the entire colour space, the colours are quantised to fewer bits per channel, which results in a much more manageable search space.

It is quite possible to easily cause the search to fail, and fall back to 24 bit colour and full alpha-channel mask. However, such images tend to be either fake (usually computer generated) or incredibly vibrant. I seem to remember that I searched quite a few pictures from Disney Land (as an example of images that have a lot of vibrant colours) and they didn't fail to find a mask colour. Passing those images through histogram equalisation with !ChangeFSI however, made it fail. The pictures looked terrible, but it did fail. I say fail - I mean it fell back to the simpler 24 bit plus alpha- channel, which uses more space. So not really fail. Just less efficient.

16 bit-per-pixel sprites are similar - actually, they are 15 actual bits (5 per channel), and an ignored top bit. But when converted to a PNG, these duplicate the top 3 bits into the lower 3 and set the 'sBIT' chunk to indicate that only 5 bits per channel are actually significant. Similarly, if the PNG to sprite decoder sees that the 'sBIT' chunk indicates that only 5 bits are used, only a 16 bit-per-pixel sprite is created - saving memory (and depending on the screen mode used to render the sprite, time).

The input sprite's resolution is passed on into the sprite, indicating that the sprites are 90 DPI (usually - they might be 45 DPI, or 180 DPI, or event 22.5 DPI... or lower). This mostly ensured that the aspect ratio of the pixels remained constant, although it does appear to have the effect of stretching the images in some systems. Windows thumbnail views can appear odd sometimes, because of this; and because Windows appears to have a different decoder for PNG for every component, other parts of the system don't exhibit this odd effect. I boggle some of the sanity of this, but it's not for me to question why...

As one final change, the sprites can be converted from the CMYK format to PNG. This loses the distinction between the components, but at least makes a PNG. Although PNG supports different colour profiles, it doesn't support non-RGB colour spaces. Oh well.

As a final, tiny flourish (if you like - partly I thought it was advertising, but its nice to at least show that I've done something useful) the PNGs created by the module would be given a comment indicating that ConvertPNG created the file. It's nothing special, but I thought it was quite a nice idea.

DrawFile

The rendering of DrawFiles was performed by the ImageFileRender module internally, so there was never any need for a separate implementation of the rendering interfaces within DrawFile. It might have made some sense to move the code there, but the differences in handling would be pretty minimal and the benefits low - except for more clearly separating the operations. Consequently, I never split the code out into the DrawFile module. I probably should have done, but no matter.

The DrawFile format was the main vector format used within the system. The only other of note was the !ArtWorks file format, which gained little in being created from a DrawFile as a conversion, and couldn't really be converted back into a DrawFile (at least not without a lot more work). However, I'd already created a SVG to DrawFile converter separately, and part of that included a DrawFile to SVG converter.

My converter was pretty trivial, and command line only, so not particularly friendly. Initially what I wanted to do was to be able to export an SVG file from within !Draw. So that's where I implemented the code - this was in the early Select 2 development, before much of the ImageFileRender stack even existed. !Draw itself uses RISC_OSLib, and the library provides the main functions for manipulating DrawFiles.

For all intents and purposes you can treat RISC_OSLib as "the library that you use to make !Paint, !Draw and !Edit". Any operation that those applications perform has (generally) a function, or helper, within RISC_OSLib. The file saving code is an enumeration that serialises the output with callback functions to fill data into buffers (if I'm remembering that rightly). Adding code that provided an SVG serialisation of the DrawFile that was already in the !Draw / RISC_OSLib data structures was really quite simple.

There were a few minor hiccups like the handling of text, the scales used, and some complete confusion about how the buffering worked, but all in all it went reasonably smoothly. However, the export was then tied to !Draw and couldn't be used by any other application.

Spin forward to Select 4 development and all the conversion modules were working and quite well used. Initially, I created a new module which was (nearly) a lift of the !Draw code to export the SVGs, and provided this as a ConvertDrawSVG module. This worked out pretty well, and once it was shown to be working correctly, the code was integrated into the main DrawFile module.

Well, I say 'integrated', but that's not quite right. The ConvertDrawSVG module can still be built, as it lives inside its own directory, has its own Makefile and module header. However, the functions that it needs are all separated out so that it can be built as part of the DrawFile module as well. Actually the old module doesn't have any properly allocated names, SWI chunks, error blocks or anything like that, so it would still need work if the code were to be separated out again. But it could be if necessary.

The conversion to SVG has a fun limitation when it comes to the sprites. Sprites in a DrawFile have to be exported in a format that is likely to be renderable. Strictly they could be exported as RISC OS sprites - 'application/riscos; name="Sprite,ff9"' - although you'd be hard pressed to find any application that understood the parameter extension to MIME types, never mind one that understood the sprite file format.

Instead the converter creates a PNG and embeds it within the output file as data: scheme content - which wasn't supported that well within the HTML world, but more so in the SVG world. Any JPEGs within the DrawFile were embedded in a similar way, although didn't need to be converted first.

The fonts used have to be given a name, which was handled by the FontMap module. This converted the font name from RISC OS format to a foreign format, so that it could be used in the SVG output. I considered (and have notes about) using the glyph references to embed the font outline for the glyphs being used directly into the SVG. However, there are problems with that - not least it increases the file significantly. Additionally, though, the license for the font itself might not allow its distribution, in raw or derived forms. As such, it wasn't really possible to do that. If there was a way to indicate licenses for use then it might have helped, but as it could not be determined automatically, such generation wasn't possible.

The converter was useful in a few instances but, since vectors still weren't used widely in RISC OS, the scope for such conversions was limited. I was still quite pleased with it, though, as it expanded the capabilities of the ImageFileConvert system beyond the mostly bitmap operations it had been used for.

CompressJPEG

The CompressJPEG module provides functions for creating JPEGs from a stream of image data. The module had been introduced in RISC OS 3.6, and was used by a few clients including !ChangeFSI. The module had been updated to a more recent version of the base library in order to support rendering of more variants of JPEGs, and this also brought in more possibilities for file conversion. I'll talk more about this in the later Sprites ramble.

The module had been previously updated to make it easy to create a JPEG from plain sprite data (rather than requiring an intermediate conversion to an RGB buffer). This made it very easy to add an ImageFileConvert handler for creating a JPEG directly from a sprite. The main use for this was to allow any ImageFileConvert capable handler to be able to create a JPEG rather than a sprite. And because of the support for implicit conversion through intermediate formats, converting from other formats to JPEG was possible too - DrawFile to JPEG would be performed by an implicit DrawFile to Sprite and then Sprite to JPEG conversion.

The base IJG library code contains conversion code to convert a JPEG into GIF, Targa, BMP and PNM files. This code needed a little tweaking to make it suitable for use as an ImageFileConvert handler, but as all the code was similar, introducing these conversions was trivial. The advantage of these conversions was that it meant that there was no need for any intermediate conversion format - for example, for BMP conversion it wasn't necessary to convert to Sprite first then convert the sprite to a BMP.

These conversions were a little memory hungry at first, and could cause some problems when they were run in low memory situations. This was partly addressed by reorganising some of the code to allocate memory differently and to use intermediate buffers in dynamic areas so that fragmentation was reduced. The memory management code in the JPEG library was quite well organised and made handling such operations significantly easier than it would have been if all the memory functions were just raw allocation and free.

There were still issues with the quantisation for GIF, which was needed because GIF only supports 256 colours per frame - without reasonable quantisation the quality of the produced image will be poor. The results were generally quite good though.

For completeness, the CompressJPEG module was also updated to provide JPEG to Sprite conversion. This conversion would be slower than the built in JPEG to Sprite, which used the SpriteExtend JPEG conversion, but had the advantage of being able to decode the image without needing to perform a separate transcode step.