UNPKG

21.9 kBJavaScriptView Raw
1const Promise = require(`bluebird`)
2const {
3 GraphQLObjectType,
4 GraphQLList,
5 GraphQLBoolean,
6 GraphQLString,
7 GraphQLInt,
8 GraphQLFloat,
9 GraphQLNonNull,
10 GraphQLJSON,
11} = require(`gatsby/graphql`)
12const {
13 queueImageResizing,
14 base64,
15 fluid,
16 fixed,
17 traceSVG,
18 generateImageData,
19} = require(`gatsby-plugin-sharp`)
20
21const sharp = require(`./safe-sharp`)
22const fs = require(`fs-extra`)
23const imageSize = require(`probe-image-size`)
24const path = require(`path`)
25
26const DEFAULT_PNG_COMPRESSION_SPEED = 4
27
28const {
29 ImageFormatType,
30 ImageCropFocusType,
31 DuotoneGradientType,
32 PotraceTurnPolicyType,
33 PotraceType,
34 ImageFitType,
35 ImageLayoutType,
36 ImagePlaceholderType,
37} = require(`./types`)
38const { stripIndent } = require(`common-tags`)
39const { prefixId, CODES } = require(`./error-utils`)
40
41function toArray(buf) {
42 const arr = new Array(buf.length)
43
44 for (let i = 0; i < buf.length; i++) {
45 arr[i] = buf[i]
46 }
47
48 return arr
49}
50
51const getTracedSVG = async ({ file, image, fieldArgs, cache, reporter }) =>
52 traceSVG({
53 file,
54 args: { ...fieldArgs.traceSVG },
55 fileArgs: fieldArgs,
56 cache,
57 reporter,
58 })
59
60const fixedNodeType = ({
61 pathPrefix,
62 getNodeAndSavePathDependency,
63 reporter,
64 name,
65 cache,
66}) => {
67 return {
68 type: new GraphQLObjectType({
69 name: name,
70 fields: {
71 base64: { type: GraphQLString },
72 tracedSVG: {
73 type: GraphQLString,
74 resolve: parent =>
75 getTracedSVG({
76 ...parent,
77 cache,
78 reporter,
79 }),
80 },
81 aspectRatio: { type: GraphQLFloat },
82 width: { type: new GraphQLNonNull(GraphQLFloat) },
83 height: { type: new GraphQLNonNull(GraphQLFloat) },
84 src: { type: new GraphQLNonNull(GraphQLString) },
85 srcSet: { type: new GraphQLNonNull(GraphQLString) },
86 srcWebp: {
87 type: GraphQLString,
88 resolve: ({ file, image, fieldArgs }) => {
89 // If the file is already in webp format or should explicitly
90 // be converted to webp, we do not create additional webp files
91 if (file.extension === `webp` || fieldArgs.toFormat === `webp`) {
92 return null
93 }
94 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
95 return Promise.resolve(
96 fixed({
97 file,
98 args,
99 reporter,
100 cache,
101 })
102 ).then(({ src }) => src)
103 },
104 },
105 srcSetWebp: {
106 type: GraphQLString,
107 resolve: ({ file, image, fieldArgs }) => {
108 if (file.extension === `webp` || fieldArgs.toFormat === `webp`) {
109 return null
110 }
111 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
112 return Promise.resolve(
113 fixed({
114 file,
115 args,
116 reporter,
117 cache,
118 })
119 ).then(({ srcSet }) => srcSet)
120 },
121 },
122 originalName: { type: GraphQLString },
123 },
124 }),
125 args: {
126 width: {
127 type: GraphQLInt,
128 },
129 height: {
130 type: GraphQLInt,
131 },
132 base64Width: {
133 type: GraphQLInt,
134 },
135 jpegProgressive: {
136 type: GraphQLBoolean,
137 defaultValue: true,
138 },
139 pngCompressionSpeed: {
140 type: GraphQLInt,
141 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
142 },
143 grayscale: {
144 type: GraphQLBoolean,
145 defaultValue: false,
146 },
147 duotone: {
148 type: DuotoneGradientType,
149 defaultValue: false,
150 },
151 traceSVG: {
152 type: PotraceType,
153 defaultValue: false,
154 },
155 quality: {
156 type: GraphQLInt,
157 },
158 jpegQuality: {
159 type: GraphQLInt,
160 },
161 pngQuality: {
162 type: GraphQLInt,
163 },
164 webpQuality: {
165 type: GraphQLInt,
166 },
167 toFormat: {
168 type: ImageFormatType,
169 defaultValue: ``,
170 },
171 toFormatBase64: {
172 type: ImageFormatType,
173 defaultValue: ``,
174 },
175 cropFocus: {
176 type: ImageCropFocusType,
177 defaultValue: sharp.strategy.attention,
178 },
179 fit: {
180 type: ImageFitType,
181 defaultValue: sharp.fit.cover,
182 },
183 background: {
184 type: GraphQLString,
185 defaultValue: `rgba(0,0,0,1)`,
186 },
187 rotate: {
188 type: GraphQLInt,
189 defaultValue: 0,
190 },
191 trim: {
192 type: GraphQLFloat,
193 defaultValue: false,
194 },
195 },
196 resolve: (image, fieldArgs, context) => {
197 const file = getNodeAndSavePathDependency(image.parent, context.path)
198 const args = { ...fieldArgs, pathPrefix }
199 return Promise.resolve(
200 fixed({
201 file,
202 args,
203 reporter,
204 cache,
205 })
206 ).then(o =>
207 Object.assign({}, o, {
208 fieldArgs: args,
209 image,
210 file,
211 })
212 )
213 },
214 }
215}
216
217const fluidNodeType = ({
218 pathPrefix,
219 getNodeAndSavePathDependency,
220 reporter,
221 name,
222 cache,
223}) => {
224 return {
225 type: new GraphQLObjectType({
226 name: name,
227 fields: {
228 base64: { type: GraphQLString },
229 tracedSVG: {
230 type: GraphQLString,
231 resolve: parent =>
232 getTracedSVG({
233 ...parent,
234 cache,
235 reporter,
236 }),
237 },
238 aspectRatio: { type: new GraphQLNonNull(GraphQLFloat) },
239 src: { type: new GraphQLNonNull(GraphQLString) },
240 srcSet: { type: new GraphQLNonNull(GraphQLString) },
241 srcWebp: {
242 type: GraphQLString,
243 resolve: ({ file, image, fieldArgs }) => {
244 if (image.extension === `webp` || fieldArgs.toFormat === `webp`) {
245 return null
246 }
247 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
248 return Promise.resolve(
249 fluid({
250 file,
251 args,
252 reporter,
253 cache,
254 })
255 ).then(({ src }) => src)
256 },
257 },
258 srcSetWebp: {
259 type: GraphQLString,
260 resolve: ({ file, image, fieldArgs }) => {
261 if (image.extension === `webp` || fieldArgs.toFormat === `webp`) {
262 return null
263 }
264 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
265 return Promise.resolve(
266 fluid({
267 file,
268 args,
269 reporter,
270 cache,
271 })
272 ).then(({ srcSet }) => srcSet)
273 },
274 },
275 sizes: { type: new GraphQLNonNull(GraphQLString) },
276 originalImg: { type: GraphQLString },
277 originalName: { type: GraphQLString },
278 presentationWidth: { type: new GraphQLNonNull(GraphQLInt) },
279 presentationHeight: { type: new GraphQLNonNull(GraphQLInt) },
280 },
281 }),
282 args: {
283 maxWidth: {
284 type: GraphQLInt,
285 },
286 maxHeight: {
287 type: GraphQLInt,
288 },
289 base64Width: {
290 type: GraphQLInt,
291 },
292 grayscale: {
293 type: GraphQLBoolean,
294 defaultValue: false,
295 },
296 jpegProgressive: {
297 type: GraphQLBoolean,
298 defaultValue: true,
299 },
300 pngCompressionSpeed: {
301 type: GraphQLInt,
302 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
303 },
304 duotone: {
305 type: DuotoneGradientType,
306 defaultValue: false,
307 },
308 traceSVG: {
309 type: PotraceType,
310 defaultValue: false,
311 },
312 quality: {
313 type: GraphQLInt,
314 },
315 jpegQuality: {
316 type: GraphQLInt,
317 },
318 pngQuality: {
319 type: GraphQLInt,
320 },
321 webpQuality: {
322 type: GraphQLInt,
323 },
324 toFormat: {
325 type: ImageFormatType,
326 defaultValue: ``,
327 },
328 toFormatBase64: {
329 type: ImageFormatType,
330 defaultValue: ``,
331 },
332 cropFocus: {
333 type: ImageCropFocusType,
334 defaultValue: sharp.strategy.attention,
335 },
336 fit: {
337 type: ImageFitType,
338 defaultValue: sharp.fit.cover,
339 },
340 background: {
341 type: GraphQLString,
342 defaultValue: `rgba(0,0,0,1)`,
343 },
344 rotate: {
345 type: GraphQLInt,
346 defaultValue: 0,
347 },
348 trim: {
349 type: GraphQLFloat,
350 defaultValue: false,
351 },
352 sizes: {
353 type: GraphQLString,
354 defaultValue: ``,
355 },
356 srcSetBreakpoints: {
357 type: GraphQLList(GraphQLInt),
358 defaultValue: [],
359 description: `A list of image widths to be generated. Example: [ 200, 340, 520, 890 ]`,
360 },
361 },
362 resolve: (image, fieldArgs, context) => {
363 const file = getNodeAndSavePathDependency(image.parent, context.path)
364 const args = { ...fieldArgs, pathPrefix }
365 return Promise.resolve(
366 fluid({
367 file,
368 args,
369 reporter,
370 cache,
371 })
372 ).then(o =>
373 Object.assign({}, o, {
374 fieldArgs: args,
375 image,
376 file,
377 })
378 )
379 },
380 }
381}
382
383const imageNodeType = ({
384 pathPrefix,
385 getNodeAndSavePathDependency,
386 reporter,
387 cache,
388}) => {
389 return {
390 type: new GraphQLObjectType({
391 name: `ImageSharpGatsbyImage`,
392 fields: {
393 imageData: {
394 type: new GraphQLNonNull(GraphQLJSON),
395 },
396 },
397 }),
398 args: {
399 layout: {
400 type: ImageLayoutType,
401 defaultValue: `fixed`,
402 description: stripIndent`
403 The layout for the image.
404 FIXED: A static image sized, that does not resize according to the screen width
405 FLUID: The image resizes to fit its container. Pass a "sizes" option if it isn't going to be the full width of the screen.
406 CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.
407 `,
408 },
409 maxWidth: {
410 type: GraphQLInt,
411 description: stripIndent`
412 Maximum display width of generated files.
413 The actual largest image resolution will be this value multipled by the largest value in outputPixelDensities
414 This only applies when layout = FLUID or CONSTRAINED. For other layout types, use "width"`,
415 },
416 maxHeight: {
417 type: GraphQLInt,
418 },
419 width: {
420 type: GraphQLInt,
421 description: stripIndent`
422 The display width of the generated image.
423 The actual largest image resolution will be this value multipled by the largest value in outputPixelDensities
424 Ignored if layout = FLUID or CONSTRAINED, where you should use "maxWidth" instead.
425 `,
426 },
427 height: {
428 type: GraphQLInt,
429 },
430 placeholder: {
431 type: ImagePlaceholderType,
432 defaultValue: `blurred`,
433 description: stripIndent`
434 Format of generated placeholder image.
435 DOMINANT_COLOR: a solid color, calculated from the dominant color of the image.
436 BASE64: a blurred, low resolution image, encoded as a base64 data URI
437 TRACED_SVG: a low-resolution traced SVG of the image.
438 NONE: no placeholder. Set "background" to use a fixed background color.`,
439 },
440 tracedSVGOptions: {
441 type: PotraceType,
442 defaultValue: false,
443 description: `Options for traced placeholder SVGs. You also should set placeholder to SVG.`,
444 },
445 webP: {
446 type: GraphQLBoolean,
447 defaultValue: true,
448 description: `Generate images in WebP format as well as matching the input format. This is the default (and strongly recommended), but will add to processing time.`,
449 },
450 outputPixelDensities: {
451 type: GraphQLList(GraphQLFloat),
452 description: stripIndent`
453 A list of image pixel densities to generate, for high-resolution (retina) screens. It will never generate images larger than the source, and will always a 1x image.
454 Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, 3x, and [0.25, 0.5, 1, 2] for fluid. In this case, an image with a fluid layout and width = 400 would generate images at 100, 200, 400 and 800px wide`,
455 },
456 sizes: {
457 type: GraphQLString,
458 defaultValue: ``,
459 description: stripIndent`
460 The "sizes" property, passed to the img tag. This describes the display size of the image.
461 This does not affect the generated images, but is used by the browser to decide which images to download. You can leave this blank for fixed images, or if the responsive image
462 container will be the full width of the screen. In these cases we will generate an appropriate value.
463 `,
464 },
465 base64Width: {
466 type: GraphQLInt,
467 },
468 grayscale: {
469 type: GraphQLBoolean,
470 defaultValue: false,
471 },
472 jpegProgressive: {
473 type: GraphQLBoolean,
474 defaultValue: true,
475 },
476 pngCompressionSpeed: {
477 type: GraphQLInt,
478 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
479 },
480 duotone: {
481 type: DuotoneGradientType,
482 defaultValue: false,
483 },
484 quality: {
485 type: GraphQLInt,
486 },
487 jpegQuality: {
488 type: GraphQLInt,
489 },
490 pngQuality: {
491 type: GraphQLInt,
492 },
493 webpQuality: {
494 type: GraphQLInt,
495 },
496 toFormat: {
497 type: ImageFormatType,
498 defaultValue: ``,
499 },
500 toFormatBase64: {
501 type: ImageFormatType,
502 defaultValue: ``,
503 description: `Force output format. Default is to use the same as the input format`,
504 },
505 cropFocus: {
506 type: ImageCropFocusType,
507 defaultValue: sharp.strategy.attention,
508 },
509 fit: {
510 type: ImageFitType,
511 defaultValue: sharp.fit.cover,
512 },
513 background: {
514 type: GraphQLString,
515 defaultValue: `rgba(0,0,0,1)`,
516 },
517 rotate: {
518 type: GraphQLInt,
519 defaultValue: 0,
520 },
521 trim: {
522 type: GraphQLFloat,
523 defaultValue: false,
524 },
525 srcSetBreakpoints: {
526 type: GraphQLList(GraphQLInt),
527 defaultValue: [],
528 description: stripIndent`\
529 A list of image widths to be generated. Example: [ 200, 340, 520, 890 ].
530 You should usually leave this blank and allow it to be generated from the width/maxWidth and outputPixelDensities`,
531 },
532 },
533 resolve: async (image, fieldArgs, context) => {
534 const file = getNodeAndSavePathDependency(image.parent, context.path)
535 const args = { ...fieldArgs, pathPrefix }
536
537 if (!generateImageData) {
538 reporter.warn(`Please upgrade gatsby-plugin-sharp`)
539 return null
540 }
541 reporter.warn(
542 stripIndent`
543 You are using the alpha version of the \`gatsbyImage\` sharp API, which is unstable and will change without notice.
544 Please do not use it in production.`
545 )
546 const imageData = await generateImageData({
547 file,
548 args,
549 reporter,
550 cache,
551 })
552
553 return {
554 imageData,
555 fieldArgs: args,
556 image,
557 file,
558 }
559 },
560 }
561}
562
563/**
564 * Keeps track of asynchronous file copy to prevent sequence errors in the
565 * underlying fs-extra module during parallel copies of the same file
566 */
567const inProgressCopy = new Set()
568
569const createFields = ({
570 pathPrefix,
571 getNodeAndSavePathDependency,
572 reporter,
573 cache,
574}) => {
575 const nodeOptions = {
576 pathPrefix,
577 getNodeAndSavePathDependency,
578 reporter,
579 cache,
580 }
581
582 // TODO: Remove resolutionsNode and sizesNode for Gatsby v3
583 const fixedNode = fixedNodeType({ name: `ImageSharpFixed`, ...nodeOptions })
584 const resolutionsNode = fixedNodeType({
585 name: `ImageSharpResolutions`,
586 ...nodeOptions,
587 })
588 resolutionsNode.deprecationReason = `Resolutions was deprecated in Gatsby v2. It's been renamed to "fixed" https://example.com/write-docs-and-fix-this-example-link`
589
590 const fluidNode = fluidNodeType({ name: `ImageSharpFluid`, ...nodeOptions })
591 const sizesNode = fluidNodeType({ name: `ImageSharpSizes`, ...nodeOptions })
592 sizesNode.deprecationReason = `Sizes was deprecated in Gatsby v2. It's been renamed to "fluid" https://example.com/write-docs-and-fix-this-example-link`
593
594 const imageNode = imageNodeType(nodeOptions)
595
596 return {
597 fixed: fixedNode,
598 resolutions: resolutionsNode,
599 fluid: fluidNode,
600 sizes: sizesNode,
601 gatsbyImage: imageNode,
602 original: {
603 type: new GraphQLObjectType({
604 name: `ImageSharpOriginal`,
605 fields: {
606 width: { type: GraphQLFloat },
607 height: { type: GraphQLFloat },
608 src: { type: GraphQLString },
609 },
610 }),
611 args: {},
612 async resolve(image, fieldArgs, context) {
613 const details = getNodeAndSavePathDependency(image.parent, context.path)
614 const dimensions = imageSize.sync(
615 toArray(fs.readFileSync(details.absolutePath))
616 )
617 const imageName = `${details.name}-${image.internal.contentDigest}${details.ext}`
618 const publicPath = path.join(
619 process.cwd(),
620 `public`,
621 `static`,
622 imageName
623 )
624
625 if (!fs.existsSync(publicPath) && !inProgressCopy.has(publicPath)) {
626 // keep track of in progress copy, we should rely on `existsSync` but
627 // a race condition exists between the exists check and the copy
628 inProgressCopy.add(publicPath)
629 fs.copy(details.absolutePath, publicPath, err => {
630 // this is no longer in progress
631 inProgressCopy.delete(publicPath)
632 if (err) {
633 reporter.panic(
634 {
635 id: prefixId(CODES.MissingResource),
636 context: {
637 sourceMessage: `error copying file from ${details.absolutePath} to ${publicPath}`,
638 },
639 },
640 err
641 )
642 }
643 })
644 }
645
646 return {
647 width: dimensions.width,
648 height: dimensions.height,
649 src: `${pathPrefix}/static/${imageName}`,
650 }
651 },
652 },
653 resize: {
654 type: new GraphQLObjectType({
655 name: `ImageSharpResize`,
656 fields: {
657 src: { type: GraphQLString },
658 tracedSVG: {
659 type: GraphQLString,
660 resolve: parent =>
661 getTracedSVG({
662 ...parent,
663 cache,
664 reporter,
665 }),
666 },
667 width: { type: GraphQLInt },
668 height: { type: GraphQLInt },
669 aspectRatio: { type: GraphQLFloat },
670 originalName: { type: GraphQLString },
671 },
672 }),
673 args: {
674 width: {
675 type: GraphQLInt,
676 },
677 height: {
678 type: GraphQLInt,
679 },
680 quality: {
681 type: GraphQLInt,
682 },
683 jpegQuality: {
684 type: GraphQLInt,
685 },
686 pngQuality: {
687 type: GraphQLInt,
688 },
689 webpQuality: {
690 type: GraphQLInt,
691 },
692 jpegProgressive: {
693 type: GraphQLBoolean,
694 defaultValue: true,
695 },
696 pngCompressionLevel: {
697 type: GraphQLInt,
698 defaultValue: 9,
699 },
700 pngCompressionSpeed: {
701 type: GraphQLInt,
702 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
703 },
704 grayscale: {
705 type: GraphQLBoolean,
706 defaultValue: false,
707 },
708 duotone: {
709 type: DuotoneGradientType,
710 defaultValue: false,
711 },
712 base64: {
713 type: GraphQLBoolean,
714 defaultValue: false,
715 },
716 traceSVG: {
717 type: PotraceType,
718 defaultValue: false,
719 },
720 toFormat: {
721 type: ImageFormatType,
722 defaultValue: ``,
723 },
724 cropFocus: {
725 type: ImageCropFocusType,
726 defaultValue: sharp.strategy.attention,
727 },
728 fit: {
729 type: ImageFitType,
730 defaultValue: sharp.fit.cover,
731 },
732 background: {
733 type: GraphQLString,
734 defaultValue: `rgba(0,0,0,1)`,
735 },
736 rotate: {
737 type: GraphQLInt,
738 defaultValue: 0,
739 },
740 trim: {
741 type: GraphQLFloat,
742 defaultValue: 0,
743 },
744 },
745 resolve: (image, fieldArgs, context) => {
746 const file = getNodeAndSavePathDependency(image.parent, context.path)
747 const args = { ...fieldArgs, pathPrefix }
748 return new Promise(resolve => {
749 if (fieldArgs.base64) {
750 resolve(
751 base64({
752 file,
753 cache,
754 })
755 )
756 } else {
757 const o = queueImageResizing({
758 file,
759 args,
760 })
761 resolve(
762 Object.assign({}, o, {
763 image,
764 file,
765 fieldArgs: args,
766 })
767 )
768 }
769 })
770 },
771 },
772 }
773}
774
775module.exports = ({
776 actions,
777 schema,
778 pathPrefix,
779 getNodeAndSavePathDependency,
780 reporter,
781 cache,
782}) => {
783 const { createTypes } = actions
784
785 const imageSharpType = schema.buildObjectType({
786 name: `ImageSharp`,
787 fields: createFields({
788 pathPrefix,
789 getNodeAndSavePathDependency,
790 reporter,
791 cache,
792 }),
793 interfaces: [`Node`],
794 extensions: {
795 infer: true,
796 childOf: {
797 types: [`File`],
798 },
799 },
800 })
801
802 if (createTypes) {
803 createTypes([
804 ImageFormatType,
805 ImageFitType,
806 ImageLayoutType,
807 ImageCropFocusType,
808 DuotoneGradientType,
809 PotraceTurnPolicyType,
810 PotraceType,
811 imageSharpType,
812 ])
813 }
814}