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.
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 . 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.
(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.
Disclaimer: By submitting comments through this form you are implicitly agreeing to allow its reproduction in the diary.