1import sys 2 3import objc 4 5from Quartz import * 6import Quartz 7 8from LaunchServices import * # kUTType* constants 9 10import Utilities 11import DrawingBasics 12import AppDrawing 13 14#import Carbon.QT 15 16 17BEST_BYTE_ALIGNMENT = 16 18def COMPUTE_BEST_BYTES_PER_ROW(bpr): 19 return ((int(bpr) + BEST_BYTE_ALIGNMENT-1) & ~(BEST_BYTE_ALIGNMENT-1)) 20 21# We need to ensure that the raster data stays alive until we clean up 22# the context, store it here. 23_rasterDataForContext = {} 24 25def createRGBBitmapContext(width, height, wantDisplayColorSpace, needsTransparentBitmap): 26 # This routine allocates data for a pixel array that contains width*height 27 # pixels where each pixel is 4 bytes. The format is 8-bit ARGB or XRGB, depending on 28 # whether needsTransparentBitmap is true. In order to get the recommended 29 # pixel alignment, the bytesPerRow is rounded up to the nearest multiple 30 # of BEST_BYTE_ALIGNMENT bytes. 31 32 # Minimum bytes per row is 4 bytes per sample * number of samples. 33 bytesPerRow = width*4; 34 # Round to nearest multiple of BEST_BYTE_ALIGNMENT. 35 bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow); 36 37 # Allocate the data for the raster. The total amount of data is bytesPerRow 38 # times the number of rows. The function 'calloc' is used so that the 39 # memory is initialized to 0. 40 try: 41 rasterData = objc.allocateBuffer(int(bytesPerRow * height)) 42 except MemoryError: 43 return None 44 45 # The wantDisplayColorSpace argument passed to the function determines 46 # whether or not to use the display color space or the generic calibrated 47 # RGB color space. The needsTransparentBitmap argument determines whether 48 # create a context that records alpha or not. 49 if wantDisplayColorSpace: 50 cs = Utilities.getTheDisplayColorSpace() 51 else: 52 cs = Utilities.getTheCalibratedRGBColorSpace() 53 54 if needsTransparentBitmap: 55 transparency = kCGImageAlphaPremultipliedFirst 56 else: 57 transparency = kCGImageAlphaPremultipliedFirst 58 59 context = CGBitmapContextCreate(rasterData, width, height, 8, bytesPerRow, 60 cs, transparency) 61 if context is None: 62 return None 63 64 _rasterDataForContext[context] = rasterData 65 66 # Either clear the rect or paint with opaque white, depending on 67 # the needs of the caller. 68 if needsTransparentBitmap: 69 # Clear the context bits so they are transparent. 70 CGContextClearRect(context, CGRectMake(0, 0, width, height)) 71 72 else: 73 # Since the drawing destination is opaque, first paint 74 # the context bits to white. 75 CGContextSaveGState(context) 76 CGContextSetFillColorWithColor(context, Utilities.getRGBOpaqueWhiteColor()) 77 CGContextFillRect(context, CGRectMake(0, 0, width, height)) 78 CGContextRestoreGState(context) 79 80 return context 81 82def myCGContextGetBitmapInfo(c): 83 if hasattr(Quartz, 'CGBitmapContextGetBitmapInfo'): 84 return CGBitmapContextGetBitmapInfo(c) 85 else: 86 return CGBitmapContextGetAlphaInfo(c) 87 88# createImageFromBitmapContext creates a CGImageRef 89# from a bitmap context. Calling this routine 90# transfers 'ownership' of the raster data 91# in the bitmap context, to the image. If the 92# image can't be created, this routine frees 93# the memory associated with the raster. 94def createImageFromBitmapContext(c): 95 rasterData = _rasterDataForContext[c] 96 # We own the data, hence remove from the mapping 97 del _rasterDataForContext[c] 98 99 imageDataSize = CGBitmapContextGetBytesPerRow(c)*CGBitmapContextGetHeight(c) 100 101 if rasterData is None: 102 fprintf(stderr, "Context is not a bitmap context!") 103 104 # Create the data provider from the image data 105 dataProvider = CGDataProviderCreateWithData(None, 106 rasterData, 107 imageDataSize, 108 None) 109 if dataProvider is None: 110 print >>sys.stderr, "Couldn't create data provider!" 111 return None 112 113 # Now create the image. The parameters for the image closely match 114 # the parameters of the bitmap context. This code uses a NULL 115 # decode array and shouldInterpolate is true. 116 image = CGImageCreate( 117 CGBitmapContextGetWidth(c), 118 CGBitmapContextGetHeight(c), 119 CGBitmapContextGetBitsPerComponent(c), 120 CGBitmapContextGetBitsPerPixel(c), 121 CGBitmapContextGetBytesPerRow(c), 122 CGBitmapContextGetColorSpace(c), 123 myCGContextGetBitmapInfo(c), 124 dataProvider, 125 None, 126 True, 127 kCGRenderingIntentDefault) 128 129 if image is None: 130 print >>sys.stderr, "Couldn't create image!" 131 return None 132 return image 133 134 135def exportCGImageToFileWithQT(image, url, outputFormat, dpi): 136 """ 137 Export an image using QuickTime API's. 138 139 This function can't possibly work, as the relevant APIs aren't available 140 through MacPython. The code below is mostly there in case someone fixes 141 the MacPython QuickTime bindings. 142 """ 143 return 144 145 if outputFormat.lower() == kUTTypeTIFF.lower(): 146 imageExportType = kQTFileTypeTIFF; 147 148 elif outputFormat.lower() == kUTTypePNG.lower(): 149 imageExportType = kQTFileTypePNG; 150 151 elif outputFormat.lower() == kUTTypeJPEG.lower(): 152 imageExportType = kQTFileTypeJPEG; 153 154 else: 155 print >>sys.stderr, "Requested image export format %@s unsupported"%(outputFormat,) 156 return 157 158 result, dataRef, dataRefType = QTNewDataReferenceFromCFURL(url, 0, None, None) 159 if result == 0: 160 result, graphicsExporter = OpenADefaultComponent(GraphicsExporterComponentType, 161 imageExportType, graphicsExporter) 162 if result == 0: 163 result = GraphicsExportSetInputCGImage(graphicsExporter, image) 164 if result == 0: 165 result = GraphicsExportSetResolution(graphicsExporter, 166 FloatToFixed(dpi), FloatToFixed(dpi)); 167 if result == 0: 168 result = GraphicsExportSetOutputDataReference( 169 graphicsExporter, dataRef, dataRefType); 170 if result == 0: 171 result, sizeWritten = GraphicsExportDoExport( 172 graphicsExporter, None) 173 CloseComponent(graphicsExporter); 174 175 if dataRef: 176 DisposeHandle(dataRef); 177 178 if result: 179 print >>sys.stderr, "QT export got bad result = %d!"%(result,) 180 181 182def exportCGImageToFileWithDestination(image, url, outputFormat, dpi): 183 # Create an image destination at the supplied URL that 184 # corresponds to the output image format. The destination will 185 # only contain 1 image. 186 imageDestination = CGImageDestinationCreateWithURL(url, outputFormat, 1, None) 187 188 if imageDestination is None: 189 print >>sys.stderr, "Couldn't create image destination!" 190 return 191 192 # Create an options dictionary with the X&Y resolution of the image 193 options = { 194 kCGImagePropertyDPIWidth: dpi, 195 kCGImagePropertyDPIHeight: dpi, 196 } 197 198 # Add the image with the options dictionary to the destination. 199 CGImageDestinationAddImage(imageDestination, image, options); 200 201 # When all the images are added to the destination, finalize it. 202 CGImageDestinationFinalize(imageDestination); 203 204def MakeImageDocument(url, imageType, exportInfo): 205 # First make a bitmap context for a US Letter size 206 # raster at the requested resolution. 207 dpi = exportInfo.dpi; 208 width = int(8.5*dpi) 209 height = int(11*dpi) 210 211 # For JPEG output type the bitmap should not be transparent. If other types are added that 212 # do not support transparency, this code should be updated to check for those types as well. 213 needTransparentBitmap = (imageType.lower() != kUTTypeJPEG.lower()) 214 215 # Create an RGB Bitmap context using the generic calibrated RGB color space 216 # instead of the display color space. 217 useDisplayColorSpace = False; 218 c = createRGBBitmapContext(width, height, useDisplayColorSpace, needTransparentBitmap) 219 220 if c is None: 221 print >>sys.stderr, "Couldn't make destination bitmap context" 222 return memFullErr; 223 224 # Scale the coordinate system based on the resolution in dots per inch. 225 CGContextScaleCTM(c, dpi/72, dpi/72); 226 227 # Set the font smoothing parameter to false since it's better to 228 # draw any text without special LCD text rendering when creating 229 # rendered data for export. 230 if hasattr(Quartz, 'CGContextSetShouldSmoothFonts'): 231 CGContextSetShouldSmoothFonts(c, False) 232 233 # Set the scaling factor for shadows. This is a hack so that 234 # drawing code that needs to know the scaling factor can 235 # obtain it. Better would be that DispatchDrawing and the code 236 # it calls would take this scaling factor as a parameter. 237 Utilities.setScalingFactor(dpi/72) 238 239 # Draw into that raster... 240 AppDrawing.DispatchDrawing(c, exportInfo.command) 241 242 # Set the scaling factor back to 1.0. 243 Utilities.setScalingFactor(1.0) 244 245 # Create an image from the raster data. Calling 246 # createImageFromBitmapContext gives up ownership 247 # of the raster data used by the context. 248 image = createImageFromBitmapContext(c); 249 250 # Release the context now that the image is created. 251 del c 252 253 if image is None: 254 # Users of this code should update this to be an error code they find useful. 255 return memFullErr 256 257 # Now export the image. 258 if exportInfo.useQTForExport: 259 exportCGImageToFileWithQT(image, url, imageType, exportInfo.dpi) 260 else: 261 exportCGImageToFileWithDestination(image, url, imageType, exportInfo.dpi) 262 263 264def MakeTIFFDocument(url, exportInfo): 265 return MakeImageDocument(url, kUTTypeTIFF, exportInfo) 266 267def MakePNGDocument(url, exportInfo): 268 return MakeImageDocument(url, kUTTypePNG, exportInfo) 269 270def MakeJPEGDocument(url, exportInfo): 271 return MakeImageDocument(url, kUTTypeJPEG, exportInfo) 272 273def createCGLayerForDrawing(c): 274 rect = CGRectMake(0, 0, 50, 50) 275 276 # Make the layer the size of the rectangle that 277 # this code draws into the layer. 278 layerSize = rect.size 279 280 # Create the layer to draw into. 281 layer = CGLayerCreateWithContext(c, layerSize, None); 282 if layer is None: 283 return None 284 285 # Get the context corresponding to the layer. 286 layerContext = CGLayerGetContext(layer) 287 if layerContext is None: 288 return None 289 290 #$ Set the fill color to opaque black. 291 CGContextSetFillColorWithColor(layerContext, Utilities.getRGBOpaqueBlackColor()) 292 293 # Draw the content into the layer. 294 CGContextFillRect(layerContext, rect) 295 296 # Now the layer has the contents needed. 297 return layer 298 299def doSimpleCGLayer(context): 300 # Create the layer. 301 layer = createCGLayerForDrawing(context) 302 303 if layer is None: 304 print >>sys.stderr, "Couldn't create layer!" 305 return 306 307 # Get the size of the layer created. 308 s = CGLayerGetSize(layer) 309 310 # Clip to a rect that corresponds to 311 # a grid of 8x8 layer objects. 312 CGContextClipToRect(context, CGRectMake(0, 0, 8*s.width, 8*s.height)) 313 314 # Paint 8 rows of layer objects. 315 for j in range(8): 316 CGContextSaveGState(context) 317 # Paint 4 columns of layer objects, moving 318 # across the drawing canvas by skipping a 319 # square on the grid each time across. 320 for i in range(4): 321 # Draw the layer at the current origin. 322 CGContextDrawLayerAtPoint(context, 323 CGPointZero, 324 layer); 325 # Translate across two layer widths. 326 CGContextTranslateCTM(context, 2*s.width, 0); 327 328 CGContextRestoreGState(context) 329 # Translate to the left one layer width on 330 # even loop counts and to the right one 331 # layer width on odd loop counts. Each 332 # time through the outer loop, translate up 333 # one layer height. 334 if j % 2: 335 CGContextTranslateCTM(context, 336 s.width, s.height) 337 else: 338 CGContextTranslateCTM(context, 339 -s.width, s.height) 340 341def createAlphaOnlyContext(width, height): 342 # This routine allocates data for a pixel array that contains 343 # width*height pixels, each pixel is 1 byte. The format is 344 # 8 bits per pixel, where the data is the alpha value of the pixel. 345 346 # Minimum bytes per row is 1 byte per sample * number of samples. 347 bytesPerRow = width; 348 # Round to nearest multiple of BEST_BYTE_ALIGNMENT. 349 bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow); 350 351 # Allocate the data for the raster. The total amount of data is bytesPerRow 352 #// times the number of rows. The function 'calloc' is used so that the 353 #// memory is initialized to 0. 354 try: 355 rasterData = objc.allocateBuffer(bytesPerRow * height); 356 except MemoryError: 357 return None 358 359 # This type of context is only available in Panther and later, otherwise 360 # this fails and returns a NULL context. The color space for an alpha 361 #// only context is NULL and the BitmapInfo value is kCGImageAlphaOnly. 362 context = CGBitmapContextCreate(rasterData, width, height, 8, bytesPerRow, 363 None, kCGImageAlphaOnly); 364 if context is None: 365 print >>sys.stderr, "Couldn't create the context!" 366 return None 367 368 _rasterDataForContext[context] = rasterData 369 370 # Clear the context bits so they are initially transparent. 371 CGContextClearRect(context, CGRectMake(0, 0, width, height)) 372 373 return context; 374 375# createMaskFromAlphaOnlyContext creates a CGImageRef 376# from an alpha-only bitmap context. Calling this routine 377# transfers 'ownership' of the raster data in the bitmap 378# context, to the image. If the image can't be created, this 379# routine frees the memory associated with the raster. 380 381def createMaskFromAlphaOnlyContext(alphaContext): 382 rasterData = _rasterDataForContext[alphaContext] 383 # We own the data, hence remove from the mapping 384 del _rasterDataForContext[alphaContext] 385 386 imageDataSize = CGBitmapContextGetBytesPerRow(alphaContext) * CGBitmapContextGetHeight(alphaContext) 387 invertDecode = [ 1.0, 0.0 ] 388 389 # Create the data provider from the image data. 390 dataProvider = CGDataProviderCreateWithData(None, 391 rasterData, 392 imageDataSize, 393 None) 394 395 if dataProvider is None: 396 print >>sys.stderr, "Couldn't create data provider!" 397 return None 398 399 mask = CGImageMaskCreate(CGBitmapContextGetWidth(alphaContext), 400 CGBitmapContextGetHeight(alphaContext), 401 CGBitmapContextGetBitsPerComponent(alphaContext), 402 CGBitmapContextGetBitsPerPixel(alphaContext), 403 CGBitmapContextGetBytesPerRow(alphaContext), 404 dataProvider, 405 # The decode is an inverted decode since a mask has the opposite 406 # sense than alpha, i.e. 0 in a mask paints 100% and 1 in a mask 407 # paints nothing. 408 invertDecode, 409 True) 410 411 if mask is None: 412 print >>sys.stderr, "Couldn't create image mask!" 413 return None 414 415 return mask 416 417def doAlphaOnlyContext(context): 418 # This code is going to capture the alpha coverage 419 # of the drawing done by the doAlphaRects routine. 420 # The value passed here as the width and height is 421 # the size of the bounding rectangle of that drawing. 422 width = 520 423 height = 400 424 alphaContext = createAlphaOnlyContext(width, height); 425 if context is None: 426 print >>sys.stderr, "Couldn't create the alpha-only context!" 427 return 428 429 # Draw the content to the alpha-only context, capturing 430 # the alpha coverage. The doAlphaRects routine paints 431 # a series of translucent red rectangles. 432 DrawingBasics.doAlphaRects(alphaContext) 433 434 # Finished drawing to the context and now the raster contains 435 # the alpha data captured from the drawing. Create 436 # the mask from the data in the context. 437 mask = createMaskFromAlphaOnlyContext(alphaContext); 438 # This code is now finshed with the context so it can 439 # release it. 440 del alphaContext 441 442 if mask is None: 443 return 444 445 # Set the fill color space. 446 CGContextSetFillColorSpace(context, Utilities.getTheCalibratedRGBColorSpace()); 447 opaqueBlue = ( 0.11, 0.208, 0.451, 1.0 ) 448 # Set the painting color to opaque blue. 449 CGContextSetFillColor(context, opaqueBlue); 450 # Draw the mask, painting the mask with blue. This colorizes 451 # the image to blue and it is as if we painted the 452 # alpha rects with blue instead of red. 453 CGContextDrawImage(context, CGRectMake(0, 0, width, height), mask); 454 455_pdfDoc = None 456_pdfURL = None 457_width = 0 458_height = 0 459def getThePDFDoc(url): 460 """ 461 This function caches a CGPDFDocumentRef for 462 the most recently requested PDF document. 463 """ 464 global _pdfDoc, _pdfURL, _width, _height 465 466 if url is None: 467 return None, 0, 0 468 469 # See whether to update the cached PDF document. 470 if _pdfDoc is None or url != _pdfURL: 471 # Release any cached document or URL. 472 _pdfDoc = CGPDFDocumentCreateWithURL(url); 473 if _pdfDoc is not None: 474 pdfMediaRect = CGPDFDocumentGetMediaBox(_pdfDoc, 1) 475 _width = pdfMediaRect.size.width; 476 _height = pdfMediaRect.size.height; 477 # Keep the URL of the PDF file being cached. 478 _pdfURL = url 479 480 else: 481 _pdfURL = None 482 483 if _pdfDoc is not None: 484 return _pdfDoc, _width, _height 485 486 else: 487 return None, 0, 0 488 489# Defining this scales the content down by 1/3. 490DOSCALING=True 491 492def TilePDFNoBuffer(context, url): 493 # The amount of area to tile should really be based on the 494 # window/document. Here it is hard coded to a US Letter 495 # size document. This may draw too many or too few tiles 496 # for the area actually being filled. 497 fillwidth = 612.0 498 fillheight = 792.0 499 extraOffset = 6.0 500 pdfDoc, tileX, tileY = getThePDFDoc(url) 501 if pdfDoc is None: 502 print >>sys.stderr, "Couldn't get the PDF document!" 503 return 504 505 if DOSCALING: 506 # Make the tiles 1/3 the size of the PDF document. 507 tileX /= 3 508 tileY /= 3 509 extraOffset /= 3 510 511 # Space the tiles by the tile width and height 512 # plus extraOffset units in each dimension. 513 tileOffsetX = extraOffset + tileX; 514 tileOffsetY = extraOffset + tileY; 515 516 # Tile the PDF document. 517 for h in range(0, int(fillheight), int(tileOffsetY)): 518 for w in range(0, int(fillwidth), int(tileOffsetX)): 519 CGContextDrawPDFDocument(context, 520 CGRectMake(w, h, tileX, tileY), pdfDoc, 1); 521 522def TilePDFWithOffscreenBitmap(context, url): 523 # Again this should really be computed based on 524 # the area intended to be tiled. 525 fillwidth = 612.0 526 fillheight = 792.0 527 extraOffset = 6.0 528 529 pdfDoc, tileX, tileY = getThePDFDoc(url) 530 if pdfDoc is None: 531 print >>sys.stderr, "Couldn't get the PDF document" 532 return 533 534 if DOSCALING: 535 # Make the tiles 1/3 the size of the PDF document. 536 tileX /= 3 537 tileY /= 3 538 extraOffset /= 3 539 540 # Space the tiles by the tile width and height 541 # plus extraOffset units in each dimension. 542 tileOffsetX = extraOffset + tileX; 543 tileOffsetY = extraOffset + tileY; 544 545 # Since the bitmap context is for use with the display 546 # and should capture alpha, these are the values 547 # to pass to createRGBBitmapContext. 548 useDisplayColorSpace = True; 549 needTransparentBitmap = True; 550 bitmapContext = createRGBBitmapContext(tileX, tileY, 551 useDisplayColorSpace, 552 needTransparentBitmap); 553 if bitmapContext is None: 554 print >>sys.stderr, "Couldn't create bitmap context!" 555 return 556 557 # Draw the PDF document one time into the bitmap context. 558 CGContextDrawPDFDocument(bitmapContext, 559 CGRectMake(0, 0, tileX, tileY), pdfDoc, 1); 560 561 # Create an image from the raster data. Calling 562 # createImageFromBitmapContext gives up ownership 563 # of the raster data used by the context. 564 image = createImageFromBitmapContext(bitmapContext); 565 566 # Release the context now that the image is created. 567 del bitmapContext 568 569 if image is None: 570 return 571 572 # Now tile the image. 573 for h in range(0, int(fillheight), int(tileOffsetY)): 574 for w in range(0, int(fillwidth), int(tileOffsetX)): 575 CGContextDrawImage(context, CGRectMake(w, h, tileX, tileY), image) 576 577 578def createLayerWithImageForContext(c, url): 579 layerSize = CGSize() 580 pdfDoc, layerSize.width, layerSize.height = getThePDFDoc(url) 581 if pdfDoc is None: 582 return None 583 584 if DOSCALING: 585 # Make the layer 1/3 the size of the PDF document. 586 layerSize.width /= 3 587 layerSize.height /= 3 588 589 # Create the layer to draw into. 590 layer = CGLayerCreateWithContext(c, layerSize, None) 591 if layer is None: 592 return NULL 593 594 # Get the context corresponding to the layer. Note 595 # that this is a 'Get' function so the code must 596 # not release the context. 597 layerContext = CGLayerGetContext(layer) 598 if layerContext is None: 599 return None 600 601 # Draw the PDF document into the layer. 602 CGContextDrawPDFDocument(layerContext, 603 CGRectMake(0, 0, layerSize.width, layerSize.height), pdfDoc, 1); 604 605 # Now the layer has the contents needed. 606 return layer 607 608def TilePDFWithCGLayer(context, url): 609 # Again this should really be computed based on 610 # the area intended to be tiled. 611 fillwidth = 612.0 612 fillheight = 792.0 613 layer = createLayerWithImageForContext(context, url) 614 if layer is None: 615 print >>sys.stderr, "Couldn't create the layer!" 616 return 617 618 # Compute the tile size and offset. 619 s = CGLayerGetSize(layer); 620 tileX = s.width 621 tileY = s.height 622 623 if DOSCALING: 624 # Space the tiles by the tile width and height 625 # plus an extra 2 units in each dimension. 626 tileOffsetX = 2.0 + tileX; 627 tileOffsetY = 2.0 + tileY; 628 else: 629 # Add 6 units to the offset in each direction 630 # if there is no scaling of the source PDF document. 631 tileOffsetX = 6. + tileX; 632 tileOffsetY = 6. + tileY; 633 634 # Now draw the contents of the layer to the context. 635 # The layer is drawn at its true size (the size of 636 # the tile) with its origin located at the corner 637 # of each tile. 638 for h in range(0, int(fillheight), int(tileOffsetY)): 639 for w in range(0, int(fillwidth), int(tileOffsetX)): 640 CGContextDrawLayerAtPoint(context, CGPointMake(w, h), layer) 641