NSImage, diagrams, resolution

KenDRhyD

Registered
I have a number of questions concerning the specific code needed to dynamically generate, display, print, and save diagrams (images) in Cocoa/Objective-C. I am relatively new to Objective-C and Cocoa, and I am not fnding it easy to track down the source of information that would permit me to develop an understanding of how to solve these problems. I spent all of yesterday looking for answers on the web and, while I found some, I have not been able to solve all of the problems, nor do I understand all of the issues.

Problem 1: I want to dynamically generate a diagram in code and then either display it on the screen, print it, or save it to a disk file for display on a web site. The diagram in this instane is a series of lines and decorative text that represent a single- or double-elimination tournament for any sport (Little League Baseball in this case), so the lines show each game and how the winners advance to the final game and the chanpionship. Text on the diagram includes the tournament title and other information, the team names and game results, game labels, and instructions (such as "loser to D5"). I should note that I have written this program in other languages, it is my attempt to achieve this in Objective-C/Cocoa that is my stumbling block.

From several different programming models I am acustomed to their being present a set of classes/procedures that can be called to 'draw' into the current graphics object, and then methods that can capture the set of drawing results and display/print/save them. The basic set of tools are points, lines, rectangles, ovals, arcs, paths, and text.

My research has lead me to the NSImage class, which it would seem that I should be able to use to generate these diagrams. I even found some sample code that used NSImage to generate a small 'picture' and save it as a TIFF file; there were also some mentions of other formats, but the material is quite vahue at that point. I was able to create and save a diagram using the following template:

float resolution = 300.0;
_Diagram = [[NSImage alloc] initWithSize:NSMakeSize(8.5*resolution, 11.0*resolution)];
[_Diagram lockFocus];
// draw the diagram
[_Diagram unlockFocus];
NSData* tiffData = [_Diagram TIFFRepresentation];
[tiffData writeToFile:[NSString stringWithFormat:mad:"/Users/DiagramExperiment/diagram.tiff"] atomically:NO];

Unfortunately the only drawing commands I could find are NSBexierPath and [NSString drawAtPoint] and related methods. In order to draw a simple line I need to program it as a bezier path with two endpoints! This seems excessive.

Question 1: Are there other drawing tools aside from bezier paths and drawing strings?

Question 2: Scaling and sizing. I wanted my diagram to appear as if it were being drawn on a standard piece of paper, 8.5x11 inches, and so I size the image in that manner, but I multiply it by the desired resolution. Is this the appropriate method for generating the image at 300dpi? When I save the file it is 35.42x45.83 inches at 72dpi; how can I change the above code so the saved file will be 8.5x11 inches at 300dpi?

Question 3: If I add some code to the above that draws three (3) lines (3 pixels thick) and draws two strings ("Team 1" and "Team 2") before the unlockFocus, then the saved TIFF file is 32.1MB in size! The documentation on other representations are quite confusing, but my old (REALbasic) code saved much more complicated diagrams that were only 28KB in size (but it turns out that they were being saved only at 72dpi, which is probably fine for the web). How can I create the image so that it is 72dpi 8.5x11 on the screen and when saved to a file, but is 200dpi 8.5x11 when I want to print it?
 
NSBezierPath is the preferred drawing method in Cocoa. If you don't want that, you're stuck with OpenGL, unless you want to use legacy QuickDraw code, which is highly unrecommended.

First worry about how to draw your image. You'd want to play around with drawing into an NSView. From that, you can easily adapt it into an NSImage. To scale it, you can use NSAffineTransform.

To save it as a TIFF but using a lower compression, you can use the representationUsingType:properties: method on NSBitmapImageRep, which you can get from an NSImage using the representations method (returns an NSArray of NSBitmapImageReps).

Also might want to take a look at cocoadev.com - there's tons of code samples and discussion there about almost all drawing related topics.

And if you don't want to do it yourself, use the SM2DGraphView Framework :)

Good luck!
 
OK, so while the bezier code seems a great deal of complication compared to using simpler calls like LINE, RECTANGE and OVAL, I can figure out how to use the bezier stuff to do what I want. I already have some code that draws the lines where I want them to be, so I can build on that and develop the diagrams that I want.

I have figured out how to draw basic lines and text into the view, and I have even managed to draw the lines and text into an in-memory NSImage and then save it to disk as a TIFF image.

To scale it, you can use NSAffineTransform.
I had not seen this one, but now that I have I shall look into it more.

To save it as a TIFF but using a lower compression, you can use the representationUsingTyperoperties: method on NSBitmapImageRep, which you can get from an NSImage using the representations method (returns an NSArray of NSBitmapImageReps).
I had come across some hints on saving as JPEG or others using the NSBitmapImageRep classes, but I ahve not been able to get them to work and the documentation does not seem to match the hints. If I declare and create an NSImage instance named _Diagram and populate it with some drawing and the unlock the focus, the following code returns nil for the JPEG data:

Code:
	NSArray*	representations = [_Diagram representations];
	NSData*		jpegData = [NSBitmapImageRep representationOfImageRepsInArray:representations
																	usingType:NSJPEGFileType
																   properties:[NSDictionary dictionaryWithObject:[NSDecimalNumber numberWithFloat:1.0] forKey:NSImageCompressionFactor]];

The documentation indicates that the above will return the representation, but the implication seems to be that this should create the representation. Perhaps if I loaded the image from disk this might work, but on the NSImage that I recently created in memory it does not work. I keep feeling that I am missing something in the creation of the NSIMage object and that a few more steps would result in a fully capable image.

Also might want to take a look at cocoadev.com - there's tons of code samples and discussion there about almost all drawing related topics.
I have been through almost all of the related entries that I could find, but I could find no direct answers to my questions. There seem to be answers to related and tangental questions, but they all seem to come from a slightly different point of view and are based on a slightly different situation.

I do not wish to have to store the image to disk before I can properly control the display properties or printing. Since the display and the web are both at 72dpi, I can live with that, but printing needs a 300dpi resolution. It would seem that I should have either one version of the imageg at 300dpi and downsize it for display or saving to the web page, rather than define the image at 72dpi and attempting to upsize the image to 300dpi. This seems especially appropriate since the image/diagram will contain a mixture of simpe lines and text.

I could simply generate the image twice, once at each resolution, or I could generate it at 300dpi and downsize it, or I could only generate the image (at the resolution I want) when I want it and for the purpose that I want, although that sometimes is a wase of effort.

Unfortunately, the only documentation or examples I can find of creating an image from scratch, as opposed to editing an existing image loaded from a disk file, involve only the creation of the image at 72dpi. I came across no information on how to create the initial image, even in TIFF, at 300 dpi rather than 72dpi -- for the latter I simply create the image at (8.5x72)x(11.0x72) and that seems to work properly.

1. How can I create a TIFF image in memory using bezier paths and drawing strings where the resulting image will be 300dpi and 8.5x11 inches in size? Using an image size of (8.5x300)x(11.0x300) results in an image that is 2550x3300 pixels at 72dpi, so 35.42x45.83 inches in size.

2. How can I create an image in memory, presuming the TIFF format, and then save it directly into another format, such as JPEG?

3. How can I create an image in memory and then save it as a PDF file directly?

-ken
 
Given the current state of development at Apple/MacOS, and given that my applications only have to run on my system (at this time), if I want to develop Cocoa/Objective-C applications that (among other things) create image files that can be viewed, printed or saved as web pages, should I use NSImage and its related classes (not certain which 'foundation' this is), the Core Image routines, OpenGL, or some other foundation supported by MacOSX?

I know there are many factors that enter into making this decision, but I do not have enough informed opinion and awareness of the available toolsets to be able to make this decision.

-ken
 
NSBezierPath and associated classes use Quartz, I believe. If your app is going to be strictly Mac-based, then this is what you'll want to use. OpenGL is good at fast graphics (such as games), Quartz is fine for everything else.

For your above questions, I'm not sure about #1, but for #2, you simply need to take your code that is called from drawRect: in your NSView subclass, and put it <here>:
Code:
NSImage *myImage = [[NSImage alloc] initWithSize:someSize];
[myImage lockFocus];
<here>
[myImage unlockFocus];

// do whatever with myImage

[myImage release];
For #3, you can use the method dataWithPDFInsideRect: on an NSView. You can then write that data to file, or in Tiger you can play around with PDFs using the PDFKit framework (specifically, PDFDoc class).
 
I have tried what you suggested with the NSImage, and the only thing that I seem to be able to do with the resulting image is display it in a custom view or save it as TIFF. Attempts to retrieve the NSData for JPEG mode returns nil for the reference!

-ken
 
As I have mentioned before, the diagram that I create is being displayed, I just cannot save it to disk as anything other than TIFF -- it seems to have only one representation and the code return nil when I ask for any other rather than creating the new representation (and the documentation is sufficiently vague to imply this may be apptopriate -- you get back waht is stored there).

The code that I use to display the image is as follows:
Code:
	// Draw the diagram image, if present
	if (_Diagram)
	{
		NSRect		imageRect;
		NSRect		drawingRect;
		
		imageRect.origin.x = 100;
		imageRect.origin.y = 200;
		imageRect.size	 = [_Diagram size];
		drawingRect = imageRect;
		[_Diagram drawInRect:drawingRect
					fromRect:imageRect
				   operation:NSCompositeSourceOver
					fraction:1.0];
	}
Note that in this sample code the diagram is being drawn at full original size, and not being scaled to the size of the view.

The code that saves the image as uncompressed TIFF is as follows:
Code:
	NSData* tiffData = [_Diagram TIFFRepresentation];
	[tiffData writeToFile:[NSString stringWithFormat:@"/Users/DiagramExperiment/diagram.tiff"] atomically:NO];

The code that attempts to retrieve a JPEG representation of the generated image is as follows:
Code:
NSArray*	representations = [_Diagram representations];
NSData*		jpegData = [NSBitmapImageRep representationOfImageRepsInArray:representations
		usingType:NSJPEGFileType
		properties:[NSDictionary dictionaryWithObject:[NSDecimalNumber numberWithFloat:1.0] forKey:NSImageCompressionFactor]];
if (jpegData == nil)
{
	NSLog(@"JPEG data representation was not returned!");
}
else
{
	[jpegData writeToFile:[NSString stringWithFormat:@"/Users/DiagramExperiment/diagram.jpg"] atomically:NO];
}
When this executes, nil is always being returned for jpegData!

-ken
 
You could try sending nil for properties to see if that helps any.

You could also try:
Code:
NSBitmapImageRep *bmp = [[_Diagram representations] objectAtIndex:0];
NSData *jpegData = [bmp representationUsingType:NSJPEGFileType properties:nil];
and see if that helps any..
 
Any idea why the following code
Code:
[_Diagram setScalesWhenResized:YES];
[_Diagram setSize:[[self bounds] size]];

would cause these errors
Code:
warning: invalid receiver type 'NSRect'
error: cannot convert to a pointer type

As far as your suggested code is concerned, this generated a runtime exception:
Code:
#1	0x928f635c in +[NSException raise:format:]
on the line where jpegData is being defined.
 
kainjow said:
You could try sending nil for properties to see if that helps any.

You could also try:
Code:
NSBitmapImageRep *bmp = [[_Diagram representations] objectAtIndex:0];
NSData *jpegData = [bmp representationUsingType:NSJPEGFileType properties:nil];
and see if that helps any..

No difference whether the propertities are nil or not.
 
KenDRhyD said:
Any idea why the following code
Code:
[_Diagram setScalesWhenResized:YES];
[_Diagram setSize:[[self bounds] size]];

would cause these errors...
[self bounds] returns an NSRect, which is a struct, not an Objective-C object. That means the proper syntax is "[self bounds].size", not "[[self bounds] size]".
 
Mikuro said:
[self bounds] returns an NSRect, which is a struct, not an Objective-C object. That means the proper syntax is "[self bounds].size", not "[[self bounds] size]".

Yes, I finally figured that part out after hitting my head agaisnt the wall for several hours -- bruises hurt!

Still having no luck on producing an image at anything other than 72dpi!
 
Well, I have made some significant progress, and while I cannot yet create anything other than a TIFF file from my newly created image, I believe that I now understand why -- I just need to figure out a way around this.

If I create my NSImage, lock focus, draw the diagram and text I want (all at 72dpi), and then unlock the focus, a quick check reports that there is a single NSBitmapImageRep associated with the image, and displaying it using NSLog reveals the following:
Code:
NSCachedImageRep 0x350ff0 Size={612, 792} ColorSpace=NSCalibratedRGBColorSpace BPS=8 Pixels=612x792 Alpha=YES
Attempts to retrieve a JPEG image from this instance results in the following error being reported:
Code:
CGImageDestinationFinalize failed for output type 'public.jpeg'
If I then open the TIFF file that I created, there is still a single NSBitmapImageRep, but is is reported as
Code:
NSBitmapImageRep 0x3f9980 Size={612, 792} ColorSpace=NSCalibratedRGBColorSpace BPS=8 BPP=32 Pixels=612x792 Alpha=YES Planar=NO Format=0
and I am now able to extract a JPEG representation and save it as a file.

The difference seems to be that initially I have an NSCachedImageRep but once it is loaded from a disk file I have an actual NSBitmapImageRep -- so the question now is how I can convince my generated in-memory image to switch from cached to actual without saving the image to disk and reloading it!

--> Can anyone suggest how to overcome this issue?

--> Also, in order to set the size of the image, which is to represent a regular piece of paper, I set the size at 8.5*72 by 11.0*72, but then I have to continually to the math in all of my calculations for the positions of the endpoints for the lines and the text. Is there some way that I can set the scale so that my drawing commands are in inches and they get translated into pixels properly?

--> I am still at a complete loss as to how I can create an initial image at more than 72dpi. Since I am drawing text as well as lines on the diagram, upsampling to scale the image will result in terrible text. I want to draw the text at high resolution and downscale to 72dpi for display on the screen (the disk and printed versions should be at 288dpi (multiple of 72) or higher for best printing results of the text. Does anyone know of a way to overcome this?
 
Yes! Creating a new NSImage bases on the NSData* TIFFRepresentation of the generated one solves the problem -- I can even create it based on the compressed representation, and the size is reduced from 1.2MB to 20KB!

But I still cannot figure out how to get my TIFF image to be at 288dpi!
 
I'm a bit late to the main part of this discussion, and I haven't read it all thoroughly, but I think I can offer some help. Skip to the end of this post for a quick example that creates an image and saves it as a 200dpi 8.5x11 TIFF.

The NSRectFill(NSRect) function is the simplest way to draw a straight line or any kind of rect. AFAIK there are no similar functions for ovals or angled lines or anything else. Don't ask me why. *shrug*

I haven't experimented with it enough to say for sure, but you might find it useful to call [_Diagram setCacheMode:NSImageCacheNever]. I'm not sure exactly when this works. I assume if it will prevent an NSImage that uses an NSBitmapImageRep from caching its contents and reducing its resolution to 72dpi. But it won't stop new images created with initWithSize from using cached reps.

If you need to convert an NSImage from a cached type (or anything else) to a bitmap, you can initialize a new NSBitmapImageRep using NSBitmapImageRep's -(id)initWithFocusedViewRect:(NSRect)rect method. Just lockFocus on your NSImage (or NSView), and make a new NSBitmapImageRep with that method.

If you want to control resolution, you probably need to manipulate your NSImageRep rather than the NSImage. NSImageRep has three relevant methods: setSize, setPixelsHigh and setPixelsWide. setSize specifies the rendered size of it in units of the base coordinate system (72dpi). pixelsHigh and pixelsWide are the actual pixel dimensions. So if you wanted a 200dpi 8.5x11, pixelsWide should be 1700, pixelsHigh should be 2200, and size should be {612,792}.

Here's an example of how to create a high-res image and save it to a TIFF at 200dpi, using NSBitmapImageRep to rip the contents of an NSImage:

Code:
//create the image at full resolution
NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(1700,2200)] autorelease]; //8.5x11 x 200
//do some drawing
[image lockFocus];
[[NSColor blackColor] set];
NSRectFill(NSMakeRect(200,200,1300,1800)); //a black rect with 1-inch borders
//copy the NSImage's contents into a new bitmapImageRep
NSBitmapImageRep *b = [[[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0,0,1700,2200)] autorelease];
//MAGIC LINE to set the resolution to 200dpi:
[b setSize:NSMakeSize(612,792)]; //612x792 = 8.5x11 @ 72dpi

[image unlockFocus];

//save the bitmap as a tiff
NSData *d = [b TIFFRepresentation];
[d writeToFile:@"/testFile.tiff" atomically:TRUE];

I'm not sure exactly why, but you can't use the vanilla -init method to create bitmapImageReps. My first idea was to create the bitmapImageRep, set its size, pixelsWide and pixelsHigh, and then create an empty NSImage and insert the rep. But with no plain -init method, I'm not sure how to go about that. Hmm.
 
Wahoo! This permits me to create a TIFF image at 288dpi!

Unfortunately there seem to be some side-effects that are not so pleasant, and I am wondering if I can achieve this in another, similar, manner.

First, it was already suggested that I could eliminate the cached aspect by initializing a new NSImage from the original one after I unlock the focus, and this is working very well. The new image even appears in the custom view after I draw it!

Based on this, do I need to create a new NSBitmapImageRep using the initWithFocusedViewRect before I unlock the focus? Or can I manipulate the NSBitmapImageRep that is part of the new NSImage that I create from the original after I unlock the focus? I am going to conduct some experiments, so I am really looking for information on what to expect as these classes seem to defy gravity at times.

One of the bad side effects is that text is now being drawn extremely tiny in my diagram -- basically at one quarter size, which makes sense since I have increased the diagram size from 612x792 (8.5x11 @ 72dpi) to 2448x3168 (@288dpi). So I increased the size of the fonts by a factor of 4 and that seemed to resolve that problem.

Now, however, the saved TIFF image, which even compressed has grown from 20KB to 140KB, which displays properly in the Preview application, does not display properly within my custom view! I suspect that the DPI is being ignored and the image is being displayed as if it were 72dpi, because if I scale the image (I am using some sample code that rescales the image as I drag the mouse to define a box) then it appears.

Do I need to convert the image to 72dpi before I attempt to view it on the screen?

I suspect that the best approach would be to generate the file at 72dpi for screen and web site use, but to regenerate the image at 288dpi for printing, but I was hopiing to generate the image once and use it for all three situations. I cannot scale it up from 72 to 288 dpi, because the text will suffer, but I do not want to store the larger sized image on the web, so I guess the two-image approach is best.
 
KenDRhyD said:
First, it was already suggested that I could eliminate the cached aspect by initializing a new NSImage from the original one after I unlock the focus, and this is working very well. The new image even appears in the custom view after I draw it!

Based on this, do I need to create a new NSBitmapImageRep using the initWithFocusedViewRect before I unlock the focus? Or can I manipulate the NSBitmapImageRep that is part of the new NSImage that I create from the original after I unlock the focus? I am going to conduct some experiments, so I am really looking for information on what to expect as these classes seem to defy gravity at times.
I'm not sure. Directly setting the size of an NSBitmapImageRep that's already inside an NSImage might cause issues, since the NSImage has its own Size property. But it might work. Or you could create a new image from the NSBitmapImageRep you get with initWithFocusedViewRect (see below).

Now, however, the saved TIFF image, which even compressed has grown from 20KB to 140KB, which displays properly in the Preview application, does not display properly within my custom view! I suspect that the DPI is being ignored and the image is being displayed as if it were 72dpi, because if I scale the image (I am using some sample code that rescales the image as I drag the mouse to define a box) then it appears.

Do I need to convert the image to 72dpi before I attempt to view it on the screen?
My example code never produced an NSImage suitable for onscreen display. However, it's easy to create one based on my above code. Just stick this at the end:
Code:
image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:b];
[image setCacheMode:NSImageCacheNever]; //this keeps it from turning into a downsampled NSCachedImageRep when it's drawn
The new NSImage will take its size from 'b', which is our 200dpi bitmap. This displays at 72dpi when drawn on screen, but retains its full 200dpi resolution on the inside.


I don't know if there's any way around the tiny-text problem besides simply increasing all your font sizes and coordinates to match the high resolution. A bit of a hassle, but I think that's just the way it is.

Hope this helps!
 
Back
Top