iOS SDK - programowe generowanie pliku PDF

79

Używanie frameworka CoreGraphics to moim zdaniem żmudna praca, jeśli chodzi o programowe rysowanie pliku PDF.

Chciałbym programowo utworzyć plik PDF , używając różnych obiektów z widoków w całej mojej aplikacji.

Jestem zainteresowany, aby wiedzieć, czy są jakieś dobre samouczki PDF dla SDK iOS, być może spadek w bibliotece.

Widziałem ten samouczek, samouczek tworzenia plików PDF , ale został on głównie napisany w C. Poszukuję bardziej stylu Objective-C. Wydaje się to również śmiesznym sposobem zapisywania do pliku PDF, wymagającego obliczenia, gdzie zostaną umieszczone linie i inne obiekty.

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);
}

EDYCJA: Oto przykład na GitHub przy użyciu libHaru w projekcie iPhone'a.

WrightsCS
źródło
1
haha, wiem, że to jest stare, ale 100 stron pdf na iPhonie to nic. będzie to obciążać serwer bardziej niż urządzenie iOS, ponieważ serwer będzie musiał pomnożyć to przez liczbę jednoczesnych użytkowników próbujących to zrobić.
Armand

Odpowiedzi:

56

Kilka rzeczy ...

Po pierwsze, występuje błąd w generowaniu plików PDF w programie CoreGraphics w systemie iOS, który powoduje uszkodzenie plików PDF. Wiem, że ten problem występuje do wersji iOS 4.1 włącznie (nie testowałem iOS 4.2). Problem dotyczy czcionek i pojawia się tylko wtedy, gdy umieścisz tekst w pliku PDF. Objawem jest to, że podczas generowania pliku PDF w konsoli debugowania pojawią się błędy, które wyglądają następująco:

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

Trudność polega na tym, że wynikowy plik PDF będzie dobrze renderowany w niektórych czytnikach PDF, ale nie będzie renderowany w innych miejscach. Tak więc, jeśli masz kontrolę nad oprogramowaniem, które będzie używane do otwierania pliku PDF, możesz zignorować ten problem (np. Jeśli zamierzasz wyświetlać plik PDF tylko na komputerach iPhone lub Mac, powinieneś dobrze używać CoreGraphics). Jeśli jednak chcesz utworzyć plik PDF, który działa w dowolnym miejscu, powinieneś bliżej przyjrzeć się temu problemowi. Oto dodatkowe informacje:

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

Aby obejść ten problem, z powodzeniem korzystałem z libHaru na iPhonie jako zamiennika generowania plików PDF CoreGraphics. Początkowo przygotowanie libHaru do kompilacji z moim projektem było trochę trudne, ale kiedy już poprawnie skonfigurowałem projekt, działało dobrze dla moich potrzeb.

Po drugie, w zależności od formatu / układu pliku PDF, możesz rozważyć użycie programu Interface Builder do utworzenia widoku, który służy jako „szablon” dla pliku wyjściowego PDF. Następnie napisałbyś kod, aby załadować widok, wypełnić dowolne dane (np. Ustawić tekst dla etykiet UIL, itp.), A następnie renderować poszczególne elementy widoku do pliku PDF. Innymi słowy, wykorzystanie IB określić współrzędne, czcionek, zdjęć, etc. i napisać kod, aby czynią różne elementy (na przykład UILabel, UIImageViewetc.) w sposób ogólny, dzięki czemu nie trzeba ciężko kodem wszystkiego. Zastosowałem to podejście i wyszło świetnie na moje potrzeby. Ponownie, może to mieć sens lub nie, w zależności od potrzeb związanych z formatowaniem / układem pliku PDF.

EDYCJA: (odpowiedź na pierwszy komentarz)

Moja implementacja jest częścią produktu komercyjnego, co oznacza, że ​​nie mogę udostępnić pełnego kodu, ale mogę podać ogólny zarys:

Utworzyłem plik .xib z widokiem i zwymiarowałem widok na 850 x 1100 (mój plik PDF był skierowany na 8,5 x 11 cali, więc ułatwia to tłumaczenie do / ze współrzędnych czasu projektowania).

W kodzie ładuję widok:

- (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;
}

Następnie uzupełniam różne elementy. Użyłem tagów, aby znaleźć odpowiednie elementy, ale możesz to zrobić w inny sposób. Przykład:

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

Następnie wywołuję funkcję renderującą widok do pliku PDF. Ta funkcja rekurencyjnie przegląda hierarchię widoków i renderuje każdy widok podrzędny. W moim projekcie muszę obsługiwać tylko etykiety, ImageView i View (dla widoków zagnieżdżonych):

- (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];
        }
    }
}

Jako przykład, oto moja implementacja addImageView (funkcje HPDF_ pochodzą z 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);
        }
    }
}

Mam nadzieję, że to daje ci pomysł.

cbranch
źródło
Czy masz jakieś przykłady wykorzystania XIB jako szablonu?
WrightsCS
12

To późna odpowiedź, ale ponieważ bardzo zmagałem się z generowaniem plików PDF, uznałem, że warto podzielić się moimi poglądami. Zamiast grafiki Core, aby stworzyć kontekst, możesz również użyć metod UIKit do wygenerowania pliku PDF.

Firma Apple dobrze to udokumentowała w przewodniku rysowania i drukowania.

Bani Uppal
źródło
4
FWIW - strona z samouczkiem jest ślepa na GoDaddy.
CuriousRabbit,
8

Wszystkie funkcje PDF na iOS są oparte na CoreGraphics, co ma sens, ponieważ prymitywy rysowania w iOS są również oparte na CoreGraphics. Jeśli chcesz renderować prymitywy 2D bezpośrednio do pliku PDF, musisz użyć CoreGraphics.

Istnieje kilka skrótów do obiektów, które również znajdują się w UIKit, takich jak obrazy. Rysowanie PNG do kontekstu PDF nadal wymaga wywołania CGContextDrawImage, ale możesz to zrobić za pomocą CGImage, który możesz uzyskać z UIImage, np .:

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

Jeśli potrzebujesz więcej wskazówek na temat ogólnego projektu, sprecyzuj, co masz na myśli przez „różne obiekty w aplikacji” i co próbujesz osiągnąć.

Ben Zotto
źródło
Dziękuję za odpowiedź. Mówiąc o obiektach, rozumiem po prostu, że biorę tekst wprowadzony przez użytkownika i chcę umieścić je w różnych miejscach w pliku PDF. Jak już zauważyłeś, trochę grafiki. Coś jak formularz, który można przesłać pocztą elektroniczną.
WrightsCS
Cześć stary, mam pytanie: w ten sposób (CGContextDrawImage) plik PDF nie jest już wektorem. Co powinienem zrobić, jeśli chcę wyrenderować wiele grafik wektorowych na jednej stronie pliku PDF, w międzyczasie upewnij się, że plik PDF jest również w formacie wektorowym
Paradise
2

Spóźniony, ale prawdopodobnie pomocny dla innych. Wygląda na to, że styl działania szablonu PDF może być podejściem, które chcesz, zamiast budować plik PDF w kodzie. Ponieważ i tak chcesz wysłać to e-mailem, jesteś podłączony do sieci, więc możesz użyć czegoś takiego jak usługa w chmurze Docmosis , która jest w rzeczywistości usługą korespondencji seryjnej. Wyślij mu dane / obrazy, aby połączyć je z szablonem. Takie podejście ma tę zaletę, że jest dużo mniej kodu i odciąża większość przetwarzania z aplikacji na iPada. Widziałem to w aplikacji na iPada i było fajnie.

Paul Jowett
źródło