PARQORE Print Studio Plugin for NativePHP Mobile#
Native document printing and PDF generation for NativePHP Mobile apps — UIPrintInteractionController on iOS, PrintManager on Android — driven entirely from PHP.
Overview#
partek/print-studio gives a NativePHP Mobile app two things: a way to hand HTML, a Blade view, plain text, an existing PDF, or an existing image to the platform's own print UI, and a way to render the same sources to a PDF file on disk instead. Both are reached through one fluent builder:
PrintStudio::html('<h1>Invoice #1042</h1>')->paper(PaperSize::Letter)->print();
What this is not#
- Not a guarantee that paper came out. Neither iOS nor Android reliably reports that a physical printer finished a job —
PrintJobPresentedmeans the native print UI was shown to the user, not that ink hit paper. See Events and Platform behavior. - Not a thermal/ESC-POS receipt printer SDK.
PaperSize::Receipt58mm/Receipt80mmare page-size presets for the platform print UI and PDF rendering — they don't speak ESC/POS, don't discover Bluetooth/USB receipt printers, and don't drive a continuous paper feed directly. - Not a cloud print service. There's no queue, no remote printer directory, no email-to-print. Every job goes through whatever printer the OS print UI already knows about (AirPrint / a Mopria-compatible Android print service) or is rendered straight to a local PDF file.
Installation#
composer require partek/print-studio
Register the plugin:
php artisan native:plugin:register partek/print-studio
If you haven't published NativePHP's plugin provider yet:
php artisan vendor:publish --tag=nativephp-plugins-provider
Optionally publish the config file if you want to change storage_root, output_subdirectory, or the size limits:
php artisan vendor:publish --tag=print-studio-config
Rebuild after installing or after a manifest change:
php artisan native:run
No Android permissions and no iOS Info.plist entries are required — printing needs no runtime permission grant on either platform, and nativephp.json declares an empty permission/info_plist set for both.
Usage#
print() and toPdf() are asynchronous — read this first#
PrintJobBuilder::print() and ::toPdf() both return bool, not a result. The return value only tells you whether the native bridge accepted the call — not whether the user actually printed, cancelled, or whether the PDF finished writing. The OS print dialog is an interactive, indefinite-duration UI (the user might leave it open for minutes), and native HTML rendering has no synchronous "done" signal either — so there is nothing for these methods to return but "did the request get through."
use PARTek\PrintStudio\Facades\PrintStudio; $accepted = PrintStudio::html('<h1>Invoice #1042</h1>')->print(); // $accepted === true means only "the native side took the job." It is NOT// "the document printed." For the real outcome, listen for the events below —// PrintJobPresented, PrintJobCompleted, PrintJobCancelled, or PrintJobFailed.if (! $accepted) { // The call never reached native code at all (see Troubleshooting).}
The same is true for toPdf(): it returns whether native accepted the render request, not the final byte size or page count — those arrive on PdfGenerated.
A full, listener-driven example:
use Native\Mobile\Attributes\On;use PARTek\PrintStudio\Enums\Orientation;use PARTek\PrintStudio\Enums\PaperSize;use PARTek\PrintStudio\Events\PrintJobCancelled;use PARTek\PrintStudio\Events\PrintJobCompleted;use PARTek\PrintStudio\Events\PrintJobFailed;use PARTek\PrintStudio\Events\PrintJobPresented;use PARTek\PrintStudio\Facades\PrintStudio; class InvoiceScreen extends NativeComponent{ public bool $printing = false; public ?string $status = null; public function printInvoice(): void { $this->printing = PrintStudio::html($this->renderInvoiceHtml()) ->jobName('Invoice #1042') ->paper(PaperSize::Letter) ->orientation(Orientation::Portrait) ->margins(24) ->print(); // $this->printing here is only "was the request accepted," not // "the print dialog is open." The events below carry the real state. } #[On(PrintJobPresented::class)] public function onPresented(string $jobId, string $jobName): void { $this->status = "{$jobName}: print dialog shown"; } #[On(PrintJobCompleted::class)] public function onCompleted(string $jobId, string $jobName): void { $this->printing = false; $this->status = "{$jobName}: done"; } #[On(PrintJobCancelled::class)] public function onCancelled(string $jobId, string $jobName): void { $this->printing = false; $this->status = "{$jobName}: cancelled by user"; } #[On(PrintJobFailed::class)] public function onFailed(string $jobId, string $jobName, string $errorCode, string $errorMessage): void { $this->printing = false; $this->status = "{$jobName}: failed ({$errorMessage})"; }}
html()->print()#
PrintStudio::html('<h1>Invoice #1042</h1><p>Total: $128.00</p>') ->jobName('Invoice #1042') ->paper(PaperSize::Letter) ->margins(24) ->print();
view()->toPdf()#
view() renders a Blade view to an HTML string in PHP (via Illuminate\View) and hands that HTML to the same builder html() does — by the time a job reaches native code it's always plain HTML, there's no separate "Blade" wire type.
PrintStudio::view('invoices.print', ['invoice' => $invoice]) ->jobName("Invoice #{$invoice->number}") ->paper(PaperSize::A4) ->toPdf("invoice-{$invoice->number}.pdf");
pdf()->print()#
Prints an existing local PDF file. The path is validated by PathGuard (see Security) before the builder is even returned — an unsafe or missing path throws immediately, in PHP, before any native call.
PrintStudio::pdf('app/downloads/contract-signed.pdf') ->jobName('Signed Contract') ->copies(2) ->print();
image()->fit()->print()#
Prints an existing local PNG/JPEG. fit() only applies to an image source — calling it after html()/pdf()/file() throws InvalidPrintableSourceException rather than silently doing nothing.
use PARTek\PrintStudio\Enums\ImageFit; PrintStudio::image('app/labels/shipping-label.png') ->fit(ImageFit::ActualSize) ->paper(PaperSize::ShippingLabel4x6) ->print();
ImageFit::Fit (default) and ImageFit::Fill both preserve aspect ratio — Fit letterboxes to stay fully visible, Fill crops to cover the page. ImageFit::ActualSize applies no scaling at all and may overflow the page.
text()->print()#
Wraps a raw string as escaped, print-safe HTML — the string is never interpreted as markup, so this is the one printable-source path that guarantees its input can't inject script or markup into the rendered document.
PrintStudio::text("Pickup code: 48213\nReady in 10 minutes") ->paper(PaperSize::Receipt58mm) ->print();
file() — an existing local HTML file#
PrintStudio::file('app/exports/report.html')->paper(PaperSize::Legal)->print();
Custom page size#
PrintStudio::html($labelHtml) ->customSize(widthPoints: 216, heightPoints: 360) // selects PaperSize::Custom ->print();
Cancelling a job#
PrintStudio::cancel($jobId); // bool — accepted, not confirmed; watch for PrintJobCancelled
JavaScript (Vue / React / Inertia)#
resources/js/index.js mirrors the PHP builder for apps that never touch a PHP request cycle for printing — it computes page dimensions itself (PAPER_SIZES, matching PaperSize::dimensions() point-for-point), so a pure-JS/Inertia screen never needs a PHP round trip just to print.
import { printHtml, printPdfFile, printImageFile, generatePdf, cancel, Events } from '../../vendor/partek/print-studio/resources/js/index.js'; // Same async-acceptance contract as the PHP builder — the resolved value is// "native accepted the call," not "printing finished."await printHtml('<h1>Invoice #1042</h1>', { jobName: 'Invoice #1042', paper: 'letter', margins: 24 }); await printPdfFile('app/downloads/contract-signed.pdf', { copies: 2 }); await printImageFile('app/labels/shipping-label.png', 'actual_size', { paper: 'shipping_label_4x6' }); await generatePdf('<h1>Invoice #1042</h1>', 'invoice-1042.pdf', { paper: 'a4' }); await cancel(jobId);
Each call POSTs to /_native/api/call with an X-CSRF-TOKEN header (read from <meta name="csrf-token">) and throws on an HTTP error or a {status: 'error'} response.
Listening for the outcome events from JS follows NativePHP Mobile's normal native-event mechanism — the Events export gives you the fully-qualified PHP class name string each event is dispatched under, for whatever event-bridging your app already uses (e.g. broadcasting the PHP event to the frontend). The events themselves are always dispatched in PHP (see Events) — this plugin does not fire a separate JS-only event.
Events#
| Event | Fires | Properties |
|---|---|---|
PrintJobCreated |
Synchronously in PHP, the moment print()/toPdf() is called — before any native call, so a listener sees every job that was attempted even if native never responds. |
job — a PARTek\PrintStudio\DTO\PrintJob (id, name, sourceType, outputPath), not a flat scalar list. |
PrintJobPresented |
The native print UI was shown to the user. Not confirmation that a physical printer produced paper. | jobId (string), jobName (string) |
PrintJobCompleted |
The print flow finished successfully from the OS's point of view. | jobId (string), jobName (string) |
PrintJobCancelled |
The user dismissed the native print UI without printing — a normal, first-class outcome, not an error. | jobId (string), jobName (string) |
PrintJobFailed |
The native side reported a failure. | jobId (string), jobName (string), errorCode (string), errorMessage (string, sanitized — never a raw platform exception or stack trace) |
PdfGenerated |
toPdf() finished writing the file. |
jobId (string), jobName (string), path (string), filename (string), mimeType (string, always application/pdf today), byteSize (int), pageCount (?int — null when native couldn't determine it, never fabricated) |
PdfGenerationFailed |
toPdf() failed. |
jobId (string), jobName (string), errorCode (string), errorMessage (string, sanitized) |
PrintJobCreated is the one exception to "flat scalars only" — it's dispatched directly in PHP from the already-constructed PrintJob object, so it carries that object whole. The other six are populated from a native event payload bound by parameter name (see NativeComponent::makeEventInstance() in nativephp/mobile), which only works with flat scalar constructor parameters — and every one of those six is verified to have a flat, no-nested-DTO constructor by this package's own test suite (tests/PluginTest.php).
Methods#
PrintStudio (facade)#
html(string $html): PrintJobBuilder
Starts a builder from a raw HTML string.
view(string $view, array $data = []): PrintJobBuilder
Renders a Blade view to HTML in PHP, then starts a builder exactly as html() would.
text(string $text): PrintJobBuilder
Escapes $text and wraps it in print-safe HTML (see Support\Text) — never interpreted as markup.
file(string $path): PrintJobBuilder
Starts a builder from an existing local HTML file. $path is resolved and validated by PathGuard before this method returns.
pdf(string $path): PrintJobBuilder
Starts a builder from an existing local PDF file, PathGuard-validated.
image(string $path, ImageFit $fit = ImageFit::Fit): PrintJobBuilder
Starts a builder from an existing local PNG/JPEG, PathGuard-validated. $fit can also be set/overridden later with ->fit().
cancel(string $jobId): bool
Requests cancellation of an in-flight job by the id from its PrintJobCreated event. Returns whether native accepted the cancellation request, not confirmation it stopped — watch for PrintJobCancelled.
PrintJobBuilder (returned by every method above)#
Each PrintStudio:: call above returns a fresh, unshared builder — PrintStudio::html($a) and PrintStudio::html($b) in the same request never interfere with each other.
jobName(string $name): static
Sets the job's display name. Default: 'Print Job'.
paper(PaperSize $paper): static
Sets the page size. Default: PaperSize::Letter.
iOS print() selects the closest paper that the system offers to the app. The size (minus margins) also sets the width your HTML is laid out at. On the physical iPad and HP ENVY used for verification, iPadOS passed only US Letter to the plugin even though A4 was requested, so the sheet remained on Letter. The user can choose another paper when iPadOS exposes it. toPdf() is unaffected: it renders at exactly the size you request. Android maps the named paper sizes to the platform's recognized media identifiers so the system sheet can select them. See Platform behavior.
customSize(float $widthPoints, float $heightPoints): static
Selects PaperSize::Custom and sets its dimensions (in points) in one call. PaperSize::Custom alone has no fixed dimensions — this is the only way to give it any.
orientation(Orientation $orientation): static
Orientation::Portrait (default) or Orientation::Landscape. Landscape swaps the resolved width/height.
margins(PrintMargins|array|int|float $margins): static
A single number sets all four sides; an array (['top' => ..., 'right' => ..., 'bottom' => ..., 'left' => ...], missing sides default to 0) or a PrintMargins instance sets them individually. Default: PrintMargins::zero().
copies(int $copies): static
1–999. Default: 1. print() only — meaningless for toPdf() (a PDF has no copy count).
Neither platform applies this value. iOS: UIKit gives no way to preset the copy count, so the value is accepted but the user sets copies on the system print sheet. Android: PrintAttributes/PrintManager.print() has no public API to force a copy count either, so it's treated the same way — a preference left to the system print dialog, not a value the plugin can enforce (buildPrintAttributes() in PrintStudioModels.kt documents this in its own comment). Confirmed from both platforms' source; not tested on a device on either platform. See Platform behavior.
dpi(int $dpi): static
72–2400. toPdf() only — print() ignores it and uses the OS's own print resolution handling.
fit(ImageFit $fit): static
Image sources only. Throws InvalidPrintableSourceException if the builder's source isn't PrintStudio::image() — invalid usage fails predictably rather than being silently ignored.
print(): bool
Presents the native print UI. Asynchronous — see above. Returns whether the native call was accepted.
toPdf(string $filename, bool $overwrite = false): bool
Renders to a PDF at a PathGuard-resolved destination. Asynchronous, same contract as print(). Throws DestinationExistsException before any native call if the destination already exists and $overwrite is false.
Page sizes & margins#
All dimensions are in points (1/72 inch) — the unit both iOS's PDFKit/UIPrintInteractionController and Android's PdfDocument.PageInfo already use natively, so PrintStudio does no unit conversion on either platform.
PaperSize |
Label | Width × Height (points, portrait) |
|---|---|---|
Letter |
Letter | 612 × 792 |
Legal |
Legal | 612 × 1008 |
A4 |
A4 | 595 × 842 |
A5 |
A5 | 420 × 595 |
ShippingLabel4x6 |
4×6 Shipping Label | 288 × 432 |
Receipt58mm |
58mm Receipt | 164 × 595 |
Receipt80mm |
80mm Receipt | 227 × 842 |
Custom |
Custom | none — requires ->customSize($widthPoints, $heightPoints) |
orientation(Orientation::Landscape) swaps width and height for whichever size you picked.
Receipt presets use a fixed nominal length, not a true continuous roll. Receipt58mm/Receipt80mm give you a generous fixed page height (595pt / 842pt) rather than modeling an actually-continuous thermal roll — content longer than one nominal page paginates onto a second page instead of growing a single strip. If your receipt content is genuinely variable-length and you want one continuous page sized to fit it, use paper(PaperSize::Custom)->customSize($width, $height) with a height you compute yourself as the documented escape hatch.
customSize() requires both width and height (points), each greater than 0 and at most 5000 — passing only one, or a value outside that range, throws InvalidPageConfigurationException before anything reaches native code. Passing a custom width/height without also selecting PaperSize::Custom also throws.
Margins#
PrintMargins values are points, 0–288 (4 inches) per side — the 288pt ceiling is a sanity bound to catch an obvious unit mistake (e.g. passing pixels), not a platform limit.
use PARTek\PrintStudio\DTO\PrintMargins; PrintMargins::all(24.0); // all four sides = 24ptPrintMargins::symmetric(10.0, 20.0); // top/bottom = 10pt, left/right = 20ptPrintMargins::zero(); // no margins (the builder's default)PrintMargins::from(24); // same as ::all(24.0) — a bare number works tooPrintMargins::from(['top' => 10, 'left' => 5]); // right/bottom default to 0
Platform behavior#
The bridge function names, event names, and parameters below are the fixed contract between this package and the native plugin code (resources/ios/, resources/android/). Native implementations exist for both platforms and have been verified on real devices. On iOS (a physical iPad), toPdf() and print() both produced correct one-page output that was opened and inspected. On Android (a physical Galaxy A54, 1.1.1), toPdf() output was opened and measured: one-page and multi-page receipts and A4 documents up to 25 pages, with a 72pt box measuring exactly one inch at 96, 150 and 200 dpi.
| Behavior | iOS | Android |
|---|---|---|
| Print UI | UIPrintInteractionController (Apple's standard print/AirPrint sheet) |
PrintManager (the standard Android print dialog, compatible with Mopria print services) |
Paper size for print() |
paper() sets the layout width and asks iOS to select the closest paper it offers. With the tested HP ENVY selected, iPadOS offered only US Letter for an A4 request, so the sheet remained on Letter. toPdf() renders at exactly the requested size. |
Named papers use Android's recognized media identifiers. A physical Galaxy A54 opened on ISO A4 for an A4 request; custom and receipt sizes retain their exact dimensions. |
Copies for print() |
Not applied — the user sets the copy count on the print sheet (per the iOS source; not yet tested on a device) | Not applied — PrintAttributes/PrintManager.print() has no API to force a copy count, so it's a preference left to the print dialog (per the Android source; not yet tested on a device) |
| HTML rendering | The platform's native HTML rendering engine, used for layout only | The platform's native HTML rendering engine, used for layout only |
| PDF output | PdfResult/PdfGenerated report byte size and, when determinable, page count |
Same contract — pageCount is null rather than fabricated when the native layer can't determine it |
toPdf() from HTML |
Paginated by the system print renderer | Laid out at the page's content width with 1 CSS px = 1/96 inch on paper (as in a desktop browser), whatever the phone's screen density or font-size setting, then drawn at dpi(). Pages are cut by page height, so CSS page-break hints are not honored and a line can be split across two pages. The whole document is laid out at once: tested up to 25 A4 pages; split very long documents (100+ pages) into several PDFs. If rendering stalls, the job fails with rendering_failed after 10 s instead of never finishing. |
| Minimum OS version | 18.0 | API 26 |
| Extra permissions | None — printing needs no runtime permission on iOS | None — printing needs no runtime permission on Android (nativephp.json declares an empty permissions array) |
| "Printed" confirmation | Not available — PrintJobPresented only means the sheet was shown |
Not available — same caveat |
The exact rendering engine's edge-case behavior (CSS support, font substitution, how it paginates content taller than one page) has not been visually verified against real printed/PDF output on either platform — treat print output fidelity as design intent, not a verified guarantee until you've checked a real print/PDF against your own content. The Testing section below, by contrast, is fully accurate today — it exercises real, passing PHP code.
Security#
- Local paths are validated in PHP (
PathGuard) before any native print/PDF call. - Error messages returned to the app are sanitized — never raw platform exceptions or stack traces.
- This plugin does not handle API keys or shared secrets.
Report a vulnerability privately: email [email protected] with impact, repro steps, and plugin/NativePHP versions. Do not open a public GitHub issue. Please allow reasonable time for a fix before disclosure.
Testing#
There are two ways to test code that uses this plugin. PrintStudio::fake() replaces the facade, which suits service classes and plain PHP tests. The test helpers work with NativePHP's component test harness (Native::test()), which suits screens: the screen calls the real facade, the helpers answer the bridge the way the device does, and they deliver the native events.
Testing with the fake#
PrintStudio::fake() swaps the bound manager for FakePrintStudio, which records every print()/toPdf()/cancel() call instead of crossing the native bridge and reports success unless you configure a failing scenario. Path validation (PathGuard) still runs — it's plain PHP logic, not a native call — so the fake still catches an unsafe or missing path in a test.
use PARTek\PrintStudio\DTO\PrintJob;use PARTek\PrintStudio\Facades\PrintStudio; it('prints the invoice with the right job name', function () { PrintStudio::fake(); PrintStudio::html('<p>Invoice</p>')->jobName('Invoice 1042')->print(); PrintStudio::assertPrinted(fn (PrintJob $job) => $job->name === 'Invoice 1042');}); it('generates a PDF with the expected filename', function () { PrintStudio::fake(); PrintStudio::html('<p>Invoice</p>')->toPdf('invoice-1042.pdf'); PrintStudio::assertPdfGenerated('invoice-1042.pdf');}); it('reports failure when the native side rejects the job', function () { $fake = PrintStudio::fake()->failing(); expect(PrintStudio::html('<p>x</p>')->print())->toBeFalse(); $fake->assertPrinted(); // still recorded as attempted, even though it "failed"}); it('asserts nothing was printed', function () { PrintStudio::fake(); PrintStudio::assertNothingPrinted();});
| Assertion | Checks |
|---|---|
assertPrinted(?callable $callback = null) |
A print() call happened; the optional callback receives the resulting PrintJob to narrow the match. |
assertPdfGenerated(callable|string|null $filenameOrCallback = null) |
A toPdf() call happened. A plain string matches the destination's basename; a callback receives the PrintJob. |
assertCancelled(?string $jobId = null) |
A cancel() call happened; pass a job id to require that specific one. |
assertNothingPrinted() |
No print()/toPdf()/cancel() call was issued at all. |
->failing(bool $failing = true) (chainable off fake()) makes every subsequent call report failure, as if native rejected it — useful for testing your app's own failure-handling UI without a real device.
Test helpers#
On NativePHP Mobile 4.0 or later, the plugin registers these helpers on Native::fakeBridge() and on the Native::test() harness whenever the app runs under a test runner (APP_ENV=testing). Production boots never register them, and there is nothing extra to install.
use App\NativeComponents\InvoiceScreen;use Native\Mobile\Testing\Native; it('prints the invoice', function () { Native::fakeBridge()->withPrintStudio(); Native::test(InvoiceScreen::class) ->tap('Print') ->assertPrintRequested('Invoice #1042') ->presentPrintJob() ->completePrintJob() ->assertSee('Invoice #1042: done');});
Script the bridge first. With nothing scripted, the fake bridge answers null and the facade treats that as a rejected call: print(), toPdf() and cancel() return false. The bridge assertions would still pass while your screen runs its failure branch.
| Helper | Called on | What it does |
|---|---|---|
withPrintStudio() |
bridge | print(), toPdf() and cancel() are accepted. Each answers {accepted: true}, as the native side does. |
assertPrintRequested(?string $jobName = null, ?string $sourceType = null) |
bridge | print() reached the bridge, for any job or one with that job name and/or source type (html, html_file, pdf, image). |
assertPdfRequested(?string $filename = null) |
bridge | toPdf() reached the bridge, for any file or one whose destination file name is $filename. |
assertPrintCancelled(?string $jobId = null) |
bridge | cancel() reached the bridge, for any job or $jobId. |
presentPrintJob(?string $jobId = null) |
harness | Delivers PrintJobPresented: the print sheet was shown. |
completePrintJob(?string $jobId = null) |
harness | Delivers PrintJobCompleted. |
cancelPrintJob(?string $jobId = null) |
harness | Delivers PrintJobCancelled, as when the user dismisses the print sheet. |
failPrintJob(string $errorCode = 'failed', string $errorMessage = '', ?string $jobId = null) |
harness | Delivers PrintJobFailed. |
completePdfGeneration(?string $path = null, ?int $pageCount = 1, ?int $byteSize = null, ?string $jobId = null) |
harness | Delivers PdfGenerated. path defaults to the toPdf() destination (/tmp/document.pdf for a job id the bridge never saw), filename is its base name, mimeType is application/pdf, and byteSize defaults to the size of the file at path if one exists, else 0. Pass pageCount: null to leave the page count out, as iOS does when it can't read it. |
failPdfGeneration(string $errorCode = 'failed', string $errorMessage = '', ?string $jobId = null) |
harness | Delivers PdfGenerationFailed. |
Bridge helpers work on Native::fakeBridge() or straight off the harness. Called off the harness, they return the harness, so the chain continues.
- Job ids. The print helpers use the
job_idof the lastprint()call, and the PDF helpers the lasttoPdf()call, unless you pass$jobId. They fail with a message naming the helper if there was no such call.jobNamecomes from the call that started the job; for a job id the bridge never saw, it isPrint Job(the default job name). - Error codes. The default
failedis a placeholder, so pass the code your screen handles. Print failures:invalid_source,unsupported_source,rendering_failedandpresentation_failedon both platforms,print_unavailableandalready_presentingon iOS,print_service_unavailableandprint_failedon Android. PDF failures:invalid_source,unsupported_source,rendering_failedandwrite_failedon both, andcancelledon Android. - Cancelling a PDF. When
cancel()stops atoPdf()job, iOS sendsPrintJobCancelledand Android sendsPdfGenerationFailedwith codecancelled. UsecancelPrintJob($jobId)orfailPdfGeneration('cancelled', 'PDF generation was cancelled.')to match the platform you're testing. - With
PrintStudio::fake(). The fake replaces the facade, so nothing reaches the bridge while it is active: the bridge helpers see no calls, and the harness helpers can't find a job id. The harness helpers still work if you pass the id, for example one taken fromPrintJobCreated.
Troubleshooting#
toPdf() throws DestinationExistsException. The target file already exists. Pass overwrite: true if you intend to replace it: ->toPdf('invoice-1042.pdf', overwrite: true).
pdf()/image()/file() throws UnsafePathException. The path you gave resolves outside config('print-studio.storage_root') (which defaults to the app's whole storage_path() tree) — move the file somewhere inside it, or widen/adjust storage_root in config/print-studio.php if that's the intended location.
A destination toPdf() path throws UnsafePathException even though it looks like a normal filename. Destination filenames only allow A-Za-z0-9 _.- per path segment, and reject ../. segments and absolute paths outright — rename the file or strip unusual characters before passing it in.
print()/toPdf() returns false immediately. The native call was never accepted — check that the plugin is registered (php artisan native:plugin:register partek/print-studio) and that you've rebuilt (php artisan native:run) since installing or updating it. This is a PHP-side "the call didn't get through" signal, distinct from a PrintJobFailed/PdfGenerationFailed event, which means it did get through and then failed natively.
No printers available on the device. The OS print UI itself handles this (e.g. AirPrint discovery, Android's print service picker) — expect a PrintJobCancelled or PrintJobFailed depending on how the user backs out, rather than a distinct "no printers" event from this plugin.
fit() throws InvalidPrintableSourceException. fit() only applies to a builder started from PrintStudio::image() — remove the call, or switch the source, if you're printing HTML/PDF/text.
Performance & limits#
max_html_bytes(config, default 5 MiB) bounds an HTML string source (html()/view()) before it's allowed to cross the bridge —InvalidPrintableSourceExceptionif exceeded.max_image_dimension(config, default 8000) is present inconfig/print-studio.phpbut not enforced in this version: image sources are not size-checked by the plugin.- These limits protect the PHP side; equally, the native renderer should avoid holding multiple full documents in memory at once when handling large HTML/PDF/image sources or high
copiescounts — this remains project guidance rather than a device-verified property, since large-document memory behavior specifically hasn't been profiled on either platform yet.
Complete example#
See examples/ for full Blade/print-CSS templates (invoice, receipt, packing slip, 4×6 shipping label, event ticket) and a walkthrough of wiring each into PrintStudio::view(...)->paper(...)->print() or ->toPdf(...).
License#
Proprietary commercial. All rights reserved © 2026 PARTek. Unauthorized copying or redistribution is prohibited except under a written license with PARTek. The full license text ships in the LICENSE file included with this package (not linked from a private GitHub URL).