venerdì 3 ottobre 2014

Background threads

Studying background threads

These days FitPlot 5.1 is under the lens of the MAS examiners to be finally (hoping) approved.
In the meantime I want to write down, so I'll remember when needed, about the try to use a background thread.



I do not like the rainbow spinning cursor in my app. The user loose its control and have just to wait the end of the operation.
This usually appears in loops (for cycles) and the only way to avoid the user to panic (and ctrl-alt-esc the program), is to show the progress of the operation with a progress bar or a with a field showing what is happening.

This is what I have tried to do in the latest added feature in imposition (see  2-Up Saddle Stitch update).


The problem

I had a - loop in a loop - situation and a progress bar showing only the inner loop.
For example, in a 120 pages imposition, the pages grouped in 12 pages booklets, I had a first for cycle (from 1 to 10) that at each loop called the imposition procedure of the first 12 pages and so on.
The imposition procedure has its own loop showing a progress bar (from 1 to 12 in this case).
What the user was seeing while running was the rainbow spinning wheel and a progress bar going from 0 to 12 for twelve times.
The user knows that the program is not idle, but he does not have idea unless it counts the number of times the progress bar reaches the end.
So first idea was to couple the progress bar with a field showing the first loop cycle value (1/12 to 12/12 in this case).
I prepared the NIB, the necessary outlet was connected, program ok, but the field does not changed until the end of the loop taking suddenly the 12/12 value. Why?
NSLog testified that the values was changing in the loop but the field value was not updated until the end.


The answer

A search on the web led me to this discussion:

http://stackoverflow.com/questions/343088/nstextview-not-updating-in-a-loop-while-writing-to-a-usb-device

Where Chris Hanson answered textually:
"Mac OS X does not flush changes to views to the screen immediately, to avoid the flicker and tearing that's common in such situations. This means you can't just sit in a loop and perform blocking operations while you update a view; if you do this, neither the view nor anything else in your application's human interface will update, and your application will appear hung and eventually get the spinning cursor."

That was the problem! The next step was to use a background thread (as suggested) while updating the field on the main thread.


Implementation

Implementation of background threads it is straightforward:


            __block BOOL redraw = NO;

            dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                
                for (int i = 0; i < numeroCicli; i++)
                {
                    // Update the text field on the main queue
                    dispatch_async(dispatch_get_main_queue(), ^{
                    //Code here to which needs to update the UI in the UI thread goes here
                    [progressFiled setStringValue setStringValue: [NSString stringWithFormat:@"%i / %i",i+1, numeroCicli];

                    });
                    
                  
                    [self impose:sender
                             pdf:elem
                   startingFromY:newImpoSize.height*i*[groupImpo selectedTag]/[pagineFormatoImpo selectedTag]*2
                          redraw:redraw];
                    
                    //NSLog(@"giro %i di %i, da: %i a: %i", i, (int)numeroCicli, (int)[rangeImpoDa integerValue], (int)[rangeImpoA integerValue]);
                }
            });

Put your loop in the bracketed block, leave a dispatch in the main queue to update the field in the same bracketed way. Just remember to  __block needed variables declared outside the block and it is done.


Results

The result is that the spinning rainbow cursor disappear, the field is updated and the user maintains the control throughout all the time of the operation.


What? Has he the control?
Yes!
Can he change, move, delete objects?
Yes! What's wrong?
What's wrong? He can delete an object while another thread is using this same object!
This is utterly dangerous!


And the winner is…

At the end I learned a new powerful programming technique I didn't know, but I am not using it.
Instead I have, for this particular doble loop case, two independent progress bar, the first counting the cycles in the first loop and the second one for the second loop.
NSProgressBar bar are updated regularly while in a loop, where NSTextField is not.

You will see it in FitPlot 5.1.0, soon, I hope.


domenica 28 settembre 2014

Trim Marks at distance

FitPlot 5.1

Trim Mark Offset Implementation

As requested from a user, It has been easy to add an offset distance value to be used together when trim marks style is selected.

Trim marks are thin lines that when printed facilitate the cutting.


Now these thin lines can be distanced from the border of a user given value (in the current unit of measure).
Negative values move lines inside (sliding on relative border fixed distance).
Positive values move outside.

 offset = -1(cm)     offset = +1(cm)

To insert the trim marks offset value, it may be necessary to expand the info panel.
The field is enabled when trim marks style is selected.



Don't worry about packing or aligning, trim marks are considered in the bound of an image (since version 5).

Here's the code (called by - (void) drawElement: (NSRect) inRect inContext: (CGContextRef) contextCG in the FP_Image class).

#pragma mark DRAW STYLES
- (void) drawStyles
{
    NSInteger trueStyle = [self imageStyle];
    if ([self linkIsBroken]) //trattamento speciale per fileNotFound
        [self setImageStyle:2];
    
    [self impostaStile: imageStyle];
    
    //ricavo l'indice dash
    NSInteger iDash = styleDash/8-1;
    NSInteger iStroke = styleStroke/32-1;
    
    float scala = [self imageScale];
    float spessore = 0;
    if (iStroke >=0) spessore = [[[[NSApp delegate] strokes ] objectAtIndex: iStroke] floatValue] / scala;
    
    //trim
    if (styleTrim) {
        [[NSColor blackColor] set];
        NSBezierPath *path = [NSBezierPath bezierPath];
        
        float trimSliding = imageTrimOffset/scala;
        float trimOffset = imageTrimOffset > 0 ? trimSliding:0;
        
        // crocino alto / sx
        [path moveToPoint: NSMakePoint([self cropRect].origin.x-trimOffset-CM2PX(0.2)/scala,
                                       [self cropRect].origin.y-trimSliding)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x-trimOffset-CM2PX(0.7)/scala,
                                       [self cropRect].origin.y-trimSliding)];

        [path moveToPoint: NSMakePoint([self cropRect].origin.x-trimSliding,
                                       [self cropRect].origin.y-trimOffset-CM2PX(0.2)/scala)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x-trimSliding,
                                       [self cropRect].origin.y-trimOffset-CM2PX(0.7)/scala)];

        //crocino basso / sx
        [path moveToPoint: NSMakePoint([self cropRect].origin.x-trimOffset-CM2PX(0.2)/scala,
                                       [self cropRect].origin.y+trimSliding+[self cropRect].size.height)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x-trimOffset-CM2PX(0.7)/scala,
                                       [self cropRect].origin.y+trimSliding+[self cropRect].size.height)];
        
        [path moveToPoint: NSMakePoint([self cropRect].origin.x-trimSliding,
                                       [self cropRect].origin.y+trimOffset+[self cropRect].size.height+CM2PX(0.2)/scala)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x-trimSliding,
                                       [self cropRect].origin.y+trimOffset+[self cropRect].size.height+CM2PX(0.7)/scala)];

        // crocino alto / dx
        [path moveToPoint: NSMakePoint([self cropRect].origin.x+trimOffset+[self cropRect].size.width+CM2PX(0.2)/scala,
                                       [self cropRect].origin.y-trimSliding)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x+trimOffset+[self cropRect].size.width+CM2PX(0.7)/scala,
                                       [self cropRect].origin.y-trimSliding)];
        
        [path moveToPoint: NSMakePoint([self cropRect].origin.x+trimSliding+[self cropRect].size.width,
                                       [self cropRect].origin.y-trimOffset-CM2PX(0.2)/scala)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x+trimSliding+[self cropRect].size.width,
                                       [self cropRect].origin.y-trimOffset- CM2PX(0.7)/scala)];
        
        // crocino basso / dx
        [path moveToPoint: NSMakePoint([self cropRect].origin.x+[self cropRect].size.width+trimOffset+CM2PX(0.2)/scala,
                                       [self cropRect].origin.y+[self cropRect].size.height+trimSliding)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x+[self cropRect].size.width+trimOffset+CM2PX(0.7)/scala,
                                       [self cropRect].origin.y+[self cropRect].size.height+trimSliding)];
        
        [path moveToPoint: NSMakePoint([self cropRect].origin.x+trimSliding+[self cropRect].size.width,
                                       [self cropRect].origin.y+trimOffset+[self cropRect].size.height+CM2PX(0.2)/scala)];
        [path lineToPoint: NSMakePoint([self cropRect].origin.x+trimSliding+[self cropRect].size.width,
                                       [self cropRect].origin.y+trimOffset+[self cropRect].size.height+CM2PX(0.7)/scala)];

        [path setLineWidth: HAIRLINE/scala];
        [path stroke];
        
        NSBezierPath * tempPath = [NSBezierPath bezierPathWithRect:NSMakeRect(cropRect.origin.x-CM2PX(0.7)/scala-trimOffset,
                                                                              cropRect.origin.y-CM2PX(0.7)/scala-trimOffset,
                                                                              cropRect.size.width+2*CM2PX(0.7)/scala+trimOffset*2,
                                                                              cropRect.size.height+2*CM2PX(0.7)/scala+trimOffset*2)] ;

        NSAffineTransform * transform = [ NSAffineTransform transform] ;
        [transform appendTransform:localTF];
        [tempPath transformUsingAffineTransform:transform];
        
        [self setBounds:NSUnionRect([tempPath bounds], [self bounds])]; //aumento i bounds considerando le linee di taglio
    }
    
    if (iDash >= 0)
    {
        [[NSColor blackColor] set];
        NSBezierPath *contourRect = [NSBezierPath bezierPathWithRect:[self cropRect]];
        
        if (styleDash == 32) {
            CGFloat array [4];
            array[0] = [[[[NSApp delegate] dashes ] objectAtIndex: iDash] floatValue]/scala;
            array[1] =  [[[[NSApp delegate] dashgaps ] objectAtIndex: iDash] floatValue]/scala;
            array[2] = array[0]/4;
            array[3] =  array[1];
            [contourRect setLineDash: array count: 4 phase: 0.0];
        }
        else
        {
            CGFloat array [2];
            array[0] = [[[[NSApp delegate] dashes ] objectAtIndex: iDash] floatValue]/scala;
            array[1] =  [[[[NSApp delegate] dashgaps ] objectAtIndex: iDash] floatValue]/scala;
            [contourRect setLineDash: array count: 2 phase: 0.0];
        }
        
        [contourRect setLineWidth: spessore];
        [contourRect stroke];
    }
    
    if (iStroke >= 0 && iDash <0)
    {
        [[NSColor blackColor] set];
        NSBezierPath *contourRect = [NSBezierPath bezierPathWithRect:[self cropRect]];
        
        [contourRect setLineWidth: spessore];
        [contourRect stroke];
    }
    
    
    NSBezierPath * tempPath = [NSBezierPath bezierPathWithRect:NSMakeRect(cropRect.origin.x-spessore/2,
                                                                          cropRect.origin.y-spessore/2,
                                                                          cropRect.size.width+spessore,
                                                                          cropRect.size.height+spessore)] ;
    
    
    NSAffineTransform * transform = [ NSAffineTransform transform] ;
    //[ transform scaleBy: 1/imageGroupScale];

    [transform appendTransform:localTF];
    
    [tempPath transformUsingAffineTransform:transform];
    
    [self setBounds:NSUnionRect([tempPath bounds], [self bounds])];  //aumento i bounds considerando lo spessore
    //NSLog(@"setBounds in drawStyle");
    
    
    //NSGraphicsContext *contextStyle = [NSGraphicsContext currentContext];
    //[contextStyle saveGraphicsState];
    
    [self setImageStyle:trueStyle];
    
    //[contextStyle restoreGraphicsState];
}



venerdì 26 settembre 2014

2-Up Saddle Stitch update

FitPlot 5.1

2-Up Saddle Stitch

Think about a 100 pages PDF: you are able to use the 2-up Saddle imposition, then print front / back the result, but, when you goes to stitch, you realise that the pages thickness is too much to bend and stitch.
The user who suggest me to find a solution, had 991 pages to print grouped in booklets by 24 pages (to give to the bookbinder), so a solution had to be found…
Reverse page order

Technically speaking, the imposition is done by few actions / procedures:
Reverse page order
2-Up Saddle Stitch is an imposition mode that is present in FitPlot since the first imposition implementation (FitPlot 2.5, 2008-05-31).
It has remained unchanged until now, when, on a user suggestion, I have introduced the option to group the imposition in groups (booklets).


Of course, there has always been the possibility to impose from page 1 to page X then from page X to Y … and then from Z to the end, but…



- (IBAction) checkImpo:(id) sender
has the task to control the user input in the imposition dialog. Based on user input, checkImpo  calculates the number of empty pages to add (to complete the 24 pages booklet, in this case), to show the number of sheets needed etc.


- (IBAction) imposition:(id)sender
When Confirm is pressed, imposition is called. It is an intermediate action to leave things that worked unchanged (2-Up Perfect Bound and 2-Up Saddle Stitch with a sigle booklet as it was before).
The two old modes (just above mentioned) are then sent to the old action impose: (see below) while the 2-Up Saddle Stitch with booklet splitting has to enter a loop and at each cycle it calls impose: changing the Y (multiplying by the page vertical size).

- (IBAction) impose:(id) sender pdf:(FP_Image*) elem startingFromY:(float) Y

It works! Few changes in a well structured IBAction allowed to easily implement such useful new feature (and make happy Mr. Franco!).
The only problem, still unresolved, is that 991 pages means 41 loops of impose: and it is a pain to arrive at an end. Each impose: has its own undoManager and this cause a RAM and CPU payment in terms of process time.
But it works and, thanks to the 64 bit architecture, it doesn't crash (at least in my tests), just take a coffee while waiting.

Reverse page order

Asked from a friend from Pakistan, in its FitPlot customer review, it has been easy to introduce a reverse page order check box.

- (void) imponi: (NSInteger) sinistra
         destra: (NSInteger) destra
           dove: (NSPoint) quadrante
       centrato: (NSPoint) centro
     daElemento: (FP_Image*) elemento
   conRotazione: (BOOL) rotazione
    perLePagine: (NSRange) pagineEsistenti
     inversione:(BOOL)inverti

Just introduced the inversione:(BOOL)inverti variable in the core of the imposition task and the reversion is obtained.
For what I understand, inversion means that the last page become first and so all other pager are exchanged symmetrically, obtained by the following simple function:

- (NSInteger) inverti:(NSInteger) numero specchia:(BOOL)specchia da:(int)impoDa a:(int)impoA
{
    if (!specchia)
        return numero;
    
    return impoDa-1+impoA-1-numero;
}

I just need a confirmation that what I have done is right. The request was the following:
Arabic or Right hand book support - I think this is an affordable alternate to Imposition Studio. It lacks just right to left books support. Shibli313