SDK de iOS: genere un archivo PDF mediante programación

79

Usar el marco CoreGraphics es un trabajo tedioso, en mi sincera opinión, cuando se trata de dibujar un archivo PDF mediante programación.

Me gustaría crear un PDF mediante programación , utilizando varios objetos de las vistas en toda mi aplicación.

Estoy interesado en saber si hay buenos tutoriales en PDF para el SDK de iOS, tal vez una caída en la biblioteca.

He visto este tutorial, Tutorial de creación de PDF , pero fue escrito principalmente en C. Buscando más estilo Objective-C. Esta también parece una forma ridícula de escribir en un archivo PDF, teniendo que calcular dónde se colocarán las líneas y otros objetos.

void CreatePDFFile (CGRect pageRect, const char *filename) 
{   
    // This code block sets up our PDF Context so that we can draw to it
    CGContextRef pdfContext;
    CFStringRef path;
    CFURLRef url;
    CFMutableDictionaryRef myDictionary = NULL;

    // Create a CFString from the filename we provide to this method when we call it
    path = CFStringCreateWithCString (NULL, filename,
                                      kCFStringEncodingUTF8);

    // Create a CFURL using the CFString we just defined
    url = CFURLCreateWithFileSystemPath (NULL, path,
                                         kCFURLPOSIXPathStyle, 0);
    CFRelease (path);
    // This dictionary contains extra options mostly for 'signing' the PDF
    myDictionary = CFDictionaryCreateMutable(NULL, 0,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

    CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("My PDF File"));
    CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("My Name"));
    // Create our PDF Context with the CFURL, the CGRect we provide, and the above defined dictionary
    pdfContext = CGPDFContextCreateWithURL (url, &pageRect, myDictionary);
    // Cleanup our mess
    CFRelease(myDictionary);
    CFRelease(url);
    // Done creating our PDF Context, now it's time to draw to it

    // Starts our first page
    CGContextBeginPage (pdfContext, &pageRect);

    // Draws a black rectangle around the page inset by 50 on all sides
    CGContextStrokeRect(pdfContext, CGRectMake(50, 50, pageRect.size.width - 100, pageRect.size.height - 100));

    // This code block will create an image that we then draw to the page
    const char *picture = "Picture";
    CGImageRef image;
    CGDataProviderRef provider;
    CFStringRef picturePath;
    CFURLRef pictureURL;

    picturePath = CFStringCreateWithCString (NULL, picture,
                                             kCFStringEncodingUTF8);
    pictureURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), picturePath, CFSTR("png"), NULL);
    CFRelease(picturePath);
    provider = CGDataProviderCreateWithURL (pictureURL);
    CFRelease (pictureURL);
    image = CGImageCreateWithPNGDataProvider (provider, NULL, true, kCGRenderingIntentDefault);
    CGDataProviderRelease (provider);
    CGContextDrawImage (pdfContext, CGRectMake(200, 200, 207, 385),image);
    CGImageRelease (image);
    // End image code

    // Adding some text on top of the image we just added
    CGContextSelectFont (pdfContext, "Helvetica", 16, kCGEncodingMacRoman);
    CGContextSetTextDrawingMode (pdfContext, kCGTextFill);
    CGContextSetRGBFillColor (pdfContext, 0, 0, 0, 1);
    const char *text = "Hello World!";
    CGContextShowTextAtPoint (pdfContext, 260, 390, text, strlen(text));
    // End text

    // We are done drawing to this page, let's end it
    // We could add as many pages as we wanted using CGContextBeginPage/CGContextEndPage
    CGContextEndPage (pdfContext);

    // We are done with our context now, so we release it
    CGContextRelease (pdfContext);
}

EDITAR: Aquí hay un ejemplo en GitHub usando libHaru en un proyecto de iPhone.

WrightsCS
fuente
1
jaja, sé que esto es antiguo, pero un pdf de 100 páginas en un iPhone no es nada. estresará más al servidor que al dispositivo iOS, ya que el servidor tendrá que multiplicar eso por el número de usuarios simultáneos que intentan hacerlo.
Armand

Respuestas:

56

Un par de cosas ...

Primero, hay un error con la generación de PDF de CoreGraphics en iOS que da como resultado archivos PDF corruptos. Sé que este problema existe hasta iOS 4.1 inclusive (no he probado iOS 4.2). El problema está relacionado con las fuentes y solo aparece si incluye texto en su PDF. El síntoma es que, al generar el PDF, verá errores en la consola de depuración que se ven así:

<Error>: can't get CIDs for glyphs for 'TimesNewRomanPSMT'

El aspecto complicado es que el PDF resultante se procesará bien en algunos lectores de PDF, pero no se procesará en otros lugares. Por lo tanto, si tiene control sobre el software que se usará para abrir su PDF, es posible que pueda ignorar este problema (por ejemplo, si solo tiene la intención de mostrar el PDF en los escritorios de iPhone o Mac, entonces debería estar bien usando CoreGraphics). Sin embargo, si necesita crear un PDF que funcione en cualquier lugar, debería analizar más de cerca este problema. Aquí hay información adicional:

http://www.iphonedevsdk.com/forum/iphone-sdk-development/15505-pdf-font-problem-cant-get-cids-glyphs.html#post97854

Como solución alternativa, he usado libHaru con éxito en iPhone como reemplazo de la generación de PDF de CoreGraphics. Fue un poco complicado lograr que libHaru compilara con mi proyecto inicialmente, pero una vez que lo configuré correctamente, funcionó bien para mis necesidades.

En segundo lugar, dependiendo del formato / diseño de su PDF, podría considerar usar Interface Builder para crear una vista que sirva como una "plantilla" para su salida PDF. Luego, escribiría código para cargar la vista, completaría cualquier dato (por ejemplo, establecer texto para UILabels, etc.), luego renderizaría los elementos individuales de la vista en el PDF. En otras palabras, el uso del IB para especificar las coordenadas, fuentes, imágenes, etc., y escribir código para prestar diversos elementos (por ejemplo, UILabel, UIImageView, etc.) de una manera genérica por lo que no tiene que codificar todo. Usé este enfoque y funcionó muy bien para mis necesidades. Nuevamente, esto puede o no tener sentido para su situación dependiendo de las necesidades de formato / diseño de su PDF.

EDITAR: (respuesta al primer comentario)

Mi implementación es parte de un producto comercial, lo que significa que no puedo compartir el código completo, pero puedo dar un resumen general:

Creé un archivo .xib con una vista y el tamaño de la vista era de 850 x 1100 (mi PDF tenía como objetivo 8.5 x 11 pulgadas, por lo que esto facilita la traducción hacia / desde las coordenadas de tiempo de diseño).

En código, cargo la vista:

- (UIView *)loadTemplate
{
    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ReportTemplate" owner:self options:nil];
    for (id view in nib) {
        if ([view isKindOfClass: [UIView class]]) {
            return view;
        }
    }

    return nil;
}

Luego completo varios elementos. Usé etiquetas para encontrar los elementos apropiados, pero podrías hacerlo de otras formas. Ejemplo:

UILabel *label = (UILabel *)[templateView viewWithTag:TAG_FIRST_NAME];
if (label != nil) {
    label.text = (firstName != nil) ? firstName : @"None";

Luego llamo a una función para representar la vista en el archivo PDF. Esta función recorre de forma recursiva la jerarquía de vistas y representa cada subvista. Para mi proyecto, necesito admitir solo Label, ImageView y View (para vistas anidadas):

- (void)addObject:(UIView *)view
{
    if (view != nil && !view.hidden) {
        if ([view isKindOfClass:[UILabel class]]) {
            [self addLabel:(UILabel *)view];
        } else if ([view isKindOfClass:[UIImageView class]]) {
            [self addImageView:(UIImageView *)view];
        } else if ([view isKindOfClass:[UIView class]]) {
            [self addContainer:view];
        }
    }
}

Como ejemplo, aquí está mi implementación de addImageView (las funciones HPDF_ son de libHaru):

- (void)addImageView:(UIImageView *)imageView
{
    NSData *pngData = UIImagePNGRepresentation(imageView.image);
    if (pngData != nil) {
        HPDF_Image image = HPDF_LoadPngImageFromMem(_pdf, [pngData bytes], [pngData length]);
        if (image != NULL) {
            CGRect destRect = [self rectToPDF:imageView.frame];

            float x = destRect.origin.x;
            float y = destRect.origin.y - destRect.size.height;
            float width = destRect.size.width;
            float height = destRect.size.height;

            HPDF_Page page = HPDF_GetCurrentPage(_pdf);
            HPDF_Page_DrawImage(page, image, x, y, width, height);
        }
    }
}

Ojalá te dé la idea.

cbranch
fuente
¿Tiene algún ejemplo de cómo utilizar un XIB como plantilla?
WrightsCS
12

Es una respuesta tardía, pero como luché mucho con la generación de PDF, consideré que valía la pena compartir mis puntos de vista. En lugar de gráficos básicos, para crear un contexto, también puede usar métodos UIKit para generar un pdf.

Apple lo ha documentado bien en la guía de dibujo e impresión.

Bani Uppal
fuente
4
FWIW: el sitio del tutorial está sin salida en Godaddy.
CuriousRabbit
8

Las funciones de PDF en iOS están todas basadas en CoreGraphics, lo cual tiene sentido, porque las primitivas de dibujo en iOS también están basadas en CoreGraphics. Si desea renderizar primitivas 2D directamente en un PDF, tendrá que usar CoreGraphics.

También hay algunos atajos para objetos que viven en UIKit, como imágenes. Dibujar un PNG en un contexto PDF aún requiere la llamada a CGContextDrawImage, pero puede hacerlo con CGImage que puede obtener de un UIImage, por ejemplo:

UIImage * myPNG = [UIImage imageNamed:@"mypng.png"];
CGContextDrawImage (pdfContext, CGRectMake(200, 200, 207, 385), [myPNG CGImage]);

Si desea más orientación sobre el diseño general, sea más explícito acerca de lo que quiere decir con "varios objetos en su aplicación" y lo que está tratando de lograr.

Ben Zotto
fuente
Gracias por la respuesta. Por objetos, simplemente me refiero a que estoy tomando texto ingresado por el usuario y quiero colocarlos en varias ubicaciones en el PDF. Y como has señalado, unos gráficos. Algo así como un formulario que se puede enviar por correo electrónico.
WrightsCS
Hola hombre, tengo una pregunta: de esta manera (CGContextDrawImage), el pdf ya no es vector. ¿Qué debo hacer si quiero renderizar múltiples gráficos vectoriales en una sola página de un pdf? Mientras tanto, asegúrese de que el pdf también sea vectorial
Paradise
2

Tarde, pero posiblemente útil para los demás. Parece que un estilo de operación de plantilla PDF podría ser el enfoque que desea en lugar de crear el PDF en código. Como desea enviarlo por correo electrónico de todos modos, está conectado a la red, por lo que podría usar algo como el servicio en la nube de Docmosis , que es efectivamente un servicio de combinación de correo. Envíele los datos / imágenes para fusionarlos con su plantilla. Ese tipo de enfoque tiene el beneficio de mucho menos código y descarga la mayor parte del procesamiento de su aplicación de iPad. Lo he visto usar en una aplicación para iPad y fue agradable.

Paul Jowett
fuente